scopecairn 0.2.1 → 0.4.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.
package/README.md CHANGED
@@ -1,12 +1,38 @@
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** — skills for Claude Code (plugin + marketplace
32
+ ready), Antigravity, Cursor, Windsurf, Copilot, Kiro, and an OpenCode
33
+ slash command. ScopeCairn never touches your `AGENTS.md`/`CLAUDE.md` —
34
+ those hold your repo's details, not tool instructions.
35
+
10
36
  ## Install
11
37
 
12
38
  ```bash
@@ -39,7 +65,20 @@ it runs project setup itself.
39
65
 
40
66
  ```bash
41
67
  cd your-repo
42
- scopecairn init # index + generate AGENTS.md, Skill, Workflow
68
+ scopecairn init # index + Antigravity Skill + Workflow
69
+ ```
70
+
71
+ Claude Code (project skill, committed with the repo):
72
+
73
+ ```bash
74
+ scopecairn claude install
75
+ ```
76
+
77
+ Or as a plugin from the bundled marketplace (this repo):
78
+
79
+ ```
80
+ /plugin marketplace add <github-user>/scopecairn
81
+ /plugin install scopecairn@scopecairn
43
82
  ```
44
83
 
45
84
  After setup, your agent calls ScopeCairn automatically on every coding task.
@@ -47,24 +86,40 @@ To query manually:
47
86
 
48
87
  ```bash
49
88
  scopecairn context "add approval workflow"
89
+ scopecairn context ./ # repository orientation map
50
90
  scopecairn impact src/request/RequestService.ts
51
91
  scopecairn read symbol approveRequest
52
92
  scopecairn graph RequestService
53
93
  scopecairn doctor
54
94
  ```
55
95
 
96
+ Per-agent installers (instead of `init --agents …`):
97
+
98
+ ```bash
99
+ scopecairn antigravity install # Skill + Workflow + allowlist guide
100
+ scopecairn claude install # project skill .claude/skills (also: cursor, windsurf, copilot, kiro)
101
+ scopecairn opencode install # /scopecairn slash command + permission snippet
102
+ scopecairn claude uninstall # clean removal, user content preserved
103
+ scopecairn agents list
104
+ ```
105
+
56
106
  ## Commands
107
+
57
108
  | Command | Description |
58
109
  |---|---|
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 |
110
+ | `init [path]` | Initial indexing + Skill, Workflow, detected agent skills (`--agents …\|all`; never writes `AGENTS.md`) |
111
+ | `scan` / `rebuild` | Manual index management (`scan` inkremental; `rebuild` dari nol) |
112
+ | `status` / `doctor` | Index status (incl. adapters, invocations) and health checks |
113
+ | `context "<task>"` | Main entry point: context + scope (`--escalate`, `--no-refresh`); a path task returns an orientation map |
114
+ | `impact <path\|symbol>` | Change impact: direct, indirect, tests, UI, queries, routes |
64
115
  | `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 |
116
+ | `read symbol <name>` | Symbol-level source excerpt (not whole files) |
117
+ | `<agent> install` | Per-agent setup: `antigravity`, `claude`, `gemini`, `cursor`, `windsurf`, `copilot`, `kiro` (each also `uninstall`; `agents list` to see all) |
118
+ | `benchmark [--tune]` | Retrieval recall, irrelevant ratio, context reduction + weight calibration |
119
+
120
+ Agent commands (`context`, `impact`, `read`, `graph`, `status`, `doctor`)
121
+ are read-only toward source and only write to `.scopecairn/` — safe to
122
+ allowlist. `init`, `scan`, and `rebuild` are manual/user-side commands.
68
123
 
69
124
  ## Framework adapters
70
125
 
@@ -77,11 +132,22 @@ No configuration needed.
77
132
  | `prisma` | `schema.prisma` | `model` symbols for `QUERIES` resolution |
78
133
  | `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
134
 
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
135
+ Limits (honest): Server Actions, middleware, non-Prisma/Drizzle ORMs,
136
+ inter-table references, raw SQL strings, and fully dynamic table names
137
+ are not mapped yet — those edges are skipped silently, never hallucinated.
138
+ QUERIES re-derive on every scan so new models resolve without a rebuild.
139
+ Contributing a new adapter = one file + one registration line
83
140
  in `src/adapters/index.ts` (see `FrameworkAdapter` in `src/adapters/types.ts`).
84
141
 
142
+ ## Typical loop
143
+
144
+ ```
145
+ prompt → scopecairn context (refresh at start) → agent edits code
146
+ → scopecairn scan (refresh at end, incremental, ~instant)
147
+ ```
148
+
149
+ Measure retrieval quality anytime: `scopecairn benchmark [--tune]`.
150
+
85
151
  ## License
86
152
 
87
153
  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,18 +2007,42 @@ 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
  `;
1765
2023
  }
1766
- function agentsMd(prefix) {
1767
- return `${MARKER}
1768
- ${ruleBody(prefix)}`;
2024
+ function claudeSkillMd(prefix) {
2025
+ return `---
2026
+ name: scopecairn
2027
+ description: ScopeCairn codebase context + scope. Use for ALL coding tasks
2028
+ (feature, bugfix, refactor, code questions) before exploring the repo,
2029
+ and when the user says "scopecairn", asks for a repo overview, or asks
2030
+ to set up ScopeCairn.
2031
+ allowed-tools: Bash
2032
+ ---
2033
+
2034
+ # ScopeCairn
2035
+
2036
+ 1. Jika \`.scopecairn/scopecairn.db\` belum ada: jalankan \`${prefix} init\`
2037
+ (bila CLI belum ada: \`npm install -g scopecairn\`), laporkan file/simbol/relasi, berhenti.
2038
+ 2. Jika sudah ada: jalankan \`${prefix} context "<task user>"\`.
2039
+ Mulai dari Relevant Files. Jangan menjelajah dari nol.
2040
+ 3. Ubah hanya Required/Optional; Protected perlu persetujuan user.
2041
+ Simbol bersama \u2192 \`${prefix} impact <path>\`.
2042
+ Butuh isi fungsi \u2192 \`${prefix} read symbol <nama>\`, bukan baca file penuh.
2043
+ 4. Task membesar (>2 file)? Panggil ulang \`${prefix} context --escalate\`.
2044
+ 5. Setelah mengedit: \`${prefix} scan\`. Berhenti saat acceptance criteria terpenuhi.
2045
+ `;
1769
2046
  }
1770
2047
  function genericRuleMd(prefix) {
1771
2048
  return `${MARKER}
@@ -1792,6 +2069,7 @@ Langkah:
1792
2069
  2. Baca bagian Relevant Files dan Task Scope.
1793
2070
  3. Jika ada perubahan pada simbol bersama, jalankan \`${prefix} impact <path>\`.
1794
2071
  4. Kerjakan task sesuai scope, lalu berhenti saat selesai.
2072
+ 5. Setelah mengedit, jalankan \`${prefix} scan\` agar indeks ScopeCairn mutakhir.
1795
2073
  `;
1796
2074
  }
1797
2075
  function workflowMd(prefix) {
@@ -1805,11 +2083,41 @@ Shortcut manual bila agent lupa memanggil ScopeCairn otomatis.
1805
2083
  4. Perlu source simbol? \`${prefix} read symbol <nama>\`.
1806
2084
  `;
1807
2085
  }
2086
+ function opencodeCommandMd(prefix) {
2087
+ return `---
2088
+ description: ScopeCairn codebase context + scope for a task
2089
+ ---
2090
+
2091
+ Run \`${prefix} context "$ARGUMENTS"\` with the bash tool, then:
2092
+
2093
+ 1. Use Relevant Files as the starting point \u2014 do not explore from zero.
2094
+ 2. Change only Required and Optional files; Protected needs user approval.
2095
+ 3. If shared symbols change, run \`${prefix} impact <path>\`.
2096
+ 4. Need a function body? Use \`${prefix} read symbol <nama>\`, not full reads.
2097
+ 5. After editing, run \`${prefix} scan\` so the ScopeCairn index stays fresh.
2098
+ <!-- scopecairn:managed -->
2099
+ `;
2100
+ }
2101
+ function opencodePermissionGuide(prefix) {
2102
+ const cmds = ["context", "impact", "read", "graph", "status", "doctor"];
2103
+ const rules = cmds.map((c) => ` "${prefix} ${c} *": "allow"`).join(",\n");
2104
+ return `Agar command di atas jalan tanpa persetujuan, gabungkan ke opencode.json
2105
+ (project root atau ~/.config/opencode/opencode.json):
2106
+
2107
+ {
2108
+ "permission": {
2109
+ "bash": {
2110
+ ${rules}
2111
+ }
2112
+ }
2113
+ }
2114
+
2115
+ Catatan: ScopeCairn tidak mengubah opencode.json Anda secara otomatis.`;
2116
+ }
1808
2117
  var ALLOWLIST_COMMANDS = [
1809
2118
  "context",
1810
2119
  "impact",
1811
2120
  "read",
1812
- "query",
1813
2121
  "graph",
1814
2122
  "status",
1815
2123
  "doctor"
@@ -1827,29 +2135,13 @@ Catatan: ScopeCairn tidak mengubah pengaturan IDE Anda secara otomatis.`;
1827
2135
 
1828
2136
  // src/integrate/matrix.ts
1829
2137
  var AGENT_MATRIX = [
1830
- {
1831
- id: "agents",
1832
- label: "AGENTS.md (universal: Codex, Aider, Opencode, Amp, Jules, ...)",
1833
- rel: "AGENTS.md",
1834
- mode: "merge",
1835
- render: agentsMd,
1836
- presentMarker: "AGENTS.md"
1837
- },
1838
2138
  {
1839
2139
  id: "claude",
1840
2140
  label: "Claude Code",
1841
- rel: "CLAUDE.md",
1842
- mode: "merge",
1843
- render: genericRuleMd,
1844
- presentMarker: "CLAUDE.md"
1845
- },
1846
- {
1847
- id: "gemini",
1848
- label: "Gemini / Antigravity CLI",
1849
- rel: "GEMINI.md",
1850
- mode: "merge",
1851
- render: genericRuleMd,
1852
- presentMarker: "GEMINI.md"
2141
+ rel: ".claude/skills/scopecairn/SKILL.md",
2142
+ mode: "new",
2143
+ render: claudeSkillMd,
2144
+ presentMarker: ".claude"
1853
2145
  },
1854
2146
  {
1855
2147
  id: "cursor",
@@ -1882,6 +2174,14 @@ var AGENT_MATRIX = [
1882
2174
  mode: "new",
1883
2175
  render: genericRuleMd,
1884
2176
  presentMarker: ".kiro"
2177
+ },
2178
+ {
2179
+ id: "opencode",
2180
+ label: "OpenCode",
2181
+ rel: ".opencode/commands/scopecairn.md",
2182
+ mode: "new",
2183
+ render: opencodeCommandMd,
2184
+ presentMarker: ".opencode"
1885
2185
  }
1886
2186
  ];
1887
2187
  function findTarget(id) {
@@ -1920,23 +2220,23 @@ async function cmdDoctor(repoRoot2) {
1920
2220
  const s = db.prepare(`SELECT COUNT(*) AS n FROM symbols`).get().n;
1921
2221
  const r = db.prepare(`SELECT COUNT(*) AS n FROM relationships`).get().n;
1922
2222
  db.close();
1923
- dbOk = fs10.existsSync(dbPath(repoRoot2));
2223
+ dbOk = fs11.existsSync(dbPath(repoRoot2));
1924
2224
  graphDetail = ` (${s} symbols, ${r} rels)`;
1925
2225
  } catch {
1926
2226
  dbOk = false;
1927
2227
  }
1928
2228
  console.log(`${tick(dbOk)} Database ${dbPath(repoRoot2)}${graphDetail}`);
1929
2229
  const protPath = path11.join(dataDir(repoRoot2), "protected.yml");
1930
- console.log(`${tick(fs10.existsSync(protPath))} Protected ${protPath}`);
2230
+ console.log(`${tick(fs11.existsSync(protPath))} Protected ${protPath}`);
1931
2231
  const expectedPrefix = detectPrefix(repoRoot2);
1932
2232
  const agentDir = detectAgentDir(repoRoot2);
1933
2233
  let prefixOk = true;
1934
2234
  let matrixOk = 0;
1935
2235
  for (const t of AGENT_MATRIX) {
1936
2236
  const abs = path11.join(repoRoot2, t.rel);
1937
- if (!fs10.existsSync(abs)) continue;
1938
- const text = fs10.readFileSync(abs, "utf8");
1939
- if (!text.includes(MARKER)) continue;
2237
+ if (!fs11.existsSync(abs)) continue;
2238
+ const text = fs11.readFileSync(abs, "utf8");
2239
+ if (t.mode === "merge" && !text.includes(MARKER)) continue;
1940
2240
  matrixOk++;
1941
2241
  const used = extractPrefixUsed(text);
1942
2242
  if (used && used !== expectedPrefix) {
@@ -1953,12 +2253,12 @@ async function cmdDoctor(repoRoot2) {
1953
2253
  console.log(`\u2713 Prefix konsisten (\`${expectedPrefix} context\`)`);
1954
2254
  }
1955
2255
  const skillPath = path11.join(repoRoot2, agentDir, "skills", "scopecairn", "SKILL.md");
1956
- const skillOk = fs10.existsSync(skillPath);
2256
+ const skillOk = fs11.existsSync(skillPath);
1957
2257
  console.log(`${tick(skillOk)} Skill (${agentDir}/skills/scopecairn/SKILL.md)`);
1958
2258
  const wfPath = path11.join(repoRoot2, "workflows", "scopecairn.md");
1959
- console.log(`${tick(fs10.existsSync(wfPath))} Workflow (workflows/scopecairn.md, opsional)`);
2259
+ console.log(`${tick(fs11.existsSync(wfPath))} Workflow (workflows/scopecairn.md, opsional)`);
1960
2260
  console.log(`i Strict Mode mengabaikan allowlist \u2014 semua perintah tetap perlu persetujuan.`);
1961
- const healthy = nodeOk && sqliteOk && ts.ok && dbOk && matrixOk > 0 && skillOk && prefixOk;
2261
+ const healthy = nodeOk && sqliteOk && ts.ok && dbOk && (matrixOk > 0 || skillOk) && prefixOk;
1962
2262
  console.log(`Status: ${healthy ? "HEALTHY" : "DEGRADED (see \u2717 above)"}`);
1963
2263
  }
1964
2264
 
@@ -2075,32 +2375,8 @@ function cmdGraph(repoRoot2, key) {
2075
2375
  }
2076
2376
  }
2077
2377
 
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
2378
  // src/commands/read.ts
2103
- import fs11 from "fs";
2379
+ import fs12 from "fs";
2104
2380
  import path12 from "path";
2105
2381
  function cmdRead(repoRoot2, kind, name) {
2106
2382
  if (kind !== "symbol") {
@@ -2120,7 +2396,7 @@ function cmdRead(repoRoot2, kind, name) {
2120
2396
  const abs = path12.join(repoRoot2, node.file);
2121
2397
  let lines;
2122
2398
  try {
2123
- lines = fs11.readFileSync(abs, "utf8").split("\n");
2399
+ lines = fs12.readFileSync(abs, "utf8").split("\n");
2124
2400
  } catch {
2125
2401
  console.log(`Cannot read ${node.file}.`);
2126
2402
  return;
@@ -2230,29 +2506,36 @@ function retrieve(db, repoRoot2, task, opts) {
2230
2506
  proximity.set(s.symbolId, Math.max(proximity.get(s.symbolId) ?? 0, s.bm25));
2231
2507
  visited.add(s.symbolId);
2232
2508
  });
2233
- const edgeStmt = db.prepare(
2234
- `SELECT source_id, target_id, weight FROM relationships`
2235
- );
2236
- const adj = /* @__PURE__ */ new Map();
2237
- for (const r of edgeStmt.all()) {
2238
- if (!adj.has(r.source_id)) adj.set(r.source_id, []);
2239
- adj.get(r.source_id).push({ to: r.target_id, w: r.weight });
2240
- if (!adj.has(r.target_id)) adj.set(r.target_id, []);
2241
- adj.get(r.target_id).push({ to: r.source_id, w: r.weight * 0.8 });
2509
+ const CHUNK = 2e3;
2510
+ function neighborsOf(ids) {
2511
+ const out = [];
2512
+ for (let i = 0; i < ids.length; i += CHUNK) {
2513
+ const c = ids.slice(i, i + CHUNK);
2514
+ const ph = c.map(() => "?").join(",");
2515
+ for (const r of db.prepare(
2516
+ `SELECT source_id AS f, target_id AS t, weight AS w FROM relationships WHERE source_id IN (${ph})`
2517
+ ).all(...c)) {
2518
+ out.push({ from: r.f, to: r.t, w: r.w });
2519
+ }
2520
+ for (const r of db.prepare(
2521
+ `SELECT target_id AS f, source_id AS t, weight * 0.8 AS w FROM relationships WHERE target_id IN (${ph})`
2522
+ ).all(...c)) {
2523
+ out.push({ from: r.f, to: r.t, w: r.w });
2524
+ }
2525
+ }
2526
+ return out;
2242
2527
  }
2243
2528
  let hopScore = 1;
2244
2529
  for (let depth = 1; depth <= config.maxDepth; depth++) {
2245
2530
  hopScore *= config.decayPerHop;
2246
2531
  const next = [];
2247
- for (const id of frontier) {
2248
- const base = proximity.get(id) ?? 0;
2249
- for (const e of adj.get(id) ?? []) {
2250
- const cand = base * e.w * hopScore;
2251
- if (cand > (proximity.get(e.to) ?? 0)) proximity.set(e.to, cand);
2252
- if (!visited.has(e.to)) {
2253
- visited.add(e.to);
2254
- next.push(e.to);
2255
- }
2532
+ for (const e of neighborsOf(frontier)) {
2533
+ const base = proximity.get(e.from) ?? 0;
2534
+ const cand = base * e.w * hopScore;
2535
+ if (cand > (proximity.get(e.to) ?? 0)) proximity.set(e.to, cand);
2536
+ if (!visited.has(e.to)) {
2537
+ visited.add(e.to);
2538
+ next.push(e.to);
2256
2539
  }
2257
2540
  }
2258
2541
  frontier = next;
@@ -2425,7 +2708,8 @@ var DEFINITION_OF_DONE = [
2425
2708
  "Test relevan lulus",
2426
2709
  "Tidak ada file berubah tanpa alasan",
2427
2710
  "Tidak ada refactoring tak terkait",
2428
- "Acceptance criteria terpenuhi"
2711
+ "Acceptance criteria terpenuhi",
2712
+ "Index dimutakhirkan (`scopecairn scan`)"
2429
2713
  ];
2430
2714
  function guardBlock() {
2431
2715
  return `## Task Guard
@@ -2545,7 +2829,7 @@ ${tests.map((t) => `- ${t}`).join("\n") || "-"}
2545
2829
  }
2546
2830
 
2547
2831
  // src/commands/orientasi.ts
2548
- import fs12 from "fs";
2832
+ import fs13 from "fs";
2549
2833
  import path13 from "path";
2550
2834
  function isPathLike(task) {
2551
2835
  const t = task.trim().replace(/^["']|["']$/g, "");
@@ -2556,7 +2840,7 @@ function looksLikeRepoPath(db, repoRoot2, task) {
2556
2840
  if (/\s/.test(t)) return false;
2557
2841
  if (isPathLike(task)) return true;
2558
2842
  try {
2559
- fs12.statSync(path13.resolve(repoRoot2, t));
2843
+ fs13.statSync(path13.resolve(repoRoot2, t));
2560
2844
  return true;
2561
2845
  } catch {
2562
2846
  }
@@ -2595,7 +2879,7 @@ function cmdOrientasi(repoRoot2, task) {
2595
2879
  const abs = path13.resolve(repoRoot2, t);
2596
2880
  let rel = null;
2597
2881
  try {
2598
- const stat = fs12.statSync(abs);
2882
+ const stat = fs13.statSync(abs);
2599
2883
  if (stat.isFile()) rel = path13.relative(repoRoot2, abs).split(path13.sep).join("/");
2600
2884
  } catch {
2601
2885
  rel = null;
@@ -2846,31 +3130,152 @@ function cmdImpact(repoRoot2, target, opts = {}) {
2846
3130
  }
2847
3131
 
2848
3132
  // src/commands/init.ts
2849
- import fs13 from "fs";
3133
+ import fs15 from "fs";
3134
+ import path15 from "path";
3135
+
3136
+ // src/integrate/install.ts
3137
+ import fs14 from "fs";
2850
3138
  import path14 from "path";
2851
- function writeTarget(repoRoot2, target, prefix) {
3139
+ function installTarget(repoRoot2, target, prefix) {
2852
3140
  const abs = path14.join(repoRoot2, target.rel);
2853
3141
  const block = target.render(prefix);
3142
+ const finish = (how) => ({
3143
+ agent: target.id,
3144
+ rel: target.rel,
3145
+ how
3146
+ });
2854
3147
  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";
3148
+ const existed = fs14.existsSync(abs);
3149
+ fs14.mkdirSync(path14.dirname(abs), { recursive: true });
3150
+ fs14.writeFileSync(abs, block.endsWith("\n") ? block : block + "\n");
3151
+ return finish(existed ? "updated" : "generated");
2859
3152
  }
2860
- if (fs13.existsSync(abs)) {
2861
- const cur = fs13.readFileSync(abs, "utf8");
3153
+ if (fs14.existsSync(abs)) {
3154
+ const cur = fs14.readFileSync(abs, "utf8");
2862
3155
  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";
3156
+ const { next: next2 } = removeManagedBlock(cur, (p) => target.render(p));
3157
+ fs14.writeFileSync(abs, (next2 + "\n\n" + block).trimEnd() + "\n");
3158
+ return finish("updated");
3159
+ }
3160
+ const { next, changed } = removeManagedBlock(cur, (p) => target.render(p));
3161
+ const base = changed ? next : cur;
3162
+ fs14.writeFileSync(abs, (base.trimEnd() + "\n\n" + block).trimEnd() + "\n");
3163
+ return finish("extended");
3164
+ }
3165
+ fs14.mkdirSync(path14.dirname(abs), { recursive: true });
3166
+ fs14.writeFileSync(abs, block.endsWith("\n") ? block : block + "\n");
3167
+ return finish("generated");
3168
+ }
3169
+ function removeManagedBlock(content, render) {
3170
+ for (const p of ["scopecairn", "npx scopecairn"]) {
3171
+ for (const cand of [render(p), render(p).trimEnd(), render(p).trimEnd() + "\n"]) {
3172
+ if (cand && content.includes(cand)) {
3173
+ return { next: foldBlank(content.split(cand).join("")), changed: true };
3174
+ }
2866
3175
  }
2867
- fs13.writeFileSync(abs, cur.trimEnd() + "\n\n" + block);
2868
- return "extended";
2869
3176
  }
2870
- fs13.mkdirSync(path14.dirname(abs), { recursive: true });
2871
- fs13.writeFileSync(abs, block.endsWith("\n") ? block : block + "\n");
2872
- return "generated";
3177
+ const anchored = new RegExp(
3178
+ `\\n*${MARKER}\\n# ScopeCairn[\\s\\S]*?pertanyaan non-coding\\.[ \\t]*(\\n|$)`,
3179
+ ""
3180
+ );
3181
+ if (anchored.test(content)) {
3182
+ return { next: foldBlank(content.replace(anchored, "\n")), changed: true };
3183
+ }
3184
+ const orphan = /# ScopeCairn[\s\S]*?pertanyaan non-coding\.[ \t]*(\n|$)/g;
3185
+ if (orphan.test(content)) {
3186
+ return { next: foldBlank(content.replace(orphan, "")), changed: true };
3187
+ }
3188
+ return { next: content, changed: false };
3189
+ }
3190
+ function foldBlank(s) {
3191
+ return s.replace(/\n{3,}/g, "\n\n").trim();
3192
+ }
3193
+ function uninstallTarget(repoRoot2, target) {
3194
+ const abs = path14.join(repoRoot2, target.rel);
3195
+ const finish = (how) => ({
3196
+ agent: target.id,
3197
+ rel: target.rel,
3198
+ how
3199
+ });
3200
+ if (!fs14.existsSync(abs)) return finish("absent");
3201
+ if (target.mode === "new") {
3202
+ fs14.rmSync(abs, { force: true });
3203
+ pruneEmptyDirs(path14.dirname(abs), repoRoot2);
3204
+ return finish("removed");
3205
+ }
3206
+ const cur = fs14.readFileSync(abs, "utf8");
3207
+ if (!cur.includes(MARKER)) return finish("skipped");
3208
+ const render = (p) => {
3209
+ const t = resolveAgent(target.id);
3210
+ return t ? t.render(p) : "";
3211
+ };
3212
+ const { next, changed } = removeManagedBlock(cur, render);
3213
+ if (!changed) return finish("skipped");
3214
+ if (!next) {
3215
+ fs14.rmSync(abs, { force: true });
3216
+ return finish("removed");
3217
+ }
3218
+ fs14.writeFileSync(abs, next + "\n");
3219
+ return finish("removed");
3220
+ }
3221
+ function pruneEmptyDirs(dir, stop) {
3222
+ let cur = dir;
3223
+ while (cur.startsWith(stop) && cur !== stop) {
3224
+ try {
3225
+ fs14.rmdirSync(cur);
3226
+ } catch {
3227
+ return;
3228
+ }
3229
+ cur = path14.dirname(cur);
3230
+ }
2873
3231
  }
3232
+ function installAntigravity(repoRoot2, prefix = detectPrefix(repoRoot2)) {
3233
+ const agentDir = detectAgentDir(repoRoot2);
3234
+ const out = [];
3235
+ const skillPath = path14.join(repoRoot2, agentDir, "skills", "scopecairn", "SKILL.md");
3236
+ fs14.mkdirSync(path14.dirname(skillPath), { recursive: true });
3237
+ fs14.writeFileSync(skillPath, skillMd(prefix));
3238
+ out.push(`Skill installed (${path14.relative(repoRoot2, skillPath)})`);
3239
+ const wfPath = path14.join(repoRoot2, "workflows", "scopecairn.md");
3240
+ fs14.mkdirSync(path14.dirname(wfPath), { recursive: true });
3241
+ fs14.writeFileSync(wfPath, workflowMd(prefix));
3242
+ out.push(`Workflow installed (${path14.relative(repoRoot2, wfPath)})`);
3243
+ return out;
3244
+ }
3245
+ function uninstallAntigravity(repoRoot2) {
3246
+ const out = [];
3247
+ for (const dir of [".agents", ".agent"]) {
3248
+ const p = path14.join(repoRoot2, dir, "skills", "scopecairn", "SKILL.md");
3249
+ if (fs14.existsSync(p)) {
3250
+ fs14.rmSync(p, { force: true });
3251
+ pruneEmptyDirs(path14.dirname(p), repoRoot2);
3252
+ out.push(`Skill removed (${path14.relative(repoRoot2, p)})`);
3253
+ }
3254
+ }
3255
+ const wf = path14.join(repoRoot2, "workflows", "scopecairn.md");
3256
+ if (fs14.existsSync(wf)) {
3257
+ const text = fs14.readFileSync(wf, "utf8");
3258
+ if (text.includes("scopecairn")) {
3259
+ fs14.rmSync(wf, { force: true });
3260
+ out.push(`Workflow removed (${path14.relative(repoRoot2, wf)})`);
3261
+ } else {
3262
+ out.push(`Workflow skipped (bukan milik ScopeCairn)`);
3263
+ }
3264
+ }
3265
+ if (out.length === 0) out.push("Nothing installed.");
3266
+ return out;
3267
+ }
3268
+ function agentIds() {
3269
+ return ["antigravity", ...AGENT_MATRIX.map((t) => t.id)];
3270
+ }
3271
+ function allowlistText(repoRoot2) {
3272
+ return allowlistGuide(detectPrefix(repoRoot2));
3273
+ }
3274
+ function resolveAgent(id) {
3275
+ return findTarget(id);
3276
+ }
3277
+
3278
+ // src/commands/init.ts
2874
3279
  function cmdInit(repoRoot2, opts = {}) {
2875
3280
  console.log("ScopeCairn");
2876
3281
  console.log("\u2713 Repository detected");
@@ -2879,49 +3284,42 @@ function cmdInit(repoRoot2, opts = {}) {
2879
3284
  if (stats.metaFiles > 0) console.log(`\u2713 ${stats.metaFiles} config/schema files`);
2880
3285
  console.log(`\u2713 ${stats.symbols} symbols`);
2881
3286
  console.log(`\u2713 ${stats.relationships} relationships`);
3287
+ if (stats.adapters.length > 0) console.log(`\u2713 adapters: ${stats.adapters.join(", ")}`);
2882
3288
  const prefix = detectPrefix(repoRoot2);
2883
- const agentDir = detectAgentDir(repoRoot2);
2884
3289
  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
- }
3290
+ const targets = requested === "all" ? [...AGENT_MATRIX] : requested && requested.length > 0 ? withCore(requested) : withDetected(repoRoot2);
2905
3291
  for (const t of targets) {
2906
- const how = writeTarget(repoRoot2, t, prefix);
2907
- console.log(`\u2713 ${t.label}: ${t.rel} (${how})`);
3292
+ const r = installTarget(repoRoot2, t, prefix);
3293
+ console.log(`\u2713 ${t.label}: ${r.rel} (${r.how})`);
2908
3294
  }
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)})`);
3295
+ for (const line of installAntigravity(repoRoot2, prefix)) console.log(`\u2713 ${line}`);
2917
3296
  console.log("Ready.");
2918
3297
  console.log("");
2919
- console.log(allowlistGuide(prefix));
3298
+ console.log(allowlistText(repoRoot2));
3299
+ }
3300
+ function withCore(ids) {
3301
+ const out = [];
3302
+ for (const id of ids) {
3303
+ const t = findTarget(id);
3304
+ if (!t) {
3305
+ console.log(`! Unknown agent "${id}" (pilihan: ${AGENT_MATRIX.map((x) => x.id).join(", ")}, all)`);
3306
+ continue;
3307
+ }
3308
+ if (!out.includes(t)) out.push(t);
3309
+ }
3310
+ return out;
3311
+ }
3312
+ function withDetected(repoRoot2) {
3313
+ const targets = [];
3314
+ for (const t of AGENT_MATRIX) {
3315
+ if (fs15.existsSync(path15.join(repoRoot2, t.presentMarker))) targets.push(t);
3316
+ }
3317
+ return targets;
2920
3318
  }
2921
3319
 
2922
3320
  // src/benchmark/run.ts
2923
- import fs14 from "fs";
2924
- import path15 from "path";
3321
+ import fs16 from "fs";
3322
+ import path16 from "path";
2925
3323
 
2926
3324
  // src/benchmark/dataset.ts
2927
3325
  var BENCHMARK_TASKS = [
@@ -2978,7 +3376,7 @@ var BENCHMARK_TASKS = [
2978
3376
  // src/benchmark/run.ts
2979
3377
  function fileChars(repoRoot2, rel) {
2980
3378
  try {
2981
- return fs14.readFileSync(path15.join(repoRoot2, rel), "utf8").length;
3379
+ return fs16.readFileSync(path16.join(repoRoot2, rel), "utf8").length;
2982
3380
  } catch {
2983
3381
  return 0;
2984
3382
  }
@@ -3127,10 +3525,47 @@ function cmdBenchmark(repoRoot2, opts = {}) {
3127
3525
  }
3128
3526
  }
3129
3527
 
3528
+ // src/commands/agents.ts
3529
+ function registerAgentCommands(program2, repoRoot2) {
3530
+ const list = program2.command("agents").description("List supported agent installers");
3531
+ list.command("list").description("Show agent ids").action(() => {
3532
+ console.log(agentIds().join("\n"));
3533
+ });
3534
+ const anti = program2.command("antigravity").description("Antigravity IDE integration");
3535
+ anti.command("install").description("Install Skill + Workflow, print allowlist guide").action(() => {
3536
+ const root = repoRoot2();
3537
+ const prefix = detectPrefix(root);
3538
+ for (const line of installAntigravity(root, prefix)) console.log(`\u2713 ${line}`);
3539
+ console.log("");
3540
+ console.log(allowlistText(root));
3541
+ });
3542
+ anti.command("uninstall").description("Remove Skill + Workflow").action(() => {
3543
+ for (const line of uninstallAntigravity(repoRoot2())) console.log(`\u2713 ${line}`);
3544
+ });
3545
+ for (const t of AGENT_MATRIX) {
3546
+ if (t.id === "agents") continue;
3547
+ const cmd = program2.command(t.id).description(`${t.label} integration`);
3548
+ cmd.command("install").description(`Write ${t.rel}`).action(() => {
3549
+ const root = repoRoot2();
3550
+ const prefix = detectPrefix(root);
3551
+ const r = installTarget(root, t, prefix);
3552
+ console.log(`\u2713 ${t.label}: ${r.rel} (${r.how})`);
3553
+ if (t.id === "opencode") {
3554
+ console.log("");
3555
+ console.log(opencodePermissionGuide(prefix));
3556
+ }
3557
+ });
3558
+ cmd.command("uninstall").description(`Remove ScopeCairn block from ${t.rel}`).action(() => {
3559
+ const r = uninstallTarget(repoRoot2(), t);
3560
+ console.log(`\u2713 ${t.label}: ${r.rel} (${r.how})`);
3561
+ });
3562
+ }
3563
+ }
3564
+
3130
3565
  // package.json
3131
3566
  var package_default = {
3132
3567
  name: "scopecairn",
3133
- version: "0.2.1",
3568
+ version: "0.4.0",
3134
3569
  description: "ScopeCairn \u2014 Codebase Intelligence & Agent Optimization Layer (local-first)",
3135
3570
  license: "MIT",
3136
3571
  type: "module",
@@ -3180,23 +3615,36 @@ var package_default = {
3180
3615
 
3181
3616
  // src/cli.ts
3182
3617
  var program = new Command();
3183
- program.name("scopecairn").description("ScopeCairn \u2014 local-first codebase intelligence layer").version(package_default.version);
3618
+ program.name("scopecairn").description("ScopeCairn \u2014 local-first codebase intelligence layer").version(package_default.version).addHelpText(
3619
+ "after",
3620
+ `
3621
+ Examples:
3622
+ $ scopecairn init index repo + setup agent integration
3623
+ $ scopecairn init ./myapp --agents all setup another directory, all agents
3624
+ $ scopecairn context "add login" relevant context + scope for a task
3625
+ $ scopecairn context ./ repository orientation map
3626
+ $ scopecairn impact src/auth/login.ts change impact analysis
3627
+ $ scopecairn read symbol approveRequest symbol-level source excerpt
3628
+ $ scopecairn doctor health + integration check
3629
+
3630
+ Agent commands (context, impact, read, graph, status, doctor) are
3631
+ read-only toward source and only write to .scopecairn/ \u2014 safe to allowlist.
3632
+ `
3633
+ );
3184
3634
  function repoRoot() {
3185
- return path16.resolve(process.cwd());
3635
+ return path17.resolve(process.cwd());
3186
3636
  }
3187
3637
  function resolveRoot(given) {
3188
3638
  if (!given) return repoRoot();
3189
- return path16.resolve(process.cwd(), given);
3639
+ return path17.resolve(process.cwd(), given);
3190
3640
  }
3191
- program.command("scan").description("Index repository (Fase 1: files + hash + language)").action(() => cmdScan(repoRoot()));
3641
+ program.command("scan").description("Index repository (files + symbols + relations)").action(() => cmdScan(repoRoot()));
3192
3642
  program.command("status").description("Show index status").action(() => cmdStatus(repoRoot()));
3193
- program.command("clean").description("Remove generated index data").action(() => cmdClean(repoRoot()));
3194
3643
  program.command("rebuild").description("Rebuild index from scratch (recover from corrupt index)").action(() => cmdRebuild(repoRoot()));
3195
3644
  program.command("doctor").description("Check integration health").action(async () => {
3196
3645
  await cmdDoctor(repoRoot());
3197
3646
  });
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));
3647
+ program.command("graph <symbol>").description("Show relations of a symbol").action((symbol) => cmdGraph(repoRoot(), symbol));
3200
3648
  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
3649
  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
3650
  (task, opts) => cmdContext(repoRoot(), task, opts)
@@ -3204,12 +3652,13 @@ program.command("context <task>").description("Main entry point: auto-refresh +
3204
3652
  program.command("impact <target>").description("Change impact analysis: file, symbol, function, or class").option("--no-refresh", "skip incremental refresh (debugging/benchmark)").action(
3205
3653
  (target, opts) => cmdImpact(repoRoot(), target, opts)
3206
3654
  );
3207
- program.command("init [path]").description("Initial indexing + generate AGENTS.md, Skill, Workflow, agent matrix").option("--agents <list>", "agent targets: comma list from agents,claude,gemini,cursor,windsurf,copilot,kiro, or 'all'").action(
3655
+ program.command("init [path]").description("Initial indexing + Skill, Workflow, agent skills (never touches AGENTS.md)").option("--agents <list>", "agent targets: comma list from claude,cursor,windsurf,copilot,kiro,opencode, or 'all'").action(
3208
3656
  (targetPath, opts) => cmdInit(resolveRoot(targetPath), opts)
3209
3657
  );
3210
3658
  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
3659
  (opts) => cmdBenchmark(repoRoot(), opts)
3212
3660
  );
3661
+ registerAgentCommands(program, repoRoot);
3213
3662
  program.parseAsync(process.argv);
3214
3663
  export {
3215
3664
  resolveRoot
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scopecairn",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "ScopeCairn — Codebase Intelligence & Agent Optimization Layer (local-first)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,31 +1,30 @@
1
1
  ---
2
2
  name: scopecairn
3
- description: Set up ScopeCairn codebase intelligence in a repository. Use when the user says "scopecairn", "scopecairn ./", "scopecairn <path>", asks to index a repo for an AI agent, or wants the agent to understand a codebase with ScopeCairn.
3
+ description: Set up ScopeCairn codebase intelligence, then use it for coding
4
+ tasks. Use when the user says "scopecairn", "scopecairn ./",
5
+ "scopecairn <path>", asks to index a repo for an AI agent, asks for a
6
+ repo overview, or for ALL coding tasks (feature, bugfix, refactor)
7
+ before exploring the repo.
8
+ allowed-tools: Bash
4
9
  ---
5
10
 
6
- # ScopeCairn setup
11
+ # ScopeCairn
7
12
 
8
- The user invoked ScopeCairn with a path (e.g. `scopecairn ./`).
9
- Your job: run project setup so this repo becomes agent-readable.
13
+ ## Setup (bila `.scopecairn/scopecairn.db` belum ada)
10
14
 
11
- ## Steps
15
+ 1. Resolve path: `.`/`./` = direktori proyek saat ini.
16
+ 2. Jalankan `scopecairn init <path>` (`npx -y scopecairn init <path>`
17
+ bila CLI belum ada; atau `npm install -g scopecairn` dulu).
18
+ 3. Laporkan singkat: file terindex, simbol, relasi, adapter,
19
+ file integrasi yang ditulis. Berhenti setelah setup.
12
20
 
13
- 1. Resolve the path: `.` or `./` means the current project directory.
14
- A relative or absolute path means that directory.
15
- 2. Run setup in that directory:
16
- `scopecairn init <path>` (or `npx -y scopecairn init <path>` if it is not installed).
17
- 3. If the command is not found, install globally first
18
- (`npm install -g scopecairn`), then retry step 2.
19
- 4. Report back, briefly: files indexed, symbols, relationships,
20
- detected adapters, and which integration files were written
21
- (AGENTS.md, Skill, Workflow, agent matrix).
21
+ ## Tiap task coding (bila index sudah ada)
22
22
 
23
- ## Rules
24
-
25
- - Setup is read-only toward source code. `init` only adds/updates
26
- `AGENTS.md` (marker block), the agent Skill, the Workflow file,
27
- and the `.scopecairn/` index directory. Never refactor user code.
28
- - If a terminal command needs approval, ask once — after setup,
29
- recommend the user allowlist the read-only commands printed by `init`.
30
- - After setup, coding tasks go through `scopecairn context "<task>"`,
31
- never by re-exploring the repository from zero.
23
+ 1. Jalankan `scopecairn context "<task user>"`.
24
+ Mulai dari Relevant Files. Jangan menjelajah dari nol.
25
+ 2. Ubah hanya Required/Optional; Protected perlu persetujuan user.
26
+ Simbol bersama → `scopecairn impact <path>`.
27
+ Butuh isi fungsi → `scopecairn read symbol <nama>`, bukan baca file penuh.
28
+ 3. Task membesar (>2 file)? Panggil ulang `scopecairn context --escalate`.
29
+ 4. Setelah mengedit: `scopecairn scan`.
30
+ Berhenti saat acceptance criteria terpenuhi.