scopecairn 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -14
- package/dist/cli.js +625 -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
|
|
@@ -2544,6 +2779,116 @@ ${tests.map((t) => `- ${t}`).join("\n") || "-"}
|
|
|
2544
2779
|
return { markdown: md, high: highFiles, medium: medFiles, tests, dependencies: deps };
|
|
2545
2780
|
}
|
|
2546
2781
|
|
|
2782
|
+
// src/commands/orientasi.ts
|
|
2783
|
+
import fs13 from "fs";
|
|
2784
|
+
import path13 from "path";
|
|
2785
|
+
function isPathLike(task) {
|
|
2786
|
+
const t = task.trim().replace(/^["']|["']$/g, "");
|
|
2787
|
+
return t === "." || t === "./" || t === "/" || t.startsWith("./") || t.startsWith("../") || t.startsWith("/") || t.startsWith("~");
|
|
2788
|
+
}
|
|
2789
|
+
function looksLikeRepoPath(db, repoRoot2, task) {
|
|
2790
|
+
const t = task.trim().replace(/^["']|["']$/g, "");
|
|
2791
|
+
if (/\s/.test(t)) return false;
|
|
2792
|
+
if (isPathLike(task)) return true;
|
|
2793
|
+
try {
|
|
2794
|
+
fs13.statSync(path13.resolve(repoRoot2, t));
|
|
2795
|
+
return true;
|
|
2796
|
+
} catch {
|
|
2797
|
+
}
|
|
2798
|
+
try {
|
|
2799
|
+
const row = db.prepare(`SELECT 1 AS ok FROM files WHERE path = ? OR path LIKE ? LIMIT 1`).get(t, `%/${t}`);
|
|
2800
|
+
return !!row;
|
|
2801
|
+
} catch {
|
|
2802
|
+
return false;
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
function topCentral(db, limit = 10) {
|
|
2806
|
+
try {
|
|
2807
|
+
return db.prepare(
|
|
2808
|
+
`SELECT s.name, s.type, f.path AS file, COUNT(*) AS fanin
|
|
2809
|
+
FROM relationships r
|
|
2810
|
+
JOIN symbols s ON s.id = r.target_id
|
|
2811
|
+
JOIN files f ON f.id = s.file_id
|
|
2812
|
+
WHERE s.type IN ('function','class','method','component','interface','type')
|
|
2813
|
+
GROUP BY r.target_id ORDER BY fanin DESC LIMIT ${limit}`
|
|
2814
|
+
).all();
|
|
2815
|
+
} catch {
|
|
2816
|
+
return [];
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
function cmdOrientasi(repoRoot2, task) {
|
|
2820
|
+
const db = openDb(repoRoot2);
|
|
2821
|
+
try {
|
|
2822
|
+
const files = db.prepare(`SELECT COUNT(*) AS n FROM files`).get().n;
|
|
2823
|
+
if (files === 0) {
|
|
2824
|
+
console.log("Index kosong. Jalankan `scopecairn scan` dulu.");
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
const byLang = db.prepare(`SELECT language, COUNT(*) AS n FROM files GROUP BY language ORDER BY n DESC`).all();
|
|
2828
|
+
const symbols = db.prepare(`SELECT COUNT(*) AS n FROM symbols`).get().n;
|
|
2829
|
+
const t = task.trim().replace(/^["']|["']$/g, "");
|
|
2830
|
+
const abs = path13.resolve(repoRoot2, t);
|
|
2831
|
+
let rel = null;
|
|
2832
|
+
try {
|
|
2833
|
+
const stat = fs13.statSync(abs);
|
|
2834
|
+
if (stat.isFile()) rel = path13.relative(repoRoot2, abs).split(path13.sep).join("/");
|
|
2835
|
+
} catch {
|
|
2836
|
+
rel = null;
|
|
2837
|
+
}
|
|
2838
|
+
if (rel) {
|
|
2839
|
+
const row = db.prepare(`SELECT id FROM files WHERE path = ?`).get(rel);
|
|
2840
|
+
if (row) {
|
|
2841
|
+
const syms = db.prepare(`SELECT name, type FROM symbols WHERE file_id = ? AND type != 'file' LIMIT 20`).all(row.id);
|
|
2842
|
+
const fileSym = db.prepare(`SELECT id FROM symbols WHERE file_id = ? AND type = 'file'`).get(row.id);
|
|
2843
|
+
const linked = /* @__PURE__ */ new Set();
|
|
2844
|
+
if (fileSym) {
|
|
2845
|
+
for (const e of neighbors(db, fileSym.id, 30)) {
|
|
2846
|
+
if (e.other.file !== rel) linked.add(e.other.file);
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
console.log(
|
|
2850
|
+
`# Task Context (orientasi)
|
|
2851
|
+
|
|
2852
|
+
## Task
|
|
2853
|
+
${task}
|
|
2854
|
+
|
|
2855
|
+
## File
|
|
2856
|
+
${rel}
|
|
2857
|
+
|
|
2858
|
+
## Simbol (${syms.length})
|
|
2859
|
+
${syms.map((s) => `- ${s.name} (${s.type})`).join("\n") || "-"}
|
|
2860
|
+
|
|
2861
|
+
## Terkait langsung
|
|
2862
|
+
${[...linked].slice(0, 10).map((f) => `- ${f}`).join("\n") || "-"}
|
|
2863
|
+
|
|
2864
|
+
Butuh konteks kerja? Jalankan \`scopecairn context "<deskripsi task>"\`.
|
|
2865
|
+
`
|
|
2866
|
+
);
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
const central = topCentral(db);
|
|
2871
|
+
console.log(
|
|
2872
|
+
`# Task Context (orientasi)
|
|
2873
|
+
|
|
2874
|
+
## Task
|
|
2875
|
+
${task}
|
|
2876
|
+
|
|
2877
|
+
## Ringkasan index
|
|
2878
|
+
- ${files} file (${byLang.map((l) => `${l.language} ${l.n}`).join(", ")})
|
|
2879
|
+
- ${symbols} simbol
|
|
2880
|
+
|
|
2881
|
+
## Simbol paling dipakai
|
|
2882
|
+
${central.map((c) => `- ${c.name} (${c.type}, ${c.file}) \u2190 ${c.fanin}x`).join("\n") || "-"}
|
|
2883
|
+
|
|
2884
|
+
Mulai dari simbol di atas, atau jalankan \`scopecairn context "<deskripsi task>"\` untuk konteks kerja.
|
|
2885
|
+
`
|
|
2886
|
+
);
|
|
2887
|
+
} finally {
|
|
2888
|
+
db.close();
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2547
2892
|
// src/commands/context.ts
|
|
2548
2893
|
function cmdContext(repoRoot2, task, opts = {}) {
|
|
2549
2894
|
if (!opts.noRefresh) {
|
|
@@ -2562,6 +2907,11 @@ function cmdContext(repoRoot2, task, opts = {}) {
|
|
|
2562
2907
|
console.log("Index kosong. Jalankan `scopecairn scan` dulu.");
|
|
2563
2908
|
return;
|
|
2564
2909
|
}
|
|
2910
|
+
if (looksLikeRepoPath(db, repoRoot2, task)) {
|
|
2911
|
+
logInvocation(db, "context");
|
|
2912
|
+
cmdOrientasi(repoRoot2, task);
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2565
2915
|
const result = retrieve(db, repoRoot2, task);
|
|
2566
2916
|
if (result.ranked.length === 0) {
|
|
2567
2917
|
logInvocation(db, "context");
|
|
@@ -2731,31 +3081,152 @@ function cmdImpact(repoRoot2, target, opts = {}) {
|
|
|
2731
3081
|
}
|
|
2732
3082
|
|
|
2733
3083
|
// src/commands/init.ts
|
|
2734
|
-
import
|
|
2735
|
-
import
|
|
2736
|
-
|
|
2737
|
-
|
|
3084
|
+
import fs15 from "fs";
|
|
3085
|
+
import path15 from "path";
|
|
3086
|
+
|
|
3087
|
+
// src/integrate/install.ts
|
|
3088
|
+
import fs14 from "fs";
|
|
3089
|
+
import path14 from "path";
|
|
3090
|
+
function installTarget(repoRoot2, target, prefix) {
|
|
3091
|
+
const abs = path14.join(repoRoot2, target.rel);
|
|
2738
3092
|
const block = target.render(prefix);
|
|
3093
|
+
const finish = (how) => ({
|
|
3094
|
+
agent: target.id,
|
|
3095
|
+
rel: target.rel,
|
|
3096
|
+
how
|
|
3097
|
+
});
|
|
2739
3098
|
if (target.mode === "new") {
|
|
2740
|
-
const existed =
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
return existed ? "updated" : "generated";
|
|
3099
|
+
const existed = fs14.existsSync(abs);
|
|
3100
|
+
fs14.mkdirSync(path14.dirname(abs), { recursive: true });
|
|
3101
|
+
fs14.writeFileSync(abs, block.endsWith("\n") ? block : block + "\n");
|
|
3102
|
+
return finish(existed ? "updated" : "generated");
|
|
2744
3103
|
}
|
|
2745
|
-
if (
|
|
2746
|
-
const cur =
|
|
3104
|
+
if (fs14.existsSync(abs)) {
|
|
3105
|
+
const cur = fs14.readFileSync(abs, "utf8");
|
|
2747
3106
|
if (cur.includes(MARKER)) {
|
|
2748
|
-
const
|
|
2749
|
-
|
|
2750
|
-
return "updated";
|
|
3107
|
+
const { next: next2 } = removeManagedBlock(cur, (p) => target.render(p));
|
|
3108
|
+
fs14.writeFileSync(abs, (next2 + "\n\n" + block).trimEnd() + "\n");
|
|
3109
|
+
return finish("updated");
|
|
3110
|
+
}
|
|
3111
|
+
const { next, changed } = removeManagedBlock(cur, (p) => target.render(p));
|
|
3112
|
+
const base = changed ? next : cur;
|
|
3113
|
+
fs14.writeFileSync(abs, (base.trimEnd() + "\n\n" + block).trimEnd() + "\n");
|
|
3114
|
+
return finish("extended");
|
|
3115
|
+
}
|
|
3116
|
+
fs14.mkdirSync(path14.dirname(abs), { recursive: true });
|
|
3117
|
+
fs14.writeFileSync(abs, block.endsWith("\n") ? block : block + "\n");
|
|
3118
|
+
return finish("generated");
|
|
3119
|
+
}
|
|
3120
|
+
function removeManagedBlock(content, render) {
|
|
3121
|
+
for (const p of ["scopecairn", "npx scopecairn"]) {
|
|
3122
|
+
for (const cand of [render(p), render(p).trimEnd(), render(p).trimEnd() + "\n"]) {
|
|
3123
|
+
if (cand && content.includes(cand)) {
|
|
3124
|
+
return { next: foldBlank(content.split(cand).join("")), changed: true };
|
|
3125
|
+
}
|
|
2751
3126
|
}
|
|
2752
|
-
fs12.writeFileSync(abs, cur.trimEnd() + "\n\n" + block);
|
|
2753
|
-
return "extended";
|
|
2754
3127
|
}
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
3128
|
+
const anchored = new RegExp(
|
|
3129
|
+
`\\n*${MARKER}\\n# ScopeCairn[\\s\\S]*?pertanyaan non-coding\\.[ \\t]*(\\n|$)`,
|
|
3130
|
+
""
|
|
3131
|
+
);
|
|
3132
|
+
if (anchored.test(content)) {
|
|
3133
|
+
return { next: foldBlank(content.replace(anchored, "\n")), changed: true };
|
|
3134
|
+
}
|
|
3135
|
+
const orphan = /# ScopeCairn[\s\S]*?pertanyaan non-coding\.[ \t]*(\n|$)/g;
|
|
3136
|
+
if (orphan.test(content)) {
|
|
3137
|
+
return { next: foldBlank(content.replace(orphan, "")), changed: true };
|
|
3138
|
+
}
|
|
3139
|
+
return { next: content, changed: false };
|
|
3140
|
+
}
|
|
3141
|
+
function foldBlank(s) {
|
|
3142
|
+
return s.replace(/\n{3,}/g, "\n\n").trim();
|
|
3143
|
+
}
|
|
3144
|
+
function uninstallTarget(repoRoot2, target) {
|
|
3145
|
+
const abs = path14.join(repoRoot2, target.rel);
|
|
3146
|
+
const finish = (how) => ({
|
|
3147
|
+
agent: target.id,
|
|
3148
|
+
rel: target.rel,
|
|
3149
|
+
how
|
|
3150
|
+
});
|
|
3151
|
+
if (!fs14.existsSync(abs)) return finish("absent");
|
|
3152
|
+
if (target.mode === "new") {
|
|
3153
|
+
fs14.rmSync(abs, { force: true });
|
|
3154
|
+
pruneEmptyDirs(path14.dirname(abs), repoRoot2);
|
|
3155
|
+
return finish("removed");
|
|
3156
|
+
}
|
|
3157
|
+
const cur = fs14.readFileSync(abs, "utf8");
|
|
3158
|
+
if (!cur.includes(MARKER)) return finish("skipped");
|
|
3159
|
+
const render = (p) => {
|
|
3160
|
+
const t = resolveAgent(target.id);
|
|
3161
|
+
return t ? t.render(p) : "";
|
|
3162
|
+
};
|
|
3163
|
+
const { next, changed } = removeManagedBlock(cur, render);
|
|
3164
|
+
if (!changed) return finish("skipped");
|
|
3165
|
+
if (!next) {
|
|
3166
|
+
fs14.rmSync(abs, { force: true });
|
|
3167
|
+
return finish("removed");
|
|
3168
|
+
}
|
|
3169
|
+
fs14.writeFileSync(abs, next + "\n");
|
|
3170
|
+
return finish("removed");
|
|
3171
|
+
}
|
|
3172
|
+
function pruneEmptyDirs(dir, stop) {
|
|
3173
|
+
let cur = dir;
|
|
3174
|
+
while (cur.startsWith(stop) && cur !== stop) {
|
|
3175
|
+
try {
|
|
3176
|
+
fs14.rmdirSync(cur);
|
|
3177
|
+
} catch {
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
cur = path14.dirname(cur);
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
function installAntigravity(repoRoot2, prefix = detectPrefix(repoRoot2)) {
|
|
3184
|
+
const agentDir = detectAgentDir(repoRoot2);
|
|
3185
|
+
const out = [];
|
|
3186
|
+
const skillPath = path14.join(repoRoot2, agentDir, "skills", "scopecairn", "SKILL.md");
|
|
3187
|
+
fs14.mkdirSync(path14.dirname(skillPath), { recursive: true });
|
|
3188
|
+
fs14.writeFileSync(skillPath, skillMd(prefix));
|
|
3189
|
+
out.push(`Skill installed (${path14.relative(repoRoot2, skillPath)})`);
|
|
3190
|
+
const wfPath = path14.join(repoRoot2, "workflows", "scopecairn.md");
|
|
3191
|
+
fs14.mkdirSync(path14.dirname(wfPath), { recursive: true });
|
|
3192
|
+
fs14.writeFileSync(wfPath, workflowMd(prefix));
|
|
3193
|
+
out.push(`Workflow installed (${path14.relative(repoRoot2, wfPath)})`);
|
|
3194
|
+
return out;
|
|
3195
|
+
}
|
|
3196
|
+
function uninstallAntigravity(repoRoot2) {
|
|
3197
|
+
const out = [];
|
|
3198
|
+
for (const dir of [".agents", ".agent"]) {
|
|
3199
|
+
const p = path14.join(repoRoot2, dir, "skills", "scopecairn", "SKILL.md");
|
|
3200
|
+
if (fs14.existsSync(p)) {
|
|
3201
|
+
fs14.rmSync(p, { force: true });
|
|
3202
|
+
pruneEmptyDirs(path14.dirname(p), repoRoot2);
|
|
3203
|
+
out.push(`Skill removed (${path14.relative(repoRoot2, p)})`);
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
const wf = path14.join(repoRoot2, "workflows", "scopecairn.md");
|
|
3207
|
+
if (fs14.existsSync(wf)) {
|
|
3208
|
+
const text = fs14.readFileSync(wf, "utf8");
|
|
3209
|
+
if (text.includes("scopecairn")) {
|
|
3210
|
+
fs14.rmSync(wf, { force: true });
|
|
3211
|
+
out.push(`Workflow removed (${path14.relative(repoRoot2, wf)})`);
|
|
3212
|
+
} else {
|
|
3213
|
+
out.push(`Workflow skipped (bukan milik ScopeCairn)`);
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
if (out.length === 0) out.push("Nothing installed.");
|
|
3217
|
+
return out;
|
|
3218
|
+
}
|
|
3219
|
+
function agentIds() {
|
|
3220
|
+
return ["antigravity", ...AGENT_MATRIX.map((t) => t.id)];
|
|
3221
|
+
}
|
|
3222
|
+
function allowlistText(repoRoot2) {
|
|
3223
|
+
return allowlistGuide(detectPrefix(repoRoot2));
|
|
2758
3224
|
}
|
|
3225
|
+
function resolveAgent(id) {
|
|
3226
|
+
return findTarget(id);
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
// src/commands/init.ts
|
|
2759
3230
|
function cmdInit(repoRoot2, opts = {}) {
|
|
2760
3231
|
console.log("ScopeCairn");
|
|
2761
3232
|
console.log("\u2713 Repository detected");
|
|
@@ -2764,49 +3235,43 @@ function cmdInit(repoRoot2, opts = {}) {
|
|
|
2764
3235
|
if (stats.metaFiles > 0) console.log(`\u2713 ${stats.metaFiles} config/schema files`);
|
|
2765
3236
|
console.log(`\u2713 ${stats.symbols} symbols`);
|
|
2766
3237
|
console.log(`\u2713 ${stats.relationships} relationships`);
|
|
3238
|
+
if (stats.adapters.length > 0) console.log(`\u2713 adapters: ${stats.adapters.join(", ")}`);
|
|
2767
3239
|
const prefix = detectPrefix(repoRoot2);
|
|
2768
|
-
const agentDir = detectAgentDir(repoRoot2);
|
|
2769
3240
|
const requested = parseAgentsOption(opts.agents);
|
|
2770
|
-
|
|
2771
|
-
if (requested === "all") {
|
|
2772
|
-
targets = [...AGENT_MATRIX];
|
|
2773
|
-
} else if (requested && requested.length > 0) {
|
|
2774
|
-
targets = [];
|
|
2775
|
-
for (const id of requested) {
|
|
2776
|
-
const t = findTarget(id);
|
|
2777
|
-
if (!t) {
|
|
2778
|
-
console.log(`! Unknown agent "${id}" (pilihan: ${AGENT_MATRIX.map((x) => x.id).join(", ")}, all)`);
|
|
2779
|
-
continue;
|
|
2780
|
-
}
|
|
2781
|
-
targets.push(t);
|
|
2782
|
-
}
|
|
2783
|
-
if (!targets.includes(AGENT_MATRIX[0])) targets.unshift(AGENT_MATRIX[0]);
|
|
2784
|
-
} else {
|
|
2785
|
-
targets = [AGENT_MATRIX[0]];
|
|
2786
|
-
for (const t of AGENT_MATRIX.slice(1)) {
|
|
2787
|
-
if (fs12.existsSync(path13.join(repoRoot2, t.presentMarker))) targets.push(t);
|
|
2788
|
-
}
|
|
2789
|
-
}
|
|
3241
|
+
const targets = requested === "all" ? [...AGENT_MATRIX] : requested && requested.length > 0 ? withCore(requested) : withDetected(repoRoot2);
|
|
2790
3242
|
for (const t of targets) {
|
|
2791
|
-
const
|
|
2792
|
-
console.log(`\u2713 ${t.label}: ${
|
|
2793
|
-
}
|
|
2794
|
-
const
|
|
2795
|
-
fs12.mkdirSync(path13.dirname(skillPath), { recursive: true });
|
|
2796
|
-
fs12.writeFileSync(skillPath, skillMd(prefix));
|
|
2797
|
-
console.log(`\u2713 Skill installed (${path13.relative(repoRoot2, skillPath)})`);
|
|
2798
|
-
const wfPath = path13.join(repoRoot2, "workflows", "scopecairn.md");
|
|
2799
|
-
fs12.mkdirSync(path13.dirname(wfPath), { recursive: true });
|
|
2800
|
-
fs12.writeFileSync(wfPath, workflowMd(prefix));
|
|
2801
|
-
console.log(`\u2713 Workflow installed (${path13.relative(repoRoot2, wfPath)})`);
|
|
3243
|
+
const r = installTarget(repoRoot2, t, prefix);
|
|
3244
|
+
console.log(`\u2713 ${t.label}: ${r.rel} (${r.how})`);
|
|
3245
|
+
}
|
|
3246
|
+
for (const line of installAntigravity(repoRoot2, prefix)) console.log(`\u2713 ${line}`);
|
|
2802
3247
|
console.log("Ready.");
|
|
2803
3248
|
console.log("");
|
|
2804
|
-
console.log(
|
|
3249
|
+
console.log(allowlistText(repoRoot2));
|
|
3250
|
+
}
|
|
3251
|
+
function withCore(ids) {
|
|
3252
|
+
const out = [];
|
|
3253
|
+
for (const id of ids) {
|
|
3254
|
+
const t = findTarget(id);
|
|
3255
|
+
if (!t) {
|
|
3256
|
+
console.log(`! Unknown agent "${id}" (pilihan: ${AGENT_MATRIX.map((x) => x.id).join(", ")}, all)`);
|
|
3257
|
+
continue;
|
|
3258
|
+
}
|
|
3259
|
+
out.push(t);
|
|
3260
|
+
}
|
|
3261
|
+
if (!out.includes(AGENT_MATRIX[0])) out.unshift(AGENT_MATRIX[0]);
|
|
3262
|
+
return out;
|
|
3263
|
+
}
|
|
3264
|
+
function withDetected(repoRoot2) {
|
|
3265
|
+
const targets = [AGENT_MATRIX[0]];
|
|
3266
|
+
for (const t of AGENT_MATRIX.slice(1)) {
|
|
3267
|
+
if (fs15.existsSync(path15.join(repoRoot2, t.presentMarker))) targets.push(t);
|
|
3268
|
+
}
|
|
3269
|
+
return targets;
|
|
2805
3270
|
}
|
|
2806
3271
|
|
|
2807
3272
|
// src/benchmark/run.ts
|
|
2808
|
-
import
|
|
2809
|
-
import
|
|
3273
|
+
import fs16 from "fs";
|
|
3274
|
+
import path16 from "path";
|
|
2810
3275
|
|
|
2811
3276
|
// src/benchmark/dataset.ts
|
|
2812
3277
|
var BENCHMARK_TASKS = [
|
|
@@ -2863,7 +3328,7 @@ var BENCHMARK_TASKS = [
|
|
|
2863
3328
|
// src/benchmark/run.ts
|
|
2864
3329
|
function fileChars(repoRoot2, rel) {
|
|
2865
3330
|
try {
|
|
2866
|
-
return
|
|
3331
|
+
return fs16.readFileSync(path16.join(repoRoot2, rel), "utf8").length;
|
|
2867
3332
|
} catch {
|
|
2868
3333
|
return 0;
|
|
2869
3334
|
}
|
|
@@ -3012,10 +3477,42 @@ function cmdBenchmark(repoRoot2, opts = {}) {
|
|
|
3012
3477
|
}
|
|
3013
3478
|
}
|
|
3014
3479
|
|
|
3480
|
+
// src/commands/agents.ts
|
|
3481
|
+
function registerAgentCommands(program2, repoRoot2) {
|
|
3482
|
+
const list = program2.command("agents").description("List supported agent installers");
|
|
3483
|
+
list.command("list").description("Show agent ids").action(() => {
|
|
3484
|
+
console.log(agentIds().join("\n"));
|
|
3485
|
+
});
|
|
3486
|
+
const anti = program2.command("antigravity").description("Antigravity IDE integration");
|
|
3487
|
+
anti.command("install").description("Install Skill + Workflow, print allowlist guide").action(() => {
|
|
3488
|
+
const root = repoRoot2();
|
|
3489
|
+
const prefix = detectPrefix(root);
|
|
3490
|
+
for (const line of installAntigravity(root, prefix)) console.log(`\u2713 ${line}`);
|
|
3491
|
+
console.log("");
|
|
3492
|
+
console.log(allowlistText(root));
|
|
3493
|
+
});
|
|
3494
|
+
anti.command("uninstall").description("Remove Skill + Workflow").action(() => {
|
|
3495
|
+
for (const line of uninstallAntigravity(repoRoot2())) console.log(`\u2713 ${line}`);
|
|
3496
|
+
});
|
|
3497
|
+
for (const t of AGENT_MATRIX) {
|
|
3498
|
+
if (t.id === "agents") continue;
|
|
3499
|
+
const cmd = program2.command(t.id).description(`${t.label} integration`);
|
|
3500
|
+
cmd.command("install").description(`Write ${t.rel}`).action(() => {
|
|
3501
|
+
const root = repoRoot2();
|
|
3502
|
+
const r = installTarget(root, t, detectPrefix(root));
|
|
3503
|
+
console.log(`\u2713 ${t.label}: ${r.rel} (${r.how})`);
|
|
3504
|
+
});
|
|
3505
|
+
cmd.command("uninstall").description(`Remove ScopeCairn block from ${t.rel}`).action(() => {
|
|
3506
|
+
const r = uninstallTarget(repoRoot2(), t);
|
|
3507
|
+
console.log(`\u2713 ${t.label}: ${r.rel} (${r.how})`);
|
|
3508
|
+
});
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3015
3512
|
// package.json
|
|
3016
3513
|
var package_default = {
|
|
3017
3514
|
name: "scopecairn",
|
|
3018
|
-
version: "0.
|
|
3515
|
+
version: "0.3.0",
|
|
3019
3516
|
description: "ScopeCairn \u2014 Codebase Intelligence & Agent Optimization Layer (local-first)",
|
|
3020
3517
|
license: "MIT",
|
|
3021
3518
|
type: "module",
|
|
@@ -3065,23 +3562,36 @@ var package_default = {
|
|
|
3065
3562
|
|
|
3066
3563
|
// src/cli.ts
|
|
3067
3564
|
var program = new Command();
|
|
3068
|
-
program.name("scopecairn").description("ScopeCairn \u2014 local-first codebase intelligence layer").version(package_default.version)
|
|
3565
|
+
program.name("scopecairn").description("ScopeCairn \u2014 local-first codebase intelligence layer").version(package_default.version).addHelpText(
|
|
3566
|
+
"after",
|
|
3567
|
+
`
|
|
3568
|
+
Examples:
|
|
3569
|
+
$ scopecairn init index repo + setup agent integration
|
|
3570
|
+
$ scopecairn init ./myapp --agents all setup another directory, all agents
|
|
3571
|
+
$ scopecairn context "add login" relevant context + scope for a task
|
|
3572
|
+
$ scopecairn context ./ repository orientation map
|
|
3573
|
+
$ scopecairn impact src/auth/login.ts change impact analysis
|
|
3574
|
+
$ scopecairn read symbol approveRequest symbol-level source excerpt
|
|
3575
|
+
$ scopecairn doctor health + integration check
|
|
3576
|
+
|
|
3577
|
+
Agent commands (context, impact, read, graph, status, doctor) are
|
|
3578
|
+
read-only toward source and only write to .scopecairn/ \u2014 safe to allowlist.
|
|
3579
|
+
`
|
|
3580
|
+
);
|
|
3069
3581
|
function repoRoot() {
|
|
3070
|
-
return
|
|
3582
|
+
return path17.resolve(process.cwd());
|
|
3071
3583
|
}
|
|
3072
3584
|
function resolveRoot(given) {
|
|
3073
3585
|
if (!given) return repoRoot();
|
|
3074
|
-
return
|
|
3586
|
+
return path17.resolve(process.cwd(), given);
|
|
3075
3587
|
}
|
|
3076
|
-
program.command("scan").description("Index repository (
|
|
3588
|
+
program.command("scan").description("Index repository (files + symbols + relations)").action(() => cmdScan(repoRoot()));
|
|
3077
3589
|
program.command("status").description("Show index status").action(() => cmdStatus(repoRoot()));
|
|
3078
|
-
program.command("clean").description("Remove generated index data").action(() => cmdClean(repoRoot()));
|
|
3079
3590
|
program.command("rebuild").description("Rebuild index from scratch (recover from corrupt index)").action(() => cmdRebuild(repoRoot()));
|
|
3080
3591
|
program.command("doctor").description("Check integration health").action(async () => {
|
|
3081
3592
|
await cmdDoctor(repoRoot());
|
|
3082
3593
|
});
|
|
3083
|
-
program.command("graph <symbol>").description("Show relations of a symbol
|
|
3084
|
-
program.command("query <text>").description("Search symbols/codebase knowledge (substring match)").action((text) => cmdQuery(repoRoot(), text));
|
|
3594
|
+
program.command("graph <symbol>").description("Show relations of a symbol").action((symbol) => cmdGraph(repoRoot(), symbol));
|
|
3085
3595
|
program.command("read").argument("<kind>", "what to read (symbol)").argument("<name>", "symbol name").description("Read a symbol's source excerpt (not the whole file)").action((kind, name) => cmdRead(repoRoot(), kind, name));
|
|
3086
3596
|
program.command("context <task>").description("Main entry point: auto-refresh + classify + relevant context + scope").option("--escalate", "force full COMPLEX context").option("--no-refresh", "skip incremental refresh (debugging/benchmark)").action(
|
|
3087
3597
|
(task, opts) => cmdContext(repoRoot(), task, opts)
|
|
@@ -3095,6 +3605,7 @@ program.command("init [path]").description("Initial indexing + generate AGENTS.m
|
|
|
3095
3605
|
program.command("benchmark").description("Retrieval recall, irrelevant ratio, context reduction + weight tuning").option("--tune", "grid search bobot FR-05").option("--no-refresh", "skip incremental refresh (debugging/benchmark)").action(
|
|
3096
3606
|
(opts) => cmdBenchmark(repoRoot(), opts)
|
|
3097
3607
|
);
|
|
3608
|
+
registerAgentCommands(program, repoRoot);
|
|
3098
3609
|
program.parseAsync(process.argv);
|
|
3099
3610
|
export {
|
|
3100
3611
|
resolveRoot
|