scopecairn 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,7 +15,27 @@ npm install -g scopecairn
15
15
 
16
16
  Requires Node.js ≥ 20.
17
17
 
18
- ## Quickstart
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 path13 from "path";
5
+ import path15 from "path";
6
6
 
7
7
  // src/scanner.ts
8
- import fs5 from "fs";
9
- import path6 from "path";
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 path6.relative(repoRoot2, abs).split(path6.sep).join("/");
1465
+ return path8.relative(repoRoot2, abs).split(path8.sep).join("/");
990
1466
  }
991
1467
  function walk(absDir, out) {
992
- const entries = fs5.readdirSync(absDir, { withFileTypes: true });
1468
+ const entries = fs7.readdirSync(absDir, { withFileTypes: true });
993
1469
  for (const e of entries) {
994
- const abs = path6.join(absDir, e.name);
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 = fs5.readFileSync(abs);
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 fs6 from "fs";
1136
- import path7 from "path";
1623
+ import fs8 from "fs";
1624
+ import path9 from "path";
1137
1625
  function cmdStatus(repoRoot2) {
1138
1626
  const dir = dataDir(repoRoot2);
1139
- if (!fs6.existsSync(path7.join(dir, "scopecairn.db"))) {
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
- fs6.rmSync(dir, { recursive: true, force: true });
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 fs8 from "fs";
1195
- import path9 from "path";
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 fs7 from "fs";
1221
- import path8 from "path";
1716
+ import fs9 from "fs";
1717
+ import path10 from "path";
1222
1718
  function detectAgentDir(repoRoot2) {
1223
- const plural = fs7.existsSync(path8.join(repoRoot2, ".agents"));
1224
- const singular = fs7.existsSync(path8.join(repoRoot2, ".agent"));
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
- fs7.readFileSync(path8.join(repoRoot2, "package.json"), "utf8")
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 agentsMd(prefix) {
1249
- return `${MARKER}
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 = fs8.existsSync(dbPath(repoRoot2));
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 = path9.join(dataDir(repoRoot2), "protected.yml");
1350
- console.log(`${tick(fs8.existsSync(protPath))} Protected ${protPath}`);
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
- if (agentsOk && agentsPrefix && agentsPrefix !== expectedPrefix) {
1369
- prefixOk = false;
1370
- console.log(
1371
- `\u2717 Prefix tak konsisten: AGENTS.md memakai \`${agentsPrefix}\`, instalasi terdeteksi \`${expectedPrefix}\`. Jalankan ulang \`scopecairn init\`.`
1372
- );
1373
- } else if (agentsOk) {
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 && agentsOk && skillOk && prefixOk;
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 fs9 from "fs";
1520
- import path10 from "path";
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 = path10.join(repoRoot2, node.file);
2120
+ const abs = path12.join(repoRoot2, node.file);
1537
2121
  let lines;
1538
2122
  try {
1539
- lines = fs9.readFileSync(abs, "utf8").split("\n");
2123
+ lines = fs11.readFileSync(abs, "utf8").split("\n");
1540
2124
  } catch {
1541
2125
  console.log(`Cannot read ${node.file}.`);
1542
2126
  return;
@@ -2072,7 +2656,7 @@ function cmdImpact(repoRoot2, target, opts = {}) {
2072
2656
  }
2073
2657
  if (out.length >= 15) break;
2074
2658
  }
2075
- if (out.length === 0 && fileHit != null && (rel === "CALLS" || rel === "USES")) {
2659
+ if (out.length === 0 && fileHit != null && (rel === "CALLS" || rel === "USES" || rel === "QUERIES")) {
2076
2660
  try {
2077
2661
  const rows = db.prepare(
2078
2662
  `SELECT DISTINCT t.name AS n, f.path AS p
@@ -2124,6 +2708,9 @@ function cmdImpact(repoRoot2, target, opts = {}) {
2124
2708
  `Diimpor oleh: ${byRel("IMPORTS", "in").join(", ") || "-"}`,
2125
2709
  `Memanggil: ${byRel("CALLS", "out").join(", ") || "-"}`,
2126
2710
  `Dipanggil oleh: ${byRel("CALLS", "in").join(", ") || "-"}`,
2711
+ `Mengquery: ${byRel("QUERIES", "out").join(", ") || "-"}`,
2712
+ `Diquery oleh: ${byRel("QUERIES", "in").join(", ") || "-"}`,
2713
+ `Rute: ${[...byRel("ROUTES_TO", "out"), ...byRel("ROUTES_TO", "in")].join(", ") || "-"}`,
2127
2714
  ``,
2128
2715
  `## Indirect (${indirect.size})`,
2129
2716
  ...[...indirect.values()].slice(0, 15).map((v) => `- ${v.name} (${v.type}, ${v.file}) via ${v.via}`),
@@ -2144,9 +2731,32 @@ function cmdImpact(repoRoot2, target, opts = {}) {
2144
2731
  }
2145
2732
 
2146
2733
  // src/commands/init.ts
2147
- import fs10 from "fs";
2148
- import path11 from "path";
2149
- function cmdInit(repoRoot2) {
2734
+ import fs12 from "fs";
2735
+ import path13 from "path";
2736
+ function writeTarget(repoRoot2, target, prefix) {
2737
+ const abs = path13.join(repoRoot2, target.rel);
2738
+ const block = target.render(prefix);
2739
+ if (target.mode === "new") {
2740
+ const existed = fs12.existsSync(abs);
2741
+ fs12.mkdirSync(path13.dirname(abs), { recursive: true });
2742
+ fs12.writeFileSync(abs, block.endsWith("\n") ? block : block + "\n");
2743
+ return existed ? "updated" : "generated";
2744
+ }
2745
+ if (fs12.existsSync(abs)) {
2746
+ const cur = fs12.readFileSync(abs, "utf8");
2747
+ if (cur.includes(MARKER)) {
2748
+ const re = new RegExp(`${MARKER}[\\s\\S]*?(?=\\n#[^#]|$)`, "");
2749
+ fs12.writeFileSync(abs, cur.replace(re, block.trimEnd()).trimEnd() + "\n");
2750
+ return "updated";
2751
+ }
2752
+ fs12.writeFileSync(abs, cur.trimEnd() + "\n\n" + block);
2753
+ return "extended";
2754
+ }
2755
+ fs12.mkdirSync(path13.dirname(abs), { recursive: true });
2756
+ fs12.writeFileSync(abs, block.endsWith("\n") ? block : block + "\n");
2757
+ return "generated";
2758
+ }
2759
+ function cmdInit(repoRoot2, opts = {}) {
2150
2760
  console.log("ScopeCairn");
2151
2761
  console.log("\u2713 Repository detected");
2152
2762
  const stats = scanRepository(repoRoot2);
@@ -2156,39 +2766,47 @@ function cmdInit(repoRoot2) {
2156
2766
  console.log(`\u2713 ${stats.relationships} relationships`);
2157
2767
  const prefix = detectPrefix(repoRoot2);
2158
2768
  const agentDir = detectAgentDir(repoRoot2);
2159
- const agentsPath = path11.join(repoRoot2, "AGENTS.md");
2160
- const block = agentsMd(prefix);
2161
- if (fs10.existsSync(agentsPath)) {
2162
- const cur = fs10.readFileSync(agentsPath, "utf8");
2163
- if (cur.includes(MARKER)) {
2164
- const re = new RegExp(`${MARKER}[\\s\\S]*?(?=\\n#[^#]|$)`, "");
2165
- const next = cur.replace(re, block.trimEnd());
2166
- fs10.writeFileSync(agentsPath, next.trimEnd() + "\n");
2167
- console.log("\u2713 AGENTS.md updated (blok ScopeCairn diganti)");
2168
- } else {
2169
- fs10.writeFileSync(agentsPath, cur.trimEnd() + "\n\n" + block);
2170
- console.log("\u2713 AGENTS.md extended (blok ScopeCairn ditambahkan)");
2769
+ const requested = parseAgentsOption(opts.agents);
2770
+ let targets;
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);
2171
2782
  }
2783
+ if (!targets.includes(AGENT_MATRIX[0])) targets.unshift(AGENT_MATRIX[0]);
2172
2784
  } else {
2173
- fs10.writeFileSync(agentsPath, block);
2174
- console.log("\u2713 AGENTS.md generated");
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
+ }
2175
2789
  }
2176
- const skillPath = path11.join(repoRoot2, agentDir, "skills", "scopecairn", "SKILL.md");
2177
- fs10.mkdirSync(path11.dirname(skillPath), { recursive: true });
2178
- fs10.writeFileSync(skillPath, skillMd(prefix));
2179
- console.log(`\u2713 Skill installed (${path11.relative(repoRoot2, skillPath)})`);
2180
- const wfPath = path11.join(repoRoot2, "workflows", "scopecairn.md");
2181
- fs10.mkdirSync(path11.dirname(wfPath), { recursive: true });
2182
- fs10.writeFileSync(wfPath, workflowMd(prefix));
2183
- console.log(`\u2713 Workflow installed (${path11.relative(repoRoot2, wfPath)})`);
2790
+ for (const t of targets) {
2791
+ const how = writeTarget(repoRoot2, t, prefix);
2792
+ console.log(`\u2713 ${t.label}: ${t.rel} (${how})`);
2793
+ }
2794
+ const skillPath = path13.join(repoRoot2, agentDir, "skills", "scopecairn", "SKILL.md");
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)})`);
2184
2802
  console.log("Ready.");
2185
2803
  console.log("");
2186
2804
  console.log(allowlistGuide(prefix));
2187
2805
  }
2188
2806
 
2189
2807
  // src/benchmark/run.ts
2190
- import fs11 from "fs";
2191
- import path12 from "path";
2808
+ import fs13 from "fs";
2809
+ import path14 from "path";
2192
2810
 
2193
2811
  // src/benchmark/dataset.ts
2194
2812
  var BENCHMARK_TASKS = [
@@ -2245,7 +2863,7 @@ var BENCHMARK_TASKS = [
2245
2863
  // src/benchmark/run.ts
2246
2864
  function fileChars(repoRoot2, rel) {
2247
2865
  try {
2248
- return fs11.readFileSync(path12.join(repoRoot2, rel), "utf8").length;
2866
+ return fs13.readFileSync(path14.join(repoRoot2, rel), "utf8").length;
2249
2867
  } catch {
2250
2868
  return 0;
2251
2869
  }
@@ -2394,11 +3012,66 @@ function cmdBenchmark(repoRoot2, opts = {}) {
2394
3012
  }
2395
3013
  }
2396
3014
 
3015
+ // package.json
3016
+ var package_default = {
3017
+ name: "scopecairn",
3018
+ version: "0.2.0",
3019
+ description: "ScopeCairn \u2014 Codebase Intelligence & Agent Optimization Layer (local-first)",
3020
+ license: "MIT",
3021
+ type: "module",
3022
+ engines: {
3023
+ node: ">=20"
3024
+ },
3025
+ bin: {
3026
+ scopecairn: "dist/cli.js"
3027
+ },
3028
+ files: [
3029
+ "dist",
3030
+ "skills",
3031
+ "README.md",
3032
+ "LICENSE"
3033
+ ],
3034
+ keywords: [
3035
+ "code-intelligence",
3036
+ "knowledge-graph",
3037
+ "ai-agents",
3038
+ "coding-assistant",
3039
+ "static-analysis",
3040
+ "developer-tools"
3041
+ ],
3042
+ scripts: {
3043
+ build: "tsup",
3044
+ prepublishOnly: "npm run typecheck && npm run build",
3045
+ dev: "tsx src/cli.ts",
3046
+ scan: "tsx src/cli.ts scan",
3047
+ status: "tsx src/cli.ts status",
3048
+ doctor: "tsx src/cli.ts doctor",
3049
+ clean: "tsx src/cli.ts clean",
3050
+ rebuild: "tsx src/cli.ts rebuild",
3051
+ typecheck: "tsc --noEmit"
3052
+ },
3053
+ dependencies: {
3054
+ commander: "^12.1.0",
3055
+ ignore: "^7.0.0",
3056
+ "web-tree-sitter": "^0.25.0"
3057
+ },
3058
+ devDependencies: {
3059
+ "@types/node": "^22.7.0",
3060
+ tsup: "^8.3.0",
3061
+ tsx: "^4.19.0",
3062
+ typescript: "^5.6.0"
3063
+ }
3064
+ };
3065
+
2397
3066
  // src/cli.ts
2398
3067
  var program = new Command();
2399
- program.name("scopecairn").description("ScopeCairn \u2014 local-first codebase intelligence layer").version("0.1.0");
3068
+ program.name("scopecairn").description("ScopeCairn \u2014 local-first codebase intelligence layer").version(package_default.version);
2400
3069
  function repoRoot() {
2401
- return path13.resolve(process.cwd());
3070
+ return path15.resolve(process.cwd());
3071
+ }
3072
+ function resolveRoot(given) {
3073
+ if (!given) return repoRoot();
3074
+ return path15.resolve(process.cwd(), given);
2402
3075
  }
2403
3076
  program.command("scan").description("Index repository (Fase 1: files + hash + language)").action(() => cmdScan(repoRoot()));
2404
3077
  program.command("status").description("Show index status").action(() => cmdStatus(repoRoot()));
@@ -2416,8 +3089,13 @@ program.command("context <task>").description("Main entry point: auto-refresh +
2416
3089
  program.command("impact <target>").description("Change impact analysis: file, symbol, function, or class").option("--no-refresh", "skip incremental refresh (debugging/benchmark)").action(
2417
3090
  (target, opts) => cmdImpact(repoRoot(), target, opts)
2418
3091
  );
2419
- program.command("init").description("Initial indexing + generate AGENTS.md, Skill, Workflow").action(() => cmdInit(repoRoot()));
3092
+ 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(
3093
+ (targetPath, opts) => cmdInit(resolveRoot(targetPath), opts)
3094
+ );
2420
3095
  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
3096
  (opts) => cmdBenchmark(repoRoot(), opts)
2422
3097
  );
2423
3098
  program.parseAsync(process.argv);
3099
+ export {
3100
+ resolveRoot
3101
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scopecairn",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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.