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 +81 -15
- package/dist/cli.js +607 -158
- package/package.json +1 -1
- package/skills/scopecairn/SKILL.md +22 -23
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 +
|
|
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 +
|
|
60
|
-
| `scan` / `rebuild`
|
|
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
|
|
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
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `benchmark [--tune]` | Retrieval recall
|
|
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,
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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
|
-
".
|
|
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
|
|
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:
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
1969
|
+
import fs10 from "fs";
|
|
1717
1970
|
import path10 from "path";
|
|
1718
1971
|
function detectAgentDir(repoRoot2) {
|
|
1719
|
-
const plural =
|
|
1720
|
-
const singular =
|
|
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
|
-
|
|
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
|
|
1767
|
-
return
|
|
1768
|
-
|
|
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: "
|
|
1842
|
-
mode: "
|
|
1843
|
-
render:
|
|
1844
|
-
presentMarker: "
|
|
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 =
|
|
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(
|
|
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 (!
|
|
1938
|
-
const text =
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
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
|
|
2248
|
-
const base = proximity.get(
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
2856
|
-
|
|
2857
|
-
|
|
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 (
|
|
2861
|
-
const cur =
|
|
3153
|
+
if (fs14.existsSync(abs)) {
|
|
3154
|
+
const cur = fs14.readFileSync(abs, "utf8");
|
|
2862
3155
|
if (cur.includes(MARKER)) {
|
|
2863
|
-
const
|
|
2864
|
-
|
|
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
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
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
|
-
|
|
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
|
|
2907
|
-
console.log(`\u2713 ${t.label}: ${
|
|
3292
|
+
const r = installTarget(repoRoot2, t, prefix);
|
|
3293
|
+
console.log(`\u2713 ${t.label}: ${r.rel} (${r.how})`);
|
|
2908
3294
|
}
|
|
2909
|
-
const
|
|
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(
|
|
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
|
|
2924
|
-
import
|
|
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
|
|
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.
|
|
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
|
|
3635
|
+
return path17.resolve(process.cwd());
|
|
3186
3636
|
}
|
|
3187
3637
|
function resolveRoot(given) {
|
|
3188
3638
|
if (!given) return repoRoot();
|
|
3189
|
-
return
|
|
3639
|
+
return path17.resolve(process.cwd(), given);
|
|
3190
3640
|
}
|
|
3191
|
-
program.command("scan").description("Index repository (
|
|
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
|
|
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 +
|
|
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,31 +1,30 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: scopecairn
|
|
3
|
-
description: Set up ScopeCairn codebase intelligence
|
|
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
|
|
11
|
+
# ScopeCairn
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
Your job: run project setup so this repo becomes agent-readable.
|
|
13
|
+
## Setup (bila `.scopecairn/scopecairn.db` belum ada)
|
|
10
14
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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.
|