scopecairn 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +64 -14
  2. package/dist/cli.js +510 -114
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,12 +1,36 @@
1
1
  # ScopeCairn
2
2
 
3
3
  Local-first codebase intelligence layer for AI coding agents.
4
- Builds a knowledge graph of your repository, retrieves only the context
5
- relevant to the current task, analyzes change impact, and enforces
6
- minimal-work scope. No MCP server, no embeddings, no network calls.
4
+ Understand the codebase, then give the agent only what it needs:
5
+ a knowledge graph of the repository, the relevant context per task,
6
+ change impact analysis, and enforced minimal-work scope.
7
+ No MCP server, no embeddings, no network calls, no telemetry.
7
8
 
8
9
  > Understand before exploring. Retrieve before reading.
9
10
 
11
+ ## Features
12
+
13
+ - **Knowledge graph** — files, symbols (functions, classes, methods,
14
+ interfaces, components, routes, models…), and typed relations
15
+ (`IMPORTS`, `CALLS`, `EXTENDS`, `TESTS`, `ROUTES_TO`, `QUERIES`…)
16
+ in local SQLite, fully deterministic.
17
+ - **Graph retrieval, no embeddings** — token-matched seeds → weighted
18
+ expansion → 5-signal ranking (seed, proximity, centrality, git
19
+ recency, co-change). Weights configurable, calibrated by benchmark.
20
+ - **Task scope control** — every complex task returns Required /
21
+ Optional / Protected file lists plus Task Guard rules and a
22
+ Definition of Done, so the agent changes only what matters.
23
+ - **Change impact** — direct, indirect (2-hop), tests, and UI components
24
+ affected by a file or symbol.
25
+ - **Auto-refresh** — `context` and `impact` re-index changed files first;
26
+ run `scan` after editing to close the loop.
27
+ - **Framework adapters** — Next.js (App + Pages Router routes),
28
+ Prisma models, and Drizzle tables/queries detected automatically.
29
+ - **Multi-language** — TypeScript, JavaScript, Python, Java, Go, Rust,
30
+ PHP, C#, C/C++, Ruby, HTML (plus config/schema files as graph nodes).
31
+ - **Multi-agent setup** — one `init` writes rules for AGENTS.md,
32
+ Claude Code, Gemini, Cursor, Windsurf, Copilot, Kiro, or all at once.
33
+
10
34
  ## Install
11
35
 
12
36
  ```bash
@@ -47,24 +71,39 @@ To query manually:
47
71
 
48
72
  ```bash
49
73
  scopecairn context "add approval workflow"
74
+ scopecairn context ./ # repository orientation map
50
75
  scopecairn impact src/request/RequestService.ts
51
76
  scopecairn read symbol approveRequest
52
77
  scopecairn graph RequestService
53
78
  scopecairn doctor
54
79
  ```
55
80
 
81
+ Per-agent installers (instead of `init --agents …`):
82
+
83
+ ```bash
84
+ scopecairn antigravity install # Skill + Workflow + allowlist guide
85
+ scopecairn claude install # CLAUDE.md (also: gemini, cursor, windsurf, copilot, kiro)
86
+ scopecairn claude uninstall # clean removal, user content preserved
87
+ scopecairn agents list
88
+ ```
89
+
56
90
  ## Commands
91
+
57
92
  | Command | Description |
58
93
  |---|---|
59
- | `init` | Initial indexing + generate `AGENTS.md`, Skill, Workflow |
60
- | `scan` / `rebuild` / `clean` | Manual index management |
61
- | `status` / `doctor` | Index status and health checks |
62
- | `context "<task>"` | Main entry point: context + scope (`--escalate`, `--no-refresh`) |
63
- | `impact <path\|symbol>` | Change impact analysis |
94
+ | `init [path]` | Initial indexing + `AGENTS.md`, Skill, Workflow, agent matrix (`--agents …\|all`) |
95
+ | `scan` / `rebuild` | Manual index management (`scan` inkremental; `rebuild` dari nol) |
96
+ | `status` / `doctor` | Index status (incl. adapters, invocations) and health checks |
97
+ | `context "<task>"` | Main entry point: context + scope (`--escalate`, `--no-refresh`); a path task returns an orientation map |
98
+ | `impact <path\|symbol>` | Change impact: direct, indirect, tests, UI, queries, routes |
64
99
  | `graph <symbol>` | Symbol relations |
65
- | `query "<text>"` | Symbol search |
66
- | `read symbol <name>` | Symbol-level source excerpt |
67
- | `benchmark [--tune]` | Retrieval recall and weight calibration |
100
+ | `read symbol <name>` | Symbol-level source excerpt (not whole files) |
101
+ | `<agent> install` | Per-agent setup: `antigravity`, `claude`, `gemini`, `cursor`, `windsurf`, `copilot`, `kiro` (each also `uninstall`; `agents list` to see all) |
102
+ | `benchmark [--tune]` | Retrieval recall, irrelevant ratio, context reduction + weight calibration |
103
+
104
+ Agent commands (`context`, `impact`, `read`, `graph`, `status`, `doctor`)
105
+ are read-only toward source and only write to `.scopecairn/` — safe to
106
+ allowlist. `init`, `scan`, and `rebuild` are manual/user-side commands.
68
107
 
69
108
  ## Framework adapters
70
109
 
@@ -77,11 +116,22 @@ No configuration needed.
77
116
  | `prisma` | `schema.prisma` | `model` symbols for `QUERIES` resolution |
78
117
  | `drizzle` | `drizzle*` paths/config, `db/` schemas | `model` symbols from `pgTable`/`sqliteTable`/`mysqlTable`; `QUERIES` from `db.query.*` relational and `db.select/insert/update/delete` builder calls |
79
118
 
80
- Limits (honest): Server Actions, middleware, and non-Prisma/Drizzle ORMs are not
81
- mapped yet. QUERIES re-derive on every scan so new models resolve without
82
- a rebuild. Contributing a new adapter = one file + one registration line
119
+ Limits (honest): Server Actions, middleware, non-Prisma/Drizzle ORMs,
120
+ inter-table references, raw SQL strings, and fully dynamic table names
121
+ are not mapped yet — those edges are skipped silently, never hallucinated.
122
+ QUERIES re-derive on every scan so new models resolve without a rebuild.
123
+ Contributing a new adapter = one file + one registration line
83
124
  in `src/adapters/index.ts` (see `FrameworkAdapter` in `src/adapters/types.ts`).
84
125
 
126
+ ## Typical loop
127
+
128
+ ```
129
+ prompt → scopecairn context (refresh at start) → agent edits code
130
+ → scopecairn scan (refresh at end, incremental, ~instant)
131
+ ```
132
+
133
+ Measure retrieval quality anytime: `scopecairn benchmark [--tune]`.
134
+
85
135
  ## License
86
136
 
87
137
  MIT — see [LICENSE](./LICENSE).
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { Command } from "commander";
5
- import path16 from "path";
5
+ import path17 from "path";
6
6
 
7
7
  // src/scanner.ts
8
8
  import fs7 from "fs";
@@ -12,9 +12,28 @@ import path8 from "path";
12
12
  var EXT_MAP = {
13
13
  ".ts": "typescript",
14
14
  ".tsx": "typescript",
15
+ ".mts": "typescript",
16
+ ".cts": "typescript",
15
17
  ".js": "javascript",
16
18
  ".jsx": "javascript",
17
- ".py": "python"
19
+ ".mjs": "javascript",
20
+ ".cjs": "javascript",
21
+ ".py": "python",
22
+ ".pyw": "python",
23
+ ".java": "java",
24
+ ".go": "go",
25
+ ".rs": "rust",
26
+ ".php": "php",
27
+ ".cs": "csharp",
28
+ ".cpp": "cpp",
29
+ ".cc": "cpp",
30
+ ".cxx": "cpp",
31
+ ".hpp": "cpp",
32
+ ".h": "c",
33
+ ".c": "c",
34
+ ".rb": "ruby",
35
+ ".html": "html",
36
+ ".htm": "html"
18
37
  };
19
38
  function detectLanguage(filePath) {
20
39
  const dot = filePath.lastIndexOf(".");
@@ -498,6 +517,231 @@ function extractPython(source) {
498
517
  return { symbols, relations };
499
518
  }
500
519
 
520
+ // src/extract/generic.ts
521
+ function lineOf3(text, index) {
522
+ return text.slice(0, index).split("\n").length;
523
+ }
524
+ function stripNoise(src) {
525
+ let out = "";
526
+ let i = 0;
527
+ let quote = null;
528
+ while (i < src.length) {
529
+ const c = src[i];
530
+ const nxt = src[i + 1] ?? "";
531
+ if (quote) {
532
+ if (c === "\\") {
533
+ out += " ";
534
+ i += 2;
535
+ continue;
536
+ }
537
+ if (c === "\n") {
538
+ out += "\n";
539
+ if (quote !== "`") quote = null;
540
+ i++;
541
+ continue;
542
+ }
543
+ if (c === quote) quote = null;
544
+ out += " ";
545
+ i++;
546
+ continue;
547
+ }
548
+ if (c === '"' || c === "'" || c === "`") {
549
+ quote = c;
550
+ out += " ";
551
+ i++;
552
+ continue;
553
+ }
554
+ if (c === "/" && nxt === "/") {
555
+ while (i < src.length && src[i] !== "\n") {
556
+ out += " ";
557
+ i++;
558
+ }
559
+ continue;
560
+ }
561
+ if (c === "/" && nxt === "*") {
562
+ out += " ";
563
+ i += 2;
564
+ while (i < src.length && !(src[i] === "*" && src[i + 1] === "/")) {
565
+ out += src[i] === "\n" ? "\n" : " ";
566
+ i++;
567
+ }
568
+ out += " ";
569
+ i += 2;
570
+ continue;
571
+ }
572
+ if (c === "#") {
573
+ while (i < src.length && src[i] !== "\n") {
574
+ out += " ";
575
+ i++;
576
+ }
577
+ continue;
578
+ }
579
+ out += c;
580
+ i++;
581
+ }
582
+ return out;
583
+ }
584
+ var CALL_KEYWORDS = /* @__PURE__ */ new Set([
585
+ "if",
586
+ "for",
587
+ "while",
588
+ "switch",
589
+ "catch",
590
+ "return",
591
+ "function",
592
+ "class",
593
+ "import",
594
+ "export",
595
+ "new",
596
+ "typeof",
597
+ "instanceof",
598
+ "def",
599
+ "fn",
600
+ "func",
601
+ "elsif",
602
+ "unless",
603
+ "sizeof"
604
+ ]);
605
+ function extractGeneric(source, lang) {
606
+ const symbols = [];
607
+ const relations = [];
608
+ const pushRel = (from, to, rel, confidence = 1) => relations.push({ from, to, rel, weight: WEIGHT[rel], confidence });
609
+ let m;
610
+ const importLine = /^\s*import\s+(.+?)\s*$/gm;
611
+ while ((m = importLine.exec(source)) !== null) {
612
+ const mod = m[1].replace(/["';]/g, "").trim().split(/\s+/)[0];
613
+ if (!mod || mod === "*") continue;
614
+ const ln = lineOf3(source, m.index);
615
+ symbols.push({ name: `import:${mod}`, type: "import", signature: m[0].trim().slice(0, 120), startLine: ln, endLine: ln });
616
+ pushRel("__file__", mod, "IMPORTS", 0.8);
617
+ }
618
+ if (lang === "c" || lang === "cpp" || lang === "csharp") {
619
+ const incRe = /#include\s+[<"]([^>"]+)[>"]/g;
620
+ while ((m = incRe.exec(source)) !== null) {
621
+ const ln = lineOf3(source, m.index);
622
+ symbols.push({ name: `import:${m[1]}`, type: "import", signature: m[0].slice(0, 120), startLine: ln, endLine: ln });
623
+ pushRel("__file__", m[1], "IMPORTS", 0.8);
624
+ }
625
+ }
626
+ if (lang === "php" || lang === "rust") {
627
+ const useRe = lang === "php" ? /^\s*use\s+([\w\\]+)\s*;/gm : /^\s*use\s+([\w:]+)(?:::\s*\{[^}]*\})?\s*;/gm;
628
+ while ((m = useRe.exec(source)) !== null) {
629
+ const ln = lineOf3(source, m.index);
630
+ symbols.push({ name: `import:${m[1]}`, type: "import", signature: m[0].trim().slice(0, 120), startLine: ln, endLine: ln });
631
+ pushRel("__file__", m[1], "IMPORTS", 0.8);
632
+ }
633
+ }
634
+ if (lang === "go") {
635
+ const goImp = /^\s*(?:(\w+)\s+)?["`]([^"`]+)["`]/gm;
636
+ const block = /^\s*import\s*\(\s*$/gm;
637
+ let b;
638
+ while ((b = block.exec(source)) !== null) {
639
+ const end = source.indexOf(")", b.index);
640
+ const body = source.slice(b.index, end === -1 ? b.index + 500 : end);
641
+ let im;
642
+ goImp.lastIndex = 0;
643
+ while ((im = goImp.exec(body)) !== null) {
644
+ const mod = im[2];
645
+ const ln = lineOf3(source, b.index);
646
+ symbols.push({ name: `import:${mod}`, type: "import", signature: mod.slice(0, 120), startLine: ln, endLine: ln });
647
+ pushRel("__file__", mod, "IMPORTS", 0.9);
648
+ }
649
+ }
650
+ }
651
+ const classRe = /(?:class|struct|interface|trait|enum)\s+(\w+)/g;
652
+ const classNames = /* @__PURE__ */ new Set();
653
+ while ((m = classRe.exec(source)) !== null) {
654
+ classNames.add(m[1]);
655
+ const ln = lineOf3(source, m.index);
656
+ symbols.push({ name: m[1], type: "class", signature: m[0].slice(0, 120), startLine: ln, endLine: ln });
657
+ }
658
+ if (lang === "go") {
659
+ const typeRe = /^\s*type\s+(\w+)\s+(struct|interface)\b/gm;
660
+ while ((m = typeRe.exec(source)) !== null) {
661
+ if (classNames.has(m[1])) continue;
662
+ classNames.add(m[1]);
663
+ const ln = lineOf3(source, m.index);
664
+ symbols.push({ name: m[1], type: "class", signature: m[0].trim().slice(0, 120), startLine: ln, endLine: ln });
665
+ }
666
+ }
667
+ const funcNames = /* @__PURE__ */ new Set();
668
+ const addFunc = (name, sig, idx, type = "function") => {
669
+ if (CALL_KEYWORDS.has(name) || classNames.has(name)) return;
670
+ funcNames.add(name);
671
+ const ln = lineOf3(source, idx);
672
+ symbols.push({ name, type, signature: sig.slice(0, 120), startLine: ln, endLine: ln });
673
+ };
674
+ if (lang === "go") {
675
+ const re = /func\s+(?:\([^)]*\)\s+)?(\w+)\s*\(/g;
676
+ while ((m = re.exec(source)) !== null) addFunc(m[1], m[0], m.index);
677
+ } else if (lang === "rust") {
678
+ const re = /fn\s+(\w+)\s*[<(]/g;
679
+ while ((m = re.exec(source)) !== null) addFunc(m[1], m[0], m.index);
680
+ } else if (lang === "ruby") {
681
+ const re = /^\s*def\s+([\w.]+)/gm;
682
+ while ((m = re.exec(source)) !== null) addFunc(m[1], m[0].trim(), m.index);
683
+ } else {
684
+ const re = /(?:^|[{};\n])\s*(?:[\w<>\[\],\s*&:|?]+\s+)?(\w+)\s*\([^();]*\)\s*(?:throws\s+[\w,\s]+)?\{/gm;
685
+ while ((m = re.exec(source)) !== null) {
686
+ if (m[0].includes(".") && !m[0].trimStart().startsWith(m[1])) continue;
687
+ addFunc(m[1], m[0].trim(), m.index);
688
+ }
689
+ }
690
+ const clean = stripNoise(source);
691
+ const funcsByLine = symbols.filter((s) => s.type === "function" || s.type === "method").sort((a, b) => a.startLine - b.startLine);
692
+ const callRe = /(\w+)\s*\(/g;
693
+ let guard = 0;
694
+ while ((m = callRe.exec(clean)) !== null && guard++ < 4e3) {
695
+ const callee = m[1];
696
+ if (CALL_KEYWORDS.has(callee)) continue;
697
+ const ln = lineOf3(clean, m.index);
698
+ let caller = "__file__";
699
+ for (const f of funcsByLine) {
700
+ if (f.startLine <= ln) caller = f.name;
701
+ else break;
702
+ }
703
+ if (caller === callee) continue;
704
+ relations.push({
705
+ from: caller,
706
+ to: callee,
707
+ rel: "CALLS",
708
+ weight: WEIGHT.CALLS,
709
+ confidence: 0.6,
710
+ methodCall: clean[m.index - 1] === "."
711
+ });
712
+ }
713
+ return { symbols, relations };
714
+ }
715
+ function extractHtml(source) {
716
+ const symbols = [];
717
+ const relations = [];
718
+ let m;
719
+ const idRe = /<(\w+)[^>]*\sid\s*=\s*["']([^"']+)["']/gi;
720
+ while ((m = idRe.exec(source)) !== null) {
721
+ const ln = lineOf3(source, m.index);
722
+ symbols.push({
723
+ name: m[2],
724
+ type: "element",
725
+ signature: `<${m[1]} id="${m[2]}">`.slice(0, 120),
726
+ startLine: ln,
727
+ endLine: ln
728
+ });
729
+ }
730
+ const refRe = /<(script|link)[^>]*(src|href)\s*=\s*["']([^"']+)["']/gi;
731
+ while ((m = refRe.exec(source)) !== null) {
732
+ const ln = lineOf3(source, m.index);
733
+ symbols.push({
734
+ name: `import:${m[3]}`,
735
+ type: "import",
736
+ signature: m[0].slice(0, 120),
737
+ startLine: ln,
738
+ endLine: ln
739
+ });
740
+ relations.push({ from: "__file__", to: m[3], rel: "IMPORTS", weight: WEIGHT.IMPORTS, confidence: 0.7 });
741
+ }
742
+ return { symbols, relations };
743
+ }
744
+
501
745
  // src/extract/index.ts
502
746
  function extractFile(relPath, source) {
503
747
  const lang = detectLanguage(relPath);
@@ -508,6 +752,17 @@ function extractFile(relPath, source) {
508
752
  return extractTypeScript(source, relPath.endsWith("x"));
509
753
  case "python":
510
754
  return extractPython(source);
755
+ case "html":
756
+ return extractHtml(source);
757
+ case "java":
758
+ case "go":
759
+ case "rust":
760
+ case "php":
761
+ case "csharp":
762
+ case "cpp":
763
+ case "c":
764
+ case "ruby":
765
+ return extractGeneric(source, lang);
511
766
  default:
512
767
  return { symbols: [], relations: [] };
513
768
  }
@@ -1017,7 +1272,7 @@ function insertRelation(db, sourceId, targetId, rel, weight, confidence) {
1017
1272
  VALUES (?, ?, ?, ?, ?)`
1018
1273
  ).run(sourceId, targetId, rel, weight, confidence);
1019
1274
  }
1020
- function lineOf3(content, index) {
1275
+ function lineOf4(content, index) {
1021
1276
  return content.slice(0, index).split("\n").length;
1022
1277
  }
1023
1278
  function enclosingFunction(db, fileId, line) {
@@ -1121,7 +1376,7 @@ function routeHandlers(content) {
1121
1376
  const re = /export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(/g;
1122
1377
  let m;
1123
1378
  while ((m = re.exec(content)) !== null) {
1124
- out.push({ method: m[1], line: lineOf3(content, m.index) });
1379
+ out.push({ method: m[1], line: lineOf4(content, m.index) });
1125
1380
  }
1126
1381
  return out;
1127
1382
  }
@@ -1154,7 +1409,7 @@ function refreshQueries(db, files) {
1154
1409
  for (const call of prismaCalls(f.content)) {
1155
1410
  const modelId = resolveModel(db, call.model);
1156
1411
  if (!modelId) continue;
1157
- const line = lineOf3(f.content, call.index);
1412
+ const line = lineOf4(f.content, call.index);
1158
1413
  const caller = enclosingFunction(db, f.fileId, line) ?? fileSym;
1159
1414
  if (caller === modelId) continue;
1160
1415
  ins.run(caller, modelId);
@@ -1315,7 +1570,7 @@ function refreshDrizzleQueries(db, files) {
1315
1570
  for (const q of parseDrizzleQueries(f.content)) {
1316
1571
  const target = findModelInFile(db, f.fileId, q.table) ?? findModelGlobal(db, q.table);
1317
1572
  if (!target) continue;
1318
- const caller = enclosingFunction(db, f.fileId, lineOf3(f.content, q.index)) ?? fileSym;
1573
+ const caller = enclosingFunction(db, f.fileId, lineOf4(f.content, q.index)) ?? fileSym;
1319
1574
  if (caller === target) continue;
1320
1575
  ins.run(caller, target);
1321
1576
  n++;
@@ -1603,7 +1858,7 @@ function cmdScan(repoRoot2) {
1603
1858
  console.log("\u2713 Repository detected");
1604
1859
  const stats = scanRepository(repoRoot2);
1605
1860
  console.log(`\u2713 ${stats.totalFilesSeen} files found (non-ignored)`);
1606
- console.log(`\u2713 ${stats.sourceFiles} source files (ts/tsx/js/jsx/py)`);
1861
+ console.log(`\u2713 ${stats.sourceFiles} source files`);
1607
1862
  if (stats.metaFiles > 0) {
1608
1863
  console.log(`\u2713 ${stats.metaFiles} config/schema files (json/yml/sql/prisma, file-level)`);
1609
1864
  }
@@ -1674,20 +1929,18 @@ function cmdStatus(repoRoot2) {
1674
1929
  db.close();
1675
1930
  }
1676
1931
  }
1677
- function cmdClean(repoRoot2) {
1678
- const dir = dataDir(repoRoot2);
1679
- fs8.rmSync(dir, { recursive: true, force: true });
1680
- console.log(`Cleaned ${dir}`);
1681
- }
1682
1932
 
1683
1933
  // src/commands/rebuild.ts
1934
+ import fs9 from "fs";
1684
1935
  function cmdRebuild(repoRoot2) {
1685
- cmdClean(repoRoot2);
1936
+ const dir = dataDir(repoRoot2);
1937
+ fs9.rmSync(dir, { recursive: true, force: true });
1938
+ console.log(`Cleaned ${dir}`);
1686
1939
  cmdScan(repoRoot2);
1687
1940
  }
1688
1941
 
1689
1942
  // src/commands/doctor.ts
1690
- import fs10 from "fs";
1943
+ import fs11 from "fs";
1691
1944
  import path11 from "path";
1692
1945
 
1693
1946
  // src/treesitter.ts
@@ -1713,18 +1966,18 @@ async function checkTreeSitter() {
1713
1966
  }
1714
1967
 
1715
1968
  // src/integrate/detect.ts
1716
- import fs9 from "fs";
1969
+ import fs10 from "fs";
1717
1970
  import path10 from "path";
1718
1971
  function detectAgentDir(repoRoot2) {
1719
- const plural = fs9.existsSync(path10.join(repoRoot2, ".agents"));
1720
- const singular = fs9.existsSync(path10.join(repoRoot2, ".agent"));
1972
+ const plural = fs10.existsSync(path10.join(repoRoot2, ".agents"));
1973
+ const singular = fs10.existsSync(path10.join(repoRoot2, ".agent"));
1721
1974
  if (singular && !plural) return ".agent";
1722
1975
  return ".agents";
1723
1976
  }
1724
1977
  function detectPrefix(repoRoot2) {
1725
1978
  try {
1726
1979
  const pkg = JSON.parse(
1727
- fs9.readFileSync(path10.join(repoRoot2, "package.json"), "utf8")
1980
+ fs10.readFileSync(path10.join(repoRoot2, "package.json"), "utf8")
1728
1981
  );
1729
1982
  if (pkg.dependencies?.["scopecairn"] || pkg.devDependencies?.["scopecairn"]) {
1730
1983
  return "npx scopecairn";
@@ -1754,11 +2007,16 @@ Gunakan hasilnya sebagai titik awal. Jangan menjelajah repository dari nol.
1754
2007
  ## Selama bekerja
1755
2008
  - Ubah hanya file di daftar Required dan Optional.
1756
2009
  - Jangan sentuh file di daftar Protected tanpa persetujuan user.
2010
+ - Bila ternyata lebih dari 2 file harus diubah, panggil ulang konteks penuh:
2011
+ \`${prefix} context --escalate "<permintaan user>"\`
1757
2012
  - Sebelum mengubah simbol yang dipakai banyak tempat, jalankan:
1758
2013
  \`${prefix} impact <path>\`
1759
2014
  - Butuh source sebuah fungsi? Gunakan \`${prefix} read symbol <nama>\`
1760
2015
  alih-alih membaca file penuh.
1761
2016
 
2017
+ ## Setelah selesai
2018
+ Jalankan \`${prefix} scan\` agar indeks ScopeCairn mencerminkan perubahanmu.
2019
+
1762
2020
  ## Pengecualian
1763
2021
  Lewati ScopeCairn hanya untuk pertanyaan non-coding.
1764
2022
  `;
@@ -1792,6 +2050,7 @@ Langkah:
1792
2050
  2. Baca bagian Relevant Files dan Task Scope.
1793
2051
  3. Jika ada perubahan pada simbol bersama, jalankan \`${prefix} impact <path>\`.
1794
2052
  4. Kerjakan task sesuai scope, lalu berhenti saat selesai.
2053
+ 5. Setelah mengedit, jalankan \`${prefix} scan\` agar indeks ScopeCairn mutakhir.
1795
2054
  `;
1796
2055
  }
1797
2056
  function workflowMd(prefix) {
@@ -1809,7 +2068,6 @@ var ALLOWLIST_COMMANDS = [
1809
2068
  "context",
1810
2069
  "impact",
1811
2070
  "read",
1812
- "query",
1813
2071
  "graph",
1814
2072
  "status",
1815
2073
  "doctor"
@@ -1920,22 +2178,22 @@ async function cmdDoctor(repoRoot2) {
1920
2178
  const s = db.prepare(`SELECT COUNT(*) AS n FROM symbols`).get().n;
1921
2179
  const r = db.prepare(`SELECT COUNT(*) AS n FROM relationships`).get().n;
1922
2180
  db.close();
1923
- dbOk = fs10.existsSync(dbPath(repoRoot2));
2181
+ dbOk = fs11.existsSync(dbPath(repoRoot2));
1924
2182
  graphDetail = ` (${s} symbols, ${r} rels)`;
1925
2183
  } catch {
1926
2184
  dbOk = false;
1927
2185
  }
1928
2186
  console.log(`${tick(dbOk)} Database ${dbPath(repoRoot2)}${graphDetail}`);
1929
2187
  const protPath = path11.join(dataDir(repoRoot2), "protected.yml");
1930
- console.log(`${tick(fs10.existsSync(protPath))} Protected ${protPath}`);
2188
+ console.log(`${tick(fs11.existsSync(protPath))} Protected ${protPath}`);
1931
2189
  const expectedPrefix = detectPrefix(repoRoot2);
1932
2190
  const agentDir = detectAgentDir(repoRoot2);
1933
2191
  let prefixOk = true;
1934
2192
  let matrixOk = 0;
1935
2193
  for (const t of AGENT_MATRIX) {
1936
2194
  const abs = path11.join(repoRoot2, t.rel);
1937
- if (!fs10.existsSync(abs)) continue;
1938
- const text = fs10.readFileSync(abs, "utf8");
2195
+ if (!fs11.existsSync(abs)) continue;
2196
+ const text = fs11.readFileSync(abs, "utf8");
1939
2197
  if (!text.includes(MARKER)) continue;
1940
2198
  matrixOk++;
1941
2199
  const used = extractPrefixUsed(text);
@@ -1953,10 +2211,10 @@ async function cmdDoctor(repoRoot2) {
1953
2211
  console.log(`\u2713 Prefix konsisten (\`${expectedPrefix} context\`)`);
1954
2212
  }
1955
2213
  const skillPath = path11.join(repoRoot2, agentDir, "skills", "scopecairn", "SKILL.md");
1956
- const skillOk = fs10.existsSync(skillPath);
2214
+ const skillOk = fs11.existsSync(skillPath);
1957
2215
  console.log(`${tick(skillOk)} Skill (${agentDir}/skills/scopecairn/SKILL.md)`);
1958
2216
  const wfPath = path11.join(repoRoot2, "workflows", "scopecairn.md");
1959
- console.log(`${tick(fs10.existsSync(wfPath))} Workflow (workflows/scopecairn.md, opsional)`);
2217
+ console.log(`${tick(fs11.existsSync(wfPath))} Workflow (workflows/scopecairn.md, opsional)`);
1960
2218
  console.log(`i Strict Mode mengabaikan allowlist \u2014 semua perintah tetap perlu persetujuan.`);
1961
2219
  const healthy = nodeOk && sqliteOk && ts.ok && dbOk && matrixOk > 0 && skillOk && prefixOk;
1962
2220
  console.log(`Status: ${healthy ? "HEALTHY" : "DEGRADED (see \u2717 above)"}`);
@@ -2075,32 +2333,8 @@ function cmdGraph(repoRoot2, key) {
2075
2333
  }
2076
2334
  }
2077
2335
 
2078
- // src/commands/query.ts
2079
- function cmdQuery(repoRoot2, text) {
2080
- const db = openDb(repoRoot2);
2081
- try {
2082
- logInvocation(db, "query");
2083
- const rows = db.prepare(
2084
- `SELECT s.name, s.type, s.signature, f.path AS file
2085
- FROM symbols s JOIN files f ON f.id = s.file_id
2086
- WHERE s.name LIKE ? OR f.path LIKE ? OR s.signature LIKE ?
2087
- ORDER BY CASE WHEN s.name = ? THEN 0 WHEN s.name LIKE ? THEN 1 ELSE 2 END,
2088
- s.name LIMIT 30`
2089
- ).all(`%${text}%`, `%${text}%`, `%${text}%`, text, `${text}%`);
2090
- if (rows.length === 0) {
2091
- console.log(`No matches for "${text}".`);
2092
- return;
2093
- }
2094
- for (const r of rows) {
2095
- console.log(`${r.name} (${r.type}) \u2014 ${r.file}${r.signature ? ` \u2014 ${r.signature.slice(0, 80)}` : ""}`);
2096
- }
2097
- } finally {
2098
- db.close();
2099
- }
2100
- }
2101
-
2102
2336
  // src/commands/read.ts
2103
- import fs11 from "fs";
2337
+ import fs12 from "fs";
2104
2338
  import path12 from "path";
2105
2339
  function cmdRead(repoRoot2, kind, name) {
2106
2340
  if (kind !== "symbol") {
@@ -2120,7 +2354,7 @@ function cmdRead(repoRoot2, kind, name) {
2120
2354
  const abs = path12.join(repoRoot2, node.file);
2121
2355
  let lines;
2122
2356
  try {
2123
- lines = fs11.readFileSync(abs, "utf8").split("\n");
2357
+ lines = fs12.readFileSync(abs, "utf8").split("\n");
2124
2358
  } catch {
2125
2359
  console.log(`Cannot read ${node.file}.`);
2126
2360
  return;
@@ -2425,7 +2659,8 @@ var DEFINITION_OF_DONE = [
2425
2659
  "Test relevan lulus",
2426
2660
  "Tidak ada file berubah tanpa alasan",
2427
2661
  "Tidak ada refactoring tak terkait",
2428
- "Acceptance criteria terpenuhi"
2662
+ "Acceptance criteria terpenuhi",
2663
+ "Index dimutakhirkan (`scopecairn scan`)"
2429
2664
  ];
2430
2665
  function guardBlock() {
2431
2666
  return `## Task Guard
@@ -2545,7 +2780,7 @@ ${tests.map((t) => `- ${t}`).join("\n") || "-"}
2545
2780
  }
2546
2781
 
2547
2782
  // src/commands/orientasi.ts
2548
- import fs12 from "fs";
2783
+ import fs13 from "fs";
2549
2784
  import path13 from "path";
2550
2785
  function isPathLike(task) {
2551
2786
  const t = task.trim().replace(/^["']|["']$/g, "");
@@ -2556,7 +2791,7 @@ function looksLikeRepoPath(db, repoRoot2, task) {
2556
2791
  if (/\s/.test(t)) return false;
2557
2792
  if (isPathLike(task)) return true;
2558
2793
  try {
2559
- fs12.statSync(path13.resolve(repoRoot2, t));
2794
+ fs13.statSync(path13.resolve(repoRoot2, t));
2560
2795
  return true;
2561
2796
  } catch {
2562
2797
  }
@@ -2595,7 +2830,7 @@ function cmdOrientasi(repoRoot2, task) {
2595
2830
  const abs = path13.resolve(repoRoot2, t);
2596
2831
  let rel = null;
2597
2832
  try {
2598
- const stat = fs12.statSync(abs);
2833
+ const stat = fs13.statSync(abs);
2599
2834
  if (stat.isFile()) rel = path13.relative(repoRoot2, abs).split(path13.sep).join("/");
2600
2835
  } catch {
2601
2836
  rel = null;
@@ -2846,31 +3081,152 @@ function cmdImpact(repoRoot2, target, opts = {}) {
2846
3081
  }
2847
3082
 
2848
3083
  // src/commands/init.ts
2849
- import fs13 from "fs";
3084
+ import fs15 from "fs";
3085
+ import path15 from "path";
3086
+
3087
+ // src/integrate/install.ts
3088
+ import fs14 from "fs";
2850
3089
  import path14 from "path";
2851
- function writeTarget(repoRoot2, target, prefix) {
3090
+ function installTarget(repoRoot2, target, prefix) {
2852
3091
  const abs = path14.join(repoRoot2, target.rel);
2853
3092
  const block = target.render(prefix);
3093
+ const finish = (how) => ({
3094
+ agent: target.id,
3095
+ rel: target.rel,
3096
+ how
3097
+ });
2854
3098
  if (target.mode === "new") {
2855
- const existed = fs13.existsSync(abs);
2856
- fs13.mkdirSync(path14.dirname(abs), { recursive: true });
2857
- fs13.writeFileSync(abs, block.endsWith("\n") ? block : block + "\n");
2858
- return existed ? "updated" : "generated";
3099
+ const existed = fs14.existsSync(abs);
3100
+ fs14.mkdirSync(path14.dirname(abs), { recursive: true });
3101
+ fs14.writeFileSync(abs, block.endsWith("\n") ? block : block + "\n");
3102
+ return finish(existed ? "updated" : "generated");
2859
3103
  }
2860
- if (fs13.existsSync(abs)) {
2861
- const cur = fs13.readFileSync(abs, "utf8");
3104
+ if (fs14.existsSync(abs)) {
3105
+ const cur = fs14.readFileSync(abs, "utf8");
2862
3106
  if (cur.includes(MARKER)) {
2863
- const re = new RegExp(`${MARKER}[\\s\\S]*?(?=\\n#[^#]|$)`, "");
2864
- fs13.writeFileSync(abs, cur.replace(re, block.trimEnd()).trimEnd() + "\n");
2865
- return "updated";
3107
+ const { next: next2 } = removeManagedBlock(cur, (p) => target.render(p));
3108
+ fs14.writeFileSync(abs, (next2 + "\n\n" + block).trimEnd() + "\n");
3109
+ return finish("updated");
3110
+ }
3111
+ const { next, changed } = removeManagedBlock(cur, (p) => target.render(p));
3112
+ const base = changed ? next : cur;
3113
+ fs14.writeFileSync(abs, (base.trimEnd() + "\n\n" + block).trimEnd() + "\n");
3114
+ return finish("extended");
3115
+ }
3116
+ fs14.mkdirSync(path14.dirname(abs), { recursive: true });
3117
+ fs14.writeFileSync(abs, block.endsWith("\n") ? block : block + "\n");
3118
+ return finish("generated");
3119
+ }
3120
+ function removeManagedBlock(content, render) {
3121
+ for (const p of ["scopecairn", "npx scopecairn"]) {
3122
+ for (const cand of [render(p), render(p).trimEnd(), render(p).trimEnd() + "\n"]) {
3123
+ if (cand && content.includes(cand)) {
3124
+ return { next: foldBlank(content.split(cand).join("")), changed: true };
3125
+ }
2866
3126
  }
2867
- fs13.writeFileSync(abs, cur.trimEnd() + "\n\n" + block);
2868
- return "extended";
2869
3127
  }
2870
- fs13.mkdirSync(path14.dirname(abs), { recursive: true });
2871
- fs13.writeFileSync(abs, block.endsWith("\n") ? block : block + "\n");
2872
- return "generated";
3128
+ const anchored = new RegExp(
3129
+ `\\n*${MARKER}\\n# ScopeCairn[\\s\\S]*?pertanyaan non-coding\\.[ \\t]*(\\n|$)`,
3130
+ ""
3131
+ );
3132
+ if (anchored.test(content)) {
3133
+ return { next: foldBlank(content.replace(anchored, "\n")), changed: true };
3134
+ }
3135
+ const orphan = /# ScopeCairn[\s\S]*?pertanyaan non-coding\.[ \t]*(\n|$)/g;
3136
+ if (orphan.test(content)) {
3137
+ return { next: foldBlank(content.replace(orphan, "")), changed: true };
3138
+ }
3139
+ return { next: content, changed: false };
3140
+ }
3141
+ function foldBlank(s) {
3142
+ return s.replace(/\n{3,}/g, "\n\n").trim();
2873
3143
  }
3144
+ function uninstallTarget(repoRoot2, target) {
3145
+ const abs = path14.join(repoRoot2, target.rel);
3146
+ const finish = (how) => ({
3147
+ agent: target.id,
3148
+ rel: target.rel,
3149
+ how
3150
+ });
3151
+ if (!fs14.existsSync(abs)) return finish("absent");
3152
+ if (target.mode === "new") {
3153
+ fs14.rmSync(abs, { force: true });
3154
+ pruneEmptyDirs(path14.dirname(abs), repoRoot2);
3155
+ return finish("removed");
3156
+ }
3157
+ const cur = fs14.readFileSync(abs, "utf8");
3158
+ if (!cur.includes(MARKER)) return finish("skipped");
3159
+ const render = (p) => {
3160
+ const t = resolveAgent(target.id);
3161
+ return t ? t.render(p) : "";
3162
+ };
3163
+ const { next, changed } = removeManagedBlock(cur, render);
3164
+ if (!changed) return finish("skipped");
3165
+ if (!next) {
3166
+ fs14.rmSync(abs, { force: true });
3167
+ return finish("removed");
3168
+ }
3169
+ fs14.writeFileSync(abs, next + "\n");
3170
+ return finish("removed");
3171
+ }
3172
+ function pruneEmptyDirs(dir, stop) {
3173
+ let cur = dir;
3174
+ while (cur.startsWith(stop) && cur !== stop) {
3175
+ try {
3176
+ fs14.rmdirSync(cur);
3177
+ } catch {
3178
+ return;
3179
+ }
3180
+ cur = path14.dirname(cur);
3181
+ }
3182
+ }
3183
+ function installAntigravity(repoRoot2, prefix = detectPrefix(repoRoot2)) {
3184
+ const agentDir = detectAgentDir(repoRoot2);
3185
+ const out = [];
3186
+ const skillPath = path14.join(repoRoot2, agentDir, "skills", "scopecairn", "SKILL.md");
3187
+ fs14.mkdirSync(path14.dirname(skillPath), { recursive: true });
3188
+ fs14.writeFileSync(skillPath, skillMd(prefix));
3189
+ out.push(`Skill installed (${path14.relative(repoRoot2, skillPath)})`);
3190
+ const wfPath = path14.join(repoRoot2, "workflows", "scopecairn.md");
3191
+ fs14.mkdirSync(path14.dirname(wfPath), { recursive: true });
3192
+ fs14.writeFileSync(wfPath, workflowMd(prefix));
3193
+ out.push(`Workflow installed (${path14.relative(repoRoot2, wfPath)})`);
3194
+ return out;
3195
+ }
3196
+ function uninstallAntigravity(repoRoot2) {
3197
+ const out = [];
3198
+ for (const dir of [".agents", ".agent"]) {
3199
+ const p = path14.join(repoRoot2, dir, "skills", "scopecairn", "SKILL.md");
3200
+ if (fs14.existsSync(p)) {
3201
+ fs14.rmSync(p, { force: true });
3202
+ pruneEmptyDirs(path14.dirname(p), repoRoot2);
3203
+ out.push(`Skill removed (${path14.relative(repoRoot2, p)})`);
3204
+ }
3205
+ }
3206
+ const wf = path14.join(repoRoot2, "workflows", "scopecairn.md");
3207
+ if (fs14.existsSync(wf)) {
3208
+ const text = fs14.readFileSync(wf, "utf8");
3209
+ if (text.includes("scopecairn")) {
3210
+ fs14.rmSync(wf, { force: true });
3211
+ out.push(`Workflow removed (${path14.relative(repoRoot2, wf)})`);
3212
+ } else {
3213
+ out.push(`Workflow skipped (bukan milik ScopeCairn)`);
3214
+ }
3215
+ }
3216
+ if (out.length === 0) out.push("Nothing installed.");
3217
+ return out;
3218
+ }
3219
+ function agentIds() {
3220
+ return ["antigravity", ...AGENT_MATRIX.map((t) => t.id)];
3221
+ }
3222
+ function allowlistText(repoRoot2) {
3223
+ return allowlistGuide(detectPrefix(repoRoot2));
3224
+ }
3225
+ function resolveAgent(id) {
3226
+ return findTarget(id);
3227
+ }
3228
+
3229
+ // src/commands/init.ts
2874
3230
  function cmdInit(repoRoot2, opts = {}) {
2875
3231
  console.log("ScopeCairn");
2876
3232
  console.log("\u2713 Repository detected");
@@ -2879,49 +3235,43 @@ function cmdInit(repoRoot2, opts = {}) {
2879
3235
  if (stats.metaFiles > 0) console.log(`\u2713 ${stats.metaFiles} config/schema files`);
2880
3236
  console.log(`\u2713 ${stats.symbols} symbols`);
2881
3237
  console.log(`\u2713 ${stats.relationships} relationships`);
3238
+ if (stats.adapters.length > 0) console.log(`\u2713 adapters: ${stats.adapters.join(", ")}`);
2882
3239
  const prefix = detectPrefix(repoRoot2);
2883
- const agentDir = detectAgentDir(repoRoot2);
2884
3240
  const requested = parseAgentsOption(opts.agents);
2885
- let targets;
2886
- if (requested === "all") {
2887
- targets = [...AGENT_MATRIX];
2888
- } else if (requested && requested.length > 0) {
2889
- targets = [];
2890
- for (const id of requested) {
2891
- const t = findTarget(id);
2892
- if (!t) {
2893
- console.log(`! Unknown agent "${id}" (pilihan: ${AGENT_MATRIX.map((x) => x.id).join(", ")}, all)`);
2894
- continue;
2895
- }
2896
- targets.push(t);
2897
- }
2898
- if (!targets.includes(AGENT_MATRIX[0])) targets.unshift(AGENT_MATRIX[0]);
2899
- } else {
2900
- targets = [AGENT_MATRIX[0]];
2901
- for (const t of AGENT_MATRIX.slice(1)) {
2902
- if (fs13.existsSync(path14.join(repoRoot2, t.presentMarker))) targets.push(t);
2903
- }
2904
- }
3241
+ const targets = requested === "all" ? [...AGENT_MATRIX] : requested && requested.length > 0 ? withCore(requested) : withDetected(repoRoot2);
2905
3242
  for (const t of targets) {
2906
- const how = writeTarget(repoRoot2, t, prefix);
2907
- console.log(`\u2713 ${t.label}: ${t.rel} (${how})`);
3243
+ const r = installTarget(repoRoot2, t, prefix);
3244
+ console.log(`\u2713 ${t.label}: ${r.rel} (${r.how})`);
2908
3245
  }
2909
- const skillPath = path14.join(repoRoot2, agentDir, "skills", "scopecairn", "SKILL.md");
2910
- fs13.mkdirSync(path14.dirname(skillPath), { recursive: true });
2911
- fs13.writeFileSync(skillPath, skillMd(prefix));
2912
- console.log(`\u2713 Skill installed (${path14.relative(repoRoot2, skillPath)})`);
2913
- const wfPath = path14.join(repoRoot2, "workflows", "scopecairn.md");
2914
- fs13.mkdirSync(path14.dirname(wfPath), { recursive: true });
2915
- fs13.writeFileSync(wfPath, workflowMd(prefix));
2916
- console.log(`\u2713 Workflow installed (${path14.relative(repoRoot2, wfPath)})`);
3246
+ for (const line of installAntigravity(repoRoot2, prefix)) console.log(`\u2713 ${line}`);
2917
3247
  console.log("Ready.");
2918
3248
  console.log("");
2919
- console.log(allowlistGuide(prefix));
3249
+ console.log(allowlistText(repoRoot2));
3250
+ }
3251
+ function withCore(ids) {
3252
+ const out = [];
3253
+ for (const id of ids) {
3254
+ const t = findTarget(id);
3255
+ if (!t) {
3256
+ console.log(`! Unknown agent "${id}" (pilihan: ${AGENT_MATRIX.map((x) => x.id).join(", ")}, all)`);
3257
+ continue;
3258
+ }
3259
+ out.push(t);
3260
+ }
3261
+ if (!out.includes(AGENT_MATRIX[0])) out.unshift(AGENT_MATRIX[0]);
3262
+ return out;
3263
+ }
3264
+ function withDetected(repoRoot2) {
3265
+ const targets = [AGENT_MATRIX[0]];
3266
+ for (const t of AGENT_MATRIX.slice(1)) {
3267
+ if (fs15.existsSync(path15.join(repoRoot2, t.presentMarker))) targets.push(t);
3268
+ }
3269
+ return targets;
2920
3270
  }
2921
3271
 
2922
3272
  // src/benchmark/run.ts
2923
- import fs14 from "fs";
2924
- import path15 from "path";
3273
+ import fs16 from "fs";
3274
+ import path16 from "path";
2925
3275
 
2926
3276
  // src/benchmark/dataset.ts
2927
3277
  var BENCHMARK_TASKS = [
@@ -2978,7 +3328,7 @@ var BENCHMARK_TASKS = [
2978
3328
  // src/benchmark/run.ts
2979
3329
  function fileChars(repoRoot2, rel) {
2980
3330
  try {
2981
- return fs14.readFileSync(path15.join(repoRoot2, rel), "utf8").length;
3331
+ return fs16.readFileSync(path16.join(repoRoot2, rel), "utf8").length;
2982
3332
  } catch {
2983
3333
  return 0;
2984
3334
  }
@@ -3127,10 +3477,42 @@ function cmdBenchmark(repoRoot2, opts = {}) {
3127
3477
  }
3128
3478
  }
3129
3479
 
3480
+ // src/commands/agents.ts
3481
+ function registerAgentCommands(program2, repoRoot2) {
3482
+ const list = program2.command("agents").description("List supported agent installers");
3483
+ list.command("list").description("Show agent ids").action(() => {
3484
+ console.log(agentIds().join("\n"));
3485
+ });
3486
+ const anti = program2.command("antigravity").description("Antigravity IDE integration");
3487
+ anti.command("install").description("Install Skill + Workflow, print allowlist guide").action(() => {
3488
+ const root = repoRoot2();
3489
+ const prefix = detectPrefix(root);
3490
+ for (const line of installAntigravity(root, prefix)) console.log(`\u2713 ${line}`);
3491
+ console.log("");
3492
+ console.log(allowlistText(root));
3493
+ });
3494
+ anti.command("uninstall").description("Remove Skill + Workflow").action(() => {
3495
+ for (const line of uninstallAntigravity(repoRoot2())) console.log(`\u2713 ${line}`);
3496
+ });
3497
+ for (const t of AGENT_MATRIX) {
3498
+ if (t.id === "agents") continue;
3499
+ const cmd = program2.command(t.id).description(`${t.label} integration`);
3500
+ cmd.command("install").description(`Write ${t.rel}`).action(() => {
3501
+ const root = repoRoot2();
3502
+ const r = installTarget(root, t, detectPrefix(root));
3503
+ console.log(`\u2713 ${t.label}: ${r.rel} (${r.how})`);
3504
+ });
3505
+ cmd.command("uninstall").description(`Remove ScopeCairn block from ${t.rel}`).action(() => {
3506
+ const r = uninstallTarget(repoRoot2(), t);
3507
+ console.log(`\u2713 ${t.label}: ${r.rel} (${r.how})`);
3508
+ });
3509
+ }
3510
+ }
3511
+
3130
3512
  // package.json
3131
3513
  var package_default = {
3132
3514
  name: "scopecairn",
3133
- version: "0.2.1",
3515
+ version: "0.3.0",
3134
3516
  description: "ScopeCairn \u2014 Codebase Intelligence & Agent Optimization Layer (local-first)",
3135
3517
  license: "MIT",
3136
3518
  type: "module",
@@ -3180,23 +3562,36 @@ var package_default = {
3180
3562
 
3181
3563
  // src/cli.ts
3182
3564
  var program = new Command();
3183
- program.name("scopecairn").description("ScopeCairn \u2014 local-first codebase intelligence layer").version(package_default.version);
3565
+ program.name("scopecairn").description("ScopeCairn \u2014 local-first codebase intelligence layer").version(package_default.version).addHelpText(
3566
+ "after",
3567
+ `
3568
+ Examples:
3569
+ $ scopecairn init index repo + setup agent integration
3570
+ $ scopecairn init ./myapp --agents all setup another directory, all agents
3571
+ $ scopecairn context "add login" relevant context + scope for a task
3572
+ $ scopecairn context ./ repository orientation map
3573
+ $ scopecairn impact src/auth/login.ts change impact analysis
3574
+ $ scopecairn read symbol approveRequest symbol-level source excerpt
3575
+ $ scopecairn doctor health + integration check
3576
+
3577
+ Agent commands (context, impact, read, graph, status, doctor) are
3578
+ read-only toward source and only write to .scopecairn/ \u2014 safe to allowlist.
3579
+ `
3580
+ );
3184
3581
  function repoRoot() {
3185
- return path16.resolve(process.cwd());
3582
+ return path17.resolve(process.cwd());
3186
3583
  }
3187
3584
  function resolveRoot(given) {
3188
3585
  if (!given) return repoRoot();
3189
- return path16.resolve(process.cwd(), given);
3586
+ return path17.resolve(process.cwd(), given);
3190
3587
  }
3191
- program.command("scan").description("Index repository (Fase 1: files + hash + language)").action(() => cmdScan(repoRoot()));
3588
+ program.command("scan").description("Index repository (files + symbols + relations)").action(() => cmdScan(repoRoot()));
3192
3589
  program.command("status").description("Show index status").action(() => cmdStatus(repoRoot()));
3193
- program.command("clean").description("Remove generated index data").action(() => cmdClean(repoRoot()));
3194
3590
  program.command("rebuild").description("Rebuild index from scratch (recover from corrupt index)").action(() => cmdRebuild(repoRoot()));
3195
3591
  program.command("doctor").description("Check integration health").action(async () => {
3196
3592
  await cmdDoctor(repoRoot());
3197
3593
  });
3198
- program.command("graph <symbol>").description("Show relations of a symbol (Fase 2: 1-hop traversal)").action((symbol) => cmdGraph(repoRoot(), symbol));
3199
- program.command("query <text>").description("Search symbols/codebase knowledge (substring match)").action((text) => cmdQuery(repoRoot(), text));
3594
+ program.command("graph <symbol>").description("Show relations of a symbol").action((symbol) => cmdGraph(repoRoot(), symbol));
3200
3595
  program.command("read").argument("<kind>", "what to read (symbol)").argument("<name>", "symbol name").description("Read a symbol's source excerpt (not the whole file)").action((kind, name) => cmdRead(repoRoot(), kind, name));
3201
3596
  program.command("context <task>").description("Main entry point: auto-refresh + classify + relevant context + scope").option("--escalate", "force full COMPLEX context").option("--no-refresh", "skip incremental refresh (debugging/benchmark)").action(
3202
3597
  (task, opts) => cmdContext(repoRoot(), task, opts)
@@ -3210,6 +3605,7 @@ program.command("init [path]").description("Initial indexing + generate AGENTS.m
3210
3605
  program.command("benchmark").description("Retrieval recall, irrelevant ratio, context reduction + weight tuning").option("--tune", "grid search bobot FR-05").option("--no-refresh", "skip incremental refresh (debugging/benchmark)").action(
3211
3606
  (opts) => cmdBenchmark(repoRoot(), opts)
3212
3607
  );
3608
+ registerAgentCommands(program, repoRoot);
3213
3609
  program.parseAsync(process.argv);
3214
3610
  export {
3215
3611
  resolveRoot
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scopecairn",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "ScopeCairn — Codebase Intelligence & Agent Optimization Layer (local-first)",
5
5
  "license": "MIT",
6
6
  "type": "module",