From d2d4d231567084ee3838f88e0a22f8d5bd13465b Mon Sep 17 00:00:00 2001
From: summer <44016063+xiazl1993@users.noreply.github.com>
Date: Sun, 31 May 2026 23:14:38 +0800
Subject: [PATCH 01/12] chore: clean up duplicate gitignore patterns
---
.gitignore | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/.gitignore b/.gitignore
index 8087a9e..004f0a9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,5 +12,8 @@ __pycache__/
.worktrees/
homepage/public/demo/
.private/
-__pycache__/
+.venv/
+venv/
*.pyc
+*.pyo
+Thumbs.db
From eea73b656d168ec91754d9436e60f285c36f2e85 Mon Sep 17 00:00:00 2001
From: Tirth Kanani
Date: Sun, 31 May 2026 20:25:39 +0100
Subject: [PATCH 02/12] perf(understand): parallelise file I/O in
compute-batches + extract-import-map (#76)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The /understand pipeline reads every code file twice during analysis:
once in compute-batches (`extractExports` for the cross-batch neighbour
map) and once again in extract-import-map (per-language config loaders).
Both sites used sequential `readFileSync` loops, so on the iOS repo in
issue #226 (~15k files) the disk-read time was effectively serialised
behind a single libuv thread while the rest of the pool sat idle.
## Changes
- `extractExports` now batches files into `IO_PARALLELISM = 64` slices
and issues all `readFile` calls in each slice through `Promise.all`,
letting libuv's worker-thread pool overlap disk reads. The
tree-sitter parse stays on the main thread because `web-tree-sitter`
is single-threaded WASM — pipelining the I/O while parses run is
where the wall-time savings come from.
- `loadTsConfigs`, `loadGoModules`, `loadPhpAutoloads` and
`buildResolutionContext` switch to async / `Promise.all` for the
same reason. `buildResolutionContext` also runs the three loader
passes concurrently (`Promise.all([...])`) since they're independent.
- A small `readFilesParallel(paths)` helper is added at the top of
`extract-import-map.mjs` so the three loaders share the same
error-preserving shape.
## Why behavior stays identical
- Each loader collects its candidate paths in `files[]` order *before*
issuing reads, then iterates `reads` in the same order to emit
warnings + populate output maps. So stderr order and the final map
contents are byte-identical to the previous sequential loops.
- `extractExports` collects per-file errors in-place in the
`Promise.all` callbacks and emits warnings during the post-read
serial loop, again in chunk order — so warning text and order match
the previous implementation.
- Tree-sitter parsing is unchanged: parses still run serially on the
main thread, just with reads pipelined alongside.
## What's NOT in this PR
- `buildFingerprintStore` and `analyzeChanges` in `core/fingerprint.ts`
have the same sequential pattern. They're left alone here because
they're part of the public `@understand-anything/core` API; making
them async would be a breaking change worth its own discussion.
Internal-only `.mjs` scripts are safe to refactor without API churn.
- No change to scan-project: most of its sync I/O is `statSync`
(metadata, not content) plus a handful of small `.gitignore` /
`.understandignore` reads. The parallelism win is marginal there.
## Verification
- `pnpm lint` clean
- `pnpm --filter @understand-anything/core build` clean
- `pnpm --filter @understand-anything/skill build` clean
- `pnpm test`: 196/196 — including
`test_compute_batches.test.mjs` (19 tests) and
`test_extract_import_map.test.mjs` (40 tests), which exercise both
changed pipelines end-to-end with fixture projects. No output
diff vs main.
Refs #76
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.../skills/understand/compute-batches.mjs | 83 ++++++++++++-----
.../skills/understand/extract-import-map.mjs | 93 +++++++++++++------
2 files changed, 123 insertions(+), 53 deletions(-)
diff --git a/understand-anything-plugin/skills/understand/compute-batches.mjs b/understand-anything-plugin/skills/understand/compute-batches.mjs
index b7cce34..f78d46a 100644
--- a/understand-anything-plugin/skills/understand/compute-batches.mjs
+++ b/understand-anything-plugin/skills/understand/compute-batches.mjs
@@ -13,10 +13,19 @@
*/
import { readFileSync, writeFileSync, existsSync, realpathSync } from 'node:fs';
+import { readFile } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { createRequire } from 'node:module';
+/**
+ * Chunk size for parallel file I/O. Bounded so a 15k-file repo doesn't try
+ * to open every descriptor at once (would hit `EMFILE`) while still keeping
+ * libuv's worker-thread pool saturated. Empirically chosen to keep memory
+ * around tens of MB even when the average file is ~10 KB.
+ */
+const IO_PARALLELISM = 64;
+
const __filename = fileURLToPath(import.meta.url);
const PLUGIN_ROOT = resolve(dirname(__filename), '../..');
const require = createRequire(resolve(PLUGIN_ROOT, 'package.json'));
@@ -57,31 +66,55 @@ async function extractExports(projectRoot, codeFiles) {
}
const exportsByPath = new Map();
- for (const file of codeFiles) {
- const abs = join(projectRoot, file.path);
- let content;
- try {
- content = readFileSync(abs, 'utf-8');
- } catch (err) {
- process.stderr.write(
- `Warning: compute-batches: exports extraction failed for ${file.path} ` +
- `(read error: ${err.message}) — symbols=[] in neighborMap — ` +
- `cross-batch edges to this file limited to file-level\n`,
- );
- exportsByPath.set(file.path, []);
- continue;
- }
- try {
- const analysis = registry.analyzeFile(file.path, content);
- const names = (analysis?.exports || []).map(e => e.name).filter(Boolean);
- exportsByPath.set(file.path, names);
- } catch (err) {
- process.stderr.write(
- `Warning: compute-batches: exports extraction failed for ${file.path} ` +
- `(analyze error: ${err.message}) — symbols=[] in neighborMap — ` +
- `cross-batch edges to this file limited to file-level\n`,
- );
- exportsByPath.set(file.path, []);
+
+ // I/O is parallelised in bounded chunks (libuv worker threads handle the
+ // disk reads concurrently) while the actual tree-sitter parse stays on
+ // the main thread, since web-tree-sitter is single-threaded WASM. For a
+ // 15k-file iOS repo (#226), the sequential `readFileSync` loop dominated;
+ // letting reads pipeline drops wall time roughly proportional to the
+ // share of the loop spent waiting on disk.
+ for (let start = 0; start < codeFiles.length; start += IO_PARALLELISM) {
+ const slice = codeFiles.slice(start, start + IO_PARALLELISM);
+
+ // Read every file in the slice concurrently. Errors per file are
+ // captured in-place so a single bad file does not abort the chunk.
+ const reads = await Promise.all(
+ slice.map(async (file) => {
+ const abs = join(projectRoot, file.path);
+ try {
+ const content = await readFile(abs, 'utf-8');
+ return { file, content, readError: null };
+ } catch (err) {
+ return { file, content: null, readError: err };
+ }
+ }),
+ );
+
+ // Serialise the CPU-bound tree-sitter work and the stderr warning emits
+ // so log order remains identical to the previous sequential loop. This
+ // also keeps existing fixture-comparison tests stable.
+ for (const { file, content, readError } of reads) {
+ if (readError) {
+ process.stderr.write(
+ `Warning: compute-batches: exports extraction failed for ${file.path} ` +
+ `(read error: ${readError.message}) — symbols=[] in neighborMap — ` +
+ `cross-batch edges to this file limited to file-level\n`,
+ );
+ exportsByPath.set(file.path, []);
+ continue;
+ }
+ try {
+ const analysis = registry.analyzeFile(file.path, content);
+ const names = (analysis?.exports || []).map(e => e.name).filter(Boolean);
+ exportsByPath.set(file.path, names);
+ } catch (err) {
+ process.stderr.write(
+ `Warning: compute-batches: exports extraction failed for ${file.path} ` +
+ `(analyze error: ${err.message}) — symbols=[] in neighborMap — ` +
+ `cross-batch edges to this file limited to file-level\n`,
+ );
+ exportsByPath.set(file.path, []);
+ }
}
}
return exportsByPath;
diff --git a/understand-anything-plugin/skills/understand/extract-import-map.mjs b/understand-anything-plugin/skills/understand/extract-import-map.mjs
index 6c547d3..ec31856 100644
--- a/understand-anything-plugin/skills/understand/extract-import-map.mjs
+++ b/understand-anything-plugin/skills/understand/extract-import-map.mjs
@@ -37,6 +37,29 @@ import { createRequire } from 'node:module';
import { dirname, resolve, join, posix } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
+import { readFile } from 'node:fs/promises';
+
+/**
+ * Read a list of files concurrently while preserving result order. Failures
+ * are returned in-place as `{ raw: null, err }` so callers can emit the same
+ * per-file warnings they did under the previous sequential `readFileSync`
+ * loops.
+ *
+ * `paths` is a list of `{ key, absPath }` pairs; `key` is whatever the caller
+ * wants to attach the result to (typically a project-relative POSIX path).
+ */
+async function readFilesParallel(paths) {
+ return Promise.all(
+ paths.map(async ({ key, absPath }) => {
+ try {
+ const raw = await readFile(absPath, 'utf-8');
+ return { key, raw, err: null };
+ } catch (err) {
+ return { key, raw: null, err };
+ }
+ }),
+ );
+}
const __dirname = dirname(fileURLToPath(import.meta.url));
// skills/understand/ -> plugin root is two dirs up
@@ -180,20 +203,25 @@ function parseTsConfigText(raw) {
* where the stripper damaged a string literal containing `//`.
* 3. If both fail, warn and skip — that tsconfig contributes no aliases.
*/
-function loadTsConfigs(projectRoot, files) {
+async function loadTsConfigs(projectRoot, files) {
const out = new Map();
+ // Collect the candidate paths in the original file order before reading,
+ // so warning emit order matches the previous sequential implementation.
+ const candidates = [];
for (const f of files) {
const p = toPosix(f.path);
const base = p.includes('/') ? p.slice(p.lastIndexOf('/') + 1) : p;
if (base !== 'tsconfig.json') continue;
const absPath = join(projectRoot, p);
if (!existsSync(absPath)) continue;
- let raw;
- try {
- raw = readFileSync(absPath, 'utf-8');
- } catch (err) {
+ candidates.push({ key: p, absPath });
+ }
+ const reads = await readFilesParallel(candidates);
+ for (const { key: p, raw, err } of reads) {
+ if (err) {
+ // absPath isn't carried through the helper return shape; reconstruct it.
process.stderr.write(
- `Warning: extract-import-map: tsconfig.json at ${absPath} failed ` +
+ `Warning: extract-import-map: tsconfig.json at ${join(projectRoot, p)} failed ` +
`to read (${err.message}) — path aliases from this config will ` +
`not be applied — relative imports unaffected\n`,
);
@@ -202,7 +230,7 @@ function loadTsConfigs(projectRoot, files) {
const parsed = parseTsConfigText(raw);
if (!parsed) {
process.stderr.write(
- `Warning: extract-import-map: tsconfig.json at ${absPath} failed ` +
+ `Warning: extract-import-map: tsconfig.json at ${join(projectRoot, p)} failed ` +
`to parse — path aliases from this config will not be applied ` +
`— relative imports unaffected\n`,
);
@@ -237,20 +265,20 @@ function loadTsConfigs(projectRoot, files) {
* The resolver uses each module's prefix to translate
* `import "github.com/foo/bar/x"` into the project-internal `x/.go`.
*/
-function loadGoModules(projectRoot, files) {
+async function loadGoModules(projectRoot, files) {
const out = new Map();
+ const candidates = [];
for (const f of files) {
const p = toPosix(f.path);
const base = p.includes('/') ? p.slice(p.lastIndexOf('/') + 1) : p;
if (base !== 'go.mod') continue;
const absPath = join(projectRoot, p);
if (!existsSync(absPath)) continue;
- let raw;
- try {
- raw = readFileSync(absPath, 'utf-8');
- } catch {
- continue;
- }
+ candidates.push({ key: p, absPath });
+ }
+ const reads = await readFilesParallel(candidates);
+ for (const { key: p, raw, err } of reads) {
+ if (err) continue;
let moduleName = '';
for (const line of raw.split(/\r?\n/)) {
const trimmed = line.replace(/\/\/.*$/, '').trim();
@@ -306,10 +334,17 @@ function findNearestConfigDir(startDir, configMap) {
*
* Build once; pass everywhere.
*/
-function buildResolutionContext(projectRoot, files) {
+async function buildResolutionContext(projectRoot, files) {
const fileSet = new Set(files.map(f => toPosix(f.path)));
- const tsConfigs = loadTsConfigs(projectRoot, files);
- const goModules = loadGoModules(projectRoot, files);
+
+ // The three config-loader passes are independent and each does its own
+ // batched parallel I/O; run them concurrently so the wait for a slow
+ // tsconfig.json read doesn't block go.mod / composer.json scanning.
+ const [tsConfigs, goModules, phpAutoloads] = await Promise.all([
+ loadTsConfigs(projectRoot, files),
+ loadGoModules(projectRoot, files),
+ loadPhpAutoloads(projectRoot, files),
+ ]);
// Index .go files by their parent directory so the Go resolver can
// expand a package-level import to all member .go files in O(1).
@@ -331,8 +366,6 @@ function buildResolutionContext(projectRoot, files) {
const kotlinIndex = buildSuffixIndex(files, p => p.endsWith('.kt'));
const csIndex = buildSuffixIndex(files, p => p.endsWith('.cs'));
- const phpAutoloads = loadPhpAutoloads(projectRoot, files);
-
return {
projectRoot,
fileSet,
@@ -1019,20 +1052,22 @@ function parseComposerAutoloadText(raw) {
* at the bad file and skips it. The rest of the project's PHP imports keep
* resolving via whichever composer.json files parsed cleanly.
*/
-function loadPhpAutoloads(projectRoot, files) {
+async function loadPhpAutoloads(projectRoot, files) {
const out = new Map();
+ const candidates = [];
for (const f of files) {
const p = toPosix(f.path);
const base = p.includes('/') ? p.slice(p.lastIndexOf('/') + 1) : p;
if (base !== 'composer.json') continue;
const absPath = join(projectRoot, p);
if (!existsSync(absPath)) continue;
- let raw;
- try {
- raw = readFileSync(absPath, 'utf-8');
- } catch (err) {
+ candidates.push({ key: p, absPath });
+ }
+ const reads = await readFilesParallel(candidates);
+ for (const { key: p, raw, err } of reads) {
+ if (err) {
process.stderr.write(
- `Warning: extract-import-map: composer.json at ${absPath} failed ` +
+ `Warning: extract-import-map: composer.json at ${join(projectRoot, p)} failed ` +
`to read (${err.message}) — PSR-4 namespace mapping from this ` +
`composer.json unavailable — PHP imports under this package ` +
`will not resolve\n`,
@@ -1042,7 +1077,7 @@ function loadPhpAutoloads(projectRoot, files) {
const parsed = parseComposerAutoloadText(raw);
if (parsed === null) {
process.stderr.write(
- `Warning: extract-import-map: composer.json at ${absPath} failed ` +
+ `Warning: extract-import-map: composer.json at ${join(projectRoot, p)} failed ` +
`to parse — PSR-4 namespace mapping unavailable — PHP imports ` +
`under this package will not resolve\n`,
);
@@ -1412,8 +1447,10 @@ async function main() {
);
}
- // Build resolution context (cached configs)
- const ctx = buildResolutionContext(projectRoot, files);
+ // Build resolution context (cached configs). The loader pass for the
+ // tsconfig/go.mod/composer.json files inside is parallelised — see
+ // `buildResolutionContext`.
+ const ctx = await buildResolutionContext(projectRoot, files);
const importMap = {};
let filesWithImports = 0;
From 235f2fafc84474243a79294f12b97318698cd639 Mon Sep 17 00:00:00 2001
From: Tirth Kanani
Date: Sun, 31 May 2026 20:42:12 +0100
Subject: [PATCH 03/12] feat(core): add Kotlin structural analysis via
tree-sitter
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Wires Kotlin into the existing tree-sitter pipeline so .kt and .kts
files now produce functions, classes, data classes, sealed classes,
interfaces, objects, imports, exports, and call-graph edges — matching
the behavior of the other language extractors.
## Why @tree-sitter-grammars/tree-sitter-kotlin
The standard `tree-sitter-kotlin` (v0.3.8) ships only native bindings.
The new `@tree-sitter-grammars/tree-sitter-kotlin@1.1.0` ships a
prebuilt `.wasm` (loads cleanly with `web-tree-sitter@^0.26.6`,
nodeTypeCount=289, parses class_declaration / function_declaration as
expected). Same shape that PR1 used for Swift, just a different
publisher because the repomix WASM bundle does not include Kotlin.
`@tree-sitter-grammars` is the official tree-sitter org's GitHub
account, so this is the canonical upstream WASM source for Kotlin.
## Notes for reviewers
- `kotlinConfig` already existed as a stub (no `treeSitter` field), so
Android / JVM / Gradle codebases currently produce no structural
edges between `.kt` files. This PR adds the `treeSitter` field; the
existing plugin loader picks it up unchanged.
- **Visibility rule differs from Swift**: Kotlin's default visibility
is `public`, so the extractor treats *every* declaration with no
modifier as exported. Only an explicit `private` opts out. `internal`
and `protected` remain exported in the project-graph sense because
they are still resolvable from other files (within the module / via
inheritance).
- `class_declaration` in tree-sitter-kotlin is overloaded for class,
data class, sealed class, and interface (distinguished by the keyword
child and `modifiers > class_modifier`). The extractor handles all
four uniformly.
- `object_declaration` is a separate node type (Kotlin singletons) —
treated as a class-like entry with its own `name` and members.
- Primary-constructor parameters marked `val` / `var` are surfaced as
class properties; plain `parameter`s without `val/var` are
constructor-only and are NOT counted as properties (matching Kotlin
semantics).
- Import handling distinguishes the three forms: plain dotted
(`import a.b.C`), wildcard (`import a.b.*` → specifier `"*"`), and
aliased (`import a.b.C as Foo` → specifier `"Foo"`).
## Verification
- `pnpm lint` clean
- `pnpm --filter @understand-anything/core build` clean
- `pnpm --filter @understand-anything/skill build` clean
- `pnpm --filter @understand-anything/core test`: **692/692** (+22 new
Kotlin tests, matching the bar set by go-extractor.test.ts /
swift-extractor.test.ts)
- `pnpm test`: 196/196 (no regressions)
Co-Authored-By: Claude Opus 4.7 (1M context)
---
pnpm-lock.yaml | 24 +
.../packages/core/package.json | 1 +
.../core/src/languages/configs/kotlin.ts | 4 +
.../__tests__/kotlin-extractor.test.ts | 364 +++++++++++++++
.../core/src/plugins/extractors/index.ts | 3 +
.../plugins/extractors/kotlin-extractor.ts | 425 ++++++++++++++++++
6 files changed, 821 insertions(+)
create mode 100644 understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/kotlin-extractor.test.ts
create mode 100644 understand-anything-plugin/packages/core/src/plugins/extractors/kotlin-extractor.ts
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3eb7e7a..0b9b06e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -57,6 +57,9 @@ importers:
understand-anything-plugin/packages/core:
dependencies:
+ '@tree-sitter-grammars/tree-sitter-kotlin':
+ specifier: 1.1.0
+ version: 1.1.0
fuse.js:
specifier: ^7.1.0
version: 7.1.0
@@ -1120,6 +1123,14 @@ packages:
peerDependencies:
vite: ^5.2.0 || ^6 || ^7
+ '@tree-sitter-grammars/tree-sitter-kotlin@1.1.0':
+ resolution: {integrity: sha512-vlVXaxEE8t2kpJgfZpa8XVvxcnKw9AYtRTgy7KWjsDmAsadk06RxAT80IXOgGQnmM9i/orQn1nD84gPNUHu6DQ==}
+ peerDependencies:
+ tree-sitter: ^0.22.4
+ peerDependenciesMeta:
+ tree-sitter:
+ optional: true
+
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@@ -2442,6 +2453,11 @@ packages:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
+ npm-check-updates@17.1.18:
+ resolution: {integrity: sha512-bkUy2g4v1i+3FeUf5fXMLbxmV95eG4/sS7lYE32GrUeVgQRfQEk39gpskksFunyaxQgTIdrvYbnuNbO/pSUSqw==}
+ engines: {node: ^18.18.0 || >=20.0.0, npm: '>=8.12.1'}
+ hasBin: true
+
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
@@ -4048,6 +4064,12 @@ snapshots:
tailwindcss: 4.2.1
vite: 6.4.2(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
+ '@tree-sitter-grammars/tree-sitter-kotlin@1.1.0':
+ dependencies:
+ node-addon-api: 8.6.0
+ node-gyp-build: 4.8.4
+ npm-check-updates: 17.1.18
+
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.29.0
@@ -5760,6 +5782,8 @@ snapshots:
normalize-path@3.0.0: {}
+ npm-check-updates@17.1.18: {}
+
nth-check@2.1.1:
dependencies:
boolbase: 1.0.0
diff --git a/understand-anything-plugin/packages/core/package.json b/understand-anything-plugin/packages/core/package.json
index c4cd4f3..7ea50ae 100644
--- a/understand-anything-plugin/packages/core/package.json
+++ b/understand-anything-plugin/packages/core/package.json
@@ -37,6 +37,7 @@
"vitest": "^3.1.0"
},
"dependencies": {
+ "@tree-sitter-grammars/tree-sitter-kotlin": "1.1.0",
"fuse.js": "^7.1.0",
"ignore": "^7.0.5",
"tree-sitter-c-sharp": "^0.23.1",
diff --git a/understand-anything-plugin/packages/core/src/languages/configs/kotlin.ts b/understand-anything-plugin/packages/core/src/languages/configs/kotlin.ts
index f02dc79..dd700d5 100644
--- a/understand-anything-plugin/packages/core/src/languages/configs/kotlin.ts
+++ b/understand-anything-plugin/packages/core/src/languages/configs/kotlin.ts
@@ -4,6 +4,10 @@ export const kotlinConfig = {
id: "kotlin",
displayName: "Kotlin",
extensions: [".kt", ".kts"],
+ treeSitter: {
+ wasmPackage: "@tree-sitter-grammars/tree-sitter-kotlin",
+ wasmFile: "tree-sitter-kotlin.wasm",
+ },
concepts: [
"coroutines",
"data classes",
diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/kotlin-extractor.test.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/kotlin-extractor.test.ts
new file mode 100644
index 0000000..db12d0f
--- /dev/null
+++ b/understand-anything-plugin/packages/core/src/plugins/extractors/__tests__/kotlin-extractor.test.ts
@@ -0,0 +1,364 @@
+import { describe, it, expect, beforeAll } from "vitest";
+import { createRequire } from "node:module";
+import { KotlinExtractor } from "../kotlin-extractor.js";
+
+const require = createRequire(import.meta.url);
+
+let Parser: any;
+let Language: any;
+let kotlinLang: any;
+
+beforeAll(async () => {
+ const mod = await import("web-tree-sitter");
+ Parser = mod.Parser;
+ Language = mod.Language;
+ await Parser.init();
+ const wasmPath = require.resolve(
+ "@tree-sitter-grammars/tree-sitter-kotlin/tree-sitter-kotlin.wasm",
+ );
+ kotlinLang = await Language.load(wasmPath);
+});
+
+function parse(code: string) {
+ const parser = new Parser();
+ parser.setLanguage(kotlinLang);
+ const tree = parser.parse(code);
+ const root = tree.rootNode;
+ return { tree, parser, root };
+}
+
+describe("KotlinExtractor", () => {
+ const extractor = new KotlinExtractor();
+
+ it("has correct languageIds", () => {
+ expect(extractor.languageIds).toEqual(["kotlin"]);
+ });
+
+ describe("extractStructure - functions", () => {
+ it("extracts a simple top-level function with params and return type", () => {
+ const { tree, parser, root } = parse(`fun add(a: Int, b: Int): Int = a + b
+`);
+ const result = extractor.extractStructure(root);
+
+ expect(result.functions).toHaveLength(1);
+ expect(result.functions[0].name).toBe("add");
+ expect(result.functions[0].params).toEqual(["a", "b"]);
+ expect(result.functions[0].returnType).toBe("Int");
+
+ tree.delete();
+ parser.delete();
+ });
+
+ it("extracts function with no params and no return type", () => {
+ const { tree, parser, root } = parse(`fun noop() {}
+`);
+ const result = extractor.extractStructure(root);
+
+ expect(result.functions).toHaveLength(1);
+ expect(result.functions[0].name).toBe("noop");
+ expect(result.functions[0].params).toEqual([]);
+ expect(result.functions[0].returnType).toBeUndefined();
+
+ tree.delete();
+ parser.delete();
+ });
+
+ it("extracts suspending and generic functions", () => {
+ const { tree, parser, root } = parse(`suspend fun fetch(id: String): T? {
+ return null
+}
+`);
+ const result = extractor.extractStructure(root);
+
+ expect(result.functions).toHaveLength(1);
+ expect(result.functions[0].name).toBe("fetch");
+ expect(result.functions[0].params).toEqual(["id"]);
+ // Nullable type — Kotlin formats it as "T?"
+ expect(result.functions[0].returnType).toBe("T?");
+
+ tree.delete();
+ parser.delete();
+ });
+
+ it("extracts multiple top-level functions in declaration order", () => {
+ const { tree, parser, root } = parse(`fun one() {}
+fun two(x: Int): Int = x
+fun three(): String = ""
+`);
+ const result = extractor.extractStructure(root);
+
+ expect(result.functions.map((f) => f.name)).toEqual([
+ "one",
+ "two",
+ "three",
+ ]);
+
+ tree.delete();
+ parser.delete();
+ });
+ });
+
+ describe("extractStructure - classes", () => {
+ it("extracts a class with primary-constructor val properties + methods", () => {
+ const { tree, parser, root } = parse(`class Foo(val bar: Int) {
+ val baz: String = "hi"
+ fun compute(): Int = bar * 2
+}
+`);
+ const result = extractor.extractStructure(root);
+
+ expect(result.classes).toHaveLength(1);
+ expect(result.classes[0].name).toBe("Foo");
+ // Both the constructor val and the body val are properties
+ expect(result.classes[0].properties).toEqual(
+ expect.arrayContaining(["bar", "baz"]),
+ );
+ expect(result.classes[0].methods).toContain("compute");
+
+ tree.delete();
+ parser.delete();
+ });
+
+ it("extracts an empty class", () => {
+ const { tree, parser, root } = parse(`class Empty
+`);
+ const result = extractor.extractStructure(root);
+
+ expect(result.classes).toHaveLength(1);
+ expect(result.classes[0].name).toBe("Empty");
+ expect(result.classes[0].methods).toEqual([]);
+ expect(result.classes[0].properties).toEqual([]);
+
+ tree.delete();
+ parser.delete();
+ });
+
+ it("extracts a data class and surfaces its constructor parameters as properties", () => {
+ const { tree, parser, root } = parse(`data class Point(val x: Double, val y: Double)
+`);
+ const result = extractor.extractStructure(root);
+
+ expect(result.classes).toHaveLength(1);
+ expect(result.classes[0].name).toBe("Point");
+ expect(result.classes[0].properties).toEqual(["x", "y"]);
+
+ tree.delete();
+ parser.delete();
+ });
+
+ it("extracts class methods into functions[] as well as the class's methods[]", () => {
+ // Mirrors the Go/Swift extractor convention so the graph builder can
+ // create function nodes for class methods.
+ const { tree, parser, root } = parse(`class Foo {
+ fun bar(): Int = 1
+}
+`);
+ const result = extractor.extractStructure(root);
+
+ expect(result.functions.map((f) => f.name)).toContain("bar");
+ expect(result.classes[0].methods).toContain("bar");
+
+ tree.delete();
+ parser.delete();
+ });
+ });
+
+ describe("extractStructure - interfaces", () => {
+ it("extracts an interface with method requirements as a class-like entry", () => {
+ const { tree, parser, root } = parse(`interface Greeter {
+ fun greet(name: String): String
+ fun farewell(): String
+}
+`);
+ const result = extractor.extractStructure(root);
+
+ expect(result.classes).toHaveLength(1);
+ expect(result.classes[0].name).toBe("Greeter");
+ expect(result.classes[0].methods).toEqual(
+ expect.arrayContaining(["greet", "farewell"]),
+ );
+
+ tree.delete();
+ parser.delete();
+ });
+ });
+
+ describe("extractStructure - object declarations", () => {
+ it("extracts a singleton `object` with methods", () => {
+ const { tree, parser, root } = parse(`object Logger {
+ fun info(msg: String) {}
+ fun warn(msg: String) {}
+}
+`);
+ const result = extractor.extractStructure(root);
+
+ expect(result.classes).toHaveLength(1);
+ expect(result.classes[0].name).toBe("Logger");
+ expect(result.classes[0].methods).toEqual(
+ expect.arrayContaining(["info", "warn"]),
+ );
+
+ tree.delete();
+ parser.delete();
+ });
+ });
+
+ describe("extractStructure - imports", () => {
+ it("extracts a simple dotted import", () => {
+ const { tree, parser, root } = parse(`import kotlin.io.println
+`);
+ const result = extractor.extractStructure(root);
+
+ expect(result.imports).toHaveLength(1);
+ expect(result.imports[0].source).toBe("kotlin.io.println");
+ // The specifier is the final dotted segment
+ expect(result.imports[0].specifiers).toEqual(["println"]);
+
+ tree.delete();
+ parser.delete();
+ });
+
+ it("extracts a wildcard import", () => {
+ const { tree, parser, root } = parse(`import kotlinx.coroutines.*
+`);
+ const result = extractor.extractStructure(root);
+
+ expect(result.imports).toHaveLength(1);
+ expect(result.imports[0].source).toBe("kotlinx.coroutines");
+ // Wildcard is preserved as the specifier so consumers can distinguish it
+ // from a regular dotted import of a specific symbol.
+ expect(result.imports[0].specifiers).toEqual(["*"]);
+
+ tree.delete();
+ parser.delete();
+ });
+
+ it("extracts an aliased import", () => {
+ const { tree, parser, root } = parse(`import com.example.foo.Bar as Baz
+`);
+ const result = extractor.extractStructure(root);
+
+ expect(result.imports).toHaveLength(1);
+ expect(result.imports[0].source).toBe("com.example.foo.Bar");
+ // The alias is the user-visible name in this file
+ expect(result.imports[0].specifiers).toEqual(["Baz"]);
+
+ tree.delete();
+ parser.delete();
+ });
+
+ it("extracts multiple imports in declaration order", () => {
+ const { tree, parser, root } = parse(`package com.example.app
+
+import kotlinx.coroutines.flow.Flow
+import kotlin.io.println
+import kotlinx.coroutines.*
+`);
+ const result = extractor.extractStructure(root);
+
+ expect(result.imports).toHaveLength(3);
+ expect(result.imports[0].source).toBe("kotlinx.coroutines.flow.Flow");
+ expect(result.imports[1].source).toBe("kotlin.io.println");
+ expect(result.imports[2].source).toBe("kotlinx.coroutines");
+
+ tree.delete();
+ parser.delete();
+ });
+ });
+
+ describe("extractStructure - exports / visibility", () => {
+ it("treats no-modifier declarations as exported (Kotlin default is public)", () => {
+ const { tree, parser, root } = parse(`fun greet() {}
+class Greeter {}
+`);
+ const result = extractor.extractStructure(root);
+
+ const exportNames = result.exports.map((e) => e.name);
+ expect(exportNames).toEqual(expect.arrayContaining(["greet", "Greeter"]));
+
+ tree.delete();
+ parser.delete();
+ });
+
+ it("treats public/internal/protected as exported", () => {
+ const { tree, parser, root } = parse(`public fun a() {}
+internal class B {}
+`);
+ const result = extractor.extractStructure(root);
+
+ const exportNames = result.exports.map((e) => e.name);
+ expect(exportNames).toEqual(expect.arrayContaining(["a", "B"]));
+
+ tree.delete();
+ parser.delete();
+ });
+
+ it("does NOT treat private declarations as exported", () => {
+ const { tree, parser, root } = parse(`private fun helper() {}
+private class Internal {}
+`);
+ const result = extractor.extractStructure(root);
+
+ const exportNames = result.exports.map((e) => e.name);
+ expect(exportNames).not.toContain("helper");
+ expect(exportNames).not.toContain("Internal");
+
+ tree.delete();
+ parser.delete();
+ });
+
+ it("exports an object declaration by default", () => {
+ const { tree, parser, root } = parse(`object Logger {}
+`);
+ const result = extractor.extractStructure(root);
+
+ expect(result.exports.map((e) => e.name)).toContain("Logger");
+
+ tree.delete();
+ parser.delete();
+ });
+ });
+
+ describe("extractCallGraph", () => {
+ it("extracts a call from one function to another", () => {
+ const { tree, parser, root } = parse(`fun helper(): Int = 1
+
+fun caller(): Int {
+ return helper()
+}
+`);
+ const entries = extractor.extractCallGraph(root);
+
+ const helperCall = entries.find((e) => e.callee === "helper");
+ expect(helperCall).toBeDefined();
+ expect(helperCall!.caller).toBe("caller");
+
+ tree.delete();
+ parser.delete();
+ });
+
+ it("extracts method calls (x.foo()) and attributes them to the enclosing function", () => {
+ const { tree, parser, root } = parse(`fun run() {
+ val s = "hi".uppercase()
+}
+`);
+ const entries = extractor.extractCallGraph(root);
+
+ const callees = entries.map((e) => e.callee);
+ expect(callees).toContain("uppercase");
+
+ tree.delete();
+ parser.delete();
+ });
+
+ it("returns an empty array when there are no calls", () => {
+ const { tree, parser, root } = parse(`fun a(): Int = 1
+`);
+ const entries = extractor.extractCallGraph(root);
+ expect(entries).toEqual([]);
+
+ tree.delete();
+ parser.delete();
+ });
+ });
+});
diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/index.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/index.ts
index f148c61..4f0b5a3 100644
--- a/understand-anything-plugin/packages/core/src/plugins/extractors/index.ts
+++ b/understand-anything-plugin/packages/core/src/plugins/extractors/index.ts
@@ -9,6 +9,7 @@ export { RubyExtractor } from "./ruby-extractor.js";
export { PhpExtractor } from "./php-extractor.js";
export { CppExtractor } from "./cpp-extractor.js";
export { CSharpExtractor } from "./csharp-extractor.js";
+export { KotlinExtractor } from "./kotlin-extractor.js";
import type { LanguageExtractor } from "./types.js";
import { TypeScriptExtractor } from "./typescript-extractor.js";
@@ -20,6 +21,7 @@ import { RubyExtractor } from "./ruby-extractor.js";
import { PhpExtractor } from "./php-extractor.js";
import { CppExtractor } from "./cpp-extractor.js";
import { CSharpExtractor } from "./csharp-extractor.js";
+import { KotlinExtractor } from "./kotlin-extractor.js";
export const builtinExtractors: LanguageExtractor[] = [
new TypeScriptExtractor(),
@@ -31,4 +33,5 @@ export const builtinExtractors: LanguageExtractor[] = [
new PhpExtractor(),
new CppExtractor(),
new CSharpExtractor(),
+ new KotlinExtractor(),
];
diff --git a/understand-anything-plugin/packages/core/src/plugins/extractors/kotlin-extractor.ts b/understand-anything-plugin/packages/core/src/plugins/extractors/kotlin-extractor.ts
new file mode 100644
index 0000000..2ee2818
--- /dev/null
+++ b/understand-anything-plugin/packages/core/src/plugins/extractors/kotlin-extractor.ts
@@ -0,0 +1,425 @@
+import type { StructuralAnalysis, CallGraphEntry } from "../../types.js";
+import type { LanguageExtractor, TreeSitterNode } from "./types.js";
+import { findChild, findChildren } from "./base-extractor.js";
+
+/**
+ * Extract the visibility keyword text (e.g., "public", "private") from a
+ * declaration's `modifiers` child, or return null when no modifier is present.
+ *
+ * Kotlin's default visibility is `public`, so a `null` result means the
+ * declaration IS exported — callers must treat absence as exported, not the
+ * other way around.
+ */
+function extractVisibility(declNode: TreeSitterNode): string | null {
+ const modifiers = findChild(declNode, "modifiers");
+ if (!modifiers) return null;
+ const visibility = findChild(modifiers, "visibility_modifier");
+ if (!visibility) return null;
+ return visibility.text;
+}
+
+/**
+ * Whether a Kotlin declaration is visible to other files.
+ *
+ * Default visibility in Kotlin is `public`, so a declaration with NO
+ * modifier counts as exported. Only an explicit `private` opts out.
+ * `internal` and `protected` remain exported in the project-graph sense
+ * because they are still resolvable from other files (within the module
+ * or via inheritance respectively).
+ */
+function isExported(declNode: TreeSitterNode): boolean {
+ const visibility = extractVisibility(declNode);
+ return visibility === null || visibility !== "private";
+}
+
+/**
+ * Get the identifier-text name of a Kotlin declaration. Works for
+ * function_declaration / class_declaration / object_declaration / interface
+ * — all carry the name as the first `identifier` child after the keyword.
+ */
+function extractDeclarationName(declNode: TreeSitterNode): string | null {
+ for (let i = 0; i < declNode.childCount; i++) {
+ const child = declNode.child(i);
+ if (child && child.type === "identifier") return child.text;
+ }
+ return null;
+}
+
+/**
+ * Extract parameter names from a `function_value_parameters` node. Each
+ * `parameter` child carries a leading `identifier` for the parameter name;
+ * the optional trailing `: ` annotation is ignored.
+ */
+function extractParams(declNode: TreeSitterNode): string[] {
+ const params: string[] = [];
+ const valueParams = findChild(declNode, "function_value_parameters");
+ if (!valueParams) return params;
+ for (const param of findChildren(valueParams, "parameter")) {
+ // The first `identifier` inside a parameter is its name.
+ const id = findChild(param, "identifier");
+ if (id) params.push(id.text);
+ }
+ return params;
+}
+
+/**
+ * Extract the return type text from a `function_declaration` by looking for
+ * the `:` separator and taking the next named child, which is the type node.
+ * Returns undefined for `Unit`-returning functions (no annotation present).
+ */
+function extractReturnType(declNode: TreeSitterNode): string | undefined {
+ // function_value_parameters comes before the optional `: ` block;
+ // walk children from after the parameters to find `:` followed by a type.
+ let sawParams = false;
+ for (let i = 0; i < declNode.childCount; i++) {
+ const child = declNode.child(i);
+ if (!child) continue;
+ if (child.type === "function_value_parameters") {
+ sawParams = true;
+ continue;
+ }
+ if (sawParams && child.type === ":") {
+ // The next named sibling is the type
+ for (let j = i + 1; j < declNode.childCount; j++) {
+ const next = declNode.child(j);
+ if (next && next.isNamed) return next.text;
+ }
+ }
+ }
+ return undefined;
+}
+
+/**
+ * Walk a `class_body` and collect functions + properties. Function entries
+ * are added to both the class's `methods` array and the top-level
+ * `functions` array (matching the GoExtractor / SwiftExtractor convention).
+ */
+function collectClassBody(
+ body: TreeSitterNode,
+ methods: string[],
+ properties: string[],
+ functions: StructuralAnalysis["functions"],
+ exports: StructuralAnalysis["exports"],
+): void {
+ for (let i = 0; i < body.childCount; i++) {
+ const member = body.child(i);
+ if (!member) continue;
+
+ if (member.type === "function_declaration") {
+ const name = extractDeclarationName(member);
+ if (!name) continue;
+ methods.push(name);
+ functions.push({
+ name,
+ lineRange: [member.startPosition.row + 1, member.endPosition.row + 1],
+ params: extractParams(member),
+ returnType: extractReturnType(member),
+ });
+ if (isExported(member)) {
+ exports.push({ name, lineNumber: member.startPosition.row + 1 });
+ }
+ } else if (member.type === "property_declaration") {
+ const name = extractPropertyName(member);
+ if (name) properties.push(name);
+ if (name && isExported(member)) {
+ exports.push({ name, lineNumber: member.startPosition.row + 1 });
+ }
+ } else if (member.type === "object_declaration") {
+ // Nested companion-object / object members are surfaced as a single
+ // synthetic property pointing at the inner object's name — enough for
+ // the graph builder to keep the relationship without exploding scope.
+ const name = extractDeclarationName(member);
+ if (name) properties.push(name);
+ }
+ }
+}
+
+/**
+ * Extract the property name from a `property_declaration`. The name lives
+ * inside the `variable_declaration` child as its `identifier`.
+ */
+function extractPropertyName(propNode: TreeSitterNode): string | null {
+ const varDecl = findChild(propNode, "variable_declaration");
+ if (!varDecl) return null;
+ const id = findChild(varDecl, "identifier");
+ return id ? id.text : null;
+}
+
+/**
+ * Walk a `primary_constructor`'s `class_parameters` and surface every
+ * `val` / `var` parameter as a class property. Plain `parameter` entries
+ * (no val/var keyword) are constructor-only and are NOT properties — they
+ * vanish after the constructor returns.
+ */
+function collectPrimaryConstructorProperties(
+ declNode: TreeSitterNode,
+ properties: string[],
+): void {
+ const primary = findChild(declNode, "primary_constructor");
+ if (!primary) return;
+ const params = findChild(primary, "class_parameters");
+ if (!params) return;
+ for (const param of findChildren(params, "class_parameter")) {
+ // A class_parameter that starts with `val` or `var` is a property.
+ let isProperty = false;
+ for (let i = 0; i < param.childCount; i++) {
+ const child = param.child(i);
+ if (child && (child.type === "val" || child.type === "var")) {
+ isProperty = true;
+ break;
+ }
+ }
+ if (!isProperty) continue;
+ const id = findChild(param, "identifier");
+ if (id) properties.push(id.text);
+ }
+}
+
+/**
+ * Kotlin extractor for tree-sitter structural analysis and call graph
+ * extraction. Maps Kotlin's class / interface / object / data-class
+ * declarations to the project's shared `StructuralAnalysis.classes` array.
+ */
+export class KotlinExtractor implements LanguageExtractor {
+ readonly languageIds = ["kotlin"];
+
+ extractStructure(rootNode: TreeSitterNode): StructuralAnalysis {
+ const functions: StructuralAnalysis["functions"] = [];
+ const classes: StructuralAnalysis["classes"] = [];
+ const imports: StructuralAnalysis["imports"] = [];
+ const exports: StructuralAnalysis["exports"] = [];
+
+ for (let i = 0; i < rootNode.childCount; i++) {
+ const node = rootNode.child(i);
+ if (!node) continue;
+
+ switch (node.type) {
+ case "package_header":
+ // Package is metadata about this file, not a graph member. Skip.
+ break;
+
+ case "import":
+ this.extractImport(node, imports);
+ break;
+
+ case "function_declaration":
+ this.extractTopLevelFunction(node, functions, exports);
+ break;
+
+ case "class_declaration":
+ this.extractClassDeclaration(node, classes, functions, exports);
+ break;
+
+ case "object_declaration":
+ this.extractObjectDeclaration(node, classes, functions, exports);
+ break;
+ }
+ }
+
+ return { functions, classes, imports, exports };
+ }
+
+ extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[] {
+ const entries: CallGraphEntry[] = [];
+ const functionStack: string[] = [];
+
+ const walk = (node: TreeSitterNode) => {
+ let pushed = false;
+
+ if (node.type === "function_declaration") {
+ const name = extractDeclarationName(node);
+ if (name) {
+ functionStack.push(name);
+ pushed = true;
+ }
+ }
+
+ if (node.type === "call_expression" && functionStack.length > 0) {
+ const callee = this.extractCalleeName(node);
+ if (callee) {
+ entries.push({
+ caller: functionStack[functionStack.length - 1],
+ callee,
+ lineNumber: node.startPosition.row + 1,
+ });
+ }
+ }
+
+ for (let i = 0; i < node.childCount; i++) {
+ const child = node.child(i);
+ if (child) walk(child);
+ }
+
+ if (pushed) functionStack.pop();
+ };
+
+ walk(rootNode);
+ return entries;
+ }
+
+ // ---- Private helpers ----
+
+ private extractTopLevelFunction(
+ declNode: TreeSitterNode,
+ functions: StructuralAnalysis["functions"],
+ exports: StructuralAnalysis["exports"],
+ ): void {
+ const name = extractDeclarationName(declNode);
+ if (!name) return;
+ functions.push({
+ name,
+ lineRange: [declNode.startPosition.row + 1, declNode.endPosition.row + 1],
+ params: extractParams(declNode),
+ returnType: extractReturnType(declNode),
+ });
+ if (isExported(declNode)) {
+ exports.push({ name, lineNumber: declNode.startPosition.row + 1 });
+ }
+ }
+
+ private extractClassDeclaration(
+ declNode: TreeSitterNode,
+ classes: StructuralAnalysis["classes"],
+ functions: StructuralAnalysis["functions"],
+ exports: StructuralAnalysis["exports"],
+ ): void {
+ const name = extractDeclarationName(declNode);
+ if (!name) return;
+
+ const properties: string[] = [];
+ const methods: string[] = [];
+
+ // 1. Primary-constructor `val`/`var` parameters become properties.
+ collectPrimaryConstructorProperties(declNode, properties);
+
+ // 2. Body members (if any). Some Kotlin declarations have no body
+ // (e.g. `class Empty` or `data class Point(...)` without `{}`).
+ const body = findChild(declNode, "class_body");
+ if (body) {
+ collectClassBody(body, methods, properties, functions, exports);
+ }
+
+ classes.push({
+ name,
+ lineRange: [declNode.startPosition.row + 1, declNode.endPosition.row + 1],
+ methods,
+ properties,
+ });
+
+ if (isExported(declNode)) {
+ exports.push({ name, lineNumber: declNode.startPosition.row + 1 });
+ }
+ }
+
+ private extractObjectDeclaration(
+ declNode: TreeSitterNode,
+ classes: StructuralAnalysis["classes"],
+ functions: StructuralAnalysis["functions"],
+ exports: StructuralAnalysis["exports"],
+ ): void {
+ const name = extractDeclarationName(declNode);
+ if (!name) return;
+
+ const properties: string[] = [];
+ const methods: string[] = [];
+
+ const body = findChild(declNode, "class_body");
+ if (body) {
+ collectClassBody(body, methods, properties, functions, exports);
+ }
+
+ classes.push({
+ name,
+ lineRange: [declNode.startPosition.row + 1, declNode.endPosition.row + 1],
+ methods,
+ properties,
+ });
+
+ if (isExported(declNode)) {
+ exports.push({ name, lineNumber: declNode.startPosition.row + 1 });
+ }
+ }
+
+ /**
+ * Extract a Kotlin import.
+ *
+ * The grammar gives us a single `qualified_identifier` child holding the
+ * dotted module path. Three trailing variants must be distinguished:
+ *
+ * - `import foo.bar.Baz` → source="foo.bar.Baz", specifier="Baz"
+ * - `import foo.bar.*` → source="foo.bar", specifier="*"
+ * - `import foo.bar.Baz as Quux` → source="foo.bar.Baz", specifier="Quux"
+ *
+ * The grammar represents the wildcard as a trailing `*` token AFTER the
+ * qualified_identifier (which holds the dotted prefix). The alias
+ * appears as `as` + a top-level `identifier` sibling.
+ */
+ private extractImport(
+ declNode: TreeSitterNode,
+ imports: StructuralAnalysis["imports"],
+ ): void {
+ const qualified = findChild(declNode, "qualified_identifier");
+ if (!qualified) return;
+
+ const parts: string[] = [];
+ for (const id of findChildren(qualified, "identifier")) {
+ parts.push(id.text);
+ }
+ if (parts.length === 0) return;
+
+ const source = parts.join(".");
+
+ // Look for a sibling `*` (wildcard) or an `as` keyword + identifier
+ // after the qualified_identifier.
+ let specifier = parts[parts.length - 1];
+ let sawWildcardStar = false;
+ let sawAs = false;
+ for (let i = 0; i < declNode.childCount; i++) {
+ const child = declNode.child(i);
+ if (!child) continue;
+ if (child.type === "*") sawWildcardStar = true;
+ if (child.type === "as") sawAs = true;
+ else if (sawAs && child.type === "identifier") {
+ specifier = child.text;
+ sawAs = false;
+ }
+ }
+ if (sawWildcardStar) specifier = "*";
+
+ imports.push({
+ source,
+ specifiers: [specifier],
+ lineNumber: declNode.startPosition.row + 1,
+ });
+ }
+
+ /**
+ * Extract the callee name from a Kotlin `call_expression`. Two shapes:
+ *
+ * foo(...) → first child is `identifier "foo"`
+ * target.method(...) → first child is `navigation_expression` whose
+ * last `navigation_suffix > identifier` is the
+ * method name
+ */
+ private extractCalleeName(callNode: TreeSitterNode): string | null {
+ const first = callNode.child(0);
+ if (!first) return null;
+
+ if (first.type === "identifier") return first.text;
+
+ if (first.type === "navigation_expression") {
+ // The Kotlin grammar flattens navigation: `x.foo` is
+ // navigation_expression { x, ".", identifier "foo" }
+ // The method name is the LAST `identifier` child of the navigation.
+ let lastIdentifier: string | null = null;
+ for (let i = 0; i < first.childCount; i++) {
+ const child = first.child(i);
+ if (child && child.type === "identifier") {
+ lastIdentifier = child.text;
+ }
+ }
+ return lastIdentifier;
+ }
+ return null;
+ }
+}
From aef940fcdec3499160932831376a2a7f09f6b397 Mon Sep 17 00:00:00 2001
From: Tirth Kanani
Date: Sun, 31 May 2026 21:29:13 +0100
Subject: [PATCH 04/12] chore(repo): add issue/PR templates, SECURITY.md, CoC,
package metadata; widen CI triggers
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Closes a cluster of community-profile gaps (#248, #249, #251, #252) in one
PR rather than four micro-PRs that all touch the same surface area.
### Templates (#251, #252)
- .github/ISSUE_TEMPLATE/bug_report.yml — required fields for repro
(plugin version, platform, OS, project language, file count); the four
pieces of context that are missing from ~every current bug report.
- .github/ISSUE_TEMPLATE/feature_request.yml — leads with the *problem*
rather than the proposed solution, which keeps maintainer review focused
on whether to solve, not just how.
- .github/ISSUE_TEMPLATE/question.yml — separate from bug to keep the
bug queue triagable.
- .github/ISSUE_TEMPLATE/config.yml — disables blank issues and routes
general discussion to README + Discussions.
- .github/PULL_REQUEST_TEMPLATE.md — includes the version-bump checklist
that CLAUDE.md says must stay in sync across 5 manifests; otherwise
every contributor learns this rule by getting their PR bounced.
### Community files
- CODE_OF_CONDUCT.md — short, project-specific document that names the
expectations and reporting path. Not a verbatim Contributor Covenant
to keep it readable.
- SECURITY.md — describes the project's local-only threat model
explicitly so reporters know what's in / out of scope before they
spend time on a writeup. Points at GitHub private vulnerability
reporting as the primary channel.
### CI (#249)
- ci.yml now also runs on pushes to main, not only PRs. Without this,
a direct push to main (which happens when maintainers merge a PR
branch locally) doesn't trigger CI, so a regression can land green-
looking and stay broken for days.
- Added a concurrency group that cancels stale runs for the same ref.
Saves runner minutes and keeps the per-ref status meaningful.
- Used `github.ref` (a controlled value), not user-controlled input,
so no script-injection surface.
### package.json (#248)
- Added description, license, repository, bugs, homepage, keywords —
the standard set for npm package discoverability and so GitHub's
community-profile check shows the project at 100%.
---
.github/ISSUE_TEMPLATE/bug_report.yml | 85 ++++++++++++++++++++++
.github/ISSUE_TEMPLATE/config.yml | 8 ++
.github/ISSUE_TEMPLATE/feature_request.yml | 34 +++++++++
.github/ISSUE_TEMPLATE/question.yml | 24 ++++++
.github/PULL_REQUEST_TEMPLATE.md | 26 +++++++
.github/workflows/ci.yml | 14 ++++
CODE_OF_CONDUCT.md | 39 ++++++++++
SECURITY.md | 51 +++++++++++++
package.json | 22 ++++++
9 files changed, 303 insertions(+)
create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml
create mode 100644 .github/ISSUE_TEMPLATE/config.yml
create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml
create mode 100644 .github/ISSUE_TEMPLATE/question.yml
create mode 100644 .github/PULL_REQUEST_TEMPLATE.md
create mode 100644 CODE_OF_CONDUCT.md
create mode 100644 SECURITY.md
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..0a53fd4
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,85 @@
+name: Bug report
+description: Report something that isn't working
+title: "bug: "
+labels: ["bug"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to file a bug. The more concrete you can be,
+ the faster it gets fixed. If you can include the analyzed project's
+ primary language and an approximate file count, that's gold.
+
+ - type: textarea
+ id: what-happened
+ attributes:
+ label: What happened?
+ description: What did you do, what did you expect to happen, and what actually happened?
+ placeholder: |
+ 1. Ran `/understand --full` on a ~3,000 file Rust project
+ 2. Expected: dashboard opens with the graph
+ 3. Got: dashboard shows "Failed to load graph: schema validation error"
+ validations:
+ required: true
+
+ - type: textarea
+ id: reproduce
+ attributes:
+ label: Minimal reproduction
+ description: Smallest set of steps (or a link to a public repo) that reproduces the issue.
+ validations:
+ required: false
+
+ - type: input
+ id: version
+ attributes:
+ label: Plugin version
+ description: Run `/understand --version` or check `~/.claude/plugins/cache/understand-anything/understand-anything/`.
+ placeholder: "e.g. 2.7.4"
+ validations:
+ required: true
+
+ - type: dropdown
+ id: platform
+ attributes:
+ label: Platform / client
+ multiple: true
+ options:
+ - Claude Code (CLI)
+ - Claude Code (VS Code extension)
+ - Claude Code (JetBrains)
+ - Cursor
+ - GitHub Copilot CLI
+ - opencode
+ - Other (please describe in "What happened?")
+ validations:
+ required: true
+
+ - type: input
+ id: os
+ attributes:
+ label: OS + Node version
+ placeholder: "e.g. macOS 14.5 (arm64), Node v22.6.0"
+ validations:
+ required: true
+
+ - type: input
+ id: project-language
+ attributes:
+ label: Primary language of the analyzed project
+ placeholder: "e.g. TypeScript, Python, Swift…"
+
+ - type: input
+ id: file-count
+ attributes:
+ label: Approximate file count of the analyzed project
+ placeholder: "e.g. ~3,000"
+
+ - type: textarea
+ id: logs
+ attributes:
+ label: Relevant logs
+ description: |
+ Any console output, the contents of `.understand-anything/intermediate/`
+ if it still exists, or screenshots of the dashboard error.
+ render: shell
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..3da6fc9
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: false
+contact_links:
+ - name: README & docs
+ url: https://github.com/Lum1104/Understand-Anything#readme
+ about: Most usage questions are answered in the project README.
+ - name: Discussions
+ url: https://github.com/Lum1104/Understand-Anything/discussions
+ about: For open-ended discussion, design proposals, or sharing how you use the tool.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..bbffd7d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,34 @@
+name: Feature request
+description: Suggest an idea or improvement
+title: "feat: "
+labels: ["enhancement"]
+body:
+ - type: textarea
+ id: problem
+ attributes:
+ label: What problem are you trying to solve?
+ description: Describe the user pain or workflow gap. Concrete examples help more than abstract framing.
+ placeholder: |
+ When onboarding new engineers to our 8k-file Go monorepo, they spend
+ days finding the auth boundary. /understand finds the files but the
+ dashboard doesn't visually separate "trusted" from "untrusted" zones.
+ validations:
+ required: true
+
+ - type: textarea
+ id: proposal
+ attributes:
+ label: Proposed solution (optional)
+ description: If you have an idea for what the feature should look like, share it. Skip if you'd rather just describe the problem.
+
+ - type: textarea
+ id: alternatives
+ attributes:
+ label: Alternatives you've considered
+ description: Other tools, workarounds, or approaches you've tried.
+
+ - type: input
+ id: scope
+ attributes:
+ label: Which part of the project?
+ placeholder: "skill / dashboard / core / agents / all"
diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml
new file mode 100644
index 0000000..2c0962b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/question.yml
@@ -0,0 +1,24 @@
+name: Question / usage help
+description: Ask a question about how to use the project
+title: "question: "
+labels: ["question"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ For general usage questions. If you found a bug, please use the bug
+ report template instead — it asks for the information needed to
+ reproduce.
+
+ - type: textarea
+ id: question
+ attributes:
+ label: Your question
+ validations:
+ required: true
+
+ - type: textarea
+ id: tried
+ attributes:
+ label: What have you already tried?
+ description: Helps avoid suggesting things you've already ruled out.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..fc16fb8
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,26 @@
+## Summary
+
+
+
+## Linked issue(s)
+
+
+
+## How I tested this
+
+
+
+- [ ] `pnpm lint`
+- [ ] `pnpm --filter @understand-anything/core test`
+- [ ] `pnpm test`
+- [ ] Manual smoke test (describe above)
+
+## Versioning
+
+
+
+- [ ] Version bumped in all five manifests, OR
+- [ ] N/A — internal/docs-only change
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 69771eb..9825406 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,7 +1,21 @@
name: CI
on:
+ # Run on every PR so a contributor's first push gets feedback.
pull_request:
+ # Also run on direct pushes to main so the "main is green" signal is real.
+ # Without this, main can silently break for days when someone bypasses
+ # review. (#249)
+ push:
+ branches: [main]
+
+# Cancel any in-flight CI for the same ref when a new commit is pushed —
+# saves runner minutes and keeps the latest commit's status the only one
+# anyone reads. `github.ref` is a controlled value (refs/heads/* or
+# refs/pull/*/merge), not user-controlled input, so it's safe to interpolate.
+concurrency:
+ group: ci-${{ github.ref }}
+ cancel-in-progress: true
jobs:
ci:
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..d05f492
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,39 @@
+# Code of Conduct
+
+We want this project to be a welcoming place for everyone who wants to
+contribute, learn, or use it — regardless of experience level, background, or
+identity.
+
+## In short
+
+- **Be respectful.** Treat others the way you'd want to be treated.
+- **Assume good intent.** Most disagreements are misunderstandings.
+- **Be constructive.** Critique ideas, not people. Suggest improvements.
+- **Keep it on-topic.** This project is about understanding codebases.
+
+## What's not OK
+
+- Personal attacks, insults, or sustained disruption of discussions.
+- Posting someone's private information without their explicit permission.
+- Repeatedly ignoring requests from maintainers to change behavior.
+
+## Reporting
+
+If you see behavior that violates this code, please open a private email to
+the maintainer listed in the repository profile, or use GitHub's
+[private vulnerability / abuse reporting](https://docs.github.com/en/communities/maintaining-your-safety-on-github/reporting-abuse-or-spam).
+
+Maintainers will review reports and take whatever action they think is
+appropriate — typically a private warning, sometimes a temporary or permanent
+ban from the project. Reports will be kept confidential.
+
+## Scope
+
+This code applies in all project spaces: issues, pull requests, discussions,
+commits, and any other project-affiliated channel.
+
+---
+
+This document is intentionally short. It's based on the spirit of the
+[Contributor Covenant](https://www.contributor-covenant.org/) without
+reproducing it verbatim.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..a9fe378
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,51 @@
+# Reporting security issues
+
+Thanks for taking the time to disclose responsibly.
+
+## How to report
+
+Please use GitHub's [private vulnerability reporting](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability)
+on this repository. That keeps the report visible to the maintainer without
+exposing the details publicly.
+
+If private reporting is unavailable for any reason, open a regular issue
+titled `security: brief description` **without** any exploit details, and
+the maintainer will reply with a private channel.
+
+## What to include
+
+- A description of the issue and its potential impact.
+- Steps to reproduce — minimal is fine, a full PoC is not required.
+- Affected versions if you've narrowed them down.
+- Whether you'd like to be credited in the eventual fix.
+
+## What to expect
+
+- Initial acknowledgement within a few days.
+- A fix or mitigation plan within ~30 days for confirmed issues; longer for
+ cases that require coordinated disclosure with upstream dependencies.
+- Public credit once a fix has shipped, if you'd like.
+
+## Scope
+
+This project is a **local-only** static-analysis tool. It runs on a
+developer's machine, reads the analyzed project, and writes the resulting
+graph to `.understand-anything/`. It does not phone home and the dashboard's
+file-content endpoint is gated behind an access token and a graph-derived
+path allowlist.
+
+Issues we care about:
+
+- Code execution triggered by analyzing a hostile project (e.g. a path in a
+ hostile file leaking outside the analyzed directory, or untrusted JSON in
+ the graph being executed by the dashboard).
+- The dashboard's file-content endpoint serving files outside the allowlist.
+- The `/understand` skill running shell commands derived from untrusted
+ paths or contents.
+
+Issues that are **out of scope**:
+
+- Bugs that require a malicious local user with write access to the
+ analyzed project (they could just edit the source directly).
+- Anything that requires the user to copy a malicious URL and paste it back
+ into the dashboard.
diff --git a/package.json b/package.json
index efae49a..20b19b4 100644
--- a/package.json
+++ b/package.json
@@ -2,6 +2,28 @@
"name": "understand-anything",
"private": true,
"type": "module",
+ "description": "An open-source tool combining LLM intelligence + static analysis to produce interactive dashboards for understanding codebases.",
+ "license": "MIT",
+ "homepage": "https://github.com/Lum1104/Understand-Anything#readme",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/Lum1104/Understand-Anything.git"
+ },
+ "bugs": {
+ "url": "https://github.com/Lum1104/Understand-Anything/issues"
+ },
+ "keywords": [
+ "claude-code",
+ "codebase-analysis",
+ "knowledge-graph",
+ "tree-sitter",
+ "llm",
+ "static-analysis",
+ "developer-tools",
+ "code-understanding",
+ "code-onboarding",
+ "claude-plugin"
+ ],
"main": ".opencode/plugins/understand-anything.js",
"packageManager": "pnpm@10.6.2+sha512.47870716bea1572b53df34ad8647b42962bc790ce2bf4562ba0f643237d7302a3d6a8ecef9e4bdfc01d23af1969aa90485d4cebb0b9638fa5ef1daef656f6c1b",
"scripts": {
From a6c653e36b528acb8d2d401fc8fbca6d64fd72cc Mon Sep 17 00:00:00 2001
From: Tirth Kanani
Date: Sun, 31 May 2026 22:31:23 +0100
Subject: [PATCH 05/12] =?UTF-8?q?fix(extract-import-map):=20apply=20NodeNe?=
=?UTF-8?q?xt=20.js=E2=86=92.ts=20rewrite=20(#294)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fixes the silent near-edgeless-graph regression on any modern ESM
TypeScript project. Reported in #294 with full repro + root-cause
analysis.
### Why this matters
Under `moduleResolution: NodeNext` (or `Node16` / `Bundler` with
explicit extensions — the default for new TS-ESM projects since 2023),
TypeScript does NOT rewrite import specifiers during compilation:
// src/index.ts — real, idiomatic NodeNext source
import { x } from './config.js'; // on disk: config.ts
Before this fix, `probeWithExtensions` only tried APPENDING extensions
to the import specifier:
'./config.js' → not in fileSet
'./config.js.ts', './config.js.tsx', './config.js.js', ... → all miss
→ returns null → edge dropped at merge as dangling
Net result on the reporter's repro: a knowledge graph with hundreds of
file nodes and almost no `imports` edges between them — silently
removing exactly the dependency structure the graph is meant to show.
### Fix
New `NODENEXT_REWRITES` table maps each compiled-output extension to
the TypeScript source extensions that could have produced it:
.js → [.ts, .tsx, .js, .jsx]
.jsx → [.tsx, .jsx]
.mjs → [.mts, .mjs, .ts]
.cjs → [.cts, .cjs, .ts]
`probeWithExtensions` now applies the rewrite when the import already
ends with one of these extensions and no such file exists on disk. The
rewrite runs BEFORE the legacy append-extensions loop — otherwise
`./foo.js` would generate the nonsense candidate `foo.js.ts` and the
append loop would never reach the actual `foo.ts`.
### Disambiguation
If both `config.ts` and `config.js` exist on disk (rare, but possible
during a partial migration), `import './config.js'` still resolves to
the .js — that's an exact-disk match and what NodeNext compilation
actually does. The rewrite only kicks in when the .js doesn't exist.
### Tests
6 new tests in `test_extract_import_map.test.mjs`:
- The main #294 case (`.js → .ts`)
- `.jsx → .tsx` and `.mjs → .mts` rewrites
- Disambiguation when both `.ts` and `.js` exist on disk
- Pure-JS projects still work (real `.js → .js` imports)
- Historical no-extension probes unaffected
- Missing files still return null (rewrite can't invent targets)
Total: 202 tests passing (was 196).
Closes #294
---
.../test_extract_import_map.test.mjs | 144 ++++++++++++++++++
.../skills/understand/extract-import-map.mjs | 42 ++++-
2 files changed, 185 insertions(+), 1 deletion(-)
diff --git a/tests/skill/understand/test_extract_import_map.test.mjs b/tests/skill/understand/test_extract_import_map.test.mjs
index ee64136..e25a644 100644
--- a/tests/skill/understand/test_extract_import_map.test.mjs
+++ b/tests/skill/understand/test_extract_import_map.test.mjs
@@ -237,6 +237,150 @@ describe('extract-import-map.mjs — TypeScript / JavaScript resolver', () => {
'packages/foo/src/y.ts',
);
});
+
+ // ── #294: NodeNext / ESM TypeScript `.js → .ts` rewrite ────────────────
+ //
+ // Under `moduleResolution: NodeNext`, TypeScript does NOT rewrite import
+ // specifiers during compilation — what you write in the .ts source is
+ // emitted verbatim. Because Node's ESM loader requires explicit file
+ // extensions at runtime, the TS source must already spell the import with
+ // the `.js` extension that will only be correct AFTER compilation:
+ //
+ // import { x } from './config.js'; // on disk: config.ts
+ //
+ // Before the fix, every such import resolved to null, leaving ESM-TS
+ // projects with a near-edgeless knowledge graph.
+
+ it('resolves NodeNext .js → .ts relative imports (the main #294 case)', () => {
+ projectRoot = setupTree({
+ 'src/index.ts': `import { resolveBackend } from './llm-backend-selector.js';\nimport { loadConfig } from './config.js';\n`,
+ 'src/llm-backend-selector.ts': `export function resolveBackend() {}\n`,
+ 'src/config.ts': `export function loadConfig() {}\n`,
+ });
+
+ const result = runScript(projectRoot, {
+ projectRoot,
+ files: [
+ { path: 'src/index.ts', language: 'typescript', fileCategory: 'code' },
+ { path: 'src/llm-backend-selector.ts', language: 'typescript', fileCategory: 'code' },
+ { path: 'src/config.ts', language: 'typescript', fileCategory: 'code' },
+ ],
+ });
+
+ expect(result.status).toBe(0);
+ expect(result.output.importMap['src/index.ts']).toEqual([
+ 'src/config.ts',
+ 'src/llm-backend-selector.ts',
+ ]);
+ });
+
+ it('resolves NodeNext .jsx → .tsx and .mjs → .mts rewrites', () => {
+ projectRoot = setupTree({
+ 'src/index.ts': `import Comp from './Comp.jsx';\nimport { fn } from './worker.mjs';\n`,
+ 'src/Comp.tsx': `export default function Comp() {}\n`,
+ 'src/worker.mts': `export function fn() {}\n`,
+ });
+
+ const result = runScript(projectRoot, {
+ projectRoot,
+ files: [
+ { path: 'src/index.ts', language: 'typescript', fileCategory: 'code' },
+ { path: 'src/Comp.tsx', language: 'typescript', fileCategory: 'code' },
+ { path: 'src/worker.mts', language: 'typescript', fileCategory: 'code' },
+ ],
+ });
+
+ expect(result.status).toBe(0);
+ expect(result.output.importMap['src/index.ts']).toEqual([
+ 'src/Comp.tsx',
+ 'src/worker.mts',
+ ]);
+ });
+
+ it('resolves to the .js when both .ts and .js exist on disk', () => {
+ // Rare but possible during a partial migration: both `config.ts` and
+ // `config.js` exist. An `import './config.js'` is an exact-disk match
+ // and should resolve to that exact file — the NodeNext rewrite only
+ // kicks in when the .js *doesn't* exist on disk. We assert this to
+ // pin the disambiguation and avoid future regressions where the rewrite
+ // accidentally prefers `.ts` over an existing `.js`.
+ projectRoot = setupTree({
+ 'src/index.ts': `import { x } from './config.js';\n`,
+ 'src/config.ts': `export const x = 1;\n`,
+ 'src/config.js': `export const x = 1;\n`,
+ });
+
+ const result = runScript(projectRoot, {
+ projectRoot,
+ files: [
+ { path: 'src/index.ts', language: 'typescript', fileCategory: 'code' },
+ { path: 'src/config.ts', language: 'typescript', fileCategory: 'code' },
+ { path: 'src/config.js', language: 'javascript', fileCategory: 'code' },
+ ],
+ });
+
+ expect(result.status).toBe(0);
+ expect(result.output.importMap['src/index.ts']).toEqual(['src/config.js']);
+ });
+
+ it('still resolves traditional .js → .js imports unchanged', () => {
+ // The rewrite must not break the case where `.js` IS the real file on
+ // disk (pure JavaScript projects, untyped libraries).
+ projectRoot = setupTree({
+ 'src/index.js': `import { x } from './util.js';\n`,
+ 'src/util.js': `export const x = 1;\n`,
+ });
+
+ const result = runScript(projectRoot, {
+ projectRoot,
+ files: [
+ { path: 'src/index.js', language: 'javascript', fileCategory: 'code' },
+ { path: 'src/util.js', language: 'javascript', fileCategory: 'code' },
+ ],
+ });
+
+ expect(result.status).toBe(0);
+ expect(result.output.importMap['src/index.js']).toEqual(['src/util.js']);
+ });
+
+ it('leaves the historical "no extension" probe behaviour intact', () => {
+ // An import like `./utils` (no extension) must still go through the
+ // append-extensions loop and resolve to `./utils.ts` — the new rewrite
+ // path is only triggered when the import already ends with a compiled
+ // extension.
+ projectRoot = setupTree({
+ 'src/index.ts': `import { foo } from './utils';\n`,
+ 'src/utils.ts': `export function foo() {}\n`,
+ });
+
+ const result = runScript(projectRoot, {
+ projectRoot,
+ files: [
+ { path: 'src/index.ts', language: 'typescript', fileCategory: 'code' },
+ { path: 'src/utils.ts', language: 'typescript', fileCategory: 'code' },
+ ],
+ });
+
+ expect(result.status).toBe(0);
+ expect(result.output.importMap['src/index.ts']).toEqual(['src/utils.ts']);
+ });
+
+ it('returns null (no resolution) for a .js import whose .ts source is missing', () => {
+ // The rewrite must NOT silently invent a target when neither the .js nor
+ // the .ts file exists. The old behaviour would also return null for this
+ // case — we're verifying the rewrite path doesn't regress it.
+ projectRoot = setupTree({
+ 'src/index.ts': `import './completely-missing.js';\n`,
+ });
+
+ const result = runScript(projectRoot, {
+ projectRoot,
+ files: [{ path: 'src/index.ts', language: 'typescript', fileCategory: 'code' }],
+ });
+
+ expect(result.status).toBe(0);
+ expect(result.output.importMap['src/index.ts']).toEqual([]);
+ });
});
describe('extract-import-map.mjs — Python resolver', () => {
diff --git a/understand-anything-plugin/skills/understand/extract-import-map.mjs b/understand-anything-plugin/skills/understand/extract-import-map.mjs
index 6c547d3..42dd7d4 100644
--- a/understand-anything-plugin/skills/understand/extract-import-map.mjs
+++ b/understand-anything-plugin/skills/understand/extract-import-map.mjs
@@ -369,15 +369,55 @@ const TS_EXT_PROBES = [
'/index.ts', '/index.tsx', '/index.js', '/index.jsx',
];
+/**
+ * NodeNext / Node16 / Bundler-with-explicit-extensions ESM TypeScript convention:
+ * TypeScript does NOT rewrite import specifiers during compilation, so source
+ * files import their COMPILED specifier (`./config.js`) even when only
+ * `./config.ts` exists on disk. We map each compiled-output extension to the
+ * TS source extensions that could have produced it, in priority order.
+ *
+ * Without this rewrite, ESM-TS projects (which is now the default for any new
+ * TS project) end up with a near-edgeless knowledge graph because every
+ * project-internal import fails to resolve. (#294)
+ */
+const NODENEXT_REWRITES = {
+ '.js': ['.ts', '.tsx', '.js', '.jsx'],
+ '.jsx': ['.tsx', '.jsx'],
+ '.mjs': ['.mts', '.mjs', '.ts'],
+ '.cjs': ['.cts', '.cjs', '.ts'],
+};
+
/**
* Try ext probes against the file set for the given base path. Returns the
* first matching project-relative path, or null. If the base path already has
* a code extension AND exists in the file set, returns it directly.
+ *
+ * For NodeNext-style imports (`./foo.js` where only `./foo.ts` exists), apply
+ * the source-extension rewrite — see NODENEXT_REWRITES above.
*/
function probeWithExtensions(basePath, fileSet) {
if (!basePath) return null;
- // Exact match (import already had an extension)
+ // Exact match (import already had an extension that resolves on disk)
if (fileSet.has(basePath)) return basePath;
+
+ // NodeNext rewrite: if the basePath ends with a compiled-output extension
+ // but no such file exists, try the corresponding source extensions. We do
+ // this BEFORE the legacy "append extensions" loop because for an import
+ // like `./foo.js`, appending `.ts` would produce `foo.js.ts` (always wrong)
+ // while the correct candidate is `foo.ts`.
+ for (const [outExt, srcExts] of Object.entries(NODENEXT_REWRITES)) {
+ if (!basePath.endsWith(outExt)) continue;
+ const stem = basePath.slice(0, -outExt.length);
+ for (const srcExt of srcExts) {
+ const candidate = stem + srcExt;
+ if (fileSet.has(candidate)) return candidate;
+ }
+ // The basePath had an explicit compiled extension — don't fall through
+ // to the "append extensions" loop, which would produce nonsense like
+ // `foo.js.ts`. If NodeNext rewrite didn't find anything, return null.
+ return null;
+ }
+
for (const ext of TS_EXT_PROBES) {
const candidate = basePath + ext;
if (fileSet.has(candidate)) return candidate;
From 92e76190aaa8a8ddf3ba8c9d0d2c3e7fea6998fa Mon Sep 17 00:00:00 2001
From: Bozheng Long
Date: Wed, 3 Jun 2026 21:19:52 +0800
Subject: [PATCH 06/12] feat(understand): auto-detect conversation language on
first run
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
When /understand runs with no --language flag and no stored outputLanguage,
step 3.6 now infers the conversation language and — only when it is non-English
— confirms once before generating, then persists the choice to config.json.
English conversations keep the exact same silent `en` path; --language flag and
stored config still take priority. README documents the behavior; version
bumped 2.7.5 -> 2.7.6 across all five manifests (user-visible behavior change).
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.claude-plugin/plugin.json | 2 +-
.copilot-plugin/plugin.json | 2 +-
.cursor-plugin/plugin.json | 2 +-
README.md | 2 ++
understand-anything-plugin/.claude-plugin/plugin.json | 2 +-
understand-anything-plugin/package.json | 2 +-
understand-anything-plugin/skills/understand/SKILL.md | 6 ++++--
7 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
index 5c75bd7..839168b 100644
--- a/.claude-plugin/plugin.json
+++ b/.claude-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "understand-anything",
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
- "version": "2.7.5",
+ "version": "2.7.6",
"author": {
"name": "Lum1104"
},
diff --git a/.copilot-plugin/plugin.json b/.copilot-plugin/plugin.json
index b18679d..ea367dd 100644
--- a/.copilot-plugin/plugin.json
+++ b/.copilot-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "understand-anything",
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
- "version": "2.7.5",
+ "version": "2.7.6",
"author": {
"name": "Lum1104"
},
diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json
index 075114d..a60b8bc 100644
--- a/.cursor-plugin/plugin.json
+++ b/.cursor-plugin/plugin.json
@@ -2,7 +2,7 @@
"name": "understand-anything",
"displayName": "Understand Anything",
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
- "version": "2.7.5",
+ "version": "2.7.6",
"author": {
"name": "Lum1104"
},
diff --git a/README.md b/README.md
index 3bdb4db..806d4f4 100644
--- a/README.md
+++ b/README.md
@@ -127,6 +127,8 @@ A multi-agent pipeline scans your project, extracts every file, function, class,
# Supported languages: en (default), zh, zh-TW, ja, ko, ru
```
+On the **first run** in a project — when you don't pass `--language` and no language is stored yet — `/understand` detects the language you're conversing in. If it isn't English, it asks you to confirm (or override) before generating; English conversations are unaffected. Your choice is saved to `.understand-anything/config.json` and reused on every later run.
+
The `--language` parameter affects:
- Node summaries and descriptions in the knowledge graph
- Dashboard UI labels, buttons, and tooltips
diff --git a/understand-anything-plugin/.claude-plugin/plugin.json b/understand-anything-plugin/.claude-plugin/plugin.json
index 5c75bd7..839168b 100644
--- a/understand-anything-plugin/.claude-plugin/plugin.json
+++ b/understand-anything-plugin/.claude-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "understand-anything",
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
- "version": "2.7.5",
+ "version": "2.7.6",
"author": {
"name": "Lum1104"
},
diff --git a/understand-anything-plugin/package.json b/understand-anything-plugin/package.json
index 303ff98..85f70f0 100644
--- a/understand-anything-plugin/package.json
+++ b/understand-anything-plugin/package.json
@@ -1,6 +1,6 @@
{
"name": "@understand-anything/skill",
- "version": "2.7.5",
+ "version": "2.7.6",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
diff --git a/understand-anything-plugin/skills/understand/SKILL.md b/understand-anything-plugin/skills/understand/SKILL.md
index 610a9dd..35c0b11 100644
--- a/understand-anything-plugin/skills/understand/SKILL.md
+++ b/understand-anything-plugin/skills/understand/SKILL.md
@@ -140,8 +140,10 @@ Determine whether to run a full analysis or incremental update.
- `chinese` → `zh`, `japanese` → `ja`, `korean` → `ko`, `english` → `en`, `spanish` → `es`, `french` → `fr`, `german` → `de`, `portuguese` → `pt`, `russian` → `ru`, `arabic` → `ar`, etc.
- Locale variants: `zh-TW`, `zh-HK`, `zh-CN`, `pt-BR`, etc. are preserved as-is.
- If `--language` is NOT specified:
- - Check `$PROJECT_ROOT/.understand-anything/config.json` for an existing `outputLanguage` field. If present, use that.
- - If no stored preference, default to `en` (English).
+ - **Stored preference wins.** If `$PROJECT_ROOT/.understand-anything/config.json` has an `outputLanguage` field, set `$OUTPUT_LANGUAGE` to it and skip the rest.
+ - **Otherwise detect (first run only).** Infer the predominant language of the user's conversation as an ISO 639-1 code (`$DETECTED_LANG`). If it is `en` or cannot be confidently determined, set `$OUTPUT_LANGUAGE=en` and proceed silently — no prompt (English users see no change).
+ - **If `$DETECTED_LANG` ≠ `en`, confirm once before analyzing:** tell the user you detected `` and ask whether to generate all content in it; they press Enter/"yes" to accept, or type another language code/name to override (normalize via the friendly-name map above). If running non-interactively (no reply possible), skip the wait, use `$DETECTED_LANG`, and print a one-line notice instead of blocking.
+ - **Persist** the resolved `$OUTPUT_LANGUAGE` (including `en`) into `config.json` so it never re-prompts for this project.
- If `--language` IS specified:
- Update `$PROJECT_ROOT/.understand-anything/config.json` with the new language: merge `{"outputLanguage": ""}` into existing config.
- Store as `$OUTPUT_LANGUAGE` for use throughout all phases.
From 55d0ab23362eafa789a6b4c56c34587444d64a98 Mon Sep 17 00:00:00 2001
From: Bozheng Long
Date: Wed, 3 Jun 2026 21:19:52 +0800
Subject: [PATCH 07/12] docs: design + implementation plan for language
auto-detection
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.../2026-06-03-language-auto-detection.md | 210 ++++++++++++++++++
...26-06-03-language-auto-detection-design.md | 147 ++++++++++++
2 files changed, 357 insertions(+)
create mode 100644 docs/superpowers/plans/2026-06-03-language-auto-detection.md
create mode 100644 docs/superpowers/specs/2026-06-03-language-auto-detection-design.md
diff --git a/docs/superpowers/plans/2026-06-03-language-auto-detection.md b/docs/superpowers/plans/2026-06-03-language-auto-detection.md
new file mode 100644
index 0000000..8e3e217
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-03-language-auto-detection.md
@@ -0,0 +1,210 @@
+# Conversation-Language Auto-Detection Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** On the first `/understand` run in a project, detect the user's conversation language and confirm it before generating — without changing anything for English users.
+
+**Architecture:** A pure prompt-logic change. The entire language decision lives in one place — `SKILL.md` step 3.6. We expand the `if --language NOT specified` branch into a resolution chain that adds conversation-language detection + a non-English-only, first-run-only confirmation gate before the existing `en` default. The resolved value is persisted to `config.json` so the gate fires at most once per project. A README sentence documents it.
+
+**Tech Stack:** Markdown skill prompt (`SKILL.md`), Markdown docs (`README.md`). No code, no schema, no TypeScript. There is no automated-test hook for skill-prompt behavior, so verification is the manual scenario walkthrough in Task 3 (this matches how the existing `--language` flag is verified).
+
+---
+
+## File Structure
+
+| File | Responsibility | Change |
+|---|---|---|
+| `understand-anything-plugin/skills/understand/SKILL.md` | The `/understand` skill prompt; step 3.6 resolves `$OUTPUT_LANGUAGE`. | Rewrite the `If --language is NOT specified` sub-block (currently lines 142–144). |
+| `README.md` | User-facing docs; *Localized output* section (~line 121). | Add one paragraph describing first-run auto-detection. |
+
+No other files. The `outputLanguage` field already exists in `packages/core/src/types.ts:119`; the `locales/.md` injection at `SKILL.md` step 4 (line ~424) and the `$LANGUAGE_DIRECTIVE` template (lines ~148–151) are unchanged and require no edits.
+
+---
+
+## Task 1: Expand the language-resolution branch in SKILL.md
+
+**Files:**
+- Modify: `understand-anything-plugin/skills/understand/SKILL.md` (step 3.6, the `If --language is NOT specified` sub-block)
+
+- [ ] **Step 1: Confirm the current text is unchanged**
+
+Run: `sed -n '142,144p' understand-anything-plugin/skills/understand/SKILL.md`
+
+Expected output (exactly these three lines):
+```
+ - If `--language` is NOT specified:
+ - Check `$PROJECT_ROOT/.understand-anything/config.json` for an existing `outputLanguage` field. If present, use that.
+ - If no stored preference, default to `en` (English).
+```
+
+If the text differs, STOP and re-read step 3.6 to find the equivalent block before editing — line numbers may have drifted.
+
+- [ ] **Step 2: Replace the block**
+
+Use the Edit tool on `understand-anything-plugin/skills/understand/SKILL.md`.
+
+`old_string` (match exactly, including leading spaces):
+```
+ - If `--language` is NOT specified:
+ - Check `$PROJECT_ROOT/.understand-anything/config.json` for an existing `outputLanguage` field. If present, use that.
+ - If no stored preference, default to `en` (English).
+```
+
+`new_string` (describes the confirmation *intent* in one instruction rather than
+hard-coding a literal prompt box — the skill is a prompt the model interprets):
+```
+ - If `--language` is NOT specified:
+ - **Stored preference wins.** If `$PROJECT_ROOT/.understand-anything/config.json` has an `outputLanguage` field, set `$OUTPUT_LANGUAGE` to it and skip the rest.
+ - **Otherwise detect (first run only).** Infer the predominant language of the user's conversation as an ISO 639-1 code (`$DETECTED_LANG`). If it is `en` or cannot be confidently determined, set `$OUTPUT_LANGUAGE=en` and proceed silently — no prompt (English users see no change).
+ - **If `$DETECTED_LANG` ≠ `en`, confirm once before analyzing:** tell the user you detected `` and ask whether to generate all content in it; they press Enter/"yes" to accept, or type another language code/name to override (normalize via the friendly-name map above). If running non-interactively (no reply possible), skip the wait, use `$DETECTED_LANG`, and print a one-line notice instead of blocking.
+ - **Persist** the resolved `$OUTPUT_LANGUAGE` (including `en`) into `config.json` so it never re-prompts for this project.
+```
+
+- [ ] **Step 3: Verify the edit landed and is well-formed**
+
+Run: `sed -n '142,170p' understand-anything-plugin/skills/understand/SKILL.md`
+
+Expected: the new multi-level block is present; the very next content after it is the unchanged `- If `--language` IS specified:` line. Confirm no duplicate `default to en` line remains.
+
+Run: `grep -c 'default to \`en\`' understand-anything-plugin/skills/understand/SKILL.md`
+Expected: `0` (the old standalone default line is gone; the new wording says "set `$OUTPUT_LANGUAGE` to `en`").
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add understand-anything-plugin/skills/understand/SKILL.md
+git commit -m "feat(understand): detect conversation language on first run
+
+Expand SKILL.md step 3.6 with conversation-language detection as a
+fallback before the en default, gated behind a first-run-only,
+non-English-only confirmation. Resolved value is persisted to
+config.json so the gate fires at most once. English users see no change.
+
+Co-Authored-By: Claude Opus 4.8 (1M context) "
+```
+
+---
+
+## Task 2: Document first-run auto-detection in README
+
+**Files:**
+- Modify: `README.md` (*Localized output* section, before the "The `--language` parameter affects:" line, ~line 130)
+
+- [ ] **Step 1: Confirm the anchor line exists**
+
+Run: `grep -n 'The `--language` parameter affects:' README.md`
+Expected: one match around line 130.
+
+- [ ] **Step 2: Insert the auto-detection paragraph**
+
+Use the Edit tool on `README.md`.
+
+`old_string`:
+```
+The `--language` parameter affects:
+```
+
+`new_string`:
+```
+On the **first run** in a project — when you don't pass `--language` and no language is stored yet — `/understand` detects the language you're conversing in. If it isn't English, it asks you to confirm (or override) before generating; English conversations are unaffected. Your choice is saved to `.understand-anything/config.json` and reused on every later run.
+
+The `--language` parameter affects:
+```
+
+- [ ] **Step 3: Verify**
+
+Run: `grep -n 'first run' README.md`
+Expected: one match with the new sentence, located just above "The `--language` parameter affects:".
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add README.md
+git commit -m "docs: note first-run conversation-language auto-detection
+
+Co-Authored-By: Claude Opus 4.8 (1M context) "
+```
+
+---
+
+## Task 3: Manual verification (no automated test hook exists)
+
+There is no unit-test harness for skill-prompt behavior. Verify by reasoning through each scenario against the edited step 3.6 text and confirming the prompt logic produces the right `$OUTPUT_LANGUAGE` and `config.json` write. Record the result of each in the PR description.
+
+- [ ] **Step 1: Walk the five scenarios**
+
+For each, trace the edited step 3.6 and confirm the expected outcome:
+
+| # | Situation (fresh project, no `config.json`) | Expected |
+|---|---|---|
+| 1 | Conversation in Chinese, run `/understand` | Gate appears → confirm → `$OUTPUT_LANGUAGE=zh`; `config.json` gets `"outputLanguage":"zh"` |
+| 2 | Re-run in the same project (config now has `zh`) | No gate (stored preference wins); generates `zh` |
+| 3 | Conversation in English, run `/understand` | No gate; `$OUTPUT_LANGUAGE=en`; English output (no regression) |
+| 4 | `--language ja` on a fresh project | No gate (flag wins); `config.json` gets `"outputLanguage":"ja"` |
+| 5 | Detected `zh`, user types `en` at the gate | `$OUTPUT_LANGUAGE=en`; `config.json` gets `"outputLanguage":"en"` |
+
+- [ ] **Step 2: Confirm no-regression invariant**
+
+Re-read the edited block and confirm: there is NO code path where an English-only conversation with no flag/config produces a prompt. (Scenario 3 must be silent.) This is the single most important property for upstream acceptance.
+
+- [ ] **Step 3: Optional live smoke test**
+
+If you want a real run: in a throwaway repo with no `.understand-anything/config.json`, converse briefly in Chinese, run `/understand`, and confirm the gate appears and `config.json` is written with `outputLanguage: "zh"` after confirming. (Skip if a full analysis run is too costly; the trace in Step 1 is sufficient for the PR.)
+
+---
+
+## Task 4: Open the PR
+
+- [ ] **Step 1: Push the branch**
+
+```bash
+git push -u origin feat/understand-language-auto-detection
+```
+
+- [ ] **Step 2: Create the PR**
+
+```bash
+gh pr create --title "feat(understand): detect conversation language on first run" --body "$(cat <<'EOF'
+## What
+
+On the first `/understand` run in a project (no `--language` flag, no stored `outputLanguage`), the skill now detects the language of the conversation and — **only when it is not English** — asks the user to confirm or override before generating. The choice is persisted to `.understand-anything/config.json` and the gate never fires again.
+
+## Why
+
+The output language defaulted silently to English. A user conversing in Chinese would run the simplest command and only discover the English output after paying the full cost of an analysis run, then had to re-run with `--language zh`.
+
+## Zero change for English users
+
+The confirmation gate fires **only** when the detected language is non-English and no language has been chosen yet. English conversations follow the exact same silent `en` path as before. `--language` flag and stored config both take priority over detection.
+
+## Scope
+
+- Only `understand-anything-plugin/skills/understand/SKILL.md` step 3.6 + a README sentence.
+- No code/schema changes. Other skills and the auto-update hook are unchanged (separate known gaps).
+- Non-interactive invocations fall back to the detected language with a notice instead of blocking.
+
+Design + plan: `docs/superpowers/specs/2026-06-03-language-auto-detection-design.md`, `docs/superpowers/plans/2026-06-03-language-auto-detection.md`.
+
+🤖 Generated with [Claude Code](https://claude.com/claude-code)
+EOF
+)"
+```
+
+---
+
+## Self-Review
+
+**Spec coverage:**
+- Resolution chain (param > config > detect > en) → Task 1 Step 2. ✓
+- Non-English-only, first-run-only gate → Task 1 Step 2 (gate "shown ONLY when `$DETECTED_LANG` ≠ `en`"). ✓
+- Persist resolved value incl. `en` → Task 1 Step 2 ("Persist the resolved value"). ✓
+- Edge: uncertain/mixed → `en` silent → Task 1 Step 2 first bullet. ✓
+- Edge: non-interactive fallback → Task 1 Step 2 ("Non-interactive fallback"). ✓
+- Edge: auto-update hook unaffected → no task needed (no code path touched); noted here. ✓
+- `locales/.md` already wired → unchanged, noted in File Structure. ✓
+- README sentence → Task 2. ✓
+- Manual verification scenarios (5) → Task 3. ✓
+
+**Placeholder scan:** No "TBD/TODO/handle edge cases" left vague — every edge case has explicit resolved behavior. The ``/`` tokens inside the gate are intentional template placeholders the skill fills at runtime, not plan gaps. ✓
+
+**Type/name consistency:** Variable names used consistently — `$OUTPUT_LANGUAGE`, `$DETECTED_LANG`, `outputLanguage` (config key), `--language` (flag). These match the existing names in SKILL.md step 3.6. ✓
diff --git a/docs/superpowers/specs/2026-06-03-language-auto-detection-design.md b/docs/superpowers/specs/2026-06-03-language-auto-detection-design.md
new file mode 100644
index 0000000..c3b5278
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-03-language-auto-detection-design.md
@@ -0,0 +1,147 @@
+# Conversation-Language Auto-Detection for `/understand`
+
+**Date:** 2026-06-03
+**Status:** Approved — ready for implementation plan
+**Scope:** `understand-anything-plugin/skills/understand/SKILL.md` (primary), `README.md` (docs)
+
+## Problem
+
+`/understand` generates all LLM-authored content (node summaries, tags, layer
+names, guided tours, language notes) in **English by default**. The output
+language is controlled solely by an explicit `--language ` flag or a
+previously stored `outputLanguage` value in `.understand-anything/config.json`.
+
+The skill performs **no detection** of the language the user is actually working
+in. `SKILL.md` step 3.6 (lines 142–144) hard-defaults to `en` whenever the flag
+and config are both absent:
+
+```
+- If `--language` is NOT specified:
+ - Check config.json for outputLanguage. If present, use that.
+ - If no stored preference, default to `en` (English).
+```
+
+**Observed failure:** A user conversing entirely in Chinese ran the simplest
+`/understand` command and received an English knowledge graph. They only
+discovered the mismatch after paying the full cost of an analysis run (time +
+tokens), then had to re-run with `--language zh`. The default is silent and
+undiscoverable — nothing surfaces the language decision at the point it matters.
+
+## Goal
+
+On **first analysis of a project**, infer the user's working language from the
+conversation and confirm it before spending the analysis budget — without
+changing anything for English-speaking users (the project's core audience) and
+without breaking non-interactive invocations.
+
+## Non-Goals
+
+- No side-by-side bilingual output. Output remains single-language per graph.
+- No changes to other content-generating skills (`understand-domain`,
+ `understand-knowledge`, `understand-explain`, etc.). They currently ignore
+ `outputLanguage` entirely; that is a separate, known gap left for follow-up
+ PRs.
+- No changes to the autonomous auto-update hook path
+ (`hooks/auto-update-prompt.md`). It reuses the existing graph and does not
+ resolve a language, so detection never runs there. Noted as a known separate
+ gap, out of scope here.
+- No code, schema, or TypeScript changes. The `outputLanguage` field already
+ exists in `ProjectConfig` (`packages/core/src/types.ts:119`).
+
+## Approach (chosen)
+
+Add conversation-language **detection as a fallback** in the resolution chain,
+placed *before* the `en` default, gated behind a **first-run-only, non-English-only**
+confirmation. This was selected over two alternatives discussed:
+
+- **(A) Personal workaround** (always pass `--language zh` / hand-edit config):
+ zero upstream value, rejected — the goal is a contributable fix.
+- **(B, chosen) Detection-as-fallback in `SKILL.md`:** minimal diff, strictly
+ better for non-English users, invisible to English users.
+- **(C) Always-prompt menu on every run:** rejected — reintroduces friction and
+ is the most likely to be rejected by upstream maintainers.
+
+The confirmation gate (rather than silent application) was chosen at the user's
+direction for explicitness, but constrained so it never affects English users.
+
+## Detailed Design
+
+### Resolution chain (rewrite of `SKILL.md` step 3.6)
+
+Priority, highest first:
+
+1. **`--language ` flag present** → normalize via the existing
+ friendly-name map (line 140), persist to `config.json`, use. *(unchanged)*
+2. **`outputLanguage` present in `config.json`** → use it. *(unchanged)*
+3. **First run (no flag AND no config) → NEW: detect conversation language.**
+ - Infer the predominant language of the user's messages in the current
+ conversation → `$DETECTED_LANG` (ISO 639-1 code, e.g. `zh`, `ja`).
+ - **If `$DETECTED_LANG` is `en`, or cannot be confidently determined, or the
+ conversation is mixed/ambiguous** → set `$OUTPUT_LANGUAGE = en`, persist,
+ **proceed with no prompt.** (Preserves current behavior exactly for the
+ core audience.)
+ - **If `$DETECTED_LANG` ≠ `en`** → show the confirmation gate below, resolve
+ `$OUTPUT_LANGUAGE`, persist to `config.json`.
+
+In all branches the resolved value is written to `config.json`
+(`{"outputLanguage": ""}` merged into existing config), so the gate fires
+**at most once per project**.
+
+### Confirmation gate (only when `$DETECTED_LANG` ≠ `en`)
+
+Shown before any pipeline phase runs. `SKILL.md` describes the *intent* in one
+instruction rather than hard-coding a literal prompt box — the skill is a prompt
+the model interprets, so it renders the question itself at runtime. The
+instruction tells the model to:
+
+- State the detected language and ask whether to generate all content in it.
+- Accept Enter / "yes" / the detected code as confirmation → `$OUTPUT_LANGUAGE = $DETECTED_LANG`.
+- Accept any other language code or friendly name as an override → normalize via
+ the existing friendly-name map (line ~140) and use it. This doubles as the
+ "chatting in Chinese but want English docs for my team" escape hatch.
+
+The instruction text in `SKILL.md` stays in **English** (skill prompts are
+English); only the *generated content* becomes the target language.
+
+### Edge cases
+
+| Case | Behavior |
+|---|---|
+| Detection uncertain / mixed languages | Treat as `en`, proceed silently. Never block on a guess. |
+| Non-interactive invocation (headless/CI, no user to answer) | Fall back to `$DETECTED_LANG` with a one-line notice instead of hanging on the gate; persist. Confirm is best-effort, never a hard block. |
+| Autonomous auto-update hook | Unaffected — that path does not resolve language. |
+| Detected language has a `locales/.md` file | Already wired at step 4 (line 424); no change. |
+| Detected language has no locale file | `$LANGUAGE_DIRECTIVE` still applies (existing "skip silently" behavior). |
+
+## Files Touched
+
+- `understand-anything-plugin/skills/understand/SKILL.md` — rewrite the step 3.6
+ `if --language NOT specified` branch into the 4-step resolution chain above.
+ **Primary change.**
+- `README.md` — add a sentence under the *Localized output* section (~line 121)
+ noting first-run auto-detection.
+
+No other files. No schema, code, or test-harness files change.
+
+## Testing / Verification
+
+Prompt-logic change → no unit-test hook. Verification is manual and documented
+in the PR description:
+
+1. Fresh project, no config, converse in Chinese, run `/understand` → gate
+ appears → confirm → content generated in `zh`; `config.json` contains
+ `outputLanguage: "zh"`.
+2. Re-run in same project → no gate (config wins).
+3. Fresh project, converse in English → no gate, English output (proves no
+ regression for the core audience).
+4. Fresh project, `--language ja` → no gate, flag wins, config stores `ja`.
+5. Fresh project, override at the gate (detected `zh`, type `en`) → English
+ output, config stores `en`.
+
+## Risks & Upstream Framing
+
+The main reviewer concern is *any* added interactivity. Mitigations are built in:
+the gate is **first-run-only**, **non-English-only**, and **degrades gracefully**
+when non-interactive. The PR description leads with: **"Zero behavior change for
+English users; the gate only appears when the conversation is non-English and no
+language has been chosen yet."**
From 1f8d165f86491f5931ab27aa059066fdb0c18111 Mon Sep 17 00:00:00 2001
From: Tirth Kanani
Date: Fri, 5 Jun 2026 16:19:31 +0100
Subject: [PATCH 08/12] fix(extract-import-map): preserve deterministic stderr
order across concurrent loaders
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Addresses the regression flagged by ZebangCheng on #346: under the
parallelised `buildResolutionContext`, `loadTsConfigs` /
`loadGoModules` / `loadPhpAutoloads` ran concurrently but each wrote
warnings to stderr inline as it iterated read results, so a fixture
with both a malformed `tsconfig.json` and a malformed `composer.json`
could emit `composer, tsconfig` instead of the pre-PR `tsconfig,
composer` depending on I/O timing.
Each loader now buffers its warnings into a returned array and the
caller drains them in canonical order (tsconfig → go → php) after
`Promise.all`, restoring byte-identical stderr output. Added a
regression test that fixtures both malformed configs and asserts the
tsconfig warning precedes the composer warning in stderr.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.../test_extract_import_map.test.mjs | 52 +++++++++++++++++++
.../skills/understand/extract-import-map.mjs | 39 +++++++++++---
2 files changed, 83 insertions(+), 8 deletions(-)
diff --git a/tests/skill/understand/test_extract_import_map.test.mjs b/tests/skill/understand/test_extract_import_map.test.mjs
index ee64136..a4be65a 100644
--- a/tests/skill/understand/test_extract_import_map.test.mjs
+++ b/tests/skill/understand/test_extract_import_map.test.mjs
@@ -1492,3 +1492,55 @@ describe('extract-import-map.mjs — tree-sitter init graceful failure', () => {
expect(result.output.stats.totalEdges).toBe(0);
});
});
+
+describe('extract-import-map.mjs — deterministic stderr ordering across loaders', () => {
+ let projectRoot;
+
+ afterEach(() => {
+ if (projectRoot) {
+ rmSync(projectRoot, { recursive: true, force: true });
+ projectRoot = null;
+ }
+ });
+
+ // Regression for the parallel-loader stderr-order bug surfaced in
+ // PR #346 review: tsconfig / go.mod / composer.json loaders now run
+ // concurrently, but warnings must still emit in the pre-PR canonical
+ // order (tsconfig → go → php). If the loaders streamed warnings
+ // mid-flight, I/O timing could reorder them — the assertions below
+ // catch that regression.
+ it('emits warnings in canonical order (tsconfig, go, php) regardless of I/O timing', () => {
+ projectRoot = setupTree({
+ 'tsconfig.json': '{ "compilerOptions": { "baseUrl": ".", ', // unterminated
+ 'composer.json': '{ "autoload": { "psr-4": { "App\\\\": "src/" }, ', // unterminated
+ 'src/index.ts': `import { foo } from './foo';\n`,
+ 'src/foo.ts': `export const foo = 1;\n`,
+ 'src/Http/Controller.php':
+ `
Date: Fri, 5 Jun 2026 16:56:42 +0100
Subject: [PATCH 09/12] fix(skill): use mv-to-trash + delayed purge for Phase 7
cleanup (#301)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Phase 7's `rm -rf` of the just-created `intermediate/` and `tmp/` dirs
trips destructive-action gates on hardened hosts (e.g. freshness-window
checks that flag deleting paths created moments earlier). Move them into
a timestamped `.trash-/` instead; Phase 0 reclaims the space once
the trash is older than 7 days, well past any freshness window. Behavior
on normal hosts is unchanged — disk usage is identical after the next
run's purge.
Closes #301
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.../skills/understand/SKILL.md | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/understand-anything-plugin/skills/understand/SKILL.md b/understand-anything-plugin/skills/understand/SKILL.md
index 35c0b11..10bf9cc 100644
--- a/understand-anything-plugin/skills/understand/SKILL.md
+++ b/understand-anything-plugin/skills/understand/SKILL.md
@@ -129,6 +129,10 @@ Determine whether to run a full analysis or incremental update.
mkdir -p $PROJECT_ROOT/.understand-anything/intermediate
mkdir -p $PROJECT_ROOT/.understand-anything/tmp
```
+3.1. **Purge stale trash dirs.** Phase 7 cleanup `mv`s scratch dirs into `.trash-/` rather than `rm -rf`ing them directly (see issue #301), so that destructive-action gates on hardened hosts don't trip on just-created paths. Reclaim the space here once the trash is older than 7 days — by this point any freshness-window check has long since stopped caring about those dirs:
+ ```bash
+ find $PROJECT_ROOT/.understand-anything/ -maxdepth 1 -type d -name '.trash-*' -mtime +7 -exec rm -rf {} + 2>/dev/null || true
+ ```
3.5. **Auto-update configuration:**
- If `--auto-update` is in `$ARGUMENTS`: write `{"autoUpdate": true}` to `$PROJECT_ROOT/.understand-anything/config.json`
- If `--no-auto-update` is in `$ARGUMENTS`: write `{"autoUpdate": false}` to `$PROJECT_ROOT/.understand-anything/config.json`
@@ -772,17 +776,20 @@ Report to the user: `[Phase 7/7] Saving knowledge graph...`
}
```
-4. Clean up intermediate files, **preserving `scan-result.json`** so future incremental runs can skip Phase 1 SCAN (see issue #293):
+4. Clean up intermediate files, **preserving `scan-result.json`** so future incremental runs can skip Phase 1 SCAN (see issue #293). We `mv` scratch dirs into a timestamped `.trash-*` instead of `rm -rf`ing them directly — this avoids tripping destructive-action gates on hardened hosts (e.g. freshness-window checks) that flag deleting directories created moments earlier (see issue #301). The delayed-purge step in Phase 0 reclaims the space once the trash is older than 7 days.
```bash
# Preserve scan-result.json — Phase 1's deterministic file inventory.
# Future incremental runs (Phase 2 compute-batches.mjs --changed-files=…)
# need this inventory; without it, Phase 1 must re-dispatch and pay ~157k
# tokens / ~158s per incremental run.
+ TRASH="$PROJECT_ROOT/.understand-anything/.trash-$(date +%s)"
+ mkdir -p "$TRASH"
INTER="$PROJECT_ROOT/.understand-anything/intermediate"
if [ -d "$INTER" ]; then
- find "$INTER" -mindepth 1 -maxdepth 1 -not -name 'scan-result.json' -exec rm -rf {} +
+ # Move every entry except scan-result.json into the trash dir.
+ find "$INTER" -mindepth 1 -maxdepth 1 -not -name 'scan-result.json' -exec mv {} "$TRASH/" \; 2>/dev/null || true
fi
- rm -rf $PROJECT_ROOT/.understand-anything/tmp
+ mv "$PROJECT_ROOT/.understand-anything/tmp" "$TRASH/" 2>/dev/null || true
```
5. Report a summary to the user containing:
From 8de0b3b11d395e8692fea4846d27275ad6d3ba5d Mon Sep 17 00:00:00 2001
From: chengyongru
Date: Tue, 9 Jun 2026 16:16:04 +0800
Subject: [PATCH 10/12] feat(install): add Nanobot platform support
---
.claude-plugin/plugin.json | 2 +-
.copilot-plugin/plugin.json | 2 +-
.cursor-plugin/plugin.json | 2 +-
README.md | 5 +++--
READMEs/README.es-ES.md | 5 +++--
READMEs/README.ja-JP.md | 5 +++--
READMEs/README.ko-KR.md | 5 +++--
READMEs/README.ru-RU.md | 5 +++--
READMEs/README.tr-TR.md | 5 +++--
READMEs/README.zh-CN.md | 5 +++--
READMEs/README.zh-TW.md | 5 +++--
install.ps1 | 1 +
install.sh | 1 +
understand-anything-plugin/.claude-plugin/plugin.json | 2 +-
understand-anything-plugin/package.json | 2 +-
15 files changed, 31 insertions(+), 21 deletions(-)
diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
index 839168b..adb068e 100644
--- a/.claude-plugin/plugin.json
+++ b/.claude-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "understand-anything",
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
- "version": "2.7.6",
+ "version": "2.7.7",
"author": {
"name": "Lum1104"
},
diff --git a/.copilot-plugin/plugin.json b/.copilot-plugin/plugin.json
index ea367dd..c5413bd 100644
--- a/.copilot-plugin/plugin.json
+++ b/.copilot-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "understand-anything",
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
- "version": "2.7.6",
+ "version": "2.7.7",
"author": {
"name": "Lum1104"
},
diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json
index a60b8bc..d1a52ea 100644
--- a/.cursor-plugin/plugin.json
+++ b/.cursor-plugin/plugin.json
@@ -2,7 +2,7 @@
"name": "understand-anything",
"displayName": "Understand Anything",
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
- "version": "2.7.6",
+ "version": "2.7.7",
"author": {
"name": "Lum1104"
},
diff --git a/README.md b/README.md
index 806d4f4..d3c5e82 100644
--- a/README.md
+++ b/README.md
@@ -186,7 +186,7 @@ Understand-Anything works across multiple AI coding platforms.
/plugin install understand-anything
```
-### One-line install (Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI / Trae)
+### One-line install (Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI / Trae / Nanobot)
**macOS / Linux:**
```bash
@@ -202,7 +202,7 @@ iwr -useb https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/ins
The installer clones the repo to `~/.understand-anything/repo` and creates the right symlinks for the chosen platform. Restart your CLI/IDE afterwards.
-- Supported `` values: `gemini`, `codex`, `opencode`, `pi`, `openclaw`, `antigravity`, `vibe`, `vscode`, `hermes`, `cline`, `kimi`, `trae`
+- Supported `` values: `gemini`, `codex`, `opencode`, `pi`, `openclaw`, `antigravity`, `vibe`, `vscode`, `hermes`, `cline`, `kimi`, `trae`, `nanobot`
- Update later: `./install.sh --update`
- Uninstall: `./install.sh --uninstall `
@@ -243,6 +243,7 @@ copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
| Cline | ✅ Supported | `install.sh cline` |
| KIMI CLI | ✅ Supported | `install.sh kimi` |
| Trae | ✅ Supported | `install.sh trae` |
+| Nanobot | ✅ Supported | `install.sh nanobot` |
---
diff --git a/READMEs/README.es-ES.md b/READMEs/README.es-ES.md
index 542d83b..9017977 100644
--- a/READMEs/README.es-ES.md
+++ b/READMEs/README.es-ES.md
@@ -181,7 +181,7 @@ Understand-Anything funciona en múltiples plataformas de codificación con IA.
/plugin install understand-anything
```
-### Instalación de una línea (Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI)
+### Instalación de una línea (Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI / Nanobot)
**macOS / Linux:**
```bash
@@ -197,7 +197,7 @@ iwr -useb https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/ins
El instalador clona el repositorio en `~/.understand-anything/repo` y crea los enlaces simbólicos correspondientes para la plataforma elegida. Reinicia tu CLI/IDE al terminar.
-- Valores soportados de ``: `gemini`, `codex`, `opencode`, `pi`, `openclaw`, `antigravity`, `vibe`, `vscode`, `hermes`, `cline`, `kimi`
+- Valores soportados de ``: `gemini`, `codex`, `opencode`, `pi`, `openclaw`, `antigravity`, `vibe`, `vscode`, `hermes`, `cline`, `kimi`, `nanobot`
- Actualizar más adelante: `./install.sh --update`
- Desinstalar: `./install.sh --uninstall `
@@ -237,6 +237,7 @@ copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
| Hermes | ✅ Soportado | `install.sh hermes` |
| Cline | ✅ Soportado | `install.sh cline` |
| KIMI CLI | ✅ Soportado | `install.sh kimi` |
+| Nanobot | ✅ Soportado | `install.sh nanobot` |
---
diff --git a/READMEs/README.ja-JP.md b/READMEs/README.ja-JP.md
index cbe8a43..5454e46 100644
--- a/READMEs/README.ja-JP.md
+++ b/READMEs/README.ja-JP.md
@@ -182,7 +182,7 @@ Understand-Anythingは複数のAIコーディングプラットフォームで
/plugin install understand-anything
```
-### ワンラインインストール(Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI)
+### ワンラインインストール(Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI / Nanobot)
**macOS / Linux:**
```bash
@@ -198,7 +198,7 @@ iwr -useb https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/ins
インストーラーはリポジトリを `~/.understand-anything/repo` にクローンし、選択したプラットフォーム用のシンボリックリンクを作成します。完了後はCLI/IDEを再起動してください。
-- サポートされる `` 値:`gemini`、`codex`、`opencode`、`pi`、`openclaw`、`antigravity`、`vibe`、`vscode`、`hermes`、`cline`、`kimi`
+- サポートされる `` 値:`gemini`、`codex`、`opencode`、`pi`、`openclaw`、`antigravity`、`vibe`、`vscode`、`hermes`、`cline`、`kimi`、`nanobot`
- 後で更新:`./install.sh --update`
- アンインストール:`./install.sh --uninstall `
@@ -238,6 +238,7 @@ copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
| Hermes | ✅ サポート | `install.sh hermes` |
| Cline | ✅ サポート | `install.sh cline` |
| KIMI CLI | ✅ サポート | `install.sh kimi` |
+| Nanobot | ✅ サポート | `install.sh nanobot` |
---
diff --git a/READMEs/README.ko-KR.md b/READMEs/README.ko-KR.md
index 2e51742..8de315e 100644
--- a/READMEs/README.ko-KR.md
+++ b/READMEs/README.ko-KR.md
@@ -181,7 +181,7 @@ Understand-Anything은 다양한 AI 코딩 플랫폼에서 사용할 수 있습
/plugin install understand-anything
```
-### 한 줄 설치 (Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI)
+### 한 줄 설치 (Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI / Nanobot)
**macOS / Linux:**
```bash
@@ -197,7 +197,7 @@ iwr -useb https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/ins
설치 스크립트는 저장소를 `~/.understand-anything/repo`에 클론하고 선택한 플랫폼에 맞는 심볼릭 링크를 생성합니다. 설치 후 CLI 또는 IDE를 재시작하세요.
-- 지원되는 `` 값: `gemini`, `codex`, `opencode`, `pi`, `openclaw`, `antigravity`, `vibe`, `vscode`, `hermes`, `cline`, `kimi`
+- 지원되는 `` 값: `gemini`, `codex`, `opencode`, `pi`, `openclaw`, `antigravity`, `vibe`, `vscode`, `hermes`, `cline`, `kimi`, `nanobot`
- 이후 업데이트: `./install.sh --update`
- 제거: `./install.sh --uninstall `
@@ -237,6 +237,7 @@ copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
| Hermes | ✅ 지원 | `install.sh hermes` |
| Cline | ✅ 지원 | `install.sh cline` |
| KIMI CLI | ✅ 지원 | `install.sh kimi` |
+| Nanobot | ✅ 지원 | `install.sh nanobot` |
---
diff --git a/READMEs/README.ru-RU.md b/READMEs/README.ru-RU.md
index 3b11cb3..8585525 100644
--- a/READMEs/README.ru-RU.md
+++ b/READMEs/README.ru-RU.md
@@ -182,7 +182,7 @@ Understand-Anything работает с несколькими платформ
/plugin install understand-anything
```
-### Установка одной командой (Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI)
+### Установка одной командой (Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI / Nanobot)
**macOS / Linux:**
```bash
@@ -198,7 +198,7 @@ iwr -useb https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/ins
Установщик клонирует репозиторий в `~/.understand-anything/repo` и создаёт нужные симлинки для выбранной платформы. После установки перезапустите свой CLI/IDE.
-- Поддерживаемые значения ``: `gemini`, `codex`, `opencode`, `pi`, `openclaw`, `antigravity`, `vibe`, `vscode`, `hermes`, `cline`, `kimi`
+- Поддерживаемые значения ``: `gemini`, `codex`, `opencode`, `pi`, `openclaw`, `antigravity`, `vibe`, `vscode`, `hermes`, `cline`, `kimi`, `nanobot`
- Обновление: `./install.sh --update`
- Удаление: `./install.sh --uninstall `
@@ -238,6 +238,7 @@ copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
| Hermes | ✅ Поддерживается | `install.sh hermes` |
| Cline | ✅ Поддерживается | `install.sh cline` |
| KIMI CLI | ✅ Поддерживается | `install.sh kimi` |
+| Nanobot | ✅ Поддерживается | `install.sh nanobot` |
---
diff --git a/READMEs/README.tr-TR.md b/READMEs/README.tr-TR.md
index 7f3168d..380b69c 100644
--- a/READMEs/README.tr-TR.md
+++ b/READMEs/README.tr-TR.md
@@ -182,7 +182,7 @@ Understand-Anything birden fazla AI kodlama platformunda çalışır.
/plugin install understand-anything
```
-### Tek satırlık kurulum (Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI)
+### Tek satırlık kurulum (Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI / Nanobot)
**macOS / Linux:**
```bash
@@ -198,7 +198,7 @@ iwr -useb https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/ins
Kurulum betiği depoyu `~/.understand-anything/repo` dizinine klonlar ve seçilen platform için uygun sembolik bağlantıları oluşturur. Sonrasında CLI/IDE'ni yeniden başlat.
-- Desteklenen `` değerleri: `gemini`, `codex`, `opencode`, `pi`, `openclaw`, `antigravity`, `vibe`, `vscode`, `hermes`, `cline`, `kimi`
+- Desteklenen `` değerleri: `gemini`, `codex`, `opencode`, `pi`, `openclaw`, `antigravity`, `vibe`, `vscode`, `hermes`, `cline`, `kimi`, `nanobot`
- Daha sonra güncelle: `./install.sh --update`
- Kaldır: `./install.sh --uninstall `
@@ -238,6 +238,7 @@ copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
| Hermes | ✅ Destekleniyor | `install.sh hermes` |
| Cline | ✅ Destekleniyor | `install.sh cline` |
| KIMI CLI | ✅ Destekleniyor | `install.sh kimi` |
+| Nanobot | ✅ Destekleniyor | `install.sh nanobot` |
---
diff --git a/READMEs/README.zh-CN.md b/READMEs/README.zh-CN.md
index 87fedc6..0150abb 100644
--- a/READMEs/README.zh-CN.md
+++ b/READMEs/README.zh-CN.md
@@ -181,7 +181,7 @@ Understand-Anything 可在多个 AI 编码平台上运行。
/plugin install understand-anything
```
-### 一行命令安装(Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI)
+### 一行命令安装(Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI / Nanobot)
**macOS / Linux:**
```bash
@@ -197,7 +197,7 @@ iwr -useb https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/ins
安装脚本会将仓库克隆到 `~/.understand-anything/repo`,并为所选平台创建相应的符号链接。安装完成后请重启 CLI 或 IDE。
-- 支持的 `` 取值:`gemini`、`codex`、`opencode`、`pi`、`openclaw`、`antigravity`、`vibe`、`vscode`、`hermes`、`cline`、`kimi`
+- 支持的 `` 取值:`gemini`、`codex`、`opencode`、`pi`、`openclaw`、`antigravity`、`vibe`、`vscode`、`hermes`、`cline`、`kimi`、`nanobot`
- 后续更新:`./install.sh --update`
- 卸载:`./install.sh --uninstall `
@@ -237,6 +237,7 @@ copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
| Hermes | ✅ 支持 | `install.sh hermes` |
| Cline | ✅ 支持 | `install.sh cline` |
| KIMI CLI | ✅ 支持 | `install.sh kimi` |
+| Nanobot | ✅ 支持 | `install.sh nanobot` |
---
diff --git a/READMEs/README.zh-TW.md b/READMEs/README.zh-TW.md
index 0b4ef96..20eb94b 100644
--- a/READMEs/README.zh-TW.md
+++ b/READMEs/README.zh-TW.md
@@ -181,7 +181,7 @@ Understand-Anything 可在多個 AI 編碼平台上執行。
/plugin install understand-anything
```
-### 一行指令安裝(Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI)
+### 一行指令安裝(Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI / Nanobot)
**macOS / Linux:**
```bash
@@ -197,7 +197,7 @@ iwr -useb https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/ins
安裝指令稿會將儲存庫複製到 `~/.understand-anything/repo`,並為所選平台建立相應的符號連結。安裝完成後請重新啟動 CLI 或 IDE。
-- 支援的 `` 取值:`gemini`、`codex`、`opencode`、`pi`、`openclaw`、`antigravity`、`vibe`、`vscode`、`hermes`、`cline`、`kimi`
+- 支援的 `` 取值:`gemini`、`codex`、`opencode`、`pi`、`openclaw`、`antigravity`、`vibe`、`vscode`、`hermes`、`cline`、`kimi`、`nanobot`
- 後續更新:`./install.sh --update`
- 解除安裝:`./install.sh --uninstall `
@@ -237,6 +237,7 @@ copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
| Hermes | ✅ 支援 | `install.sh hermes` |
| Cline | ✅ 支援 | `install.sh cline` |
| KIMI CLI | ✅ 支援 | `install.sh kimi` |
+| Nanobot | ✅ 支援 | `install.sh nanobot` |
---
diff --git a/install.ps1 b/install.ps1
index 1a4b4df..5c9c43f 100644
--- a/install.ps1
+++ b/install.ps1
@@ -39,6 +39,7 @@ $Platforms = [ordered]@{
cline = @{ Target = (Join-Path $HOME '.cline\skills'); Style = 'folder' }
kimi = @{ Target = (Join-Path $HOME '.kimi\skills'); Style = 'folder' }
trae = @{ Target = (Join-Path $HOME '.trae\skills'); Style = 'per-skill' }
+ nanobot = @{ Target = (Join-Path $HOME '.nanobot\workspace\skills'); Style = 'per-skill' }
}
function Show-Usage {
diff --git a/install.sh b/install.sh
index 8ff4293..7d49c2c 100755
--- a/install.sh
+++ b/install.sh
@@ -40,6 +40,7 @@ hermes|$HOME/.hermes/skills|folder
cline|$HOME/.cline/skills|folder
kimi|$HOME/.kimi/skills|folder
trae|$HOME/.trae/skills|per-skill
+nanobot|$HOME/.nanobot/workspace/skills|per-skill
EOF
}
diff --git a/understand-anything-plugin/.claude-plugin/plugin.json b/understand-anything-plugin/.claude-plugin/plugin.json
index 839168b..adb068e 100644
--- a/understand-anything-plugin/.claude-plugin/plugin.json
+++ b/understand-anything-plugin/.claude-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "understand-anything",
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
- "version": "2.7.6",
+ "version": "2.7.7",
"author": {
"name": "Lum1104"
},
diff --git a/understand-anything-plugin/package.json b/understand-anything-plugin/package.json
index 85f70f0..a47eaac 100644
--- a/understand-anything-plugin/package.json
+++ b/understand-anything-plugin/package.json
@@ -1,6 +1,6 @@
{
"name": "@understand-anything/skill",
- "version": "2.7.6",
+ "version": "2.7.7",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
From 7254852f4adab010c12fe9fbc16eca716763c88b Mon Sep 17 00:00:00 2001
From: chienvon
Date: Tue, 9 Jun 2026 14:26:48 -0700
Subject: [PATCH 11/12] sync egonex organization metadata
---
.claude-plugin/marketplace.json | 2 +-
.claude-plugin/plugin.json | 6 +-
.copilot-plugin/plugin.json | 6 +-
.cursor-plugin/plugin.json | 6 +-
.github/FUNDING.yml | 1 -
.github/ISSUE_TEMPLATE/config.yml | 4 +-
LICENSE | 1 +
README.md | 41 ++-
READMEs/README.es-ES.md | 34 +-
READMEs/README.ja-JP.md | 34 +-
READMEs/README.ko-KR.md | 34 +-
READMEs/README.ru-RU.md | 34 +-
READMEs/README.tr-TR.md | 34 +-
READMEs/README.zh-CN.md | 34 +-
READMEs/README.zh-TW.md | 34 +-
.../2026-03-15-homepage-implementation.md | 10 +-
...18-multi-platform-simple-implementation.md | 12 +-
.../specs/2026-03-15-homepage-design.md | 2 +-
...2026-03-18-multi-platform-simple-design.md | 2 +-
...-04-01-business-domain-knowledge-design.md | 2 +-
...tic-batching-and-output-chunking-design.md | 2 +-
homepage/src/components/Footer.astro | 21 +-
homepage/src/components/Hero.astro | 327 +++++-------------
homepage/src/components/Install.astro | 2 +-
homepage/src/components/Nav.astro | 16 +-
install.ps1 | 2 +-
install.sh | 6 +-
package.json | 6 +-
.../.claude-plugin/plugin.json | 6 +-
.../packages/dashboard/src/App.tsx | 2 +
.../src/components/WarningBanner.tsx | 2 +-
.../src/onboard-builder.ts | 2 +-
32 files changed, 309 insertions(+), 418 deletions(-)
delete mode 100644 .github/FUNDING.yml
diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index 39a8451..92c37f9 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -4,7 +4,7 @@
"description": "LLM-powered codebase analysis producing interactive knowledge graphs, guided tours, and deep-dive explanations"
},
"owner": {
- "name": "Lum1104"
+ "name": "Egonex"
},
"plugins": [
{
diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
index 839168b..77b67be 100644
--- a/.claude-plugin/plugin.json
+++ b/.claude-plugin/plugin.json
@@ -3,10 +3,10 @@
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
"version": "2.7.6",
"author": {
- "name": "Lum1104"
+ "name": "Egonex"
},
- "homepage": "https://github.com/Lum1104/Understand-Anything",
- "repository": "https://github.com/Lum1104/Understand-Anything",
+ "homepage": "https://github.com/Egonex-AI/Understand-Anything",
+ "repository": "https://github.com/Egonex-AI/Understand-Anything",
"license": "MIT",
"keywords": [
"codebase-analysis",
diff --git a/.copilot-plugin/plugin.json b/.copilot-plugin/plugin.json
index ea367dd..1637426 100644
--- a/.copilot-plugin/plugin.json
+++ b/.copilot-plugin/plugin.json
@@ -3,10 +3,10 @@
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
"version": "2.7.6",
"author": {
- "name": "Lum1104"
+ "name": "Egonex"
},
- "homepage": "https://github.com/Lum1104/Understand-Anything",
- "repository": "https://github.com/Lum1104/Understand-Anything",
+ "homepage": "https://github.com/Egonex-AI/Understand-Anything",
+ "repository": "https://github.com/Egonex-AI/Understand-Anything",
"license": "MIT",
"keywords": ["codebase-analysis", "knowledge-graph", "architecture", "onboarding", "dashboard"],
"skills": "./understand-anything-plugin/skills/",
diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json
index a60b8bc..42af4b2 100644
--- a/.cursor-plugin/plugin.json
+++ b/.cursor-plugin/plugin.json
@@ -4,10 +4,10 @@
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
"version": "2.7.6",
"author": {
- "name": "Lum1104"
+ "name": "Egonex"
},
- "homepage": "https://github.com/Lum1104/Understand-Anything",
- "repository": "https://github.com/Lum1104/Understand-Anything",
+ "homepage": "https://github.com/Egonex-AI/Understand-Anything",
+ "repository": "https://github.com/Egonex-AI/Understand-Anything",
"license": "MIT",
"keywords": ["codebase-analysis", "knowledge-graph", "architecture", "onboarding", "dashboard"],
"skills": "./understand-anything-plugin/skills/",
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
deleted file mode 100644
index cb75fdf..0000000
--- a/.github/FUNDING.yml
+++ /dev/null
@@ -1 +0,0 @@
-patreon: Lum1104
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 3da6fc9..069970c 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,8 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: README & docs
- url: https://github.com/Lum1104/Understand-Anything#readme
+ url: https://github.com/Egonex-AI/Understand-Anything#readme
about: Most usage questions are answered in the project README.
- name: Discussions
- url: https://github.com/Lum1104/Understand-Anything/discussions
+ url: https://github.com/Egonex-AI/Understand-Anything/discussions
about: For open-ended discussion, design proposals, or sharing how you use the tool.
diff --git a/LICENSE b/LICENSE
index 87c7ab2..5df102e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,7 @@
MIT License
Copyright (c) 2026 Yuxiang Lin
+Copyright (c) 2026 Infinite Universe, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 806d4f4..8c21786 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,13 @@
-
+ Understand Anything. Understand Anyone.
+
+ AI should help people, not replace them.
+
+
+
+
@@ -16,7 +22,7 @@
-
+
@@ -27,6 +33,7 @@
+
@@ -34,9 +41,9 @@
- 💬 Join the Discord community →
+ An open-source project from Egonex
- Ask questions, share what you've built, get help from the community.
+ Originally created by Lum1104.
---
@@ -106,7 +113,7 @@ Point `/understand-knowledge` at a [Karpathy-pattern LLM wiki](https://gist.gith
### 1. Install the plugin
```bash
-/plugin marketplace add Lum1104/Understand-Anything
+/plugin marketplace add Egonex-AI/Understand-Anything
/plugin install understand-anything
```
@@ -182,7 +189,7 @@ Understand-Anything works across multiple AI coding platforms.
### Claude Code (Native)
```bash
-/plugin marketplace add Lum1104/Understand-Anything
+/plugin marketplace add Egonex-AI/Understand-Anything
/plugin install understand-anything
```
@@ -190,14 +197,14 @@ Understand-Anything works across multiple AI coding platforms.
**macOS / Linux:**
```bash
-curl -fsSL https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/install.sh | bash
+curl -fsSL https://raw.githubusercontent.com/Egonex-AI/Understand-Anything/main/install.sh | bash
# or skip the prompt by passing the platform:
-curl -fsSL https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/install.sh | bash -s codex
+curl -fsSL https://raw.githubusercontent.com/Egonex-AI/Understand-Anything/main/install.sh | bash -s codex
```
**Windows (PowerShell):**
```powershell
-iwr -useb https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/install.ps1 | iex
+iwr -useb https://raw.githubusercontent.com/Egonex-AI/Understand-Anything/main/install.ps1 | iex
```
The installer clones the repo to `~/.understand-anything/repo` and creates the right symlinks for the chosen platform. Restart your CLI/IDE afterwards.
@@ -210,7 +217,7 @@ The installer clones the repo to `~/.understand-anything/repo` and creates the r
Cursor auto-discovers the plugin via `.cursor-plugin/plugin.json` when this repo is cloned. No manual installation needed — just clone and open in Cursor.
-If auto-discovery doesn't pick it up, install it manually: open **Cursor Settings → Plugins**, paste `https://github.com/Lum1104/Understand-Anything` into the search field, and add it from there.
+If auto-discovery doesn't pick it up, install it manually: open **Cursor Settings → Plugins**, paste `https://github.com/Egonex-AI/Understand-Anything` into the search field, and add it from there.
### VS Code + GitHub Copilot
@@ -221,7 +228,7 @@ For personal skills (available across all projects), run the `install.sh` above
### Copilot CLI
```bash
-copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
+copilot plugin install Egonex-AI/Understand-Anything:understand-anything-plugin
```
### Platform Compatibility
@@ -250,7 +257,7 @@ copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
The graph is just JSON — **commit it once, and teammates skip the pipeline**. Good for onboarding, PR reviews, and docs-as-code.
-> **Example:** [GoogleCloudPlatform/microservices-demo (fork)](https://github.com/Lum1104/microservices-demo) — Go / Java / Python / Node reference with a committed graph.
+> **Example:** [GoogleCloudPlatform/microservices-demo](https://github.com/GoogleCloudPlatform/microservices-demo) — Go / Java / Python / Node reference with a committed graph.
**What to commit:** everything in `.understand-anything/` *except* `intermediate/` and `diff-overlay.json` (those are local scratch).
@@ -333,11 +340,11 @@ Please open an issue first for major changes so we can discuss the approach.
## Star History
-
+
-
-
-
+
+
+
@@ -346,5 +353,5 @@ Please open an issue first for major changes so we can discuss the approach.
---
@@ -103,7 +103,7 @@ Apunta `/understand-knowledge` a un [wiki LLM con patrón Karpathy](https://gist
### 1. Instala el plugin
```bash
-/plugin marketplace add Lum1104/Understand-Anything
+/plugin marketplace add Egonex-AI/Understand-Anything
/plugin install understand-anything
```
@@ -177,7 +177,7 @@ Understand-Anything funciona en múltiples plataformas de codificación con IA.
### Claude Code (Nativo)
```bash
-/plugin marketplace add Lum1104/Understand-Anything
+/plugin marketplace add Egonex-AI/Understand-Anything
/plugin install understand-anything
```
@@ -185,14 +185,14 @@ Understand-Anything funciona en múltiples plataformas de codificación con IA.
**macOS / Linux:**
```bash
-curl -fsSL https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/install.sh | bash
+curl -fsSL https://raw.githubusercontent.com/Egonex-AI/Understand-Anything/main/install.sh | bash
# o pasa la plataforma directamente para saltar el prompt:
-curl -fsSL https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/install.sh | bash -s codex
+curl -fsSL https://raw.githubusercontent.com/Egonex-AI/Understand-Anything/main/install.sh | bash -s codex
```
**Windows (PowerShell):**
```powershell
-iwr -useb https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/install.ps1 | iex
+iwr -useb https://raw.githubusercontent.com/Egonex-AI/Understand-Anything/main/install.ps1 | iex
```
El instalador clona el repositorio en `~/.understand-anything/repo` y crea los enlaces simbólicos correspondientes para la plataforma elegida. Reinicia tu CLI/IDE al terminar.
@@ -205,7 +205,7 @@ El instalador clona el repositorio en `~/.understand-anything/repo` y crea los e
Cursor detecta automáticamente el plugin a través de `.cursor-plugin/plugin.json` cuando se clona este repositorio. No requiere instalación manual: simplemente clona y abre en Cursor.
-Si la detección automática no lo reconoce, instálalo manualmente: abre **Cursor Settings → Plugins**, pega `https://github.com/Lum1104/Understand-Anything` en el campo de búsqueda y añádelo desde allí.
+Si la detección automática no lo reconoce, instálalo manualmente: abre **Cursor Settings → Plugins**, pega `https://github.com/Egonex-AI/Understand-Anything` en el campo de búsqueda y añádelo desde allí.
### VS Code + GitHub Copilot
@@ -216,7 +216,7 @@ Para habilidades personales (disponibles en todos los proyectos), ejecuta el `in
### Copilot CLI
```bash
-copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
+copilot plugin install Egonex-AI/Understand-Anything:understand-anything-plugin
```
### Compatibilidad de Plataformas
@@ -244,7 +244,7 @@ copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
El grafo es solo JSON — **confírmalo una vez y tus compañeros se saltan el pipeline**. Ideal para onboarding, revisiones de PR y flujos docs-as-code.
-> **Ejemplo:** [GoogleCloudPlatform/microservices-demo (fork)](https://github.com/Lum1104/microservices-demo) — referencia políglota (Go / Java / Python / Node) con el grafo ya confirmado.
+> **Ejemplo:** [GoogleCloudPlatform/microservices-demo](https://github.com/GoogleCloudPlatform/microservices-demo) — referencia políglota (Go / Java / Python / Node) con el grafo ya confirmado.
**Qué confirmar:** todo lo que hay en `.understand-anything/` *excepto* `intermediate/` y `diff-overlay.json` (archivos temporales locales).
@@ -327,11 +327,11 @@ Para cambios importantes, abre primero un issue para que podamos discutir el enf
## Historial de Stars
-
+
-
-
-
+
+
+
@@ -340,5 +340,5 @@ Para cambios importantes, abre primero un issue para que podamos discutir el enf
- 💬 Discord topluluğuna katıl →
+ An open-source project from Egonex
- Sorular sor, yaptıklarını paylaş, topluluktan yardım al.
+ Originally created by Lum1104.
---
@@ -104,7 +104,7 @@ Alan görünümüne geçin ve kodunuzun gerçek iş süreçleriyle nasıl eşle
### 1. Eklentiyi yükle
```bash
-/plugin marketplace add Lum1104/Understand-Anything
+/plugin marketplace add Egonex-AI/Understand-Anything
/plugin install understand-anything
```
@@ -178,7 +178,7 @@ Understand-Anything birden fazla AI kodlama platformunda çalışır.
### Claude Code (Yerli)
```bash
-/plugin marketplace add Lum1104/Understand-Anything
+/plugin marketplace add Egonex-AI/Understand-Anything
/plugin install understand-anything
```
@@ -186,14 +186,14 @@ Understand-Anything birden fazla AI kodlama platformunda çalışır.
**macOS / Linux:**
```bash
-curl -fsSL https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/install.sh | bash
+curl -fsSL https://raw.githubusercontent.com/Egonex-AI/Understand-Anything/main/install.sh | bash
# veya platformu doğrudan geçirerek soruyu atla:
-curl -fsSL https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/install.sh | bash -s codex
+curl -fsSL https://raw.githubusercontent.com/Egonex-AI/Understand-Anything/main/install.sh | bash -s codex
```
**Windows (PowerShell):**
```powershell
-iwr -useb https://raw.githubusercontent.com/Lum1104/Understand-Anything/main/install.ps1 | iex
+iwr -useb https://raw.githubusercontent.com/Egonex-AI/Understand-Anything/main/install.ps1 | iex
```
Kurulum betiği depoyu `~/.understand-anything/repo` dizinine klonlar ve seçilen platform için uygun sembolik bağlantıları oluşturur. Sonrasında CLI/IDE'ni yeniden başlat.
@@ -206,7 +206,7 @@ Kurulum betiği depoyu `~/.understand-anything/repo` dizinine klonlar ve seçile
Bu depo klonlandığında Cursor, eklentiyi `.cursor-plugin/plugin.json` aracılığıyla otomatik olarak keşfeder. Manuel kurulum gerekmez — sadece klonla ve Cursor'da aç.
-Otomatik keşif çalışmazsa manuel kur: **Cursor Settings → Plugins**'i aç, arama alanına `https://github.com/Lum1104/Understand-Anything` yapıştır ve oradan ekle.
+Otomatik keşif çalışmazsa manuel kur: **Cursor Settings → Plugins**'i aç, arama alanına `https://github.com/Egonex-AI/Understand-Anything` yapıştır ve oradan ekle.
### VS Code + GitHub Copilot
@@ -217,7 +217,7 @@ Tüm projelerde kullanmak için kişisel beceri olarak kurmak istersen yukarıda
### Copilot CLI
```bash
-copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
+copilot plugin install Egonex-AI/Understand-Anything:understand-anything-plugin
```
### Platform Uyumluluğu
@@ -245,7 +245,7 @@ copilot plugin install Lum1104/Understand-Anything:understand-anything-plugin
Graf yalnızca bir JSON dosyasıdır — **bir kez commit'leyin, ekip arkadaşlarınız pipeline'ı çalıştırmadan kullansın**. Yeni üye oryantasyonu, PR incelemeleri ve docs-as-code iş akışları için idealdir.
-> **Örnek:** [GoogleCloudPlatform/microservices-demo (fork)](https://github.com/Lum1104/microservices-demo) — commit'lenmiş grafı içeren Go / Java / Python / Node çok dilli referans projesi.
+> **Örnek:** [GoogleCloudPlatform/microservices-demo](https://github.com/GoogleCloudPlatform/microservices-demo) — commit'lenmiş grafı içeren Go / Java / Python / Node çok dilli referans projesi.
**Neyi commit'leyin:** `.understand-anything/` içindeki her şey, *ancak* `intermediate/` ve `diff-overlay.json` hariç (bunlar yerel geçici dosyalardır).
@@ -328,11 +328,11 @@ Büyük değişiklikler için lütfen önce bir issue aç ki yaklaşımı tartı
## Star Geçmişi
-
+
-
-
-
+
+
+
@@ -341,5 +341,5 @@ Büyük değişiklikler için lütfen önce bir issue aç ki yaklaşımı tartı