scopecairn 0.2.0 → 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 +625 -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 path15 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
@@ -2544,6 +2779,116 @@ ${tests.map((t) => `- ${t}`).join("\n") || "-"}
2544
2779
  return { markdown: md, high: highFiles, medium: medFiles, tests, dependencies: deps };
2545
2780
  }
2546
2781
 
2782
+ // src/commands/orientasi.ts
2783
+ import fs13 from "fs";
2784
+ import path13 from "path";
2785
+ function isPathLike(task) {
2786
+ const t = task.trim().replace(/^["']|["']$/g, "");
2787
+ return t === "." || t === "./" || t === "/" || t.startsWith("./") || t.startsWith("../") || t.startsWith("/") || t.startsWith("~");
2788
+ }
2789
+ function looksLikeRepoPath(db, repoRoot2, task) {
2790
+ const t = task.trim().replace(/^["']|["']$/g, "");
2791
+ if (/\s/.test(t)) return false;
2792
+ if (isPathLike(task)) return true;
2793
+ try {
2794
+ fs13.statSync(path13.resolve(repoRoot2, t));
2795
+ return true;
2796
+ } catch {
2797
+ }
2798
+ try {
2799
+ const row = db.prepare(`SELECT 1 AS ok FROM files WHERE path = ? OR path LIKE ? LIMIT 1`).get(t, `%/${t}`);
2800
+ return !!row;
2801
+ } catch {
2802
+ return false;
2803
+ }
2804
+ }
2805
+ function topCentral(db, limit = 10) {
2806
+ try {
2807
+ return db.prepare(
2808
+ `SELECT s.name, s.type, f.path AS file, COUNT(*) AS fanin
2809
+ FROM relationships r
2810
+ JOIN symbols s ON s.id = r.target_id
2811
+ JOIN files f ON f.id = s.file_id
2812
+ WHERE s.type IN ('function','class','method','component','interface','type')
2813
+ GROUP BY r.target_id ORDER BY fanin DESC LIMIT ${limit}`
2814
+ ).all();
2815
+ } catch {
2816
+ return [];
2817
+ }
2818
+ }
2819
+ function cmdOrientasi(repoRoot2, task) {
2820
+ const db = openDb(repoRoot2);
2821
+ try {
2822
+ const files = db.prepare(`SELECT COUNT(*) AS n FROM files`).get().n;
2823
+ if (files === 0) {
2824
+ console.log("Index kosong. Jalankan `scopecairn scan` dulu.");
2825
+ return;
2826
+ }
2827
+ const byLang = db.prepare(`SELECT language, COUNT(*) AS n FROM files GROUP BY language ORDER BY n DESC`).all();
2828
+ const symbols = db.prepare(`SELECT COUNT(*) AS n FROM symbols`).get().n;
2829
+ const t = task.trim().replace(/^["']|["']$/g, "");
2830
+ const abs = path13.resolve(repoRoot2, t);
2831
+ let rel = null;
2832
+ try {
2833
+ const stat = fs13.statSync(abs);
2834
+ if (stat.isFile()) rel = path13.relative(repoRoot2, abs).split(path13.sep).join("/");
2835
+ } catch {
2836
+ rel = null;
2837
+ }
2838
+ if (rel) {
2839
+ const row = db.prepare(`SELECT id FROM files WHERE path = ?`).get(rel);
2840
+ if (row) {
2841
+ const syms = db.prepare(`SELECT name, type FROM symbols WHERE file_id = ? AND type != 'file' LIMIT 20`).all(row.id);
2842
+ const fileSym = db.prepare(`SELECT id FROM symbols WHERE file_id = ? AND type = 'file'`).get(row.id);
2843
+ const linked = /* @__PURE__ */ new Set();
2844
+ if (fileSym) {
2845
+ for (const e of neighbors(db, fileSym.id, 30)) {
2846
+ if (e.other.file !== rel) linked.add(e.other.file);
2847
+ }
2848
+ }
2849
+ console.log(
2850
+ `# Task Context (orientasi)
2851
+
2852
+ ## Task
2853
+ ${task}
2854
+
2855
+ ## File
2856
+ ${rel}
2857
+
2858
+ ## Simbol (${syms.length})
2859
+ ${syms.map((s) => `- ${s.name} (${s.type})`).join("\n") || "-"}
2860
+
2861
+ ## Terkait langsung
2862
+ ${[...linked].slice(0, 10).map((f) => `- ${f}`).join("\n") || "-"}
2863
+
2864
+ Butuh konteks kerja? Jalankan \`scopecairn context "<deskripsi task>"\`.
2865
+ `
2866
+ );
2867
+ return;
2868
+ }
2869
+ }
2870
+ const central = topCentral(db);
2871
+ console.log(
2872
+ `# Task Context (orientasi)
2873
+
2874
+ ## Task
2875
+ ${task}
2876
+
2877
+ ## Ringkasan index
2878
+ - ${files} file (${byLang.map((l) => `${l.language} ${l.n}`).join(", ")})
2879
+ - ${symbols} simbol
2880
+
2881
+ ## Simbol paling dipakai
2882
+ ${central.map((c) => `- ${c.name} (${c.type}, ${c.file}) \u2190 ${c.fanin}x`).join("\n") || "-"}
2883
+
2884
+ Mulai dari simbol di atas, atau jalankan \`scopecairn context "<deskripsi task>"\` untuk konteks kerja.
2885
+ `
2886
+ );
2887
+ } finally {
2888
+ db.close();
2889
+ }
2890
+ }
2891
+
2547
2892
  // src/commands/context.ts
2548
2893
  function cmdContext(repoRoot2, task, opts = {}) {
2549
2894
  if (!opts.noRefresh) {
@@ -2562,6 +2907,11 @@ function cmdContext(repoRoot2, task, opts = {}) {
2562
2907
  console.log("Index kosong. Jalankan `scopecairn scan` dulu.");
2563
2908
  return;
2564
2909
  }
2910
+ if (looksLikeRepoPath(db, repoRoot2, task)) {
2911
+ logInvocation(db, "context");
2912
+ cmdOrientasi(repoRoot2, task);
2913
+ return;
2914
+ }
2565
2915
  const result = retrieve(db, repoRoot2, task);
2566
2916
  if (result.ranked.length === 0) {
2567
2917
  logInvocation(db, "context");
@@ -2731,31 +3081,152 @@ function cmdImpact(repoRoot2, target, opts = {}) {
2731
3081
  }
2732
3082
 
2733
3083
  // src/commands/init.ts
2734
- import fs12 from "fs";
2735
- import path13 from "path";
2736
- function writeTarget(repoRoot2, target, prefix) {
2737
- const abs = path13.join(repoRoot2, target.rel);
3084
+ import fs15 from "fs";
3085
+ import path15 from "path";
3086
+
3087
+ // src/integrate/install.ts
3088
+ import fs14 from "fs";
3089
+ import path14 from "path";
3090
+ function installTarget(repoRoot2, target, prefix) {
3091
+ const abs = path14.join(repoRoot2, target.rel);
2738
3092
  const block = target.render(prefix);
3093
+ const finish = (how) => ({
3094
+ agent: target.id,
3095
+ rel: target.rel,
3096
+ how
3097
+ });
2739
3098
  if (target.mode === "new") {
2740
- const existed = fs12.existsSync(abs);
2741
- fs12.mkdirSync(path13.dirname(abs), { recursive: true });
2742
- fs12.writeFileSync(abs, block.endsWith("\n") ? block : block + "\n");
2743
- 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");
2744
3103
  }
2745
- if (fs12.existsSync(abs)) {
2746
- const cur = fs12.readFileSync(abs, "utf8");
3104
+ if (fs14.existsSync(abs)) {
3105
+ const cur = fs14.readFileSync(abs, "utf8");
2747
3106
  if (cur.includes(MARKER)) {
2748
- const re = new RegExp(`${MARKER}[\\s\\S]*?(?=\\n#[^#]|$)`, "");
2749
- fs12.writeFileSync(abs, cur.replace(re, block.trimEnd()).trimEnd() + "\n");
2750
- 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
+ }
2751
3126
  }
2752
- fs12.writeFileSync(abs, cur.trimEnd() + "\n\n" + block);
2753
- return "extended";
2754
3127
  }
2755
- fs12.mkdirSync(path13.dirname(abs), { recursive: true });
2756
- fs12.writeFileSync(abs, block.endsWith("\n") ? block : block + "\n");
2757
- 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();
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));
2758
3224
  }
3225
+ function resolveAgent(id) {
3226
+ return findTarget(id);
3227
+ }
3228
+
3229
+ // src/commands/init.ts
2759
3230
  function cmdInit(repoRoot2, opts = {}) {
2760
3231
  console.log("ScopeCairn");
2761
3232
  console.log("\u2713 Repository detected");
@@ -2764,49 +3235,43 @@ function cmdInit(repoRoot2, opts = {}) {
2764
3235
  if (stats.metaFiles > 0) console.log(`\u2713 ${stats.metaFiles} config/schema files`);
2765
3236
  console.log(`\u2713 ${stats.symbols} symbols`);
2766
3237
  console.log(`\u2713 ${stats.relationships} relationships`);
3238
+ if (stats.adapters.length > 0) console.log(`\u2713 adapters: ${stats.adapters.join(", ")}`);
2767
3239
  const prefix = detectPrefix(repoRoot2);
2768
- const agentDir = detectAgentDir(repoRoot2);
2769
3240
  const requested = parseAgentsOption(opts.agents);
2770
- let targets;
2771
- if (requested === "all") {
2772
- targets = [...AGENT_MATRIX];
2773
- } else if (requested && requested.length > 0) {
2774
- targets = [];
2775
- for (const id of requested) {
2776
- const t = findTarget(id);
2777
- if (!t) {
2778
- console.log(`! Unknown agent "${id}" (pilihan: ${AGENT_MATRIX.map((x) => x.id).join(", ")}, all)`);
2779
- continue;
2780
- }
2781
- targets.push(t);
2782
- }
2783
- if (!targets.includes(AGENT_MATRIX[0])) targets.unshift(AGENT_MATRIX[0]);
2784
- } else {
2785
- targets = [AGENT_MATRIX[0]];
2786
- for (const t of AGENT_MATRIX.slice(1)) {
2787
- if (fs12.existsSync(path13.join(repoRoot2, t.presentMarker))) targets.push(t);
2788
- }
2789
- }
3241
+ const targets = requested === "all" ? [...AGENT_MATRIX] : requested && requested.length > 0 ? withCore(requested) : withDetected(repoRoot2);
2790
3242
  for (const t of targets) {
2791
- const how = writeTarget(repoRoot2, t, prefix);
2792
- console.log(`\u2713 ${t.label}: ${t.rel} (${how})`);
2793
- }
2794
- const skillPath = path13.join(repoRoot2, agentDir, "skills", "scopecairn", "SKILL.md");
2795
- fs12.mkdirSync(path13.dirname(skillPath), { recursive: true });
2796
- fs12.writeFileSync(skillPath, skillMd(prefix));
2797
- console.log(`\u2713 Skill installed (${path13.relative(repoRoot2, skillPath)})`);
2798
- const wfPath = path13.join(repoRoot2, "workflows", "scopecairn.md");
2799
- fs12.mkdirSync(path13.dirname(wfPath), { recursive: true });
2800
- fs12.writeFileSync(wfPath, workflowMd(prefix));
2801
- console.log(`\u2713 Workflow installed (${path13.relative(repoRoot2, wfPath)})`);
3243
+ const r = installTarget(repoRoot2, t, prefix);
3244
+ console.log(`\u2713 ${t.label}: ${r.rel} (${r.how})`);
3245
+ }
3246
+ for (const line of installAntigravity(repoRoot2, prefix)) console.log(`\u2713 ${line}`);
2802
3247
  console.log("Ready.");
2803
3248
  console.log("");
2804
- 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;
2805
3270
  }
2806
3271
 
2807
3272
  // src/benchmark/run.ts
2808
- import fs13 from "fs";
2809
- import path14 from "path";
3273
+ import fs16 from "fs";
3274
+ import path16 from "path";
2810
3275
 
2811
3276
  // src/benchmark/dataset.ts
2812
3277
  var BENCHMARK_TASKS = [
@@ -2863,7 +3328,7 @@ var BENCHMARK_TASKS = [
2863
3328
  // src/benchmark/run.ts
2864
3329
  function fileChars(repoRoot2, rel) {
2865
3330
  try {
2866
- return fs13.readFileSync(path14.join(repoRoot2, rel), "utf8").length;
3331
+ return fs16.readFileSync(path16.join(repoRoot2, rel), "utf8").length;
2867
3332
  } catch {
2868
3333
  return 0;
2869
3334
  }
@@ -3012,10 +3477,42 @@ function cmdBenchmark(repoRoot2, opts = {}) {
3012
3477
  }
3013
3478
  }
3014
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
+
3015
3512
  // package.json
3016
3513
  var package_default = {
3017
3514
  name: "scopecairn",
3018
- version: "0.2.0",
3515
+ version: "0.3.0",
3019
3516
  description: "ScopeCairn \u2014 Codebase Intelligence & Agent Optimization Layer (local-first)",
3020
3517
  license: "MIT",
3021
3518
  type: "module",
@@ -3065,23 +3562,36 @@ var package_default = {
3065
3562
 
3066
3563
  // src/cli.ts
3067
3564
  var program = new Command();
3068
- 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
+ );
3069
3581
  function repoRoot() {
3070
- return path15.resolve(process.cwd());
3582
+ return path17.resolve(process.cwd());
3071
3583
  }
3072
3584
  function resolveRoot(given) {
3073
3585
  if (!given) return repoRoot();
3074
- return path15.resolve(process.cwd(), given);
3586
+ return path17.resolve(process.cwd(), given);
3075
3587
  }
3076
- 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()));
3077
3589
  program.command("status").description("Show index status").action(() => cmdStatus(repoRoot()));
3078
- program.command("clean").description("Remove generated index data").action(() => cmdClean(repoRoot()));
3079
3590
  program.command("rebuild").description("Rebuild index from scratch (recover from corrupt index)").action(() => cmdRebuild(repoRoot()));
3080
3591
  program.command("doctor").description("Check integration health").action(async () => {
3081
3592
  await cmdDoctor(repoRoot());
3082
3593
  });
3083
- program.command("graph <symbol>").description("Show relations of a symbol (Fase 2: 1-hop traversal)").action((symbol) => cmdGraph(repoRoot(), symbol));
3084
- 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));
3085
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));
3086
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(
3087
3597
  (task, opts) => cmdContext(repoRoot(), task, opts)
@@ -3095,6 +3605,7 @@ program.command("init [path]").description("Initial indexing + generate AGENTS.m
3095
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(
3096
3606
  (opts) => cmdBenchmark(repoRoot(), opts)
3097
3607
  );
3608
+ registerAgentCommands(program, repoRoot);
3098
3609
  program.parseAsync(process.argv);
3099
3610
  export {
3100
3611
  resolveRoot
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scopecairn",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "ScopeCairn — Codebase Intelligence & Agent Optimization Layer (local-first)",
5
5
  "license": "MIT",
6
6
  "type": "module",