scopecairn 0.1.0 → 0.2.1
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 +37 -2
- package/dist/cli.js +874 -81
- package/package.json +2 -1
- package/skills/scopecairn/SKILL.md +31 -0
package/README.md
CHANGED
|
@@ -15,7 +15,27 @@ npm install -g scopecairn
|
|
|
15
15
|
|
|
16
16
|
Requires Node.js ≥ 20.
|
|
17
17
|
|
|
18
|
-
##
|
|
18
|
+
## Agent-driven setup
|
|
19
|
+
|
|
20
|
+
Install once, then just type `scopecairn ./` to your AI agent —
|
|
21
|
+
it runs project setup itself.
|
|
22
|
+
|
|
23
|
+
1. Install the skill once (global):
|
|
24
|
+
```bash
|
|
25
|
+
npx skills add <github-user>/scopecairn -g
|
|
26
|
+
```
|
|
27
|
+
(fallback without a skill: add one line to your global agent
|
|
28
|
+
instructions — `~/.claude/CLAUDE.md` or equivalent: *When the user
|
|
29
|
+
writes "scopecairn \<path\>", run `scopecairn init \<path\>`.*)
|
|
30
|
+
2. In any project chat, type:
|
|
31
|
+
```
|
|
32
|
+
scopecairn ./
|
|
33
|
+
```
|
|
34
|
+
The agent runs `scopecairn init ./`, reports files indexed and
|
|
35
|
+
integration files written. Approve the terminal command once;
|
|
36
|
+
afterwards, add the printed allowlist so later calls need no approval.
|
|
37
|
+
|
|
38
|
+
## Quickstart (manual)
|
|
19
39
|
|
|
20
40
|
```bash
|
|
21
41
|
cd your-repo
|
|
@@ -34,7 +54,6 @@ scopecairn doctor
|
|
|
34
54
|
```
|
|
35
55
|
|
|
36
56
|
## Commands
|
|
37
|
-
|
|
38
57
|
| Command | Description |
|
|
39
58
|
|---|---|
|
|
40
59
|
| `init` | Initial indexing + generate `AGENTS.md`, Skill, Workflow |
|
|
@@ -47,6 +66,22 @@ scopecairn doctor
|
|
|
47
66
|
| `read symbol <name>` | Symbol-level source excerpt |
|
|
48
67
|
| `benchmark [--tune]` | Retrieval recall and weight calibration |
|
|
49
68
|
|
|
69
|
+
## Framework adapters
|
|
70
|
+
|
|
71
|
+
Detected automatically during `scan` (shown in output and `status`).
|
|
72
|
+
No configuration needed.
|
|
73
|
+
|
|
74
|
+
| Adapter | Detects | Produces |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `nextjs` | `app/`, `pages/`, `next.config.*` | Route symbols (`GET /api/x`, `PAGE /y` incl. route groups, dynamic segments), `ROUTES_TO` handler edges, `QUERIES` handler→model edges |
|
|
77
|
+
| `prisma` | `schema.prisma` | `model` symbols for `QUERIES` resolution |
|
|
78
|
+
| `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
|
+
|
|
80
|
+
Limits (honest): Server Actions, middleware, and non-Prisma/Drizzle ORMs are not
|
|
81
|
+
mapped yet. QUERIES re-derive on every scan so new models resolve without
|
|
82
|
+
a rebuild. Contributing a new adapter = one file + one registration line
|
|
83
|
+
in `src/adapters/index.ts` (see `FrameworkAdapter` in `src/adapters/types.ts`).
|
|
84
|
+
|
|
50
85
|
## License
|
|
51
86
|
|
|
52
87
|
MIT — see [LICENSE](./LICENSE).
|
package/dist/cli.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import path16 from "path";
|
|
6
6
|
|
|
7
7
|
// src/scanner.ts
|
|
8
|
-
import
|
|
9
|
-
import
|
|
8
|
+
import fs7 from "fs";
|
|
9
|
+
import path8 from "path";
|
|
10
10
|
|
|
11
11
|
// src/languages.ts
|
|
12
12
|
var EXT_MAP = {
|
|
@@ -178,8 +178,10 @@ var WEIGHT = {
|
|
|
178
178
|
IMPORTS: 0.9,
|
|
179
179
|
EXPORTS: 0.9,
|
|
180
180
|
TESTS: 0.9,
|
|
181
|
+
ROUTES_TO: 0.9,
|
|
181
182
|
EXTENDS: 0.8,
|
|
182
183
|
IMPLEMENTS: 0.8,
|
|
184
|
+
QUERIES: 0.8,
|
|
183
185
|
CALLS: 0.7,
|
|
184
186
|
REFERENCES: 0.5,
|
|
185
187
|
USES: 0.5
|
|
@@ -984,14 +986,488 @@ top_n: 20
|
|
|
984
986
|
ensureProtectedFile(repoRoot2);
|
|
985
987
|
}
|
|
986
988
|
|
|
989
|
+
// src/adapters/index.ts
|
|
990
|
+
import fs6 from "fs";
|
|
991
|
+
import path7 from "path";
|
|
992
|
+
|
|
993
|
+
// src/adapters/types.ts
|
|
994
|
+
function findSymbolId(db, name, types) {
|
|
995
|
+
const row = types && types.length > 0 ? db.prepare(
|
|
996
|
+
`SELECT id FROM symbols WHERE name = ? AND type IN (${types.map(() => "?").join(",")}) LIMIT 1`
|
|
997
|
+
).get(name, ...types) : db.prepare(`SELECT id FROM symbols WHERE name = ? LIMIT 1`).get(name);
|
|
998
|
+
return row ? row.id : null;
|
|
999
|
+
}
|
|
1000
|
+
function fileSymbolOf(db, fileId) {
|
|
1001
|
+
const row = db.prepare(`SELECT id FROM symbols WHERE file_id = ? AND type = 'file'`).get(fileId);
|
|
1002
|
+
return row ? row.id : null;
|
|
1003
|
+
}
|
|
1004
|
+
function ensureAdapterSymbol(db, fileId, s) {
|
|
1005
|
+
const row = db.prepare(`SELECT id FROM symbols WHERE file_id = ? AND name = ? AND type = ? LIMIT 1`).get(fileId, s.name, s.type);
|
|
1006
|
+
if (row) return row.id;
|
|
1007
|
+
const r = db.prepare(
|
|
1008
|
+
`INSERT INTO symbols(file_id, name, type, signature, start_line, end_line)
|
|
1009
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
1010
|
+
).run(fileId, s.name, s.type, s.signature, s.startLine, s.endLine);
|
|
1011
|
+
return Number(r.lastInsertRowid);
|
|
1012
|
+
}
|
|
1013
|
+
function insertRelation(db, sourceId, targetId, rel, weight, confidence) {
|
|
1014
|
+
if (sourceId === targetId) return;
|
|
1015
|
+
db.prepare(
|
|
1016
|
+
`INSERT INTO relationships(source_id, target_id, relationship_type, weight, confidence)
|
|
1017
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
1018
|
+
).run(sourceId, targetId, rel, weight, confidence);
|
|
1019
|
+
}
|
|
1020
|
+
function lineOf3(content, index) {
|
|
1021
|
+
return content.slice(0, index).split("\n").length;
|
|
1022
|
+
}
|
|
1023
|
+
function enclosingFunction(db, fileId, line) {
|
|
1024
|
+
const rows = db.prepare(
|
|
1025
|
+
`SELECT id, name FROM symbols WHERE file_id = ? AND type IN ('function','method','component') AND start_line <= ? ORDER BY start_line DESC LIMIT 5`
|
|
1026
|
+
).all(fileId, line);
|
|
1027
|
+
return rows.length > 0 ? rows[0].id : null;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// src/adapters/prisma.ts
|
|
1031
|
+
function parsePrismaModels(content) {
|
|
1032
|
+
const out = [];
|
|
1033
|
+
const re = /^model\s+(\w+)\s*\{/gm;
|
|
1034
|
+
let m;
|
|
1035
|
+
while ((m = re.exec(content)) !== null) {
|
|
1036
|
+
out.push({ name: m[1], line: content.slice(0, m.index).split("\n").length });
|
|
1037
|
+
}
|
|
1038
|
+
return out;
|
|
1039
|
+
}
|
|
1040
|
+
var prismaAdapter = {
|
|
1041
|
+
id: "prisma",
|
|
1042
|
+
label: "Prisma",
|
|
1043
|
+
detect: (files) => files.some((f) => f.endsWith("schema.prisma")),
|
|
1044
|
+
relevant: (rel) => rel.endsWith("schema.prisma"),
|
|
1045
|
+
apply(ctx, files) {
|
|
1046
|
+
let symbols = 0;
|
|
1047
|
+
let relations = 0;
|
|
1048
|
+
for (const f of files) {
|
|
1049
|
+
const fileSym = fileSymbolOf(ctx.db, f.fileId);
|
|
1050
|
+
if (!fileSym) continue;
|
|
1051
|
+
for (const model of parsePrismaModels(f.content)) {
|
|
1052
|
+
const id = ensureAdapterSymbol(ctx.db, f.fileId, {
|
|
1053
|
+
name: model.name,
|
|
1054
|
+
type: "model",
|
|
1055
|
+
signature: `model ${model.name}`,
|
|
1056
|
+
startLine: model.line,
|
|
1057
|
+
endLine: model.line
|
|
1058
|
+
});
|
|
1059
|
+
symbols++;
|
|
1060
|
+
insertRelation(ctx.db, fileSym, id, "CONTAINS", 1, 1);
|
|
1061
|
+
relations++;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return { symbols, relations };
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
// src/adapters/nextjs.ts
|
|
1069
|
+
var PRISMA_OPS = [
|
|
1070
|
+
"findMany",
|
|
1071
|
+
"findUnique",
|
|
1072
|
+
"findFirst",
|
|
1073
|
+
"findFirstOrThrow",
|
|
1074
|
+
"findUniqueOrThrow",
|
|
1075
|
+
"create",
|
|
1076
|
+
"createMany",
|
|
1077
|
+
"update",
|
|
1078
|
+
"updateMany",
|
|
1079
|
+
"upsert",
|
|
1080
|
+
"delete",
|
|
1081
|
+
"deleteMany",
|
|
1082
|
+
"count",
|
|
1083
|
+
"aggregate",
|
|
1084
|
+
"groupBy"
|
|
1085
|
+
];
|
|
1086
|
+
function stripSrcPrefix(rel) {
|
|
1087
|
+
return rel.startsWith("src/") ? rel.slice(4) : rel;
|
|
1088
|
+
}
|
|
1089
|
+
function cleanSegments(segs) {
|
|
1090
|
+
const out = [];
|
|
1091
|
+
for (let s of segs) {
|
|
1092
|
+
if (/^\(.*\)$/.test(s) || s.startsWith("@")) continue;
|
|
1093
|
+
s = s.replace(/^\(\.+\)/, "");
|
|
1094
|
+
if (s) out.push(s);
|
|
1095
|
+
}
|
|
1096
|
+
return out;
|
|
1097
|
+
}
|
|
1098
|
+
function appRouteFromRel(rel) {
|
|
1099
|
+
const p = stripSrcPrefix(rel);
|
|
1100
|
+
if (!p.startsWith("app/")) return null;
|
|
1101
|
+
const parts = p.slice(4).split("/");
|
|
1102
|
+
const file = parts[parts.length - 1];
|
|
1103
|
+
const dir = cleanSegments(parts.slice(0, -1));
|
|
1104
|
+
const url = "/" + dir.join("/");
|
|
1105
|
+
if (/^page\.(tsx|ts|jsx|js|mdx?)$/.test(file)) return { kind: "page", url };
|
|
1106
|
+
if (/^route\.(ts|js)$/.test(file)) return { kind: "route", url };
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
function pagesRouteFromRel(rel) {
|
|
1110
|
+
const p = stripSrcPrefix(rel);
|
|
1111
|
+
if (!p.startsWith("pages/")) return null;
|
|
1112
|
+
const rest = p.slice(6).replace(/\.(tsx|ts|jsx|js|mdx?)$/, "");
|
|
1113
|
+
if (rest.startsWith("_") || rest.startsWith("api/_")) return null;
|
|
1114
|
+
if (rest === "index") return { kind: "page", url: "/" };
|
|
1115
|
+
if (rest.startsWith("api/")) return { kind: "route", url: "/" + rest };
|
|
1116
|
+
if (rest.endsWith("/index")) return { kind: "page", url: "/" + rest.slice(0, -6) };
|
|
1117
|
+
return { kind: "page", url: "/" + rest };
|
|
1118
|
+
}
|
|
1119
|
+
function routeHandlers(content) {
|
|
1120
|
+
const out = [];
|
|
1121
|
+
const re = /export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(/g;
|
|
1122
|
+
let m;
|
|
1123
|
+
while ((m = re.exec(content)) !== null) {
|
|
1124
|
+
out.push({ method: m[1], line: lineOf3(content, m.index) });
|
|
1125
|
+
}
|
|
1126
|
+
return out;
|
|
1127
|
+
}
|
|
1128
|
+
function prismaCalls(content) {
|
|
1129
|
+
const out = [];
|
|
1130
|
+
const ops = PRISMA_OPS.join("|");
|
|
1131
|
+
const re = new RegExp(`prisma\\.(\\w+)\\.(${ops})\\s*\\(`, "g");
|
|
1132
|
+
let m;
|
|
1133
|
+
while ((m = re.exec(content)) !== null) {
|
|
1134
|
+
out.push({ model: m[1], op: m[2], index: m.index });
|
|
1135
|
+
}
|
|
1136
|
+
return out;
|
|
1137
|
+
}
|
|
1138
|
+
function resolveModel(db, name) {
|
|
1139
|
+
return findSymbolId(db, name, ["model"]) ?? dbLowerModel(db, name);
|
|
1140
|
+
}
|
|
1141
|
+
function refreshQueries(db, files) {
|
|
1142
|
+
let n = 0;
|
|
1143
|
+
const del = db.prepare(
|
|
1144
|
+
`DELETE FROM relationships WHERE relationship_type = 'QUERIES' AND source_id IN (SELECT id FROM symbols WHERE file_id = ?)`
|
|
1145
|
+
);
|
|
1146
|
+
const ins = db.prepare(
|
|
1147
|
+
`INSERT INTO relationships(source_id, target_id, relationship_type, weight, confidence)
|
|
1148
|
+
VALUES (?, ?, 'QUERIES', ${WEIGHT.QUERIES}, 0.9)`
|
|
1149
|
+
);
|
|
1150
|
+
for (const f of files) {
|
|
1151
|
+
const fileSym = fileSymbolOf(db, f.fileId);
|
|
1152
|
+
if (!fileSym) continue;
|
|
1153
|
+
del.run(f.fileId);
|
|
1154
|
+
for (const call of prismaCalls(f.content)) {
|
|
1155
|
+
const modelId = resolveModel(db, call.model);
|
|
1156
|
+
if (!modelId) continue;
|
|
1157
|
+
const line = lineOf3(f.content, call.index);
|
|
1158
|
+
const caller = enclosingFunction(db, f.fileId, line) ?? fileSym;
|
|
1159
|
+
if (caller === modelId) continue;
|
|
1160
|
+
ins.run(caller, modelId);
|
|
1161
|
+
n++;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return n;
|
|
1165
|
+
}
|
|
1166
|
+
var nextjsAdapter = {
|
|
1167
|
+
id: "nextjs",
|
|
1168
|
+
label: "Next.js",
|
|
1169
|
+
detect: (files) => files.some((f) => {
|
|
1170
|
+
const p = stripSrcPrefix(f);
|
|
1171
|
+
return p.startsWith("app/") || p.startsWith("pages/") || /(^|\/)next\.config\.(js|ts|mjs)$/.test(f);
|
|
1172
|
+
}),
|
|
1173
|
+
relevant: (rel) => {
|
|
1174
|
+
const p = stripSrcPrefix(rel);
|
|
1175
|
+
return p.startsWith("app/") || p.startsWith("pages/");
|
|
1176
|
+
},
|
|
1177
|
+
apply(ctx, files) {
|
|
1178
|
+
let symbols = 0;
|
|
1179
|
+
let relations = 0;
|
|
1180
|
+
for (const f of files) {
|
|
1181
|
+
const fileSym = fileSymbolOf(ctx.db, f.fileId);
|
|
1182
|
+
if (!fileSym) continue;
|
|
1183
|
+
const mapped = appRouteFromRel(f.rel) ?? pagesRouteFromRel(f.rel);
|
|
1184
|
+
if (mapped) {
|
|
1185
|
+
if (mapped.kind === "page") {
|
|
1186
|
+
const id = ensureAdapterSymbol(ctx.db, f.fileId, {
|
|
1187
|
+
name: `PAGE ${mapped.url}`,
|
|
1188
|
+
type: "route",
|
|
1189
|
+
signature: `page ${mapped.url}`,
|
|
1190
|
+
startLine: 1,
|
|
1191
|
+
endLine: 1
|
|
1192
|
+
});
|
|
1193
|
+
symbols++;
|
|
1194
|
+
insertRelation(ctx.db, fileSym, id, "CONTAINS", 1, 1);
|
|
1195
|
+
relations++;
|
|
1196
|
+
} else {
|
|
1197
|
+
const handlers = routeHandlers(f.content);
|
|
1198
|
+
if (handlers.length === 0) {
|
|
1199
|
+
const id = ensureAdapterSymbol(ctx.db, f.fileId, {
|
|
1200
|
+
name: `API ${mapped.url}`,
|
|
1201
|
+
type: "route",
|
|
1202
|
+
signature: `route ${mapped.url} (no exported handler)`,
|
|
1203
|
+
startLine: 1,
|
|
1204
|
+
endLine: 1
|
|
1205
|
+
});
|
|
1206
|
+
symbols++;
|
|
1207
|
+
insertRelation(ctx.db, fileSym, id, "CONTAINS", 1, 0.6);
|
|
1208
|
+
relations++;
|
|
1209
|
+
}
|
|
1210
|
+
for (const h of handlers) {
|
|
1211
|
+
const routeId = ensureAdapterSymbol(ctx.db, f.fileId, {
|
|
1212
|
+
name: `${h.method} ${mapped.url}`,
|
|
1213
|
+
type: "route",
|
|
1214
|
+
signature: `${h.method} ${mapped.url}`,
|
|
1215
|
+
startLine: h.line,
|
|
1216
|
+
endLine: h.line
|
|
1217
|
+
});
|
|
1218
|
+
symbols++;
|
|
1219
|
+
insertRelation(ctx.db, fileSym, routeId, "CONTAINS", 1, 1);
|
|
1220
|
+
relations++;
|
|
1221
|
+
const handlerId = findInFile(ctx.db, f.fileId, h.method);
|
|
1222
|
+
if (handlerId) {
|
|
1223
|
+
insertRelation(ctx.db, handlerId, routeId, "ROUTES_TO", WEIGHT.ROUTES_TO, 1);
|
|
1224
|
+
relations++;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
relations += refreshQueries(
|
|
1231
|
+
ctx.db,
|
|
1232
|
+
files.map((f) => ({ fileId: f.fileId, rel: f.rel, content: f.content }))
|
|
1233
|
+
);
|
|
1234
|
+
return { symbols, relations };
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
function dbLowerModel(db, name) {
|
|
1238
|
+
const row = db.prepare(`SELECT id, name FROM symbols WHERE type = 'model'`).all();
|
|
1239
|
+
const lower = name.toLowerCase();
|
|
1240
|
+
return row.find((r) => r.name.toLowerCase() === lower)?.id ?? null;
|
|
1241
|
+
}
|
|
1242
|
+
function findInFile(db, fileId, name) {
|
|
1243
|
+
const row = db.prepare(`SELECT id FROM symbols WHERE file_id = ? AND name = ? LIMIT 1`).get(fileId, name);
|
|
1244
|
+
return row ? row.id : null;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// src/adapters/drizzle.ts
|
|
1248
|
+
import fs5 from "fs";
|
|
1249
|
+
import path6 from "path";
|
|
1250
|
+
var TABLE_FNS = ["pgTable", "sqliteTable", "mysqlTable"];
|
|
1251
|
+
var RELATIONAL_OPS = [
|
|
1252
|
+
"findMany",
|
|
1253
|
+
"findFirst",
|
|
1254
|
+
"findFirstOrThrow",
|
|
1255
|
+
"findUnique"
|
|
1256
|
+
];
|
|
1257
|
+
function parseDrizzleTables(content) {
|
|
1258
|
+
const out = [];
|
|
1259
|
+
const fns = TABLE_FNS.join("|");
|
|
1260
|
+
const re = new RegExp(
|
|
1261
|
+
`(?:export\\s+)?(?:const|let|var)\\s+(\\w+)\\s*=\\s*(?:${fns})\\s*\\(\\s*["'\`]([^"'\\\`]+)["'\`]`,
|
|
1262
|
+
"g"
|
|
1263
|
+
);
|
|
1264
|
+
let m;
|
|
1265
|
+
while ((m = re.exec(content)) !== null) {
|
|
1266
|
+
out.push({
|
|
1267
|
+
name: m[1],
|
|
1268
|
+
table: m[2],
|
|
1269
|
+
line: content.slice(0, m.index).split("\n").length
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
return out;
|
|
1273
|
+
}
|
|
1274
|
+
function parseDrizzleQueries(content) {
|
|
1275
|
+
const out = [];
|
|
1276
|
+
const relOps = RELATIONAL_OPS.join("|");
|
|
1277
|
+
const relRe = new RegExp(`db\\s*\\.\\s*query\\s*\\.\\s*(\\w+)\\s*\\.\\s*(?:${relOps})\\s*\\(`, "g");
|
|
1278
|
+
let m;
|
|
1279
|
+
while ((m = relRe.exec(content)) !== null) {
|
|
1280
|
+
out.push({ table: m[1], index: m.index });
|
|
1281
|
+
}
|
|
1282
|
+
const builderRe = /db\s*\.\s*(select|selectDistinct|insert|update|delete)\b/g;
|
|
1283
|
+
while ((m = builderRe.exec(content)) !== null) {
|
|
1284
|
+
const op = m[1];
|
|
1285
|
+
const window = content.slice(m.index, m.index + 400);
|
|
1286
|
+
let tm;
|
|
1287
|
+
if (op === "select" || op === "selectDistinct") {
|
|
1288
|
+
tm = /\.from\s*\(\s*(\w+)/.exec(window);
|
|
1289
|
+
} else {
|
|
1290
|
+
tm = /\(\s*(\w+)\s*[,)]/.exec(window);
|
|
1291
|
+
}
|
|
1292
|
+
if (tm) out.push({ table: tm[1], index: m.index });
|
|
1293
|
+
}
|
|
1294
|
+
return out;
|
|
1295
|
+
}
|
|
1296
|
+
function hasDrizzleMarker(content) {
|
|
1297
|
+
if (content.includes("drizzle-orm") || TABLE_FNS.some((f) => content.includes(`${f}(`)) || /db\s*\.\s*query\s*\./.test(content)) {
|
|
1298
|
+
return true;
|
|
1299
|
+
}
|
|
1300
|
+
return /db\s*\.\s*(select|selectDistinct|insert|update|delete)\b/.test(content);
|
|
1301
|
+
}
|
|
1302
|
+
function refreshDrizzleQueries(db, files) {
|
|
1303
|
+
let n = 0;
|
|
1304
|
+
const del = db.prepare(
|
|
1305
|
+
`DELETE FROM relationships WHERE relationship_type = 'QUERIES' AND source_id IN (SELECT id FROM symbols WHERE file_id = ?)`
|
|
1306
|
+
);
|
|
1307
|
+
const ins = db.prepare(
|
|
1308
|
+
`INSERT INTO relationships(source_id, target_id, relationship_type, weight, confidence)
|
|
1309
|
+
VALUES (?, ?, 'QUERIES', ${WEIGHT.QUERIES}, 0.85)`
|
|
1310
|
+
);
|
|
1311
|
+
for (const f of files) {
|
|
1312
|
+
const fileSym = fileSymbolOf(db, f.fileId);
|
|
1313
|
+
if (!fileSym || !hasDrizzleMarker(f.content)) continue;
|
|
1314
|
+
del.run(f.fileId);
|
|
1315
|
+
for (const q of parseDrizzleQueries(f.content)) {
|
|
1316
|
+
const target = findModelInFile(db, f.fileId, q.table) ?? findModelGlobal(db, q.table);
|
|
1317
|
+
if (!target) continue;
|
|
1318
|
+
const caller = enclosingFunction(db, f.fileId, lineOf3(f.content, q.index)) ?? fileSym;
|
|
1319
|
+
if (caller === target) continue;
|
|
1320
|
+
ins.run(caller, target);
|
|
1321
|
+
n++;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return n;
|
|
1325
|
+
}
|
|
1326
|
+
function findModelInFile(db, fileId, name) {
|
|
1327
|
+
const row = db.prepare(
|
|
1328
|
+
`SELECT id FROM symbols WHERE file_id = ? AND name = ? AND type = 'model' LIMIT 1`
|
|
1329
|
+
).get(fileId, name);
|
|
1330
|
+
return row ? row.id : null;
|
|
1331
|
+
}
|
|
1332
|
+
function findModelGlobal(db, name) {
|
|
1333
|
+
const rows = db.prepare(`SELECT id, name FROM symbols WHERE type = 'model'`).all();
|
|
1334
|
+
const lower = name.toLowerCase();
|
|
1335
|
+
return rows.find((r) => r.name.toLowerCase() === lower)?.id ?? null;
|
|
1336
|
+
}
|
|
1337
|
+
var drizzleAdapter = {
|
|
1338
|
+
id: "drizzle",
|
|
1339
|
+
label: "Drizzle ORM",
|
|
1340
|
+
detect: (files) => files.some(
|
|
1341
|
+
(f) => f.includes("drizzle") || /(^|\/)drizzle\.config\.(ts|js|mjs)$/.test(f) || /(^|\/)(db|drizzle)\/.*schema.*\.ts$/.test(f)
|
|
1342
|
+
),
|
|
1343
|
+
// Pola path lemah (`src/adapters/drizzle.ts` ikut cocok) — konfirmasi isi:
|
|
1344
|
+
// path kuat (config/dir drizzle, skema db/) langsung lolos, sisanya butuh
|
|
1345
|
+
// dependensi drizzle di package.json.
|
|
1346
|
+
confirm: (repoRoot2, files) => {
|
|
1347
|
+
if (files.some(
|
|
1348
|
+
(f) => /(^|\/)drizzle\.config\.(ts|js|mjs)$/.test(f) || /(^|\/)drizzle\//.test(f) || /(^|\/)(db|drizzle)\/.*schema.*\.ts$/.test(f)
|
|
1349
|
+
)) {
|
|
1350
|
+
return true;
|
|
1351
|
+
}
|
|
1352
|
+
try {
|
|
1353
|
+
const pkg = fs5.readFileSync(path6.join(repoRoot2, "package.json"), "utf8");
|
|
1354
|
+
return pkg.includes('"drizzle-orm"') || pkg.includes('"drizzle-kit"') || pkg.includes("'drizzle-orm'");
|
|
1355
|
+
} catch {
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
},
|
|
1359
|
+
relevant: (rel) => /\.(ts|tsx|js|jsx|mjs)$/.test(rel),
|
|
1360
|
+
apply(ctx, files) {
|
|
1361
|
+
let symbols = 0;
|
|
1362
|
+
let relations = 0;
|
|
1363
|
+
for (const f of files) {
|
|
1364
|
+
if (!hasDrizzleMarker(f.content)) continue;
|
|
1365
|
+
const fileSym = fileSymbolOf(ctx.db, f.fileId);
|
|
1366
|
+
if (!fileSym) continue;
|
|
1367
|
+
for (const t of parseDrizzleTables(f.content)) {
|
|
1368
|
+
const id = ensureAdapterSymbol(ctx.db, f.fileId, {
|
|
1369
|
+
name: t.name,
|
|
1370
|
+
type: "model",
|
|
1371
|
+
signature: `table ${t.name} ("${t.table}")`,
|
|
1372
|
+
startLine: t.line,
|
|
1373
|
+
endLine: t.line
|
|
1374
|
+
});
|
|
1375
|
+
symbols++;
|
|
1376
|
+
insertRelation(ctx.db, fileSym, id, "CONTAINS", 1, 1);
|
|
1377
|
+
relations++;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
relations += refreshDrizzleQueries(
|
|
1381
|
+
ctx.db,
|
|
1382
|
+
files.map((f) => ({ fileId: f.fileId, rel: f.rel, content: f.content }))
|
|
1383
|
+
);
|
|
1384
|
+
return { symbols, relations };
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
|
|
1388
|
+
// src/adapters/index.ts
|
|
1389
|
+
var ADAPTERS = [prismaAdapter, drizzleAdapter, nextjsAdapter];
|
|
1390
|
+
function detectAdapters(files, repoRoot2) {
|
|
1391
|
+
return ADAPTERS.filter((a) => {
|
|
1392
|
+
try {
|
|
1393
|
+
if (!a.detect(files)) return false;
|
|
1394
|
+
if (repoRoot2 && a.confirm) return a.confirm(repoRoot2, files);
|
|
1395
|
+
return true;
|
|
1396
|
+
} catch {
|
|
1397
|
+
return false;
|
|
1398
|
+
}
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
function runAdapters(db, repoRoot2, changed) {
|
|
1402
|
+
const allFiles = db.prepare(`SELECT path FROM files`).all().map((r) => r.path);
|
|
1403
|
+
const active = detectAdapters(allFiles, repoRoot2);
|
|
1404
|
+
if (active.length === 0) return { adapters: [], symbols: 0, relations: 0 };
|
|
1405
|
+
let symbols = 0;
|
|
1406
|
+
let relations = 0;
|
|
1407
|
+
const touched = /* @__PURE__ */ new Set();
|
|
1408
|
+
for (const adapter of active) {
|
|
1409
|
+
const inputs = [];
|
|
1410
|
+
for (const c of changed) {
|
|
1411
|
+
if (!adapter.relevant(c.rel)) continue;
|
|
1412
|
+
const abs = path7.join(repoRoot2, ...c.rel.split("/"));
|
|
1413
|
+
let content;
|
|
1414
|
+
try {
|
|
1415
|
+
content = fs6.readFileSync(abs, "utf8");
|
|
1416
|
+
} catch {
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
inputs.push({ fileId: c.fileId, rel: c.rel, content });
|
|
1420
|
+
}
|
|
1421
|
+
if (inputs.length === 0) continue;
|
|
1422
|
+
try {
|
|
1423
|
+
const r = adapter.apply({ db, repoRoot: repoRoot2 }, inputs);
|
|
1424
|
+
symbols += r.symbols;
|
|
1425
|
+
relations += r.relations;
|
|
1426
|
+
for (const i of inputs) touched.add(i.fileId);
|
|
1427
|
+
} catch {
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
for (const fileId of touched) {
|
|
1431
|
+
const row = db.prepare(`SELECT path FROM files WHERE id = ?`).get(fileId);
|
|
1432
|
+
if (row) {
|
|
1433
|
+
try {
|
|
1434
|
+
syncFileIndex(db, fileId, row.path);
|
|
1435
|
+
} catch {
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
const refreshTargets = (id, relevant, refresh) => {
|
|
1440
|
+
if (!active.some((a) => a.id === id)) return;
|
|
1441
|
+
try {
|
|
1442
|
+
const all = db.prepare(`SELECT id, path FROM files`).all().filter((r) => relevant(r.path));
|
|
1443
|
+
const inputs = [];
|
|
1444
|
+
for (const r of all) {
|
|
1445
|
+
try {
|
|
1446
|
+
inputs.push({
|
|
1447
|
+
fileId: r.id,
|
|
1448
|
+
rel: r.path,
|
|
1449
|
+
content: fs6.readFileSync(path7.join(repoRoot2, ...r.path.split("/")), "utf8")
|
|
1450
|
+
});
|
|
1451
|
+
} catch {
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
relations += refresh(db, inputs);
|
|
1455
|
+
} catch {
|
|
1456
|
+
}
|
|
1457
|
+
};
|
|
1458
|
+
refreshTargets("nextjs", nextjsAdapter.relevant, refreshQueries);
|
|
1459
|
+
refreshTargets("drizzle", drizzleAdapter.relevant, refreshDrizzleQueries);
|
|
1460
|
+
return { adapters: active.map((a) => a.id), symbols, relations };
|
|
1461
|
+
}
|
|
1462
|
+
|
|
987
1463
|
// src/scanner.ts
|
|
988
1464
|
function toPosixRel(repoRoot2, abs) {
|
|
989
|
-
return
|
|
1465
|
+
return path8.relative(repoRoot2, abs).split(path8.sep).join("/");
|
|
990
1466
|
}
|
|
991
1467
|
function walk(absDir, out) {
|
|
992
|
-
const entries =
|
|
1468
|
+
const entries = fs7.readdirSync(absDir, { withFileTypes: true });
|
|
993
1469
|
for (const e of entries) {
|
|
994
|
-
const abs =
|
|
1470
|
+
const abs = path8.join(absDir, e.name);
|
|
995
1471
|
if (e.isDirectory()) {
|
|
996
1472
|
if (e.isSymbolicLink()) continue;
|
|
997
1473
|
walk(abs, out);
|
|
@@ -1031,6 +1507,7 @@ function scanRepository(repoRoot2) {
|
|
|
1031
1507
|
}
|
|
1032
1508
|
const backfill = symbolCount === 0 || ftsCount === 0;
|
|
1033
1509
|
const pending = [];
|
|
1510
|
+
const metaChanged = [];
|
|
1034
1511
|
for (const abs of allAbs) {
|
|
1035
1512
|
const rel = toPosixRel(repoRoot2, abs);
|
|
1036
1513
|
if (isIgnored(rel, rules)) continue;
|
|
@@ -1043,7 +1520,7 @@ function scanRepository(repoRoot2) {
|
|
|
1043
1520
|
seenPaths.add(rel);
|
|
1044
1521
|
let buf;
|
|
1045
1522
|
try {
|
|
1046
|
-
buf =
|
|
1523
|
+
buf = fs7.readFileSync(abs);
|
|
1047
1524
|
} catch {
|
|
1048
1525
|
continue;
|
|
1049
1526
|
}
|
|
@@ -1056,6 +1533,7 @@ function scanRepository(repoRoot2) {
|
|
|
1056
1533
|
pending.push({ fileId: Number(r.lastInsertRowid), rel, content: buf.toString("utf8") });
|
|
1057
1534
|
} else {
|
|
1058
1535
|
indexMetaFile(db, Number(r.lastInsertRowid), rel);
|
|
1536
|
+
metaChanged.push({ fileId: Number(r.lastInsertRowid), rel });
|
|
1059
1537
|
}
|
|
1060
1538
|
inserted++;
|
|
1061
1539
|
} else if (row.hash !== hash || backfill) {
|
|
@@ -1064,6 +1542,7 @@ function scanRepository(repoRoot2) {
|
|
|
1064
1542
|
pending.push({ fileId: row.id, rel, content: buf.toString("utf8") });
|
|
1065
1543
|
} else {
|
|
1066
1544
|
indexMetaFile(db, row.id, rel);
|
|
1545
|
+
metaChanged.push({ fileId: row.id, rel });
|
|
1067
1546
|
}
|
|
1068
1547
|
updated++;
|
|
1069
1548
|
} else {
|
|
@@ -1086,6 +1565,11 @@ function scanRepository(repoRoot2) {
|
|
|
1086
1565
|
for (const p of phased) {
|
|
1087
1566
|
indexFileRelations(db, p.fileId, p.rel, p.extraction, p.fileSym);
|
|
1088
1567
|
}
|
|
1568
|
+
const adapterChanged = [
|
|
1569
|
+
...pending.map((p) => ({ fileId: p.fileId, rel: p.rel })),
|
|
1570
|
+
...metaChanged
|
|
1571
|
+
];
|
|
1572
|
+
const adapterRun = runAdapters(db, repoRoot2, adapterChanged);
|
|
1089
1573
|
db.prepare(
|
|
1090
1574
|
`INSERT INTO meta(key, value) VALUES ('last_scan', datetime('now'))
|
|
1091
1575
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
@@ -1101,6 +1585,7 @@ function scanRepository(repoRoot2) {
|
|
|
1101
1585
|
metaFiles,
|
|
1102
1586
|
symbols,
|
|
1103
1587
|
relationships,
|
|
1588
|
+
adapters: adapterRun.adapters,
|
|
1104
1589
|
inserted,
|
|
1105
1590
|
updated,
|
|
1106
1591
|
unchanged,
|
|
@@ -1124,6 +1609,9 @@ function cmdScan(repoRoot2) {
|
|
|
1124
1609
|
}
|
|
1125
1610
|
console.log(`\u2713 ${stats.symbols} symbols`);
|
|
1126
1611
|
console.log(`\u2713 ${stats.relationships} relationships`);
|
|
1612
|
+
if (stats.adapters.length > 0) {
|
|
1613
|
+
console.log(`\u2713 adapters: ${stats.adapters.join(", ")}`);
|
|
1614
|
+
}
|
|
1127
1615
|
console.log(
|
|
1128
1616
|
`\u2713 index: +${stats.inserted} new, ~${stats.updated} updated, =${stats.unchanged} unchanged, -${stats.removed} removed`
|
|
1129
1617
|
);
|
|
@@ -1132,11 +1620,11 @@ function cmdScan(repoRoot2) {
|
|
|
1132
1620
|
}
|
|
1133
1621
|
|
|
1134
1622
|
// src/commands/status.ts
|
|
1135
|
-
import
|
|
1136
|
-
import
|
|
1623
|
+
import fs8 from "fs";
|
|
1624
|
+
import path9 from "path";
|
|
1137
1625
|
function cmdStatus(repoRoot2) {
|
|
1138
1626
|
const dir = dataDir(repoRoot2);
|
|
1139
|
-
if (!
|
|
1627
|
+
if (!fs8.existsSync(path9.join(dir, "scopecairn.db"))) {
|
|
1140
1628
|
console.log("ScopeCairn");
|
|
1141
1629
|
console.log("Status: NOT INITIALIZED (run `scopecairn scan`)");
|
|
1142
1630
|
return;
|
|
@@ -1164,6 +1652,14 @@ function cmdStatus(repoRoot2) {
|
|
|
1164
1652
|
} catch {
|
|
1165
1653
|
}
|
|
1166
1654
|
console.log(`Last scan: ${lastScan}`);
|
|
1655
|
+
try {
|
|
1656
|
+
const paths = db.prepare(`SELECT path FROM files`).all().map((r) => r.path);
|
|
1657
|
+
const adapters = detectAdapters(paths, repoRoot2);
|
|
1658
|
+
if (adapters.length > 0) {
|
|
1659
|
+
console.log(`Adapters: ${adapters.map((a) => a.id).join(", ")}`);
|
|
1660
|
+
}
|
|
1661
|
+
} catch {
|
|
1662
|
+
}
|
|
1167
1663
|
try {
|
|
1168
1664
|
const inv = db.prepare(
|
|
1169
1665
|
`SELECT command, COUNT(*) AS n FROM invocations GROUP BY command ORDER BY n DESC`
|
|
@@ -1180,7 +1676,7 @@ function cmdStatus(repoRoot2) {
|
|
|
1180
1676
|
}
|
|
1181
1677
|
function cmdClean(repoRoot2) {
|
|
1182
1678
|
const dir = dataDir(repoRoot2);
|
|
1183
|
-
|
|
1679
|
+
fs8.rmSync(dir, { recursive: true, force: true });
|
|
1184
1680
|
console.log(`Cleaned ${dir}`);
|
|
1185
1681
|
}
|
|
1186
1682
|
|
|
@@ -1191,8 +1687,8 @@ function cmdRebuild(repoRoot2) {
|
|
|
1191
1687
|
}
|
|
1192
1688
|
|
|
1193
1689
|
// src/commands/doctor.ts
|
|
1194
|
-
import
|
|
1195
|
-
import
|
|
1690
|
+
import fs10 from "fs";
|
|
1691
|
+
import path11 from "path";
|
|
1196
1692
|
|
|
1197
1693
|
// src/treesitter.ts
|
|
1198
1694
|
var cached = null;
|
|
@@ -1217,18 +1713,18 @@ async function checkTreeSitter() {
|
|
|
1217
1713
|
}
|
|
1218
1714
|
|
|
1219
1715
|
// src/integrate/detect.ts
|
|
1220
|
-
import
|
|
1221
|
-
import
|
|
1716
|
+
import fs9 from "fs";
|
|
1717
|
+
import path10 from "path";
|
|
1222
1718
|
function detectAgentDir(repoRoot2) {
|
|
1223
|
-
const plural =
|
|
1224
|
-
const singular =
|
|
1719
|
+
const plural = fs9.existsSync(path10.join(repoRoot2, ".agents"));
|
|
1720
|
+
const singular = fs9.existsSync(path10.join(repoRoot2, ".agent"));
|
|
1225
1721
|
if (singular && !plural) return ".agent";
|
|
1226
1722
|
return ".agents";
|
|
1227
1723
|
}
|
|
1228
1724
|
function detectPrefix(repoRoot2) {
|
|
1229
1725
|
try {
|
|
1230
1726
|
const pkg = JSON.parse(
|
|
1231
|
-
|
|
1727
|
+
fs9.readFileSync(path10.join(repoRoot2, "package.json"), "utf8")
|
|
1232
1728
|
);
|
|
1233
1729
|
if (pkg.dependencies?.["scopecairn"] || pkg.devDependencies?.["scopecairn"]) {
|
|
1234
1730
|
return "npx scopecairn";
|
|
@@ -1245,9 +1741,8 @@ function extractPrefixUsed(text) {
|
|
|
1245
1741
|
|
|
1246
1742
|
// src/integrate/templates.ts
|
|
1247
1743
|
var MARKER = "<!-- scopecairn:managed -->";
|
|
1248
|
-
function
|
|
1249
|
-
return
|
|
1250
|
-
# ScopeCairn
|
|
1744
|
+
function ruleBody(prefix) {
|
|
1745
|
+
return `# ScopeCairn
|
|
1251
1746
|
|
|
1252
1747
|
## Wajib untuk setiap task coding
|
|
1253
1748
|
Sebelum membaca file atau menjalankan grep, jalankan:
|
|
@@ -1268,6 +1763,23 @@ Gunakan hasilnya sebagai titik awal. Jangan menjelajah repository dari nol.
|
|
|
1268
1763
|
Lewati ScopeCairn hanya untuk pertanyaan non-coding.
|
|
1269
1764
|
`;
|
|
1270
1765
|
}
|
|
1766
|
+
function agentsMd(prefix) {
|
|
1767
|
+
return `${MARKER}
|
|
1768
|
+
${ruleBody(prefix)}`;
|
|
1769
|
+
}
|
|
1770
|
+
function genericRuleMd(prefix) {
|
|
1771
|
+
return `${MARKER}
|
|
1772
|
+
${ruleBody(prefix)}`;
|
|
1773
|
+
}
|
|
1774
|
+
function cursorRuleMdc(prefix) {
|
|
1775
|
+
return `---
|
|
1776
|
+
description: ScopeCairn codebase rules \u2014 panggil scopecairn context di tiap task coding
|
|
1777
|
+
alwaysApply: true
|
|
1778
|
+
---
|
|
1779
|
+
|
|
1780
|
+
${MARKER}
|
|
1781
|
+
${ruleBody(prefix)}`;
|
|
1782
|
+
}
|
|
1271
1783
|
function skillMd(prefix) {
|
|
1272
1784
|
return `---
|
|
1273
1785
|
name: scopecairn
|
|
@@ -1313,6 +1825,74 @@ tambahkan entri berikut ke Terminal Allow List Antigravity
|
|
|
1313
1825
|
Catatan: ScopeCairn tidak mengubah pengaturan IDE Anda secara otomatis.`;
|
|
1314
1826
|
}
|
|
1315
1827
|
|
|
1828
|
+
// src/integrate/matrix.ts
|
|
1829
|
+
var AGENT_MATRIX = [
|
|
1830
|
+
{
|
|
1831
|
+
id: "agents",
|
|
1832
|
+
label: "AGENTS.md (universal: Codex, Aider, Opencode, Amp, Jules, ...)",
|
|
1833
|
+
rel: "AGENTS.md",
|
|
1834
|
+
mode: "merge",
|
|
1835
|
+
render: agentsMd,
|
|
1836
|
+
presentMarker: "AGENTS.md"
|
|
1837
|
+
},
|
|
1838
|
+
{
|
|
1839
|
+
id: "claude",
|
|
1840
|
+
label: "Claude Code",
|
|
1841
|
+
rel: "CLAUDE.md",
|
|
1842
|
+
mode: "merge",
|
|
1843
|
+
render: genericRuleMd,
|
|
1844
|
+
presentMarker: "CLAUDE.md"
|
|
1845
|
+
},
|
|
1846
|
+
{
|
|
1847
|
+
id: "gemini",
|
|
1848
|
+
label: "Gemini / Antigravity CLI",
|
|
1849
|
+
rel: "GEMINI.md",
|
|
1850
|
+
mode: "merge",
|
|
1851
|
+
render: genericRuleMd,
|
|
1852
|
+
presentMarker: "GEMINI.md"
|
|
1853
|
+
},
|
|
1854
|
+
{
|
|
1855
|
+
id: "cursor",
|
|
1856
|
+
label: "Cursor",
|
|
1857
|
+
rel: ".cursor/rules/scopecairn.mdc",
|
|
1858
|
+
mode: "new",
|
|
1859
|
+
render: cursorRuleMdc,
|
|
1860
|
+
presentMarker: ".cursor"
|
|
1861
|
+
},
|
|
1862
|
+
{
|
|
1863
|
+
id: "windsurf",
|
|
1864
|
+
label: "Windsurf",
|
|
1865
|
+
rel: ".windsurf/rules/scopecairn.md",
|
|
1866
|
+
mode: "new",
|
|
1867
|
+
render: genericRuleMd,
|
|
1868
|
+
presentMarker: ".windsurf"
|
|
1869
|
+
},
|
|
1870
|
+
{
|
|
1871
|
+
id: "copilot",
|
|
1872
|
+
label: "GitHub Copilot",
|
|
1873
|
+
rel: ".github/copilot-instructions.md",
|
|
1874
|
+
mode: "merge",
|
|
1875
|
+
render: genericRuleMd,
|
|
1876
|
+
presentMarker: ".github"
|
|
1877
|
+
},
|
|
1878
|
+
{
|
|
1879
|
+
id: "kiro",
|
|
1880
|
+
label: "Kiro",
|
|
1881
|
+
rel: ".kiro/steering/scopecairn.md",
|
|
1882
|
+
mode: "new",
|
|
1883
|
+
render: genericRuleMd,
|
|
1884
|
+
presentMarker: ".kiro"
|
|
1885
|
+
}
|
|
1886
|
+
];
|
|
1887
|
+
function findTarget(id) {
|
|
1888
|
+
return AGENT_MATRIX.find((t) => t.id === id);
|
|
1889
|
+
}
|
|
1890
|
+
function parseAgentsOption(raw) {
|
|
1891
|
+
if (!raw) return null;
|
|
1892
|
+
if (raw.trim().toLowerCase() === "all") return "all";
|
|
1893
|
+
return raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1316
1896
|
// src/commands/doctor.ts
|
|
1317
1897
|
function tick(ok) {
|
|
1318
1898
|
return ok ? "\u2713" : "\u2717";
|
|
@@ -1340,41 +1920,45 @@ async function cmdDoctor(repoRoot2) {
|
|
|
1340
1920
|
const s = db.prepare(`SELECT COUNT(*) AS n FROM symbols`).get().n;
|
|
1341
1921
|
const r = db.prepare(`SELECT COUNT(*) AS n FROM relationships`).get().n;
|
|
1342
1922
|
db.close();
|
|
1343
|
-
dbOk =
|
|
1923
|
+
dbOk = fs10.existsSync(dbPath(repoRoot2));
|
|
1344
1924
|
graphDetail = ` (${s} symbols, ${r} rels)`;
|
|
1345
1925
|
} catch {
|
|
1346
1926
|
dbOk = false;
|
|
1347
1927
|
}
|
|
1348
1928
|
console.log(`${tick(dbOk)} Database ${dbPath(repoRoot2)}${graphDetail}`);
|
|
1349
|
-
const protPath =
|
|
1350
|
-
console.log(`${tick(
|
|
1929
|
+
const protPath = path11.join(dataDir(repoRoot2), "protected.yml");
|
|
1930
|
+
console.log(`${tick(fs10.existsSync(protPath))} Protected ${protPath}`);
|
|
1351
1931
|
const expectedPrefix = detectPrefix(repoRoot2);
|
|
1352
1932
|
const agentDir = detectAgentDir(repoRoot2);
|
|
1353
|
-
const agentsPath = path9.join(repoRoot2, "AGENTS.md");
|
|
1354
|
-
let agentsOk = false;
|
|
1355
|
-
let agentsPrefix = null;
|
|
1356
|
-
if (fs8.existsSync(agentsPath)) {
|
|
1357
|
-
const text = fs8.readFileSync(agentsPath, "utf8");
|
|
1358
|
-
agentsOk = text.includes(MARKER);
|
|
1359
|
-
agentsPrefix = extractPrefixUsed(text);
|
|
1360
|
-
}
|
|
1361
|
-
console.log(`${tick(agentsOk)} AGENTS.md (${agentsOk ? "blok ScopeCairn ada" : "hilang \u2014 jalankan `scopecairn init`"})`);
|
|
1362
|
-
const skillPath = path9.join(repoRoot2, agentDir, "skills", "scopecairn", "SKILL.md");
|
|
1363
|
-
const skillOk = fs8.existsSync(skillPath);
|
|
1364
|
-
console.log(`${tick(skillOk)} Skill (${agentDir}/skills/scopecairn/SKILL.md)`);
|
|
1365
|
-
const wfPath = path9.join(repoRoot2, "workflows", "scopecairn.md");
|
|
1366
|
-
console.log(`${tick(fs8.existsSync(wfPath))} Workflow (workflows/scopecairn.md, opsional)`);
|
|
1367
1933
|
let prefixOk = true;
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
);
|
|
1373
|
-
|
|
1934
|
+
let matrixOk = 0;
|
|
1935
|
+
for (const t of AGENT_MATRIX) {
|
|
1936
|
+
const abs = path11.join(repoRoot2, t.rel);
|
|
1937
|
+
if (!fs10.existsSync(abs)) continue;
|
|
1938
|
+
const text = fs10.readFileSync(abs, "utf8");
|
|
1939
|
+
if (!text.includes(MARKER)) continue;
|
|
1940
|
+
matrixOk++;
|
|
1941
|
+
const used = extractPrefixUsed(text);
|
|
1942
|
+
if (used && used !== expectedPrefix) {
|
|
1943
|
+
prefixOk = false;
|
|
1944
|
+
console.log(
|
|
1945
|
+
`\u2717 Prefix tak konsisten di ${t.rel}: memakai \`${used}\`, instalasi terdeteksi \`${expectedPrefix}\`. Jalankan ulang \`scopecairn init\`.`
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
console.log(`${tick(matrixOk > 0)} Agent matrix (${matrixOk}/${AGENT_MATRIX.length} target terpasang)`);
|
|
1950
|
+
if (matrixOk === 0) {
|
|
1951
|
+
console.log(` Jalankan \`scopecairn init --agents all\` untuk memasang semua.`);
|
|
1952
|
+
} else if (prefixOk) {
|
|
1374
1953
|
console.log(`\u2713 Prefix konsisten (\`${expectedPrefix} context\`)`);
|
|
1375
1954
|
}
|
|
1955
|
+
const skillPath = path11.join(repoRoot2, agentDir, "skills", "scopecairn", "SKILL.md");
|
|
1956
|
+
const skillOk = fs10.existsSync(skillPath);
|
|
1957
|
+
console.log(`${tick(skillOk)} Skill (${agentDir}/skills/scopecairn/SKILL.md)`);
|
|
1958
|
+
const wfPath = path11.join(repoRoot2, "workflows", "scopecairn.md");
|
|
1959
|
+
console.log(`${tick(fs10.existsSync(wfPath))} Workflow (workflows/scopecairn.md, opsional)`);
|
|
1376
1960
|
console.log(`i Strict Mode mengabaikan allowlist \u2014 semua perintah tetap perlu persetujuan.`);
|
|
1377
|
-
const healthy = nodeOk && sqliteOk && ts.ok && dbOk &&
|
|
1961
|
+
const healthy = nodeOk && sqliteOk && ts.ok && dbOk && matrixOk > 0 && skillOk && prefixOk;
|
|
1378
1962
|
console.log(`Status: ${healthy ? "HEALTHY" : "DEGRADED (see \u2717 above)"}`);
|
|
1379
1963
|
}
|
|
1380
1964
|
|
|
@@ -1516,8 +2100,8 @@ function cmdQuery(repoRoot2, text) {
|
|
|
1516
2100
|
}
|
|
1517
2101
|
|
|
1518
2102
|
// src/commands/read.ts
|
|
1519
|
-
import
|
|
1520
|
-
import
|
|
2103
|
+
import fs11 from "fs";
|
|
2104
|
+
import path12 from "path";
|
|
1521
2105
|
function cmdRead(repoRoot2, kind, name) {
|
|
1522
2106
|
if (kind !== "symbol") {
|
|
1523
2107
|
console.log(`Unknown read kind "${kind}". Usage: scopecairn read symbol <nama>`);
|
|
@@ -1533,10 +2117,10 @@ function cmdRead(repoRoot2, kind, name) {
|
|
|
1533
2117
|
return;
|
|
1534
2118
|
}
|
|
1535
2119
|
const row = db.prepare(`SELECT start_line, end_line FROM symbols WHERE id = ?`).get(node.id);
|
|
1536
|
-
const abs =
|
|
2120
|
+
const abs = path12.join(repoRoot2, node.file);
|
|
1537
2121
|
let lines;
|
|
1538
2122
|
try {
|
|
1539
|
-
lines =
|
|
2123
|
+
lines = fs11.readFileSync(abs, "utf8").split("\n");
|
|
1540
2124
|
} catch {
|
|
1541
2125
|
console.log(`Cannot read ${node.file}.`);
|
|
1542
2126
|
return;
|
|
@@ -1960,6 +2544,116 @@ ${tests.map((t) => `- ${t}`).join("\n") || "-"}
|
|
|
1960
2544
|
return { markdown: md, high: highFiles, medium: medFiles, tests, dependencies: deps };
|
|
1961
2545
|
}
|
|
1962
2546
|
|
|
2547
|
+
// src/commands/orientasi.ts
|
|
2548
|
+
import fs12 from "fs";
|
|
2549
|
+
import path13 from "path";
|
|
2550
|
+
function isPathLike(task) {
|
|
2551
|
+
const t = task.trim().replace(/^["']|["']$/g, "");
|
|
2552
|
+
return t === "." || t === "./" || t === "/" || t.startsWith("./") || t.startsWith("../") || t.startsWith("/") || t.startsWith("~");
|
|
2553
|
+
}
|
|
2554
|
+
function looksLikeRepoPath(db, repoRoot2, task) {
|
|
2555
|
+
const t = task.trim().replace(/^["']|["']$/g, "");
|
|
2556
|
+
if (/\s/.test(t)) return false;
|
|
2557
|
+
if (isPathLike(task)) return true;
|
|
2558
|
+
try {
|
|
2559
|
+
fs12.statSync(path13.resolve(repoRoot2, t));
|
|
2560
|
+
return true;
|
|
2561
|
+
} catch {
|
|
2562
|
+
}
|
|
2563
|
+
try {
|
|
2564
|
+
const row = db.prepare(`SELECT 1 AS ok FROM files WHERE path = ? OR path LIKE ? LIMIT 1`).get(t, `%/${t}`);
|
|
2565
|
+
return !!row;
|
|
2566
|
+
} catch {
|
|
2567
|
+
return false;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
function topCentral(db, limit = 10) {
|
|
2571
|
+
try {
|
|
2572
|
+
return db.prepare(
|
|
2573
|
+
`SELECT s.name, s.type, f.path AS file, COUNT(*) AS fanin
|
|
2574
|
+
FROM relationships r
|
|
2575
|
+
JOIN symbols s ON s.id = r.target_id
|
|
2576
|
+
JOIN files f ON f.id = s.file_id
|
|
2577
|
+
WHERE s.type IN ('function','class','method','component','interface','type')
|
|
2578
|
+
GROUP BY r.target_id ORDER BY fanin DESC LIMIT ${limit}`
|
|
2579
|
+
).all();
|
|
2580
|
+
} catch {
|
|
2581
|
+
return [];
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
function cmdOrientasi(repoRoot2, task) {
|
|
2585
|
+
const db = openDb(repoRoot2);
|
|
2586
|
+
try {
|
|
2587
|
+
const files = db.prepare(`SELECT COUNT(*) AS n FROM files`).get().n;
|
|
2588
|
+
if (files === 0) {
|
|
2589
|
+
console.log("Index kosong. Jalankan `scopecairn scan` dulu.");
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2592
|
+
const byLang = db.prepare(`SELECT language, COUNT(*) AS n FROM files GROUP BY language ORDER BY n DESC`).all();
|
|
2593
|
+
const symbols = db.prepare(`SELECT COUNT(*) AS n FROM symbols`).get().n;
|
|
2594
|
+
const t = task.trim().replace(/^["']|["']$/g, "");
|
|
2595
|
+
const abs = path13.resolve(repoRoot2, t);
|
|
2596
|
+
let rel = null;
|
|
2597
|
+
try {
|
|
2598
|
+
const stat = fs12.statSync(abs);
|
|
2599
|
+
if (stat.isFile()) rel = path13.relative(repoRoot2, abs).split(path13.sep).join("/");
|
|
2600
|
+
} catch {
|
|
2601
|
+
rel = null;
|
|
2602
|
+
}
|
|
2603
|
+
if (rel) {
|
|
2604
|
+
const row = db.prepare(`SELECT id FROM files WHERE path = ?`).get(rel);
|
|
2605
|
+
if (row) {
|
|
2606
|
+
const syms = db.prepare(`SELECT name, type FROM symbols WHERE file_id = ? AND type != 'file' LIMIT 20`).all(row.id);
|
|
2607
|
+
const fileSym = db.prepare(`SELECT id FROM symbols WHERE file_id = ? AND type = 'file'`).get(row.id);
|
|
2608
|
+
const linked = /* @__PURE__ */ new Set();
|
|
2609
|
+
if (fileSym) {
|
|
2610
|
+
for (const e of neighbors(db, fileSym.id, 30)) {
|
|
2611
|
+
if (e.other.file !== rel) linked.add(e.other.file);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
console.log(
|
|
2615
|
+
`# Task Context (orientasi)
|
|
2616
|
+
|
|
2617
|
+
## Task
|
|
2618
|
+
${task}
|
|
2619
|
+
|
|
2620
|
+
## File
|
|
2621
|
+
${rel}
|
|
2622
|
+
|
|
2623
|
+
## Simbol (${syms.length})
|
|
2624
|
+
${syms.map((s) => `- ${s.name} (${s.type})`).join("\n") || "-"}
|
|
2625
|
+
|
|
2626
|
+
## Terkait langsung
|
|
2627
|
+
${[...linked].slice(0, 10).map((f) => `- ${f}`).join("\n") || "-"}
|
|
2628
|
+
|
|
2629
|
+
Butuh konteks kerja? Jalankan \`scopecairn context "<deskripsi task>"\`.
|
|
2630
|
+
`
|
|
2631
|
+
);
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
const central = topCentral(db);
|
|
2636
|
+
console.log(
|
|
2637
|
+
`# Task Context (orientasi)
|
|
2638
|
+
|
|
2639
|
+
## Task
|
|
2640
|
+
${task}
|
|
2641
|
+
|
|
2642
|
+
## Ringkasan index
|
|
2643
|
+
- ${files} file (${byLang.map((l) => `${l.language} ${l.n}`).join(", ")})
|
|
2644
|
+
- ${symbols} simbol
|
|
2645
|
+
|
|
2646
|
+
## Simbol paling dipakai
|
|
2647
|
+
${central.map((c) => `- ${c.name} (${c.type}, ${c.file}) \u2190 ${c.fanin}x`).join("\n") || "-"}
|
|
2648
|
+
|
|
2649
|
+
Mulai dari simbol di atas, atau jalankan \`scopecairn context "<deskripsi task>"\` untuk konteks kerja.
|
|
2650
|
+
`
|
|
2651
|
+
);
|
|
2652
|
+
} finally {
|
|
2653
|
+
db.close();
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
|
|
1963
2657
|
// src/commands/context.ts
|
|
1964
2658
|
function cmdContext(repoRoot2, task, opts = {}) {
|
|
1965
2659
|
if (!opts.noRefresh) {
|
|
@@ -1978,6 +2672,11 @@ function cmdContext(repoRoot2, task, opts = {}) {
|
|
|
1978
2672
|
console.log("Index kosong. Jalankan `scopecairn scan` dulu.");
|
|
1979
2673
|
return;
|
|
1980
2674
|
}
|
|
2675
|
+
if (looksLikeRepoPath(db, repoRoot2, task)) {
|
|
2676
|
+
logInvocation(db, "context");
|
|
2677
|
+
cmdOrientasi(repoRoot2, task);
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
1981
2680
|
const result = retrieve(db, repoRoot2, task);
|
|
1982
2681
|
if (result.ranked.length === 0) {
|
|
1983
2682
|
logInvocation(db, "context");
|
|
@@ -2072,7 +2771,7 @@ function cmdImpact(repoRoot2, target, opts = {}) {
|
|
|
2072
2771
|
}
|
|
2073
2772
|
if (out.length >= 15) break;
|
|
2074
2773
|
}
|
|
2075
|
-
if (out.length === 0 && fileHit != null && (rel === "CALLS" || rel === "USES")) {
|
|
2774
|
+
if (out.length === 0 && fileHit != null && (rel === "CALLS" || rel === "USES" || rel === "QUERIES")) {
|
|
2076
2775
|
try {
|
|
2077
2776
|
const rows = db.prepare(
|
|
2078
2777
|
`SELECT DISTINCT t.name AS n, f.path AS p
|
|
@@ -2124,6 +2823,9 @@ function cmdImpact(repoRoot2, target, opts = {}) {
|
|
|
2124
2823
|
`Diimpor oleh: ${byRel("IMPORTS", "in").join(", ") || "-"}`,
|
|
2125
2824
|
`Memanggil: ${byRel("CALLS", "out").join(", ") || "-"}`,
|
|
2126
2825
|
`Dipanggil oleh: ${byRel("CALLS", "in").join(", ") || "-"}`,
|
|
2826
|
+
`Mengquery: ${byRel("QUERIES", "out").join(", ") || "-"}`,
|
|
2827
|
+
`Diquery oleh: ${byRel("QUERIES", "in").join(", ") || "-"}`,
|
|
2828
|
+
`Rute: ${[...byRel("ROUTES_TO", "out"), ...byRel("ROUTES_TO", "in")].join(", ") || "-"}`,
|
|
2127
2829
|
``,
|
|
2128
2830
|
`## Indirect (${indirect.size})`,
|
|
2129
2831
|
...[...indirect.values()].slice(0, 15).map((v) => `- ${v.name} (${v.type}, ${v.file}) via ${v.via}`),
|
|
@@ -2144,9 +2846,32 @@ function cmdImpact(repoRoot2, target, opts = {}) {
|
|
|
2144
2846
|
}
|
|
2145
2847
|
|
|
2146
2848
|
// src/commands/init.ts
|
|
2147
|
-
import
|
|
2148
|
-
import
|
|
2149
|
-
function
|
|
2849
|
+
import fs13 from "fs";
|
|
2850
|
+
import path14 from "path";
|
|
2851
|
+
function writeTarget(repoRoot2, target, prefix) {
|
|
2852
|
+
const abs = path14.join(repoRoot2, target.rel);
|
|
2853
|
+
const block = target.render(prefix);
|
|
2854
|
+
if (target.mode === "new") {
|
|
2855
|
+
const existed = fs13.existsSync(abs);
|
|
2856
|
+
fs13.mkdirSync(path14.dirname(abs), { recursive: true });
|
|
2857
|
+
fs13.writeFileSync(abs, block.endsWith("\n") ? block : block + "\n");
|
|
2858
|
+
return existed ? "updated" : "generated";
|
|
2859
|
+
}
|
|
2860
|
+
if (fs13.existsSync(abs)) {
|
|
2861
|
+
const cur = fs13.readFileSync(abs, "utf8");
|
|
2862
|
+
if (cur.includes(MARKER)) {
|
|
2863
|
+
const re = new RegExp(`${MARKER}[\\s\\S]*?(?=\\n#[^#]|$)`, "");
|
|
2864
|
+
fs13.writeFileSync(abs, cur.replace(re, block.trimEnd()).trimEnd() + "\n");
|
|
2865
|
+
return "updated";
|
|
2866
|
+
}
|
|
2867
|
+
fs13.writeFileSync(abs, cur.trimEnd() + "\n\n" + block);
|
|
2868
|
+
return "extended";
|
|
2869
|
+
}
|
|
2870
|
+
fs13.mkdirSync(path14.dirname(abs), { recursive: true });
|
|
2871
|
+
fs13.writeFileSync(abs, block.endsWith("\n") ? block : block + "\n");
|
|
2872
|
+
return "generated";
|
|
2873
|
+
}
|
|
2874
|
+
function cmdInit(repoRoot2, opts = {}) {
|
|
2150
2875
|
console.log("ScopeCairn");
|
|
2151
2876
|
console.log("\u2713 Repository detected");
|
|
2152
2877
|
const stats = scanRepository(repoRoot2);
|
|
@@ -2156,39 +2881,47 @@ function cmdInit(repoRoot2) {
|
|
|
2156
2881
|
console.log(`\u2713 ${stats.relationships} relationships`);
|
|
2157
2882
|
const prefix = detectPrefix(repoRoot2);
|
|
2158
2883
|
const agentDir = detectAgentDir(repoRoot2);
|
|
2159
|
-
const
|
|
2160
|
-
|
|
2161
|
-
if (
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2884
|
+
const requested = parseAgentsOption(opts.agents);
|
|
2885
|
+
let targets;
|
|
2886
|
+
if (requested === "all") {
|
|
2887
|
+
targets = [...AGENT_MATRIX];
|
|
2888
|
+
} else if (requested && requested.length > 0) {
|
|
2889
|
+
targets = [];
|
|
2890
|
+
for (const id of requested) {
|
|
2891
|
+
const t = findTarget(id);
|
|
2892
|
+
if (!t) {
|
|
2893
|
+
console.log(`! Unknown agent "${id}" (pilihan: ${AGENT_MATRIX.map((x) => x.id).join(", ")}, all)`);
|
|
2894
|
+
continue;
|
|
2895
|
+
}
|
|
2896
|
+
targets.push(t);
|
|
2171
2897
|
}
|
|
2898
|
+
if (!targets.includes(AGENT_MATRIX[0])) targets.unshift(AGENT_MATRIX[0]);
|
|
2172
2899
|
} else {
|
|
2173
|
-
|
|
2174
|
-
|
|
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
|
+
}
|
|
2175
2904
|
}
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
const
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
console.log(`\u2713
|
|
2905
|
+
for (const t of targets) {
|
|
2906
|
+
const how = writeTarget(repoRoot2, t, prefix);
|
|
2907
|
+
console.log(`\u2713 ${t.label}: ${t.rel} (${how})`);
|
|
2908
|
+
}
|
|
2909
|
+
const skillPath = path14.join(repoRoot2, agentDir, "skills", "scopecairn", "SKILL.md");
|
|
2910
|
+
fs13.mkdirSync(path14.dirname(skillPath), { recursive: true });
|
|
2911
|
+
fs13.writeFileSync(skillPath, skillMd(prefix));
|
|
2912
|
+
console.log(`\u2713 Skill installed (${path14.relative(repoRoot2, skillPath)})`);
|
|
2913
|
+
const wfPath = path14.join(repoRoot2, "workflows", "scopecairn.md");
|
|
2914
|
+
fs13.mkdirSync(path14.dirname(wfPath), { recursive: true });
|
|
2915
|
+
fs13.writeFileSync(wfPath, workflowMd(prefix));
|
|
2916
|
+
console.log(`\u2713 Workflow installed (${path14.relative(repoRoot2, wfPath)})`);
|
|
2184
2917
|
console.log("Ready.");
|
|
2185
2918
|
console.log("");
|
|
2186
2919
|
console.log(allowlistGuide(prefix));
|
|
2187
2920
|
}
|
|
2188
2921
|
|
|
2189
2922
|
// src/benchmark/run.ts
|
|
2190
|
-
import
|
|
2191
|
-
import
|
|
2923
|
+
import fs14 from "fs";
|
|
2924
|
+
import path15 from "path";
|
|
2192
2925
|
|
|
2193
2926
|
// src/benchmark/dataset.ts
|
|
2194
2927
|
var BENCHMARK_TASKS = [
|
|
@@ -2245,7 +2978,7 @@ var BENCHMARK_TASKS = [
|
|
|
2245
2978
|
// src/benchmark/run.ts
|
|
2246
2979
|
function fileChars(repoRoot2, rel) {
|
|
2247
2980
|
try {
|
|
2248
|
-
return
|
|
2981
|
+
return fs14.readFileSync(path15.join(repoRoot2, rel), "utf8").length;
|
|
2249
2982
|
} catch {
|
|
2250
2983
|
return 0;
|
|
2251
2984
|
}
|
|
@@ -2394,11 +3127,66 @@ function cmdBenchmark(repoRoot2, opts = {}) {
|
|
|
2394
3127
|
}
|
|
2395
3128
|
}
|
|
2396
3129
|
|
|
3130
|
+
// package.json
|
|
3131
|
+
var package_default = {
|
|
3132
|
+
name: "scopecairn",
|
|
3133
|
+
version: "0.2.1",
|
|
3134
|
+
description: "ScopeCairn \u2014 Codebase Intelligence & Agent Optimization Layer (local-first)",
|
|
3135
|
+
license: "MIT",
|
|
3136
|
+
type: "module",
|
|
3137
|
+
engines: {
|
|
3138
|
+
node: ">=20"
|
|
3139
|
+
},
|
|
3140
|
+
bin: {
|
|
3141
|
+
scopecairn: "dist/cli.js"
|
|
3142
|
+
},
|
|
3143
|
+
files: [
|
|
3144
|
+
"dist",
|
|
3145
|
+
"skills",
|
|
3146
|
+
"README.md",
|
|
3147
|
+
"LICENSE"
|
|
3148
|
+
],
|
|
3149
|
+
keywords: [
|
|
3150
|
+
"code-intelligence",
|
|
3151
|
+
"knowledge-graph",
|
|
3152
|
+
"ai-agents",
|
|
3153
|
+
"coding-assistant",
|
|
3154
|
+
"static-analysis",
|
|
3155
|
+
"developer-tools"
|
|
3156
|
+
],
|
|
3157
|
+
scripts: {
|
|
3158
|
+
build: "tsup",
|
|
3159
|
+
prepublishOnly: "npm run typecheck && npm run build",
|
|
3160
|
+
dev: "tsx src/cli.ts",
|
|
3161
|
+
scan: "tsx src/cli.ts scan",
|
|
3162
|
+
status: "tsx src/cli.ts status",
|
|
3163
|
+
doctor: "tsx src/cli.ts doctor",
|
|
3164
|
+
clean: "tsx src/cli.ts clean",
|
|
3165
|
+
rebuild: "tsx src/cli.ts rebuild",
|
|
3166
|
+
typecheck: "tsc --noEmit"
|
|
3167
|
+
},
|
|
3168
|
+
dependencies: {
|
|
3169
|
+
commander: "^12.1.0",
|
|
3170
|
+
ignore: "^7.0.0",
|
|
3171
|
+
"web-tree-sitter": "^0.25.0"
|
|
3172
|
+
},
|
|
3173
|
+
devDependencies: {
|
|
3174
|
+
"@types/node": "^22.7.0",
|
|
3175
|
+
tsup: "^8.3.0",
|
|
3176
|
+
tsx: "^4.19.0",
|
|
3177
|
+
typescript: "^5.6.0"
|
|
3178
|
+
}
|
|
3179
|
+
};
|
|
3180
|
+
|
|
2397
3181
|
// src/cli.ts
|
|
2398
3182
|
var program = new Command();
|
|
2399
|
-
program.name("scopecairn").description("ScopeCairn \u2014 local-first codebase intelligence layer").version(
|
|
3183
|
+
program.name("scopecairn").description("ScopeCairn \u2014 local-first codebase intelligence layer").version(package_default.version);
|
|
2400
3184
|
function repoRoot() {
|
|
2401
|
-
return
|
|
3185
|
+
return path16.resolve(process.cwd());
|
|
3186
|
+
}
|
|
3187
|
+
function resolveRoot(given) {
|
|
3188
|
+
if (!given) return repoRoot();
|
|
3189
|
+
return path16.resolve(process.cwd(), given);
|
|
2402
3190
|
}
|
|
2403
3191
|
program.command("scan").description("Index repository (Fase 1: files + hash + language)").action(() => cmdScan(repoRoot()));
|
|
2404
3192
|
program.command("status").description("Show index status").action(() => cmdStatus(repoRoot()));
|
|
@@ -2416,8 +3204,13 @@ program.command("context <task>").description("Main entry point: auto-refresh +
|
|
|
2416
3204
|
program.command("impact <target>").description("Change impact analysis: file, symbol, function, or class").option("--no-refresh", "skip incremental refresh (debugging/benchmark)").action(
|
|
2417
3205
|
(target, opts) => cmdImpact(repoRoot(), target, opts)
|
|
2418
3206
|
);
|
|
2419
|
-
program.command("init").description("Initial indexing + generate AGENTS.md, Skill, Workflow").
|
|
3207
|
+
program.command("init [path]").description("Initial indexing + generate AGENTS.md, Skill, Workflow, agent matrix").option("--agents <list>", "agent targets: comma list from agents,claude,gemini,cursor,windsurf,copilot,kiro, or 'all'").action(
|
|
3208
|
+
(targetPath, opts) => cmdInit(resolveRoot(targetPath), opts)
|
|
3209
|
+
);
|
|
2420
3210
|
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(
|
|
2421
3211
|
(opts) => cmdBenchmark(repoRoot(), opts)
|
|
2422
3212
|
);
|
|
2423
3213
|
program.parseAsync(process.argv);
|
|
3214
|
+
export {
|
|
3215
|
+
resolveRoot
|
|
3216
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scopecairn",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "ScopeCairn — Codebase Intelligence & Agent Optimization Layer (local-first)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"dist",
|
|
15
|
+
"skills",
|
|
15
16
|
"README.md",
|
|
16
17
|
"LICENSE"
|
|
17
18
|
],
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: scopecairn
|
|
3
|
+
description: Set up ScopeCairn codebase intelligence in a repository. Use when the user says "scopecairn", "scopecairn ./", "scopecairn <path>", asks to index a repo for an AI agent, or wants the agent to understand a codebase with ScopeCairn.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ScopeCairn setup
|
|
7
|
+
|
|
8
|
+
The user invoked ScopeCairn with a path (e.g. `scopecairn ./`).
|
|
9
|
+
Your job: run project setup so this repo becomes agent-readable.
|
|
10
|
+
|
|
11
|
+
## Steps
|
|
12
|
+
|
|
13
|
+
1. Resolve the path: `.` or `./` means the current project directory.
|
|
14
|
+
A relative or absolute path means that directory.
|
|
15
|
+
2. Run setup in that directory:
|
|
16
|
+
`scopecairn init <path>` (or `npx -y scopecairn init <path>` if it is not installed).
|
|
17
|
+
3. If the command is not found, install globally first
|
|
18
|
+
(`npm install -g scopecairn`), then retry step 2.
|
|
19
|
+
4. Report back, briefly: files indexed, symbols, relationships,
|
|
20
|
+
detected adapters, and which integration files were written
|
|
21
|
+
(AGENTS.md, Skill, Workflow, agent matrix).
|
|
22
|
+
|
|
23
|
+
## Rules
|
|
24
|
+
|
|
25
|
+
- Setup is read-only toward source code. `init` only adds/updates
|
|
26
|
+
`AGENTS.md` (marker block), the agent Skill, the Workflow file,
|
|
27
|
+
and the `.scopecairn/` index directory. Never refactor user code.
|
|
28
|
+
- If a terminal command needs approval, ask once — after setup,
|
|
29
|
+
recommend the user allowlist the read-only commands printed by `init`.
|
|
30
|
+
- After setup, coding tasks go through `scopecairn context "<task>"`,
|
|
31
|
+
never by re-exploring the repository from zero.
|