scopecairn 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -14
- package/dist/cli.js +510 -114
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,36 @@
|
|
|
1
1
|
# ScopeCairn
|
|
2
2
|
|
|
3
3
|
Local-first codebase intelligence layer for AI coding agents.
|
|
4
|
-
|
|
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** — one `init` writes rules for AGENTS.md,
|
|
32
|
+
Claude Code, Gemini, Cursor, Windsurf, Copilot, Kiro, or all at once.
|
|
33
|
+
|
|
10
34
|
## Install
|
|
11
35
|
|
|
12
36
|
```bash
|
|
@@ -47,24 +71,39 @@ To query manually:
|
|
|
47
71
|
|
|
48
72
|
```bash
|
|
49
73
|
scopecairn context "add approval workflow"
|
|
74
|
+
scopecairn context ./ # repository orientation map
|
|
50
75
|
scopecairn impact src/request/RequestService.ts
|
|
51
76
|
scopecairn read symbol approveRequest
|
|
52
77
|
scopecairn graph RequestService
|
|
53
78
|
scopecairn doctor
|
|
54
79
|
```
|
|
55
80
|
|
|
81
|
+
Per-agent installers (instead of `init --agents …`):
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
scopecairn antigravity install # Skill + Workflow + allowlist guide
|
|
85
|
+
scopecairn claude install # CLAUDE.md (also: gemini, cursor, windsurf, copilot, kiro)
|
|
86
|
+
scopecairn claude uninstall # clean removal, user content preserved
|
|
87
|
+
scopecairn agents list
|
|
88
|
+
```
|
|
89
|
+
|
|
56
90
|
## Commands
|
|
91
|
+
|
|
57
92
|
| Command | Description |
|
|
58
93
|
|---|---|
|
|
59
|
-
| `init` | Initial indexing +
|
|
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
|
|
94
|
+
| `init [path]` | Initial indexing + `AGENTS.md`, Skill, Workflow, agent matrix (`--agents …\|all`) |
|
|
95
|
+
| `scan` / `rebuild` | Manual index management (`scan` inkremental; `rebuild` dari nol) |
|
|
96
|
+
| `status` / `doctor` | Index status (incl. adapters, invocations) and health checks |
|
|
97
|
+
| `context "<task>"` | Main entry point: context + scope (`--escalate`, `--no-refresh`); a path task returns an orientation map |
|
|
98
|
+
| `impact <path\|symbol>` | Change impact: direct, indirect, tests, UI, queries, routes |
|
|
64
99
|
| `graph <symbol>` | Symbol relations |
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `benchmark [--tune]` | Retrieval recall
|
|
100
|
+
| `read symbol <name>` | Symbol-level source excerpt (not whole files) |
|
|
101
|
+
| `<agent> install` | Per-agent setup: `antigravity`, `claude`, `gemini`, `cursor`, `windsurf`, `copilot`, `kiro` (each also `uninstall`; `agents list` to see all) |
|
|
102
|
+
| `benchmark [--tune]` | Retrieval recall, irrelevant ratio, context reduction + weight calibration |
|
|
103
|
+
|
|
104
|
+
Agent commands (`context`, `impact`, `read`, `graph`, `status`, `doctor`)
|
|
105
|
+
are read-only toward source and only write to `.scopecairn/` — safe to
|
|
106
|
+
allowlist. `init`, `scan`, and `rebuild` are manual/user-side commands.
|
|
68
107
|
|
|
69
108
|
## Framework adapters
|
|
70
109
|
|
|
@@ -77,11 +116,22 @@ No configuration needed.
|
|
|
77
116
|
| `prisma` | `schema.prisma` | `model` symbols for `QUERIES` resolution |
|
|
78
117
|
| `drizzle` | `drizzle*` paths/config, `db/` schemas | `model` symbols from `pgTable`/`sqliteTable`/`mysqlTable`; `QUERIES` from `db.query.*` relational and `db.select/insert/update/delete` builder calls |
|
|
79
118
|
|
|
80
|
-
Limits (honest): Server Actions, middleware,
|
|
81
|
-
|
|
82
|
-
|
|
119
|
+
Limits (honest): Server Actions, middleware, non-Prisma/Drizzle ORMs,
|
|
120
|
+
inter-table references, raw SQL strings, and fully dynamic table names
|
|
121
|
+
are not mapped yet — those edges are skipped silently, never hallucinated.
|
|
122
|
+
QUERIES re-derive on every scan so new models resolve without a rebuild.
|
|
123
|
+
Contributing a new adapter = one file + one registration line
|
|
83
124
|
in `src/adapters/index.ts` (see `FrameworkAdapter` in `src/adapters/types.ts`).
|
|
84
125
|
|
|
126
|
+
## Typical loop
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
prompt → scopecairn context (refresh at start) → agent edits code
|
|
130
|
+
→ scopecairn scan (refresh at end, incremental, ~instant)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Measure retrieval quality anytime: `scopecairn benchmark [--tune]`.
|
|
134
|
+
|
|
85
135
|
## License
|
|
86
136
|
|
|
87
137
|
MIT — see [LICENSE](./LICENSE).
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
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,11 +2007,16 @@ Gunakan hasilnya sebagai titik awal. Jangan menjelajah repository dari nol.
|
|
|
1754
2007
|
## Selama bekerja
|
|
1755
2008
|
- Ubah hanya file di daftar Required dan Optional.
|
|
1756
2009
|
- Jangan sentuh file di daftar Protected tanpa persetujuan user.
|
|
2010
|
+
- Bila ternyata lebih dari 2 file harus diubah, panggil ulang konteks penuh:
|
|
2011
|
+
\`${prefix} context --escalate "<permintaan user>"\`
|
|
1757
2012
|
- Sebelum mengubah simbol yang dipakai banyak tempat, jalankan:
|
|
1758
2013
|
\`${prefix} impact <path>\`
|
|
1759
2014
|
- Butuh source sebuah fungsi? Gunakan \`${prefix} read symbol <nama>\`
|
|
1760
2015
|
alih-alih membaca file penuh.
|
|
1761
2016
|
|
|
2017
|
+
## Setelah selesai
|
|
2018
|
+
Jalankan \`${prefix} scan\` agar indeks ScopeCairn mencerminkan perubahanmu.
|
|
2019
|
+
|
|
1762
2020
|
## Pengecualian
|
|
1763
2021
|
Lewati ScopeCairn hanya untuk pertanyaan non-coding.
|
|
1764
2022
|
`;
|
|
@@ -1792,6 +2050,7 @@ Langkah:
|
|
|
1792
2050
|
2. Baca bagian Relevant Files dan Task Scope.
|
|
1793
2051
|
3. Jika ada perubahan pada simbol bersama, jalankan \`${prefix} impact <path>\`.
|
|
1794
2052
|
4. Kerjakan task sesuai scope, lalu berhenti saat selesai.
|
|
2053
|
+
5. Setelah mengedit, jalankan \`${prefix} scan\` agar indeks ScopeCairn mutakhir.
|
|
1795
2054
|
`;
|
|
1796
2055
|
}
|
|
1797
2056
|
function workflowMd(prefix) {
|
|
@@ -1809,7 +2068,6 @@ var ALLOWLIST_COMMANDS = [
|
|
|
1809
2068
|
"context",
|
|
1810
2069
|
"impact",
|
|
1811
2070
|
"read",
|
|
1812
|
-
"query",
|
|
1813
2071
|
"graph",
|
|
1814
2072
|
"status",
|
|
1815
2073
|
"doctor"
|
|
@@ -1920,22 +2178,22 @@ async function cmdDoctor(repoRoot2) {
|
|
|
1920
2178
|
const s = db.prepare(`SELECT COUNT(*) AS n FROM symbols`).get().n;
|
|
1921
2179
|
const r = db.prepare(`SELECT COUNT(*) AS n FROM relationships`).get().n;
|
|
1922
2180
|
db.close();
|
|
1923
|
-
dbOk =
|
|
2181
|
+
dbOk = fs11.existsSync(dbPath(repoRoot2));
|
|
1924
2182
|
graphDetail = ` (${s} symbols, ${r} rels)`;
|
|
1925
2183
|
} catch {
|
|
1926
2184
|
dbOk = false;
|
|
1927
2185
|
}
|
|
1928
2186
|
console.log(`${tick(dbOk)} Database ${dbPath(repoRoot2)}${graphDetail}`);
|
|
1929
2187
|
const protPath = path11.join(dataDir(repoRoot2), "protected.yml");
|
|
1930
|
-
console.log(`${tick(
|
|
2188
|
+
console.log(`${tick(fs11.existsSync(protPath))} Protected ${protPath}`);
|
|
1931
2189
|
const expectedPrefix = detectPrefix(repoRoot2);
|
|
1932
2190
|
const agentDir = detectAgentDir(repoRoot2);
|
|
1933
2191
|
let prefixOk = true;
|
|
1934
2192
|
let matrixOk = 0;
|
|
1935
2193
|
for (const t of AGENT_MATRIX) {
|
|
1936
2194
|
const abs = path11.join(repoRoot2, t.rel);
|
|
1937
|
-
if (!
|
|
1938
|
-
const text =
|
|
2195
|
+
if (!fs11.existsSync(abs)) continue;
|
|
2196
|
+
const text = fs11.readFileSync(abs, "utf8");
|
|
1939
2197
|
if (!text.includes(MARKER)) continue;
|
|
1940
2198
|
matrixOk++;
|
|
1941
2199
|
const used = extractPrefixUsed(text);
|
|
@@ -1953,10 +2211,10 @@ async function cmdDoctor(repoRoot2) {
|
|
|
1953
2211
|
console.log(`\u2713 Prefix konsisten (\`${expectedPrefix} context\`)`);
|
|
1954
2212
|
}
|
|
1955
2213
|
const skillPath = path11.join(repoRoot2, agentDir, "skills", "scopecairn", "SKILL.md");
|
|
1956
|
-
const skillOk =
|
|
2214
|
+
const skillOk = fs11.existsSync(skillPath);
|
|
1957
2215
|
console.log(`${tick(skillOk)} Skill (${agentDir}/skills/scopecairn/SKILL.md)`);
|
|
1958
2216
|
const wfPath = path11.join(repoRoot2, "workflows", "scopecairn.md");
|
|
1959
|
-
console.log(`${tick(
|
|
2217
|
+
console.log(`${tick(fs11.existsSync(wfPath))} Workflow (workflows/scopecairn.md, opsional)`);
|
|
1960
2218
|
console.log(`i Strict Mode mengabaikan allowlist \u2014 semua perintah tetap perlu persetujuan.`);
|
|
1961
2219
|
const healthy = nodeOk && sqliteOk && ts.ok && dbOk && matrixOk > 0 && skillOk && prefixOk;
|
|
1962
2220
|
console.log(`Status: ${healthy ? "HEALTHY" : "DEGRADED (see \u2717 above)"}`);
|
|
@@ -2075,32 +2333,8 @@ function cmdGraph(repoRoot2, key) {
|
|
|
2075
2333
|
}
|
|
2076
2334
|
}
|
|
2077
2335
|
|
|
2078
|
-
// src/commands/query.ts
|
|
2079
|
-
function cmdQuery(repoRoot2, text) {
|
|
2080
|
-
const db = openDb(repoRoot2);
|
|
2081
|
-
try {
|
|
2082
|
-
logInvocation(db, "query");
|
|
2083
|
-
const rows = db.prepare(
|
|
2084
|
-
`SELECT s.name, s.type, s.signature, f.path AS file
|
|
2085
|
-
FROM symbols s JOIN files f ON f.id = s.file_id
|
|
2086
|
-
WHERE s.name LIKE ? OR f.path LIKE ? OR s.signature LIKE ?
|
|
2087
|
-
ORDER BY CASE WHEN s.name = ? THEN 0 WHEN s.name LIKE ? THEN 1 ELSE 2 END,
|
|
2088
|
-
s.name LIMIT 30`
|
|
2089
|
-
).all(`%${text}%`, `%${text}%`, `%${text}%`, text, `${text}%`);
|
|
2090
|
-
if (rows.length === 0) {
|
|
2091
|
-
console.log(`No matches for "${text}".`);
|
|
2092
|
-
return;
|
|
2093
|
-
}
|
|
2094
|
-
for (const r of rows) {
|
|
2095
|
-
console.log(`${r.name} (${r.type}) \u2014 ${r.file}${r.signature ? ` \u2014 ${r.signature.slice(0, 80)}` : ""}`);
|
|
2096
|
-
}
|
|
2097
|
-
} finally {
|
|
2098
|
-
db.close();
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2101
|
-
|
|
2102
2336
|
// src/commands/read.ts
|
|
2103
|
-
import
|
|
2337
|
+
import fs12 from "fs";
|
|
2104
2338
|
import path12 from "path";
|
|
2105
2339
|
function cmdRead(repoRoot2, kind, name) {
|
|
2106
2340
|
if (kind !== "symbol") {
|
|
@@ -2120,7 +2354,7 @@ function cmdRead(repoRoot2, kind, name) {
|
|
|
2120
2354
|
const abs = path12.join(repoRoot2, node.file);
|
|
2121
2355
|
let lines;
|
|
2122
2356
|
try {
|
|
2123
|
-
lines =
|
|
2357
|
+
lines = fs12.readFileSync(abs, "utf8").split("\n");
|
|
2124
2358
|
} catch {
|
|
2125
2359
|
console.log(`Cannot read ${node.file}.`);
|
|
2126
2360
|
return;
|
|
@@ -2425,7 +2659,8 @@ var DEFINITION_OF_DONE = [
|
|
|
2425
2659
|
"Test relevan lulus",
|
|
2426
2660
|
"Tidak ada file berubah tanpa alasan",
|
|
2427
2661
|
"Tidak ada refactoring tak terkait",
|
|
2428
|
-
"Acceptance criteria terpenuhi"
|
|
2662
|
+
"Acceptance criteria terpenuhi",
|
|
2663
|
+
"Index dimutakhirkan (`scopecairn scan`)"
|
|
2429
2664
|
];
|
|
2430
2665
|
function guardBlock() {
|
|
2431
2666
|
return `## Task Guard
|
|
@@ -2545,7 +2780,7 @@ ${tests.map((t) => `- ${t}`).join("\n") || "-"}
|
|
|
2545
2780
|
}
|
|
2546
2781
|
|
|
2547
2782
|
// src/commands/orientasi.ts
|
|
2548
|
-
import
|
|
2783
|
+
import fs13 from "fs";
|
|
2549
2784
|
import path13 from "path";
|
|
2550
2785
|
function isPathLike(task) {
|
|
2551
2786
|
const t = task.trim().replace(/^["']|["']$/g, "");
|
|
@@ -2556,7 +2791,7 @@ function looksLikeRepoPath(db, repoRoot2, task) {
|
|
|
2556
2791
|
if (/\s/.test(t)) return false;
|
|
2557
2792
|
if (isPathLike(task)) return true;
|
|
2558
2793
|
try {
|
|
2559
|
-
|
|
2794
|
+
fs13.statSync(path13.resolve(repoRoot2, t));
|
|
2560
2795
|
return true;
|
|
2561
2796
|
} catch {
|
|
2562
2797
|
}
|
|
@@ -2595,7 +2830,7 @@ function cmdOrientasi(repoRoot2, task) {
|
|
|
2595
2830
|
const abs = path13.resolve(repoRoot2, t);
|
|
2596
2831
|
let rel = null;
|
|
2597
2832
|
try {
|
|
2598
|
-
const stat =
|
|
2833
|
+
const stat = fs13.statSync(abs);
|
|
2599
2834
|
if (stat.isFile()) rel = path13.relative(repoRoot2, abs).split(path13.sep).join("/");
|
|
2600
2835
|
} catch {
|
|
2601
2836
|
rel = null;
|
|
@@ -2846,31 +3081,152 @@ function cmdImpact(repoRoot2, target, opts = {}) {
|
|
|
2846
3081
|
}
|
|
2847
3082
|
|
|
2848
3083
|
// src/commands/init.ts
|
|
2849
|
-
import
|
|
3084
|
+
import fs15 from "fs";
|
|
3085
|
+
import path15 from "path";
|
|
3086
|
+
|
|
3087
|
+
// src/integrate/install.ts
|
|
3088
|
+
import fs14 from "fs";
|
|
2850
3089
|
import path14 from "path";
|
|
2851
|
-
function
|
|
3090
|
+
function installTarget(repoRoot2, target, prefix) {
|
|
2852
3091
|
const abs = path14.join(repoRoot2, target.rel);
|
|
2853
3092
|
const block = target.render(prefix);
|
|
3093
|
+
const finish = (how) => ({
|
|
3094
|
+
agent: target.id,
|
|
3095
|
+
rel: target.rel,
|
|
3096
|
+
how
|
|
3097
|
+
});
|
|
2854
3098
|
if (target.mode === "new") {
|
|
2855
|
-
const existed =
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
return existed ? "updated" : "generated";
|
|
3099
|
+
const existed = fs14.existsSync(abs);
|
|
3100
|
+
fs14.mkdirSync(path14.dirname(abs), { recursive: true });
|
|
3101
|
+
fs14.writeFileSync(abs, block.endsWith("\n") ? block : block + "\n");
|
|
3102
|
+
return finish(existed ? "updated" : "generated");
|
|
2859
3103
|
}
|
|
2860
|
-
if (
|
|
2861
|
-
const cur =
|
|
3104
|
+
if (fs14.existsSync(abs)) {
|
|
3105
|
+
const cur = fs14.readFileSync(abs, "utf8");
|
|
2862
3106
|
if (cur.includes(MARKER)) {
|
|
2863
|
-
const
|
|
2864
|
-
|
|
2865
|
-
return "updated";
|
|
3107
|
+
const { next: next2 } = removeManagedBlock(cur, (p) => target.render(p));
|
|
3108
|
+
fs14.writeFileSync(abs, (next2 + "\n\n" + block).trimEnd() + "\n");
|
|
3109
|
+
return finish("updated");
|
|
3110
|
+
}
|
|
3111
|
+
const { next, changed } = removeManagedBlock(cur, (p) => target.render(p));
|
|
3112
|
+
const base = changed ? next : cur;
|
|
3113
|
+
fs14.writeFileSync(abs, (base.trimEnd() + "\n\n" + block).trimEnd() + "\n");
|
|
3114
|
+
return finish("extended");
|
|
3115
|
+
}
|
|
3116
|
+
fs14.mkdirSync(path14.dirname(abs), { recursive: true });
|
|
3117
|
+
fs14.writeFileSync(abs, block.endsWith("\n") ? block : block + "\n");
|
|
3118
|
+
return finish("generated");
|
|
3119
|
+
}
|
|
3120
|
+
function removeManagedBlock(content, render) {
|
|
3121
|
+
for (const p of ["scopecairn", "npx scopecairn"]) {
|
|
3122
|
+
for (const cand of [render(p), render(p).trimEnd(), render(p).trimEnd() + "\n"]) {
|
|
3123
|
+
if (cand && content.includes(cand)) {
|
|
3124
|
+
return { next: foldBlank(content.split(cand).join("")), changed: true };
|
|
3125
|
+
}
|
|
2866
3126
|
}
|
|
2867
|
-
fs13.writeFileSync(abs, cur.trimEnd() + "\n\n" + block);
|
|
2868
|
-
return "extended";
|
|
2869
3127
|
}
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
3128
|
+
const anchored = new RegExp(
|
|
3129
|
+
`\\n*${MARKER}\\n# ScopeCairn[\\s\\S]*?pertanyaan non-coding\\.[ \\t]*(\\n|$)`,
|
|
3130
|
+
""
|
|
3131
|
+
);
|
|
3132
|
+
if (anchored.test(content)) {
|
|
3133
|
+
return { next: foldBlank(content.replace(anchored, "\n")), changed: true };
|
|
3134
|
+
}
|
|
3135
|
+
const orphan = /# ScopeCairn[\s\S]*?pertanyaan non-coding\.[ \t]*(\n|$)/g;
|
|
3136
|
+
if (orphan.test(content)) {
|
|
3137
|
+
return { next: foldBlank(content.replace(orphan, "")), changed: true };
|
|
3138
|
+
}
|
|
3139
|
+
return { next: content, changed: false };
|
|
3140
|
+
}
|
|
3141
|
+
function foldBlank(s) {
|
|
3142
|
+
return s.replace(/\n{3,}/g, "\n\n").trim();
|
|
2873
3143
|
}
|
|
3144
|
+
function uninstallTarget(repoRoot2, target) {
|
|
3145
|
+
const abs = path14.join(repoRoot2, target.rel);
|
|
3146
|
+
const finish = (how) => ({
|
|
3147
|
+
agent: target.id,
|
|
3148
|
+
rel: target.rel,
|
|
3149
|
+
how
|
|
3150
|
+
});
|
|
3151
|
+
if (!fs14.existsSync(abs)) return finish("absent");
|
|
3152
|
+
if (target.mode === "new") {
|
|
3153
|
+
fs14.rmSync(abs, { force: true });
|
|
3154
|
+
pruneEmptyDirs(path14.dirname(abs), repoRoot2);
|
|
3155
|
+
return finish("removed");
|
|
3156
|
+
}
|
|
3157
|
+
const cur = fs14.readFileSync(abs, "utf8");
|
|
3158
|
+
if (!cur.includes(MARKER)) return finish("skipped");
|
|
3159
|
+
const render = (p) => {
|
|
3160
|
+
const t = resolveAgent(target.id);
|
|
3161
|
+
return t ? t.render(p) : "";
|
|
3162
|
+
};
|
|
3163
|
+
const { next, changed } = removeManagedBlock(cur, render);
|
|
3164
|
+
if (!changed) return finish("skipped");
|
|
3165
|
+
if (!next) {
|
|
3166
|
+
fs14.rmSync(abs, { force: true });
|
|
3167
|
+
return finish("removed");
|
|
3168
|
+
}
|
|
3169
|
+
fs14.writeFileSync(abs, next + "\n");
|
|
3170
|
+
return finish("removed");
|
|
3171
|
+
}
|
|
3172
|
+
function pruneEmptyDirs(dir, stop) {
|
|
3173
|
+
let cur = dir;
|
|
3174
|
+
while (cur.startsWith(stop) && cur !== stop) {
|
|
3175
|
+
try {
|
|
3176
|
+
fs14.rmdirSync(cur);
|
|
3177
|
+
} catch {
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
cur = path14.dirname(cur);
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
function installAntigravity(repoRoot2, prefix = detectPrefix(repoRoot2)) {
|
|
3184
|
+
const agentDir = detectAgentDir(repoRoot2);
|
|
3185
|
+
const out = [];
|
|
3186
|
+
const skillPath = path14.join(repoRoot2, agentDir, "skills", "scopecairn", "SKILL.md");
|
|
3187
|
+
fs14.mkdirSync(path14.dirname(skillPath), { recursive: true });
|
|
3188
|
+
fs14.writeFileSync(skillPath, skillMd(prefix));
|
|
3189
|
+
out.push(`Skill installed (${path14.relative(repoRoot2, skillPath)})`);
|
|
3190
|
+
const wfPath = path14.join(repoRoot2, "workflows", "scopecairn.md");
|
|
3191
|
+
fs14.mkdirSync(path14.dirname(wfPath), { recursive: true });
|
|
3192
|
+
fs14.writeFileSync(wfPath, workflowMd(prefix));
|
|
3193
|
+
out.push(`Workflow installed (${path14.relative(repoRoot2, wfPath)})`);
|
|
3194
|
+
return out;
|
|
3195
|
+
}
|
|
3196
|
+
function uninstallAntigravity(repoRoot2) {
|
|
3197
|
+
const out = [];
|
|
3198
|
+
for (const dir of [".agents", ".agent"]) {
|
|
3199
|
+
const p = path14.join(repoRoot2, dir, "skills", "scopecairn", "SKILL.md");
|
|
3200
|
+
if (fs14.existsSync(p)) {
|
|
3201
|
+
fs14.rmSync(p, { force: true });
|
|
3202
|
+
pruneEmptyDirs(path14.dirname(p), repoRoot2);
|
|
3203
|
+
out.push(`Skill removed (${path14.relative(repoRoot2, p)})`);
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
const wf = path14.join(repoRoot2, "workflows", "scopecairn.md");
|
|
3207
|
+
if (fs14.existsSync(wf)) {
|
|
3208
|
+
const text = fs14.readFileSync(wf, "utf8");
|
|
3209
|
+
if (text.includes("scopecairn")) {
|
|
3210
|
+
fs14.rmSync(wf, { force: true });
|
|
3211
|
+
out.push(`Workflow removed (${path14.relative(repoRoot2, wf)})`);
|
|
3212
|
+
} else {
|
|
3213
|
+
out.push(`Workflow skipped (bukan milik ScopeCairn)`);
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
if (out.length === 0) out.push("Nothing installed.");
|
|
3217
|
+
return out;
|
|
3218
|
+
}
|
|
3219
|
+
function agentIds() {
|
|
3220
|
+
return ["antigravity", ...AGENT_MATRIX.map((t) => t.id)];
|
|
3221
|
+
}
|
|
3222
|
+
function allowlistText(repoRoot2) {
|
|
3223
|
+
return allowlistGuide(detectPrefix(repoRoot2));
|
|
3224
|
+
}
|
|
3225
|
+
function resolveAgent(id) {
|
|
3226
|
+
return findTarget(id);
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
// src/commands/init.ts
|
|
2874
3230
|
function cmdInit(repoRoot2, opts = {}) {
|
|
2875
3231
|
console.log("ScopeCairn");
|
|
2876
3232
|
console.log("\u2713 Repository detected");
|
|
@@ -2879,49 +3235,43 @@ function cmdInit(repoRoot2, opts = {}) {
|
|
|
2879
3235
|
if (stats.metaFiles > 0) console.log(`\u2713 ${stats.metaFiles} config/schema files`);
|
|
2880
3236
|
console.log(`\u2713 ${stats.symbols} symbols`);
|
|
2881
3237
|
console.log(`\u2713 ${stats.relationships} relationships`);
|
|
3238
|
+
if (stats.adapters.length > 0) console.log(`\u2713 adapters: ${stats.adapters.join(", ")}`);
|
|
2882
3239
|
const prefix = detectPrefix(repoRoot2);
|
|
2883
|
-
const agentDir = detectAgentDir(repoRoot2);
|
|
2884
3240
|
const requested = parseAgentsOption(opts.agents);
|
|
2885
|
-
|
|
2886
|
-
if (requested === "all") {
|
|
2887
|
-
targets = [...AGENT_MATRIX];
|
|
2888
|
-
} else if (requested && requested.length > 0) {
|
|
2889
|
-
targets = [];
|
|
2890
|
-
for (const id of requested) {
|
|
2891
|
-
const t = findTarget(id);
|
|
2892
|
-
if (!t) {
|
|
2893
|
-
console.log(`! Unknown agent "${id}" (pilihan: ${AGENT_MATRIX.map((x) => x.id).join(", ")}, all)`);
|
|
2894
|
-
continue;
|
|
2895
|
-
}
|
|
2896
|
-
targets.push(t);
|
|
2897
|
-
}
|
|
2898
|
-
if (!targets.includes(AGENT_MATRIX[0])) targets.unshift(AGENT_MATRIX[0]);
|
|
2899
|
-
} else {
|
|
2900
|
-
targets = [AGENT_MATRIX[0]];
|
|
2901
|
-
for (const t of AGENT_MATRIX.slice(1)) {
|
|
2902
|
-
if (fs13.existsSync(path14.join(repoRoot2, t.presentMarker))) targets.push(t);
|
|
2903
|
-
}
|
|
2904
|
-
}
|
|
3241
|
+
const targets = requested === "all" ? [...AGENT_MATRIX] : requested && requested.length > 0 ? withCore(requested) : withDetected(repoRoot2);
|
|
2905
3242
|
for (const t of targets) {
|
|
2906
|
-
const
|
|
2907
|
-
console.log(`\u2713 ${t.label}: ${
|
|
3243
|
+
const r = installTarget(repoRoot2, t, prefix);
|
|
3244
|
+
console.log(`\u2713 ${t.label}: ${r.rel} (${r.how})`);
|
|
2908
3245
|
}
|
|
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)})`);
|
|
3246
|
+
for (const line of installAntigravity(repoRoot2, prefix)) console.log(`\u2713 ${line}`);
|
|
2917
3247
|
console.log("Ready.");
|
|
2918
3248
|
console.log("");
|
|
2919
|
-
console.log(
|
|
3249
|
+
console.log(allowlistText(repoRoot2));
|
|
3250
|
+
}
|
|
3251
|
+
function withCore(ids) {
|
|
3252
|
+
const out = [];
|
|
3253
|
+
for (const id of ids) {
|
|
3254
|
+
const t = findTarget(id);
|
|
3255
|
+
if (!t) {
|
|
3256
|
+
console.log(`! Unknown agent "${id}" (pilihan: ${AGENT_MATRIX.map((x) => x.id).join(", ")}, all)`);
|
|
3257
|
+
continue;
|
|
3258
|
+
}
|
|
3259
|
+
out.push(t);
|
|
3260
|
+
}
|
|
3261
|
+
if (!out.includes(AGENT_MATRIX[0])) out.unshift(AGENT_MATRIX[0]);
|
|
3262
|
+
return out;
|
|
3263
|
+
}
|
|
3264
|
+
function withDetected(repoRoot2) {
|
|
3265
|
+
const targets = [AGENT_MATRIX[0]];
|
|
3266
|
+
for (const t of AGENT_MATRIX.slice(1)) {
|
|
3267
|
+
if (fs15.existsSync(path15.join(repoRoot2, t.presentMarker))) targets.push(t);
|
|
3268
|
+
}
|
|
3269
|
+
return targets;
|
|
2920
3270
|
}
|
|
2921
3271
|
|
|
2922
3272
|
// src/benchmark/run.ts
|
|
2923
|
-
import
|
|
2924
|
-
import
|
|
3273
|
+
import fs16 from "fs";
|
|
3274
|
+
import path16 from "path";
|
|
2925
3275
|
|
|
2926
3276
|
// src/benchmark/dataset.ts
|
|
2927
3277
|
var BENCHMARK_TASKS = [
|
|
@@ -2978,7 +3328,7 @@ var BENCHMARK_TASKS = [
|
|
|
2978
3328
|
// src/benchmark/run.ts
|
|
2979
3329
|
function fileChars(repoRoot2, rel) {
|
|
2980
3330
|
try {
|
|
2981
|
-
return
|
|
3331
|
+
return fs16.readFileSync(path16.join(repoRoot2, rel), "utf8").length;
|
|
2982
3332
|
} catch {
|
|
2983
3333
|
return 0;
|
|
2984
3334
|
}
|
|
@@ -3127,10 +3477,42 @@ function cmdBenchmark(repoRoot2, opts = {}) {
|
|
|
3127
3477
|
}
|
|
3128
3478
|
}
|
|
3129
3479
|
|
|
3480
|
+
// src/commands/agents.ts
|
|
3481
|
+
function registerAgentCommands(program2, repoRoot2) {
|
|
3482
|
+
const list = program2.command("agents").description("List supported agent installers");
|
|
3483
|
+
list.command("list").description("Show agent ids").action(() => {
|
|
3484
|
+
console.log(agentIds().join("\n"));
|
|
3485
|
+
});
|
|
3486
|
+
const anti = program2.command("antigravity").description("Antigravity IDE integration");
|
|
3487
|
+
anti.command("install").description("Install Skill + Workflow, print allowlist guide").action(() => {
|
|
3488
|
+
const root = repoRoot2();
|
|
3489
|
+
const prefix = detectPrefix(root);
|
|
3490
|
+
for (const line of installAntigravity(root, prefix)) console.log(`\u2713 ${line}`);
|
|
3491
|
+
console.log("");
|
|
3492
|
+
console.log(allowlistText(root));
|
|
3493
|
+
});
|
|
3494
|
+
anti.command("uninstall").description("Remove Skill + Workflow").action(() => {
|
|
3495
|
+
for (const line of uninstallAntigravity(repoRoot2())) console.log(`\u2713 ${line}`);
|
|
3496
|
+
});
|
|
3497
|
+
for (const t of AGENT_MATRIX) {
|
|
3498
|
+
if (t.id === "agents") continue;
|
|
3499
|
+
const cmd = program2.command(t.id).description(`${t.label} integration`);
|
|
3500
|
+
cmd.command("install").description(`Write ${t.rel}`).action(() => {
|
|
3501
|
+
const root = repoRoot2();
|
|
3502
|
+
const r = installTarget(root, t, detectPrefix(root));
|
|
3503
|
+
console.log(`\u2713 ${t.label}: ${r.rel} (${r.how})`);
|
|
3504
|
+
});
|
|
3505
|
+
cmd.command("uninstall").description(`Remove ScopeCairn block from ${t.rel}`).action(() => {
|
|
3506
|
+
const r = uninstallTarget(repoRoot2(), t);
|
|
3507
|
+
console.log(`\u2713 ${t.label}: ${r.rel} (${r.how})`);
|
|
3508
|
+
});
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3130
3512
|
// package.json
|
|
3131
3513
|
var package_default = {
|
|
3132
3514
|
name: "scopecairn",
|
|
3133
|
-
version: "0.
|
|
3515
|
+
version: "0.3.0",
|
|
3134
3516
|
description: "ScopeCairn \u2014 Codebase Intelligence & Agent Optimization Layer (local-first)",
|
|
3135
3517
|
license: "MIT",
|
|
3136
3518
|
type: "module",
|
|
@@ -3180,23 +3562,36 @@ var package_default = {
|
|
|
3180
3562
|
|
|
3181
3563
|
// src/cli.ts
|
|
3182
3564
|
var program = new Command();
|
|
3183
|
-
program.name("scopecairn").description("ScopeCairn \u2014 local-first codebase intelligence layer").version(package_default.version)
|
|
3565
|
+
program.name("scopecairn").description("ScopeCairn \u2014 local-first codebase intelligence layer").version(package_default.version).addHelpText(
|
|
3566
|
+
"after",
|
|
3567
|
+
`
|
|
3568
|
+
Examples:
|
|
3569
|
+
$ scopecairn init index repo + setup agent integration
|
|
3570
|
+
$ scopecairn init ./myapp --agents all setup another directory, all agents
|
|
3571
|
+
$ scopecairn context "add login" relevant context + scope for a task
|
|
3572
|
+
$ scopecairn context ./ repository orientation map
|
|
3573
|
+
$ scopecairn impact src/auth/login.ts change impact analysis
|
|
3574
|
+
$ scopecairn read symbol approveRequest symbol-level source excerpt
|
|
3575
|
+
$ scopecairn doctor health + integration check
|
|
3576
|
+
|
|
3577
|
+
Agent commands (context, impact, read, graph, status, doctor) are
|
|
3578
|
+
read-only toward source and only write to .scopecairn/ \u2014 safe to allowlist.
|
|
3579
|
+
`
|
|
3580
|
+
);
|
|
3184
3581
|
function repoRoot() {
|
|
3185
|
-
return
|
|
3582
|
+
return path17.resolve(process.cwd());
|
|
3186
3583
|
}
|
|
3187
3584
|
function resolveRoot(given) {
|
|
3188
3585
|
if (!given) return repoRoot();
|
|
3189
|
-
return
|
|
3586
|
+
return path17.resolve(process.cwd(), given);
|
|
3190
3587
|
}
|
|
3191
|
-
program.command("scan").description("Index repository (
|
|
3588
|
+
program.command("scan").description("Index repository (files + symbols + relations)").action(() => cmdScan(repoRoot()));
|
|
3192
3589
|
program.command("status").description("Show index status").action(() => cmdStatus(repoRoot()));
|
|
3193
|
-
program.command("clean").description("Remove generated index data").action(() => cmdClean(repoRoot()));
|
|
3194
3590
|
program.command("rebuild").description("Rebuild index from scratch (recover from corrupt index)").action(() => cmdRebuild(repoRoot()));
|
|
3195
3591
|
program.command("doctor").description("Check integration health").action(async () => {
|
|
3196
3592
|
await cmdDoctor(repoRoot());
|
|
3197
3593
|
});
|
|
3198
|
-
program.command("graph <symbol>").description("Show relations of a symbol
|
|
3199
|
-
program.command("query <text>").description("Search symbols/codebase knowledge (substring match)").action((text) => cmdQuery(repoRoot(), text));
|
|
3594
|
+
program.command("graph <symbol>").description("Show relations of a symbol").action((symbol) => cmdGraph(repoRoot(), symbol));
|
|
3200
3595
|
program.command("read").argument("<kind>", "what to read (symbol)").argument("<name>", "symbol name").description("Read a symbol's source excerpt (not the whole file)").action((kind, name) => cmdRead(repoRoot(), kind, name));
|
|
3201
3596
|
program.command("context <task>").description("Main entry point: auto-refresh + classify + relevant context + scope").option("--escalate", "force full COMPLEX context").option("--no-refresh", "skip incremental refresh (debugging/benchmark)").action(
|
|
3202
3597
|
(task, opts) => cmdContext(repoRoot(), task, opts)
|
|
@@ -3210,6 +3605,7 @@ program.command("init [path]").description("Initial indexing + generate AGENTS.m
|
|
|
3210
3605
|
program.command("benchmark").description("Retrieval recall, irrelevant ratio, context reduction + weight tuning").option("--tune", "grid search bobot FR-05").option("--no-refresh", "skip incremental refresh (debugging/benchmark)").action(
|
|
3211
3606
|
(opts) => cmdBenchmark(repoRoot(), opts)
|
|
3212
3607
|
);
|
|
3608
|
+
registerAgentCommands(program, repoRoot);
|
|
3213
3609
|
program.parseAsync(process.argv);
|
|
3214
3610
|
export {
|
|
3215
3611
|
resolveRoot
|