scopecairn 0.6.0 → 0.7.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
|
@@ -66,6 +66,13 @@ npm install -g scopecairn
|
|
|
66
66
|
|
|
67
67
|
Requires Node.js ≥ 20.
|
|
68
68
|
|
|
69
|
+
Upgrade later with:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
scopecairn update # or: npm install -g scopecairn@latest
|
|
73
|
+
scopecairn update --check # only check for a new version
|
|
74
|
+
```
|
|
75
|
+
|
|
69
76
|
## Agent-driven setup
|
|
70
77
|
|
|
71
78
|
Install once, then just type `scopecairn ./` to your AI agent —
|
|
@@ -148,6 +155,7 @@ scopecairn agents list
|
|
|
148
155
|
| `test-select <target>` | List test files affected by a file/symbol change target |
|
|
149
156
|
| `doctor --verbose` | Also surface adapter/graph errors from `.scopecairn/last-run.log` to stderr |
|
|
150
157
|
| `benchmark [--tune]` | Retrieval recall, irrelevant ratio, context reduction + weight calibration |
|
|
158
|
+
| `update [--check]` | Update ScopeCairn to the latest npm release (`--check` only checks) |
|
|
151
159
|
|
|
152
160
|
Agent commands (`context`, `impact`, `read`, `graph`, `status`, `doctor`)
|
|
153
161
|
are read-only toward source and only write to `.scopecairn/` — safe to
|
|
@@ -159,8 +159,15 @@ function ensureMetrics(db) {
|
|
|
159
159
|
try {
|
|
160
160
|
const syms = db.prepare(`SELECT COUNT(*) AS n FROM symbols`).get().n;
|
|
161
161
|
if (syms === 0) return 0;
|
|
162
|
-
const
|
|
163
|
-
|
|
162
|
+
const missing = db.prepare(
|
|
163
|
+
`SELECT COUNT(*) AS n FROM symbols s LEFT JOIN node_metrics m ON m.node_id = s.id WHERE m.node_id IS NULL`
|
|
164
|
+
).get().n;
|
|
165
|
+
const orphans = db.prepare(
|
|
166
|
+
`SELECT COUNT(*) AS n FROM node_metrics m LEFT JOIN symbols s ON s.id = m.node_id WHERE s.id IS NULL`
|
|
167
|
+
).get().n;
|
|
168
|
+
if (missing === 0 && orphans === 0) {
|
|
169
|
+
return db.prepare(`SELECT COUNT(*) AS n FROM node_metrics`).get().n;
|
|
170
|
+
}
|
|
164
171
|
return refreshMetrics(db);
|
|
165
172
|
} catch {
|
|
166
173
|
return 0;
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
ensureMetrics,
|
|
4
4
|
getMetrics
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-DY2AAM62.js";
|
|
6
6
|
import {
|
|
7
7
|
nodeInCycle
|
|
8
8
|
} from "./chunk-UU5WHX3D.js";
|
|
@@ -642,7 +642,8 @@ function extractGeneric(source, lang) {
|
|
|
642
642
|
let m;
|
|
643
643
|
const importLine = /^\s*import\s+(.+?)\s*$/gm;
|
|
644
644
|
while ((m = importLine.exec(source)) !== null) {
|
|
645
|
-
const
|
|
645
|
+
const goPath = lang === "go" ? m[1].match(/["`]([^"`]+)["`]/) : null;
|
|
646
|
+
const mod = goPath ? goPath[1] : m[1].replace(/["';]/g, "").trim().split(/\s+/)[0];
|
|
646
647
|
if (!mod || mod === "*") continue;
|
|
647
648
|
const ln = lineOf3(source, m.index);
|
|
648
649
|
symbols.push({ name: `import:${mod}`, type: "import", signature: m[0].trim().slice(0, 120), startLine: ln, endLine: ln });
|
|
@@ -780,9 +781,9 @@ function extractFile(relPath, source) {
|
|
|
780
781
|
const lang = detectLanguage(relPath);
|
|
781
782
|
switch (lang) {
|
|
782
783
|
case "typescript":
|
|
783
|
-
return extractTypeScript(source, relPath.endsWith("
|
|
784
|
+
return extractTypeScript(source, relPath.endsWith(".tsx"));
|
|
784
785
|
case "javascript":
|
|
785
|
-
return extractTypeScript(source, relPath.endsWith("
|
|
786
|
+
return extractTypeScript(source, relPath.endsWith(".jsx"));
|
|
786
787
|
case "python":
|
|
787
788
|
return extractPython(source);
|
|
788
789
|
case "html":
|
|
@@ -835,7 +836,7 @@ var STOP = /* @__PURE__ */ new Set([
|
|
|
835
836
|
"spec"
|
|
836
837
|
]);
|
|
837
838
|
function tokenizeIdentifier(raw) {
|
|
838
|
-
const parts = raw.replace(/[\\/]/g, " ").replace(/[.:\-_"']/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/g).filter((t) => t.length >= 2 &&
|
|
839
|
+
const parts = raw.replace(/[\\/]/g, " ").replace(/[.:\-_"']/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/g).filter((t) => t.length >= 2 && !STOP.has(t));
|
|
839
840
|
return [...new Set(parts)];
|
|
840
841
|
}
|
|
841
842
|
function tokenizeTask(text) {
|
|
@@ -868,9 +869,11 @@ function seedSearch(db, tokens, limit = 30) {
|
|
|
868
869
|
const q = tokens.map((t) => `"${t.replace(/"/g, "")}"`).join(" OR ");
|
|
869
870
|
let rows;
|
|
870
871
|
try {
|
|
872
|
+
const lim = Math.max(1, Math.min(500, Math.floor(limit) || 30));
|
|
871
873
|
rows = db.prepare(
|
|
872
874
|
`SELECT symbol_id, bm25(symbol_index) AS rank
|
|
873
|
-
FROM symbol_index WHERE symbol_index MATCH ?
|
|
875
|
+
FROM symbol_index WHERE symbol_index MATCH ?
|
|
876
|
+
ORDER BY rank LIMIT ${lim}`
|
|
874
877
|
).all(q);
|
|
875
878
|
} catch {
|
|
876
879
|
return [];
|
|
@@ -898,9 +901,20 @@ function symbolIdInFile(db, fileId, name) {
|
|
|
898
901
|
const r = db.prepare(`SELECT id FROM symbols WHERE file_id = ? AND name = ? LIMIT 1`).get(fileId, name);
|
|
899
902
|
return r ? r.id : null;
|
|
900
903
|
}
|
|
901
|
-
function globalSymbolId(db, name) {
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
+
function globalSymbolId(db, name, preferDir = "") {
|
|
905
|
+
const rows = db.prepare(
|
|
906
|
+
`SELECT s.id, f.path AS p FROM symbols s JOIN files f ON f.id = s.file_id
|
|
907
|
+
WHERE s.name = ? ORDER BY s.id`
|
|
908
|
+
).all(name);
|
|
909
|
+
if (rows.length === 0) return null;
|
|
910
|
+
if (preferDir) {
|
|
911
|
+
const sameDir = rows.find((r) => r.p.startsWith(preferDir + "/"));
|
|
912
|
+
if (sameDir) return sameDir.id;
|
|
913
|
+
const top = preferDir.split("/")[0];
|
|
914
|
+
const sameTop = rows.find((r) => r.p.split("/")[0] === top);
|
|
915
|
+
if (sameTop) return sameTop.id;
|
|
916
|
+
}
|
|
917
|
+
return rows[0].id;
|
|
904
918
|
}
|
|
905
919
|
function methodSymbolId(db, fileId, ids, name) {
|
|
906
920
|
const local = ids.get(name);
|
|
@@ -974,9 +988,10 @@ function indexFileRelations(db, fileId, relPath, extraction, fileSym) {
|
|
|
974
988
|
contains.run(fileSym, id);
|
|
975
989
|
rels++;
|
|
976
990
|
}
|
|
991
|
+
const srcDir = relPath.includes("/") ? relPath.slice(0, relPath.lastIndexOf("/")) : "";
|
|
977
992
|
const resolveLocal = (name) => {
|
|
978
993
|
if (name === "__file__") return fileSym;
|
|
979
|
-
return ids.get(name) ?? symbolIdInFile(db, fileId, name) ?? globalSymbolId(db, name);
|
|
994
|
+
return ids.get(name) ?? symbolIdInFile(db, fileId, name) ?? globalSymbolId(db, name, srcDir);
|
|
980
995
|
};
|
|
981
996
|
for (const r of extraction.relations) {
|
|
982
997
|
if (r.rel === "IMPORTS") {
|
|
@@ -1004,7 +1019,7 @@ function indexFileRelations(db, fileId, relPath, extraction, fileSym) {
|
|
|
1004
1019
|
continue;
|
|
1005
1020
|
}
|
|
1006
1021
|
const src = resolveLocal(r.from);
|
|
1007
|
-
const dst = r.rel === "CALLS" && r.methodCall ? methodSymbolId(db, fileId, ids, r.to) : ids.get(r.to) ?? globalSymbolId(db, r.to);
|
|
1022
|
+
const dst = r.rel === "CALLS" && r.methodCall ? methodSymbolId(db, fileId, ids, r.to) : ids.get(r.to) ?? globalSymbolId(db, r.to, srcDir);
|
|
1008
1023
|
if (src && dst && src !== dst) {
|
|
1009
1024
|
insRel.run(src, dst, r.rel, r.weight, r.confidence);
|
|
1010
1025
|
rels++;
|
|
@@ -1033,9 +1048,11 @@ function linkTests(db) {
|
|
|
1033
1048
|
for (const f of files) {
|
|
1034
1049
|
const stemBase = testTarget(f.path);
|
|
1035
1050
|
if (!stemBase) continue;
|
|
1051
|
+
const stemName = stemBase.split("/").pop();
|
|
1036
1052
|
const matches = [...byPath.keys()].filter((p) => {
|
|
1037
|
-
|
|
1038
|
-
|
|
1053
|
+
if (p === f.path || testTarget(p)) return false;
|
|
1054
|
+
const base = p.split("/").pop() ?? p;
|
|
1055
|
+
return base.replace(/\.[^.]+$/, "") === stemName;
|
|
1039
1056
|
});
|
|
1040
1057
|
const srcSym = fileSymOf(f.id);
|
|
1041
1058
|
for (const mp of matches) {
|
|
@@ -1160,13 +1177,13 @@ var META_BASENAMES = /* @__PURE__ */ new Set([
|
|
|
1160
1177
|
"makefile",
|
|
1161
1178
|
"package.json"
|
|
1162
1179
|
]);
|
|
1163
|
-
function isIndexableMetaFile(posixPath) {
|
|
1180
|
+
function isIndexableMetaFile(posixPath, patterns) {
|
|
1164
1181
|
if (isSecretFile(posixPath)) return false;
|
|
1165
1182
|
const base = (posixPath.split("/").pop() ?? "").toLowerCase();
|
|
1166
1183
|
if (META_BASENAMES.has(base)) return true;
|
|
1167
1184
|
const dot = base.lastIndexOf(".");
|
|
1168
1185
|
if (dot !== -1 && META_EXTS.has(base.slice(dot))) return true;
|
|
1169
|
-
if (isProtected(posixPath)) return true;
|
|
1186
|
+
if (patterns ? isProtected(posixPath, patterns) : isProtected(posixPath)) return true;
|
|
1170
1187
|
return false;
|
|
1171
1188
|
}
|
|
1172
1189
|
function ensureProtectedFile(repoRoot2) {
|
|
@@ -1222,6 +1239,7 @@ function loadConfig(repoRoot2) {
|
|
|
1222
1239
|
cfg.wCochange = num("w_cochange", cfg.wCochange);
|
|
1223
1240
|
cfg.maxDepth = Math.max(1, Math.min(4, Math.round(num("max_depth", cfg.maxDepth))));
|
|
1224
1241
|
cfg.topN = Math.max(5, Math.min(100, Math.round(num("top_n", cfg.topN))));
|
|
1242
|
+
cfg.decayPerHop = Math.max(0, Math.min(1, num("decay_per_hop", cfg.decayPerHop)));
|
|
1225
1243
|
} catch {
|
|
1226
1244
|
}
|
|
1227
1245
|
return cfg;
|
|
@@ -1259,6 +1277,7 @@ w_recency: 0.1
|
|
|
1259
1277
|
w_cochange: 0.1
|
|
1260
1278
|
max_depth: 2
|
|
1261
1279
|
top_n: 20
|
|
1280
|
+
decay_per_hop: 0.7
|
|
1262
1281
|
`
|
|
1263
1282
|
);
|
|
1264
1283
|
}
|
|
@@ -1314,6 +1333,32 @@ function enclosingFunction(db, fileId, line) {
|
|
|
1314
1333
|
).all(fileId, line);
|
|
1315
1334
|
return rows.length > 0 ? rows[0].id : null;
|
|
1316
1335
|
}
|
|
1336
|
+
function cleanupOrphans(db) {
|
|
1337
|
+
let n = 0;
|
|
1338
|
+
n += Number(
|
|
1339
|
+
db.prepare(
|
|
1340
|
+
`DELETE FROM relationships
|
|
1341
|
+
WHERE source_id NOT IN (SELECT id FROM symbols)
|
|
1342
|
+
OR target_id NOT IN (SELECT id FROM symbols)`
|
|
1343
|
+
).run().changes
|
|
1344
|
+
);
|
|
1345
|
+
n += Number(
|
|
1346
|
+
db.prepare(
|
|
1347
|
+
`DELETE FROM node_metrics WHERE node_id NOT IN (SELECT id FROM symbols)`
|
|
1348
|
+
).run().changes
|
|
1349
|
+
);
|
|
1350
|
+
n += Number(
|
|
1351
|
+
db.prepare(
|
|
1352
|
+
`DELETE FROM clusters WHERE node_id NOT IN (SELECT id FROM symbols)`
|
|
1353
|
+
).run().changes
|
|
1354
|
+
);
|
|
1355
|
+
n += Number(
|
|
1356
|
+
db.prepare(
|
|
1357
|
+
`DELETE FROM task_context WHERE symbol_id NOT IN (SELECT id FROM symbols)`
|
|
1358
|
+
).run().changes
|
|
1359
|
+
);
|
|
1360
|
+
return n;
|
|
1361
|
+
}
|
|
1317
1362
|
|
|
1318
1363
|
// src/adapters/prisma.ts
|
|
1319
1364
|
function parsePrismaModels(content) {
|
|
@@ -1426,6 +1471,25 @@ function prismaCalls(content) {
|
|
|
1426
1471
|
function resolveModel(db, name) {
|
|
1427
1472
|
return findSymbolId(db, name, ["model"]) ?? dbLowerModel(db, name);
|
|
1428
1473
|
}
|
|
1474
|
+
function derivePrismaQueries(db, fileId, content) {
|
|
1475
|
+
const out = [];
|
|
1476
|
+
const fileSym = fileSymbolOf(db, fileId);
|
|
1477
|
+
if (!fileSym) return out;
|
|
1478
|
+
for (const call of prismaCalls(content)) {
|
|
1479
|
+
const modelId = resolveModel(db, call.model);
|
|
1480
|
+
if (!modelId) continue;
|
|
1481
|
+
const line = lineOf4(content, call.index);
|
|
1482
|
+
const caller = enclosingFunction(db, fileId, line) ?? fileSym;
|
|
1483
|
+
if (caller === modelId) continue;
|
|
1484
|
+
out.push({
|
|
1485
|
+
callerId: caller,
|
|
1486
|
+
targetId: modelId,
|
|
1487
|
+
weight: WEIGHT.QUERIES,
|
|
1488
|
+
confidence: 0.9
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
return out;
|
|
1492
|
+
}
|
|
1429
1493
|
function refreshQueries(db, files) {
|
|
1430
1494
|
let n = 0;
|
|
1431
1495
|
const del = db.prepare(
|
|
@@ -1433,19 +1497,12 @@ function refreshQueries(db, files) {
|
|
|
1433
1497
|
);
|
|
1434
1498
|
const ins = db.prepare(
|
|
1435
1499
|
`INSERT INTO relationships(source_id, target_id, relationship_type, weight, confidence)
|
|
1436
|
-
VALUES (?, ?, 'QUERIES',
|
|
1500
|
+
VALUES (?, ?, 'QUERIES', ?, ?)`
|
|
1437
1501
|
);
|
|
1438
1502
|
for (const f of files) {
|
|
1439
|
-
const fileSym = fileSymbolOf(db, f.fileId);
|
|
1440
|
-
if (!fileSym) continue;
|
|
1441
1503
|
del.run(f.fileId);
|
|
1442
|
-
for (const
|
|
1443
|
-
|
|
1444
|
-
if (!modelId) continue;
|
|
1445
|
-
const line = lineOf4(f.content, call.index);
|
|
1446
|
-
const caller = enclosingFunction(db, f.fileId, line) ?? fileSym;
|
|
1447
|
-
if (caller === modelId) continue;
|
|
1448
|
-
ins.run(caller, modelId);
|
|
1504
|
+
for (const e of derivePrismaQueries(db, f.fileId, f.content)) {
|
|
1505
|
+
ins.run(e.callerId, e.targetId, e.weight, e.confidence);
|
|
1449
1506
|
n++;
|
|
1450
1507
|
}
|
|
1451
1508
|
}
|
|
@@ -1587,6 +1644,24 @@ function hasDrizzleMarker(content) {
|
|
|
1587
1644
|
}
|
|
1588
1645
|
return /db\s*\.\s*(select|selectDistinct|insert|update|delete)\b/.test(content);
|
|
1589
1646
|
}
|
|
1647
|
+
function deriveDrizzleQueries(db, fileId, content) {
|
|
1648
|
+
const out = [];
|
|
1649
|
+
const fileSym = fileSymbolOf(db, fileId);
|
|
1650
|
+
if (!fileSym) return out;
|
|
1651
|
+
for (const q of parseDrizzleQueries(content)) {
|
|
1652
|
+
const target = findModelInFile(db, fileId, q.table) ?? findModelGlobal(db, q.table);
|
|
1653
|
+
if (!target) continue;
|
|
1654
|
+
const caller = enclosingFunction(db, fileId, lineOf4(content, q.index)) ?? fileSym;
|
|
1655
|
+
if (caller === target) continue;
|
|
1656
|
+
out.push({
|
|
1657
|
+
callerId: caller,
|
|
1658
|
+
targetId: target,
|
|
1659
|
+
weight: WEIGHT.QUERIES,
|
|
1660
|
+
confidence: 0.85
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
return out;
|
|
1664
|
+
}
|
|
1590
1665
|
function refreshDrizzleQueries(db, files) {
|
|
1591
1666
|
let n = 0;
|
|
1592
1667
|
const del = db.prepare(
|
|
@@ -1594,18 +1669,13 @@ function refreshDrizzleQueries(db, files) {
|
|
|
1594
1669
|
);
|
|
1595
1670
|
const ins = db.prepare(
|
|
1596
1671
|
`INSERT INTO relationships(source_id, target_id, relationship_type, weight, confidence)
|
|
1597
|
-
VALUES (?, ?, 'QUERIES',
|
|
1672
|
+
VALUES (?, ?, 'QUERIES', ?, ?)`
|
|
1598
1673
|
);
|
|
1599
1674
|
for (const f of files) {
|
|
1600
|
-
|
|
1601
|
-
if (!fileSym || !hasDrizzleMarker(f.content)) continue;
|
|
1675
|
+
if (!hasDrizzleMarker(f.content)) continue;
|
|
1602
1676
|
del.run(f.fileId);
|
|
1603
|
-
for (const
|
|
1604
|
-
|
|
1605
|
-
if (!target) continue;
|
|
1606
|
-
const caller = enclosingFunction(db, f.fileId, lineOf4(f.content, q.index)) ?? fileSym;
|
|
1607
|
-
if (caller === target) continue;
|
|
1608
|
-
ins.run(caller, target);
|
|
1677
|
+
for (const e of deriveDrizzleQueries(db, f.fileId, f.content)) {
|
|
1678
|
+
ins.run(e.callerId, e.targetId, e.weight, e.confidence);
|
|
1609
1679
|
n++;
|
|
1610
1680
|
}
|
|
1611
1681
|
}
|
|
@@ -1895,6 +1965,24 @@ function findModelGlobal2(db, name) {
|
|
|
1895
1965
|
const lower = name.toLowerCase();
|
|
1896
1966
|
return rows.find((r) => r.name.toLowerCase() === lower)?.id ?? null;
|
|
1897
1967
|
}
|
|
1968
|
+
function deriveSqlAlchemyQueries(db, fileId, content) {
|
|
1969
|
+
const out = [];
|
|
1970
|
+
const fileSym = fileSymbolOf(db, fileId);
|
|
1971
|
+
if (!fileSym) return out;
|
|
1972
|
+
for (const q of parseSqlAlchemyQueries(content)) {
|
|
1973
|
+
const target = findModelGlobal2(db, q.model);
|
|
1974
|
+
if (!target) continue;
|
|
1975
|
+
const caller = enclosingFunction(db, fileId, lineOf4(content, q.index)) ?? fileSym;
|
|
1976
|
+
if (caller === target) continue;
|
|
1977
|
+
out.push({
|
|
1978
|
+
callerId: caller,
|
|
1979
|
+
targetId: target,
|
|
1980
|
+
weight: WEIGHT.QUERIES,
|
|
1981
|
+
confidence: 0.85
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
return out;
|
|
1985
|
+
}
|
|
1898
1986
|
function refreshSqlAlchemyQueries(db, files) {
|
|
1899
1987
|
let n = 0;
|
|
1900
1988
|
const del = db.prepare(
|
|
@@ -1902,19 +1990,13 @@ function refreshSqlAlchemyQueries(db, files) {
|
|
|
1902
1990
|
);
|
|
1903
1991
|
const ins = db.prepare(
|
|
1904
1992
|
`INSERT INTO relationships(source_id, target_id, relationship_type, weight, confidence)
|
|
1905
|
-
VALUES (?, ?, 'QUERIES',
|
|
1993
|
+
VALUES (?, ?, 'QUERIES', ?, ?)`
|
|
1906
1994
|
);
|
|
1907
1995
|
for (const f of files) {
|
|
1908
1996
|
if (!hasSqlAlchemyMarker(f.content)) continue;
|
|
1909
|
-
const fileSym = fileSymbolOf(db, f.fileId);
|
|
1910
|
-
if (!fileSym) continue;
|
|
1911
1997
|
del.run(f.fileId);
|
|
1912
|
-
for (const
|
|
1913
|
-
|
|
1914
|
-
if (!target) continue;
|
|
1915
|
-
const caller = enclosingFunction(db, f.fileId, lineOf4(f.content, q.index)) ?? fileSym;
|
|
1916
|
-
if (caller === target) continue;
|
|
1917
|
-
ins.run(caller, target);
|
|
1998
|
+
for (const e of deriveSqlAlchemyQueries(db, f.fileId, f.content)) {
|
|
1999
|
+
ins.run(e.callerId, e.targetId, e.weight, e.confidence);
|
|
1918
2000
|
n++;
|
|
1919
2001
|
}
|
|
1920
2002
|
}
|
|
@@ -1964,27 +2046,10 @@ var sqlalchemyAdapter = {
|
|
|
1964
2046
|
relations++;
|
|
1965
2047
|
}
|
|
1966
2048
|
}
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
const ins = ctx.db.prepare(
|
|
1971
|
-
`INSERT INTO relationships(source_id, target_id, relationship_type, weight, confidence)
|
|
1972
|
-
VALUES (?, ?, 'QUERIES', ${WEIGHT.QUERIES}, 0.85)`
|
|
2049
|
+
relations += refreshSqlAlchemyQueries(
|
|
2050
|
+
ctx.db,
|
|
2051
|
+
files.map((f) => ({ fileId: f.fileId, rel: f.rel, content: f.content }))
|
|
1973
2052
|
);
|
|
1974
|
-
for (const f of files) {
|
|
1975
|
-
if (!hasSqlAlchemyMarker(f.content)) continue;
|
|
1976
|
-
const fileSym = fileSymbolOf(ctx.db, f.fileId);
|
|
1977
|
-
if (!fileSym) continue;
|
|
1978
|
-
del.run(f.fileId);
|
|
1979
|
-
for (const q of parseSqlAlchemyQueries(f.content)) {
|
|
1980
|
-
const target = findModelGlobal2(ctx.db, q.model);
|
|
1981
|
-
if (!target) continue;
|
|
1982
|
-
const caller = enclosingFunction(ctx.db, f.fileId, lineOf4(f.content, q.index)) ?? fileSym;
|
|
1983
|
-
if (caller === target) continue;
|
|
1984
|
-
ins.run(caller, target);
|
|
1985
|
-
relations++;
|
|
1986
|
-
}
|
|
1987
|
-
}
|
|
1988
2053
|
return { symbols, relations };
|
|
1989
2054
|
}
|
|
1990
2055
|
};
|
|
@@ -2037,8 +2102,10 @@ function resolveImportRel(fromRel, source) {
|
|
|
2037
2102
|
const out = [];
|
|
2038
2103
|
for (const p of parts) {
|
|
2039
2104
|
if (p === "" || p === ".") continue;
|
|
2040
|
-
if (p === "..")
|
|
2041
|
-
|
|
2105
|
+
if (p === "..") {
|
|
2106
|
+
if (out.length === 0) return null;
|
|
2107
|
+
out.pop();
|
|
2108
|
+
} else out.push(p);
|
|
2042
2109
|
}
|
|
2043
2110
|
return out.join("/");
|
|
2044
2111
|
}
|
|
@@ -2169,29 +2236,54 @@ function runAdapters(db, repoRoot2, changed) {
|
|
|
2169
2236
|
}
|
|
2170
2237
|
}
|
|
2171
2238
|
}
|
|
2172
|
-
const
|
|
2173
|
-
|
|
2239
|
+
const derivers = [];
|
|
2240
|
+
if (active.some((a) => a.id === "nextjs")) {
|
|
2241
|
+
derivers.push({ relevant: nextjsAdapter.relevant, derive: derivePrismaQueries });
|
|
2242
|
+
}
|
|
2243
|
+
if (active.some((a) => a.id === "drizzle")) {
|
|
2244
|
+
derivers.push({ relevant: drizzleAdapter.relevant, derive: deriveDrizzleQueries });
|
|
2245
|
+
}
|
|
2246
|
+
if (active.some((a) => a.id === "sqlalchemy")) {
|
|
2247
|
+
derivers.push({
|
|
2248
|
+
relevant: sqlalchemyAdapter.relevant,
|
|
2249
|
+
derive: deriveSqlAlchemyQueries
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
if (derivers.length > 0) {
|
|
2174
2253
|
try {
|
|
2175
|
-
const all = db.prepare(`SELECT id, path FROM files`).all()
|
|
2176
|
-
const
|
|
2254
|
+
const all = db.prepare(`SELECT id, path FROM files`).all();
|
|
2255
|
+
const del = db.prepare(
|
|
2256
|
+
`DELETE FROM relationships WHERE relationship_type = 'QUERIES' AND source_id IN (SELECT id FROM symbols WHERE file_id = ?)`
|
|
2257
|
+
);
|
|
2258
|
+
const ins = db.prepare(
|
|
2259
|
+
`INSERT INTO relationships(source_id, target_id, relationship_type, weight, confidence)
|
|
2260
|
+
VALUES (?, ?, 'QUERIES', ?, ?)`
|
|
2261
|
+
);
|
|
2177
2262
|
for (const r of all) {
|
|
2263
|
+
if (!derivers.some((d) => d.relevant(r.path))) continue;
|
|
2264
|
+
let content;
|
|
2178
2265
|
try {
|
|
2179
|
-
|
|
2180
|
-
fileId: r.id,
|
|
2181
|
-
rel: r.path,
|
|
2182
|
-
content: fs11.readFileSync(path12.join(repoRoot2, ...r.path.split("/")), "utf8")
|
|
2183
|
-
});
|
|
2266
|
+
content = fs11.readFileSync(path12.join(repoRoot2, ...r.path.split("/")), "utf8");
|
|
2184
2267
|
} catch {
|
|
2268
|
+
continue;
|
|
2269
|
+
}
|
|
2270
|
+
del.run(r.id);
|
|
2271
|
+
for (const d of derivers) {
|
|
2272
|
+
for (const e of d.derive(db, r.id, content)) {
|
|
2273
|
+
ins.run(e.callerId, e.targetId, e.weight, e.confidence);
|
|
2274
|
+
relations++;
|
|
2275
|
+
}
|
|
2185
2276
|
}
|
|
2186
2277
|
}
|
|
2187
|
-
relations += refresh(db, inputs);
|
|
2188
2278
|
} catch (err) {
|
|
2189
2279
|
logError("adapters", err, repoRoot2);
|
|
2190
2280
|
}
|
|
2191
|
-
}
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2281
|
+
}
|
|
2282
|
+
try {
|
|
2283
|
+
cleanupOrphans(db);
|
|
2284
|
+
} catch (err) {
|
|
2285
|
+
logError("adapters", err, repoRoot2);
|
|
2286
|
+
}
|
|
2195
2287
|
return { adapters: active.map((a) => a.id), symbols, relations };
|
|
2196
2288
|
}
|
|
2197
2289
|
|
|
@@ -2647,9 +2739,15 @@ function ensureClusters(db) {
|
|
|
2647
2739
|
try {
|
|
2648
2740
|
const syms = db.prepare(`SELECT COUNT(*) AS n FROM symbols`).get().n;
|
|
2649
2741
|
if (syms === 0) return 0;
|
|
2650
|
-
const
|
|
2651
|
-
|
|
2652
|
-
|
|
2742
|
+
const missing = db.prepare(
|
|
2743
|
+
`SELECT COUNT(*) AS n FROM symbols s LEFT JOIN clusters c ON c.node_id = s.id WHERE c.node_id IS NULL`
|
|
2744
|
+
).get().n;
|
|
2745
|
+
const orphans = db.prepare(
|
|
2746
|
+
`SELECT COUNT(*) AS n FROM clusters c LEFT JOIN symbols s ON s.id = c.node_id WHERE s.id IS NULL`
|
|
2747
|
+
).get().n;
|
|
2748
|
+
if (missing > 0 || orphans > 0) {
|
|
2749
|
+
refreshClusters(db);
|
|
2750
|
+
}
|
|
2653
2751
|
return db.prepare(`SELECT COUNT(*) AS n FROM clusters`).get().n;
|
|
2654
2752
|
} catch {
|
|
2655
2753
|
return 0;
|
|
@@ -3402,6 +3500,8 @@ function scanRepository(repoRoot2) {
|
|
|
3402
3500
|
);
|
|
3403
3501
|
let sourceFiles = 0;
|
|
3404
3502
|
let metaFiles = 0;
|
|
3503
|
+
const protectedPatterns = loadProtectedPatterns(repoRoot2);
|
|
3504
|
+
const backfillDone = db.prepare(`SELECT value FROM meta WHERE key = 'backfill_done'`).get();
|
|
3405
3505
|
const symbolCount = db.prepare(`SELECT COUNT(*) AS n FROM symbols`).get().n;
|
|
3406
3506
|
let ftsCount = 0;
|
|
3407
3507
|
try {
|
|
@@ -3409,7 +3509,7 @@ function scanRepository(repoRoot2) {
|
|
|
3409
3509
|
} catch {
|
|
3410
3510
|
ftsCount = 0;
|
|
3411
3511
|
}
|
|
3412
|
-
const backfill = symbolCount === 0 || ftsCount === 0;
|
|
3512
|
+
const backfill = !backfillDone && (symbolCount === 0 || ftsCount === 0);
|
|
3413
3513
|
const pending = [];
|
|
3414
3514
|
const metaChanged = [];
|
|
3415
3515
|
for (const abs of allAbs) {
|
|
@@ -3417,7 +3517,7 @@ function scanRepository(repoRoot2) {
|
|
|
3417
3517
|
if (isIgnored(rel, rules)) continue;
|
|
3418
3518
|
totalFilesSeen++;
|
|
3419
3519
|
const source = isSourceFile(abs);
|
|
3420
|
-
const meta = !source && isIndexableMetaFile(rel);
|
|
3520
|
+
const meta = !source && isIndexableMetaFile(rel, protectedPatterns);
|
|
3421
3521
|
if (!source && !meta) continue;
|
|
3422
3522
|
if (source) sourceFiles++;
|
|
3423
3523
|
else metaFiles++;
|
|
@@ -3478,6 +3578,10 @@ function scanRepository(repoRoot2) {
|
|
|
3478
3578
|
`INSERT INTO meta(key, value) VALUES ('last_scan', datetime('now'))
|
|
3479
3579
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
3480
3580
|
).run();
|
|
3581
|
+
db.prepare(
|
|
3582
|
+
`INSERT INTO meta(key, value) VALUES ('backfill_done', '1')
|
|
3583
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
3584
|
+
).run();
|
|
3481
3585
|
linkTests(db);
|
|
3482
3586
|
cleanupIndex(db);
|
|
3483
3587
|
const graphDirty = inserted + updated + removed > 0 || adapterRun.symbols > 0 || adapterRun.relations > 0;
|
|
@@ -3909,7 +4013,7 @@ async function cmdDoctor(repoRoot2, opts) {
|
|
|
3909
4013
|
const db = openDb(repoRoot2);
|
|
3910
4014
|
try {
|
|
3911
4015
|
const { refreshCycles, getCycles, describeCycle } = await import("./cycles-6KSI4OPZ.js");
|
|
3912
|
-
const { ensureMetrics: ensureMetrics2, topByPageRank } = await import("./metrics-
|
|
4016
|
+
const { ensureMetrics: ensureMetrics2, topByPageRank } = await import("./metrics-BLUHSM64.js");
|
|
3913
4017
|
const cycles = getCycles(db, 5);
|
|
3914
4018
|
if (cycles.length === 0) {
|
|
3915
4019
|
try {
|
|
@@ -3951,7 +4055,13 @@ async function cmdDoctor(repoRoot2, opts) {
|
|
|
3951
4055
|
for (const t of AGENT_MATRIX) {
|
|
3952
4056
|
const abs = path17.join(repoRoot2, t.rel);
|
|
3953
4057
|
if (!fs17.existsSync(abs)) continue;
|
|
3954
|
-
|
|
4058
|
+
let text;
|
|
4059
|
+
try {
|
|
4060
|
+
text = fs17.readFileSync(abs, "utf8");
|
|
4061
|
+
} catch {
|
|
4062
|
+
console.log(`\u2717 Tidak bisa membaca ${t.rel} \u2014 dilewati.`);
|
|
4063
|
+
continue;
|
|
4064
|
+
}
|
|
3955
4065
|
if (t.mode === "merge" && !text.includes(MARKER)) continue;
|
|
3956
4066
|
matrixOk++;
|
|
3957
4067
|
const used = extractPrefixUsed(text);
|
|
@@ -4270,7 +4380,7 @@ function cmdRead(repoRoot2, kind, name) {
|
|
|
4270
4380
|
console.log(`Cannot read ${node.file}.`);
|
|
4271
4381
|
return;
|
|
4272
4382
|
}
|
|
4273
|
-
const start =
|
|
4383
|
+
const start = row.start_line;
|
|
4274
4384
|
const end = Math.min(lines.length, Math.max(row.end_line, row.start_line + 40));
|
|
4275
4385
|
console.log(`# ${node.name} (${node.type}) \u2014 ${node.file}:${start}-${end}`);
|
|
4276
4386
|
console.log(lines.slice(start - 1, end).join("\n"));
|
|
@@ -4490,7 +4600,7 @@ function moduleOf2(file) {
|
|
|
4490
4600
|
const i = file.indexOf("/");
|
|
4491
4601
|
return i === -1 ? "." : file.slice(0, i);
|
|
4492
4602
|
}
|
|
4493
|
-
function classify(ranked, faninOf, threshold = 0.15) {
|
|
4603
|
+
function classify(ranked, faninOf, threshold = 0.15, protectedPatterns) {
|
|
4494
4604
|
const relevant = ranked.filter((r) => r.score >= threshold);
|
|
4495
4605
|
const files = [...new Set(relevant.map((r) => r.file))];
|
|
4496
4606
|
const modules = [...new Set(files.map(moduleOf2))];
|
|
@@ -4504,7 +4614,9 @@ function classify(ranked, faninOf, threshold = 0.15) {
|
|
|
4504
4614
|
"type"
|
|
4505
4615
|
]);
|
|
4506
4616
|
const highFanin = relevant.filter((r) => DEFINITIONAL.has(r.type) && faninOf(r.id) >= FANIN_THRESHOLD).map((r) => r.name);
|
|
4507
|
-
const protectedHits = files.filter(
|
|
4617
|
+
const protectedHits = files.filter(
|
|
4618
|
+
(f) => protectedPatterns ? isProtected(f, protectedPatterns) : isProtected(f)
|
|
4619
|
+
);
|
|
4508
4620
|
let complexity = "SIMPLE";
|
|
4509
4621
|
if (files.length >= 3) {
|
|
4510
4622
|
complexity = "COMPLEX";
|
|
@@ -4747,7 +4859,8 @@ function looksLikeRepoPath(db, repoRoot2, task) {
|
|
|
4747
4859
|
} catch {
|
|
4748
4860
|
}
|
|
4749
4861
|
try {
|
|
4750
|
-
const
|
|
4862
|
+
const esc2 = t.replace(/[\\%_]/g, (c) => "\\" + c);
|
|
4863
|
+
const row = db.prepare(`SELECT 1 AS ok FROM files WHERE path = ? OR path LIKE ? ESCAPE '\\' LIMIT 1`).get(t, `%/${esc2}`);
|
|
4751
4864
|
return !!row;
|
|
4752
4865
|
} catch {
|
|
4753
4866
|
return false;
|
|
@@ -4874,7 +4987,31 @@ function changedFilesVsHead(repoRoot2) {
|
|
|
4874
4987
|
let p = line.slice(3).trim();
|
|
4875
4988
|
const arrow = p.indexOf(" -> ");
|
|
4876
4989
|
if (arrow >= 0) p = p.slice(arrow + 4);
|
|
4877
|
-
if (p.startsWith('"') && p.endsWith('"'))
|
|
4990
|
+
if (p.startsWith('"') && p.endsWith('"')) {
|
|
4991
|
+
const inner = p.slice(1, -1);
|
|
4992
|
+
const bytes = [];
|
|
4993
|
+
for (let i = 0; i < inner.length; i++) {
|
|
4994
|
+
if (inner[i] === "\\") {
|
|
4995
|
+
const next = inner[i + 1];
|
|
4996
|
+
if (next >= "0" && next <= "7") {
|
|
4997
|
+
bytes.push(parseInt(inner.slice(i + 1, i + 4), 8));
|
|
4998
|
+
i += 3;
|
|
4999
|
+
} else if (next === "n") {
|
|
5000
|
+
bytes.push(10);
|
|
5001
|
+
i++;
|
|
5002
|
+
} else if (next === "t") {
|
|
5003
|
+
bytes.push(9);
|
|
5004
|
+
i++;
|
|
5005
|
+
} else {
|
|
5006
|
+
bytes.push(next.charCodeAt(0));
|
|
5007
|
+
i++;
|
|
5008
|
+
}
|
|
5009
|
+
} else {
|
|
5010
|
+
bytes.push(inner.charCodeAt(i));
|
|
5011
|
+
}
|
|
5012
|
+
}
|
|
5013
|
+
p = Buffer.from(bytes).toString("utf8");
|
|
5014
|
+
}
|
|
4878
5015
|
if (p) files.push(p);
|
|
4879
5016
|
}
|
|
4880
5017
|
return files;
|
|
@@ -4924,7 +5061,7 @@ Task sederhana. Gunakan pencarian langsung. Bila istilah domain tak cocok dengan
|
|
|
4924
5061
|
const r = db.prepare(`SELECT COUNT(*) AS n FROM relationships WHERE target_id = ?`).get(id);
|
|
4925
5062
|
return r.n;
|
|
4926
5063
|
};
|
|
4927
|
-
const cls = classify(result.ranked, faninOf);
|
|
5064
|
+
const cls = classify(result.ranked, faninOf, 0.15, loadProtectedPatterns(repoRoot2));
|
|
4928
5065
|
const t = db.prepare(`INSERT INTO tasks(description, complexity) VALUES (?, ?)`).run(task, opts.escalate ? "COMPLEX" : cls.complexity);
|
|
4929
5066
|
const taskId = Number(t.lastInsertRowid);
|
|
4930
5067
|
const insCtx = db.prepare(
|
|
@@ -4937,6 +5074,9 @@ Task sederhana. Gunakan pencarian langsung. Bila istilah domain tak cocok dengan
|
|
|
4937
5074
|
const mode = opts.mode ?? "NORMAL";
|
|
4938
5075
|
const effectiveMode = mode === "NORMAL" && cls.complexity === "SIMPLE" && cls.estimatedFiles <= 1 ? "FAST" : mode;
|
|
4939
5076
|
const escalate = opts.escalate === true || effectiveMode === "SAFE" || effectiveMode === "AUDIT" || mode === "SAFE" || mode === "AUDIT";
|
|
5077
|
+
if (effectiveMode === "FAST") {
|
|
5078
|
+
result.ranked = result.ranked.slice(0, 8);
|
|
5079
|
+
}
|
|
4940
5080
|
const changed = changedFilesVsHead(repoRoot2);
|
|
4941
5081
|
if (changed.length > 0) {
|
|
4942
5082
|
console.log(
|
|
@@ -4980,8 +5120,8 @@ function cmdImpact(repoRoot2, target, opts = {}) {
|
|
|
4980
5120
|
if (seeds.length === 0) {
|
|
4981
5121
|
const rows = db.prepare(
|
|
4982
5122
|
`SELECT s.id, s.name, s.type, f.path AS file FROM symbols s
|
|
4983
|
-
JOIN files f ON f.id = s.file_id WHERE f.path LIKE ? LIMIT 20`
|
|
4984
|
-
).all(`%${target}`);
|
|
5123
|
+
JOIN files f ON f.id = s.file_id WHERE f.path LIKE ? ESCAPE '\\' LIMIT 20`
|
|
5124
|
+
).all(`%${target.replace(/[\\%_]/g, (c) => "\\" + c)}`);
|
|
4985
5125
|
seeds = rows;
|
|
4986
5126
|
}
|
|
4987
5127
|
if (seeds.length === 0) {
|
|
@@ -5188,7 +5328,7 @@ function uninstallTarget(repoRoot2, target) {
|
|
|
5188
5328
|
}
|
|
5189
5329
|
function pruneEmptyDirs(dir, stop) {
|
|
5190
5330
|
let cur = dir;
|
|
5191
|
-
while (cur
|
|
5331
|
+
while (cur !== stop && cur.toLowerCase().startsWith(stop.toLowerCase())) {
|
|
5192
5332
|
try {
|
|
5193
5333
|
fs22.rmdirSync(cur);
|
|
5194
5334
|
} catch {
|
|
@@ -5613,47 +5753,13 @@ function cmdTestSelect(repoRoot2, target) {
|
|
|
5613
5753
|
}
|
|
5614
5754
|
}
|
|
5615
5755
|
|
|
5616
|
-
// src/commands/
|
|
5617
|
-
|
|
5618
|
-
const list = program2.command("agents").description("List supported agent installers");
|
|
5619
|
-
list.command("list").description("Show agent ids").action(() => {
|
|
5620
|
-
console.log(agentIds().join("\n"));
|
|
5621
|
-
});
|
|
5622
|
-
const anti = program2.command("antigravity").description("Antigravity IDE integration");
|
|
5623
|
-
anti.command("install").description("Install Skill + Workflow, print allowlist guide").action(() => {
|
|
5624
|
-
const root = repoRoot2();
|
|
5625
|
-
const prefix = detectPrefix(root);
|
|
5626
|
-
for (const line of installAntigravity(root, prefix)) console.log(`\u2713 ${line}`);
|
|
5627
|
-
console.log("");
|
|
5628
|
-
console.log(allowlistText(root));
|
|
5629
|
-
});
|
|
5630
|
-
anti.command("uninstall").description("Remove Skill + Workflow").action(() => {
|
|
5631
|
-
for (const line of uninstallAntigravity(repoRoot2())) console.log(`\u2713 ${line}`);
|
|
5632
|
-
});
|
|
5633
|
-
for (const t of AGENT_MATRIX) {
|
|
5634
|
-
if (t.id === "agents") continue;
|
|
5635
|
-
const cmd = program2.command(t.id).description(`${t.label} integration`);
|
|
5636
|
-
cmd.command("install").description(`Write ${t.rel}`).action(() => {
|
|
5637
|
-
const root = repoRoot2();
|
|
5638
|
-
const prefix = detectPrefix(root);
|
|
5639
|
-
const r = installTarget(root, t, prefix);
|
|
5640
|
-
console.log(`\u2713 ${t.label}: ${r.rel} (${r.how})`);
|
|
5641
|
-
if (t.id === "opencode") {
|
|
5642
|
-
console.log("");
|
|
5643
|
-
console.log(opencodePermissionGuide(prefix));
|
|
5644
|
-
}
|
|
5645
|
-
});
|
|
5646
|
-
cmd.command("uninstall").description(`Remove ScopeCairn block from ${t.rel}`).action(() => {
|
|
5647
|
-
const r = uninstallTarget(repoRoot2(), t);
|
|
5648
|
-
console.log(`\u2713 ${t.label}: ${r.rel} (${r.how})`);
|
|
5649
|
-
});
|
|
5650
|
-
}
|
|
5651
|
-
}
|
|
5756
|
+
// src/commands/update.ts
|
|
5757
|
+
import { execFileSync as execFileSync3, execSync } from "child_process";
|
|
5652
5758
|
|
|
5653
5759
|
// package.json
|
|
5654
5760
|
var package_default = {
|
|
5655
5761
|
name: "scopecairn",
|
|
5656
|
-
version: "0.
|
|
5762
|
+
version: "0.7.0",
|
|
5657
5763
|
description: "ScopeCairn \u2014 Codebase Intelligence & Agent Optimization Layer (local-first)",
|
|
5658
5764
|
license: "MIT",
|
|
5659
5765
|
type: "module",
|
|
@@ -5687,7 +5793,8 @@ var package_default = {
|
|
|
5687
5793
|
clean: "tsx src/cli.ts clean",
|
|
5688
5794
|
rebuild: "tsx src/cli.ts rebuild",
|
|
5689
5795
|
test: "tsx --test tests/*.test.ts",
|
|
5690
|
-
typecheck: "tsc --noEmit"
|
|
5796
|
+
typecheck: "tsc --noEmit",
|
|
5797
|
+
changelog: "node scripts/changelog.js"
|
|
5691
5798
|
},
|
|
5692
5799
|
dependencies: {
|
|
5693
5800
|
commander: "^12.1.0",
|
|
@@ -5702,6 +5809,85 @@ var package_default = {
|
|
|
5702
5809
|
}
|
|
5703
5810
|
};
|
|
5704
5811
|
|
|
5812
|
+
// src/commands/update.ts
|
|
5813
|
+
function latestVersion() {
|
|
5814
|
+
try {
|
|
5815
|
+
const out = execFileSync3("npm", ["view", "scopecairn", "version"], {
|
|
5816
|
+
encoding: "utf8",
|
|
5817
|
+
timeout: 15e3,
|
|
5818
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
5819
|
+
shell: process.platform === "win32"
|
|
5820
|
+
}).trim();
|
|
5821
|
+
return out || null;
|
|
5822
|
+
} catch {
|
|
5823
|
+
return null;
|
|
5824
|
+
}
|
|
5825
|
+
}
|
|
5826
|
+
function cmdUpdate(opts = {}) {
|
|
5827
|
+
const current = package_default.version;
|
|
5828
|
+
const latest = latestVersion();
|
|
5829
|
+
if (!latest) {
|
|
5830
|
+
console.log("Gagal memeriksa versi terbaru (offline atau belum terpublish).");
|
|
5831
|
+
console.log("Update manual: npm i -g scopecairn@latest");
|
|
5832
|
+
return;
|
|
5833
|
+
}
|
|
5834
|
+
console.log(`Versi terpasang: ${current} | terbaru: ${latest}`);
|
|
5835
|
+
if (latest === current) {
|
|
5836
|
+
console.log("Sudah versi terbaru.");
|
|
5837
|
+
return;
|
|
5838
|
+
}
|
|
5839
|
+
if (opts.check) {
|
|
5840
|
+
console.log("Update tersedia. Jalankan `scopecairn update`.");
|
|
5841
|
+
return;
|
|
5842
|
+
}
|
|
5843
|
+
try {
|
|
5844
|
+
execSync("npm i -g scopecairn@latest", {
|
|
5845
|
+
stdio: "inherit",
|
|
5846
|
+
shell: process.platform === "win32" ? "cmd.exe" : "/bin/sh"
|
|
5847
|
+
});
|
|
5848
|
+
console.log(`\u2713 scopecairn diperbarui ke ${latest}.`);
|
|
5849
|
+
} catch {
|
|
5850
|
+
console.log("Update gagal. Jalankan manual: npm i -g scopecairn@latest");
|
|
5851
|
+
}
|
|
5852
|
+
}
|
|
5853
|
+
|
|
5854
|
+
// src/commands/agents.ts
|
|
5855
|
+
function registerAgentCommands(program2, repoRoot2) {
|
|
5856
|
+
const list = program2.command("agents").description("List supported agent installers");
|
|
5857
|
+
list.command("list").description("Show agent ids").action(() => {
|
|
5858
|
+
console.log(agentIds().join("\n"));
|
|
5859
|
+
});
|
|
5860
|
+
const anti = program2.command("antigravity").description("Antigravity IDE integration");
|
|
5861
|
+
anti.command("install").description("Install Skill + Workflow, print allowlist guide").action(() => {
|
|
5862
|
+
const root = repoRoot2();
|
|
5863
|
+
const prefix = detectPrefix(root);
|
|
5864
|
+
for (const line of installAntigravity(root, prefix)) console.log(`\u2713 ${line}`);
|
|
5865
|
+
console.log("");
|
|
5866
|
+
console.log(allowlistText(root));
|
|
5867
|
+
});
|
|
5868
|
+
anti.command("uninstall").description("Remove Skill + Workflow").action(() => {
|
|
5869
|
+
for (const line of uninstallAntigravity(repoRoot2())) console.log(`\u2713 ${line}`);
|
|
5870
|
+
});
|
|
5871
|
+
for (const t of AGENT_MATRIX) {
|
|
5872
|
+
if (t.id === "agents") continue;
|
|
5873
|
+
const cmd = program2.command(t.id).description(`${t.label} integration`);
|
|
5874
|
+
cmd.command("install").description(`Write ${t.rel}`).action(() => {
|
|
5875
|
+
const root = repoRoot2();
|
|
5876
|
+
const prefix = detectPrefix(root);
|
|
5877
|
+
const r = installTarget(root, t, prefix);
|
|
5878
|
+
console.log(`\u2713 ${t.label}: ${r.rel} (${r.how})`);
|
|
5879
|
+
if (t.id === "opencode") {
|
|
5880
|
+
console.log("");
|
|
5881
|
+
console.log(opencodePermissionGuide(prefix));
|
|
5882
|
+
}
|
|
5883
|
+
});
|
|
5884
|
+
cmd.command("uninstall").description(`Remove ScopeCairn block from ${t.rel}`).action(() => {
|
|
5885
|
+
const r = uninstallTarget(repoRoot2(), t);
|
|
5886
|
+
console.log(`\u2713 ${t.label}: ${r.rel} (${r.how})`);
|
|
5887
|
+
});
|
|
5888
|
+
}
|
|
5889
|
+
}
|
|
5890
|
+
|
|
5705
5891
|
// src/cli.ts
|
|
5706
5892
|
var program = new Command();
|
|
5707
5893
|
program.name("scopecairn").description("ScopeCairn \u2014 local-first codebase intelligence layer").version(package_default.version).addHelpText(
|
|
@@ -5758,6 +5944,7 @@ program.command("init [path]").description("Initial indexing + Skill, Workflow,
|
|
|
5758
5944
|
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(
|
|
5759
5945
|
(opts) => cmdBenchmark(repoRoot(), opts)
|
|
5760
5946
|
);
|
|
5947
|
+
program.command("update").description("Update scopecairn ke versi terbaru dari npm registry").option("--check", "hanya cek versi terbaru tanpa menginstall").action((opts) => cmdUpdate(opts));
|
|
5761
5948
|
registerAgentCommands(program, repoRoot);
|
|
5762
5949
|
program.parseAsync(process.argv);
|
|
5763
5950
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scopecairn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "ScopeCairn — Codebase Intelligence & Agent Optimization Layer (local-first)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"clean": "tsx src/cli.ts clean",
|
|
35
35
|
"rebuild": "tsx src/cli.ts rebuild",
|
|
36
36
|
"test": "tsx --test tests/*.test.ts",
|
|
37
|
-
"typecheck": "tsc --noEmit"
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"changelog": "node scripts/changelog.js"
|
|
38
39
|
},
|
|
39
40
|
"dependencies": {
|
|
40
41
|
"commander": "^12.1.0",
|