nexusmem 0.5.4 → 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/CHANGELOG.md +59 -1
- package/README.md +33 -11
- package/dist/cli/index.js +575 -25
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import pc20 from "picocolors";
|
|
6
6
|
|
|
7
7
|
// src/config/workspace.ts
|
|
8
8
|
import { existsSync } from "fs";
|
|
@@ -1164,6 +1164,38 @@ function getSupersededIds(db, projectId) {
|
|
|
1164
1164
|
function setSupersedes(db, newNodeId, staleNodeId) {
|
|
1165
1165
|
db.prepare("UPDATE nodes SET supersedes = ? WHERE id = ?").run(staleNodeId, newNodeId);
|
|
1166
1166
|
}
|
|
1167
|
+
function listStaleCandidates(db, projectId, opts = {}) {
|
|
1168
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
1169
|
+
const minAgeDays = opts.minAgeDays ?? 45;
|
|
1170
|
+
const cutoff = now.getTime() - minAgeDays * 864e5;
|
|
1171
|
+
const rows = db.prepare(
|
|
1172
|
+
`SELECT id, kind, ts, ts_epoch AS tsEpoch, source, title
|
|
1173
|
+
FROM nodes
|
|
1174
|
+
WHERE project_id = @projectId AND provenance = 'inferred' AND ts_epoch < @cutoff
|
|
1175
|
+
AND id NOT IN (SELECT supersedes FROM nodes WHERE project_id = @projectId AND supersedes IS NOT NULL)
|
|
1176
|
+
ORDER BY ts_epoch ASC
|
|
1177
|
+
LIMIT @limit`
|
|
1178
|
+
).all({ projectId, cutoff, limit: opts.limit ?? 50 });
|
|
1179
|
+
return rows.map((r) => ({
|
|
1180
|
+
id: r.id,
|
|
1181
|
+
kind: r.kind,
|
|
1182
|
+
ts: r.ts,
|
|
1183
|
+
source: r.source,
|
|
1184
|
+
title: r.title,
|
|
1185
|
+
ageDays: Math.round((now.getTime() - r.tsEpoch) / 864e5)
|
|
1186
|
+
}));
|
|
1187
|
+
}
|
|
1188
|
+
function countStaleCandidates(db, projectId, opts = {}) {
|
|
1189
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
1190
|
+
const minAgeDays = opts.minAgeDays ?? 45;
|
|
1191
|
+
const cutoff = now.getTime() - minAgeDays * 864e5;
|
|
1192
|
+
const row = db.prepare(
|
|
1193
|
+
`SELECT COUNT(*) AS count FROM nodes
|
|
1194
|
+
WHERE project_id = @projectId AND provenance = 'inferred' AND ts_epoch < @cutoff
|
|
1195
|
+
AND id NOT IN (SELECT supersedes FROM nodes WHERE project_id = @projectId AND supersedes IS NOT NULL)`
|
|
1196
|
+
).get({ projectId, cutoff });
|
|
1197
|
+
return row.count;
|
|
1198
|
+
}
|
|
1167
1199
|
|
|
1168
1200
|
// src/store/links.ts
|
|
1169
1201
|
function linkNodes(db, fromNodeId, toNodeId, relation) {
|
|
@@ -1606,6 +1638,14 @@ var MemoryStore = class _MemoryStore {
|
|
|
1606
1638
|
setSupersedes(newNodeId, staleNodeId) {
|
|
1607
1639
|
setSupersedes(this.db, newNodeId, staleNodeId);
|
|
1608
1640
|
}
|
|
1641
|
+
/** Aging `inferred` nodes nothing supersedes yet -- candidates for `nexusmem mark-stale`, not auto-applied. */
|
|
1642
|
+
listStaleCandidates(projectId, opts = {}) {
|
|
1643
|
+
return listStaleCandidates(this.db, projectId, opts);
|
|
1644
|
+
}
|
|
1645
|
+
/** Same criteria as `listStaleCandidates`, but just the count -- for `status`'s summary line. */
|
|
1646
|
+
countStaleCandidates(projectId, opts = {}) {
|
|
1647
|
+
return countStaleCandidates(this.db, projectId, opts);
|
|
1648
|
+
}
|
|
1609
1649
|
/** Escape hatch for tests and future modules. */
|
|
1610
1650
|
get raw() {
|
|
1611
1651
|
return this.db;
|
|
@@ -2418,6 +2458,7 @@ var SIGNAL_FLOOR = 0.2;
|
|
|
2418
2458
|
var RECENCY_FLOOR = 0.3;
|
|
2419
2459
|
var DEFAULT_HALF_LIFE_DAYS = 30;
|
|
2420
2460
|
var MS_PER_DAY = 864e5;
|
|
2461
|
+
var INFERRED_HALF_LIFE_RATIO = 0.5;
|
|
2421
2462
|
var SUPERSEDED_PENALTY = 0.5;
|
|
2422
2463
|
var MAX_PRIOR_OVERTURN = 2;
|
|
2423
2464
|
var PRIOR_COUNT = 2;
|
|
@@ -2452,13 +2493,15 @@ function ageDaysOf(ts, now) {
|
|
|
2452
2493
|
function rankHits(hits, opts = {}) {
|
|
2453
2494
|
if (hits.length === 0) return [];
|
|
2454
2495
|
const halfLife = opts.halfLifeDays ?? DEFAULT_HALF_LIFE_DAYS;
|
|
2496
|
+
const inferredHalfLife = opts.inferredHalfLifeDays ?? halfLife * INFERRED_HALF_LIFE_RATIO;
|
|
2455
2497
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
2456
2498
|
const relevances = opts.relevanceScores ? normalizeExternalRelevance(hits, opts.relevanceScores) : normalizeRelevance(hits);
|
|
2457
2499
|
const ranked = hits.map((hit, i) => {
|
|
2458
2500
|
const relevance = relevances[i] ?? RELEVANCE_FLOOR;
|
|
2459
2501
|
const signalWeight = SIGNAL_FLOOR + (1 - SIGNAL_FLOOR) * hit.signal;
|
|
2460
2502
|
const ageDays = ageDaysOf(hit.ts, now);
|
|
2461
|
-
const
|
|
2503
|
+
const effectiveHalfLife = hit.provenance === "inferred" ? inferredHalfLife : halfLife;
|
|
2504
|
+
const recencyFactor = RECENCY_FLOOR + (1 - RECENCY_FLOOR) * 2 ** (-ageDays / effectiveHalfLife);
|
|
2462
2505
|
const rawScore = relevance * signalWeight ** SIGNAL_EXPONENT * recencyFactor ** RECENCY_EXPONENT;
|
|
2463
2506
|
const score = opts.supersededIds?.has(hit.id) ? rawScore * SUPERSEDED_PENALTY : rawScore;
|
|
2464
2507
|
return { ...hit, relevance, signalWeight, ageDays, recencyFactor, score };
|
|
@@ -4124,6 +4167,100 @@ function extractImportSpecifiers(source) {
|
|
|
4124
4167
|
return [...seen];
|
|
4125
4168
|
}
|
|
4126
4169
|
|
|
4170
|
+
// src/structure/extract-go.ts
|
|
4171
|
+
var SINGLE_IMPORT = /\bimport\s+(?:[\w.]+\s+)?"([^"]+)"/g;
|
|
4172
|
+
var IMPORT_BLOCK = /\bimport\s*\(([\s\S]*?)\)/g;
|
|
4173
|
+
var BLOCK_LINE = /(?:^|\n)\s*(?:[\w.]+\s+)?"([^"]+)"/g;
|
|
4174
|
+
function extractGoImportSpecifiers(source) {
|
|
4175
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4176
|
+
IMPORT_BLOCK.lastIndex = 0;
|
|
4177
|
+
let block;
|
|
4178
|
+
while ((block = IMPORT_BLOCK.exec(source)) !== null) {
|
|
4179
|
+
BLOCK_LINE.lastIndex = 0;
|
|
4180
|
+
let line;
|
|
4181
|
+
while ((line = BLOCK_LINE.exec(block[1])) !== null) {
|
|
4182
|
+
seen.add(line[1]);
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
SINGLE_IMPORT.lastIndex = 0;
|
|
4186
|
+
let single;
|
|
4187
|
+
while ((single = SINGLE_IMPORT.exec(source)) !== null) {
|
|
4188
|
+
seen.add(single[1]);
|
|
4189
|
+
}
|
|
4190
|
+
return [...seen];
|
|
4191
|
+
}
|
|
4192
|
+
|
|
4193
|
+
// src/structure/extract-java.ts
|
|
4194
|
+
var IMPORT_PATTERN = /\bimport\s+(?!static\b)([\w.]+(?:\.\*)?)\s*;/g;
|
|
4195
|
+
function extractJavaImportSpecifiers(source) {
|
|
4196
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4197
|
+
IMPORT_PATTERN.lastIndex = 0;
|
|
4198
|
+
let match;
|
|
4199
|
+
while ((match = IMPORT_PATTERN.exec(source)) !== null) {
|
|
4200
|
+
seen.add(match[1]);
|
|
4201
|
+
}
|
|
4202
|
+
return [...seen];
|
|
4203
|
+
}
|
|
4204
|
+
|
|
4205
|
+
// src/structure/extract-php.ts
|
|
4206
|
+
var ANCHORED_INCLUDE = /\b(?:require|include)(?:_once)?\b\s*\(?\s*(?:__DIR__|dirname\s*\(\s*__FILE__\s*\))\s*\.\s*['"]([^'"]+)['"]/g;
|
|
4207
|
+
function extractPhpIncludeSpecifiers(source) {
|
|
4208
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4209
|
+
ANCHORED_INCLUDE.lastIndex = 0;
|
|
4210
|
+
let match;
|
|
4211
|
+
while ((match = ANCHORED_INCLUDE.exec(source)) !== null) {
|
|
4212
|
+
seen.add(match[1]);
|
|
4213
|
+
}
|
|
4214
|
+
return [...seen];
|
|
4215
|
+
}
|
|
4216
|
+
|
|
4217
|
+
// src/structure/extract-python.ts
|
|
4218
|
+
var RELATIVE_IMPORT_PATTERN = /\bfrom\s+(\.+)([\w.]*)\s+import\s+([^\n]+)/g;
|
|
4219
|
+
var BARE_FROM_IMPORT_PATTERN = /^[ \t]*from\s+([A-Za-z_]\w*)\s+import\b/gm;
|
|
4220
|
+
var BARE_IMPORT_PATTERN = /^[ \t]*import\s+([^\n]+)/gm;
|
|
4221
|
+
function cleanNames(raw) {
|
|
4222
|
+
return raw.split("#")[0].replace(/[()]/g, "").split(",").map((token) => token.trim().split(/\s+as\s+/)[0].trim()).filter((name) => /^[A-Za-z_]\w*$/.test(name));
|
|
4223
|
+
}
|
|
4224
|
+
function extractPythonImportSpecifiers(source) {
|
|
4225
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4226
|
+
RELATIVE_IMPORT_PATTERN.lastIndex = 0;
|
|
4227
|
+
let match;
|
|
4228
|
+
while ((match = RELATIVE_IMPORT_PATTERN.exec(source)) !== null) {
|
|
4229
|
+
const dots = match[1];
|
|
4230
|
+
const module = match[2];
|
|
4231
|
+
if (module) {
|
|
4232
|
+
seen.add(dots + module);
|
|
4233
|
+
continue;
|
|
4234
|
+
}
|
|
4235
|
+
for (const name of cleanNames(match[3])) {
|
|
4236
|
+
seen.add(dots + name);
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
BARE_FROM_IMPORT_PATTERN.lastIndex = 0;
|
|
4240
|
+
while ((match = BARE_FROM_IMPORT_PATTERN.exec(source)) !== null) {
|
|
4241
|
+
seen.add(match[1]);
|
|
4242
|
+
}
|
|
4243
|
+
BARE_IMPORT_PATTERN.lastIndex = 0;
|
|
4244
|
+
while ((match = BARE_IMPORT_PATTERN.exec(source)) !== null) {
|
|
4245
|
+
for (const name of cleanNames(match[1])) {
|
|
4246
|
+
seen.add(name);
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
return [...seen];
|
|
4250
|
+
}
|
|
4251
|
+
|
|
4252
|
+
// src/structure/extract-rust.ts
|
|
4253
|
+
var MOD_DECLARATION = /\b(?:pub(?:\([^)]*\))?\s+)?mod\s+([A-Za-z_]\w*)\s*;/g;
|
|
4254
|
+
function extractRustModSpecifiers(source) {
|
|
4255
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4256
|
+
MOD_DECLARATION.lastIndex = 0;
|
|
4257
|
+
let match;
|
|
4258
|
+
while ((match = MOD_DECLARATION.exec(source)) !== null) {
|
|
4259
|
+
seen.add(match[1]);
|
|
4260
|
+
}
|
|
4261
|
+
return [...seen];
|
|
4262
|
+
}
|
|
4263
|
+
|
|
4127
4264
|
// src/structure/resolve.ts
|
|
4128
4265
|
import { posix } from "path";
|
|
4129
4266
|
var REWRITE_EXTENSIONS = {
|
|
@@ -4160,12 +4297,291 @@ function resolveSpecifier(fromPath, specifier, trackedPaths) {
|
|
|
4160
4297
|
return null;
|
|
4161
4298
|
}
|
|
4162
4299
|
|
|
4300
|
+
// src/structure/resolve-go.ts
|
|
4301
|
+
import { posix as posix2 } from "path";
|
|
4302
|
+
function parseGoModulePath(goModContent) {
|
|
4303
|
+
const match = goModContent.match(/^module\s+(\S+)/m);
|
|
4304
|
+
return match ? match[1] : null;
|
|
4305
|
+
}
|
|
4306
|
+
function resolveGoSpecifier(modulePath, importPath, trackedPaths) {
|
|
4307
|
+
let relDir;
|
|
4308
|
+
if (importPath === modulePath) {
|
|
4309
|
+
relDir = ".";
|
|
4310
|
+
} else if (importPath.startsWith(`${modulePath}/`)) {
|
|
4311
|
+
relDir = importPath.slice(modulePath.length + 1);
|
|
4312
|
+
} else {
|
|
4313
|
+
return [];
|
|
4314
|
+
}
|
|
4315
|
+
return [...trackedPaths].filter((p) => p.endsWith(".go") && !p.endsWith("_test.go") && posix2.dirname(p) === relDir).sort();
|
|
4316
|
+
}
|
|
4317
|
+
|
|
4318
|
+
// src/structure/resolve-java.ts
|
|
4319
|
+
import { posix as posix3 } from "path";
|
|
4320
|
+
function endsAtSegmentBoundary(path, suffix) {
|
|
4321
|
+
return path === suffix || path.endsWith(`/${suffix}`);
|
|
4322
|
+
}
|
|
4323
|
+
function resolveJavaSpecifier(specifier, trackedPaths) {
|
|
4324
|
+
const isWildcard = specifier.endsWith(".*");
|
|
4325
|
+
const segments = (isWildcard ? specifier.slice(0, -2) : specifier).split(".").filter(Boolean);
|
|
4326
|
+
if (segments.length === 0) return [];
|
|
4327
|
+
if (isWildcard) {
|
|
4328
|
+
const dirSuffix = segments.join("/");
|
|
4329
|
+
const matches2 = [...trackedPaths].filter((p) => p.endsWith(".java") && endsAtSegmentBoundary(posix3.dirname(p), dirSuffix));
|
|
4330
|
+
const dirs = new Set(matches2.map((p) => posix3.dirname(p)));
|
|
4331
|
+
return dirs.size === 1 ? matches2.sort() : [];
|
|
4332
|
+
}
|
|
4333
|
+
const fileSuffix = `${segments.join("/")}.java`;
|
|
4334
|
+
const matches = [...trackedPaths].filter((p) => endsAtSegmentBoundary(p, fileSuffix));
|
|
4335
|
+
return matches.length === 1 ? matches : [];
|
|
4336
|
+
}
|
|
4337
|
+
|
|
4338
|
+
// src/structure/resolve-php.ts
|
|
4339
|
+
import { posix as posix4 } from "path";
|
|
4340
|
+
function resolvePhpSpecifier(fromPath, specifier, trackedPaths) {
|
|
4341
|
+
const fromDir = posix4.dirname(fromPath);
|
|
4342
|
+
const relative2 = specifier.startsWith("/") ? specifier.slice(1) : specifier;
|
|
4343
|
+
const joined = posix4.normalize(posix4.join(fromDir, relative2));
|
|
4344
|
+
if (trackedPaths.has(joined)) return joined;
|
|
4345
|
+
const withExt = joined.endsWith(".php") ? joined : `${joined}.php`;
|
|
4346
|
+
if (trackedPaths.has(withExt)) return withExt;
|
|
4347
|
+
return null;
|
|
4348
|
+
}
|
|
4349
|
+
|
|
4350
|
+
// src/structure/resolve-python.ts
|
|
4351
|
+
import { posix as posix5 } from "path";
|
|
4352
|
+
var STDLIB_MODULES = /* @__PURE__ */ new Set([
|
|
4353
|
+
"__future__",
|
|
4354
|
+
"abc",
|
|
4355
|
+
"argparse",
|
|
4356
|
+
"array",
|
|
4357
|
+
"ast",
|
|
4358
|
+
"asyncio",
|
|
4359
|
+
"base64",
|
|
4360
|
+
"bisect",
|
|
4361
|
+
"builtins",
|
|
4362
|
+
"calendar",
|
|
4363
|
+
"cgi",
|
|
4364
|
+
"cgitb",
|
|
4365
|
+
"cmd",
|
|
4366
|
+
"codecs",
|
|
4367
|
+
"collections",
|
|
4368
|
+
"colorsys",
|
|
4369
|
+
"compileall",
|
|
4370
|
+
"concurrent",
|
|
4371
|
+
"configparser",
|
|
4372
|
+
"contextlib",
|
|
4373
|
+
"contextvars",
|
|
4374
|
+
"copy",
|
|
4375
|
+
"copyreg",
|
|
4376
|
+
"cProfile",
|
|
4377
|
+
"csv",
|
|
4378
|
+
"ctypes",
|
|
4379
|
+
"curses",
|
|
4380
|
+
"dataclasses",
|
|
4381
|
+
"datetime",
|
|
4382
|
+
"dbm",
|
|
4383
|
+
"decimal",
|
|
4384
|
+
"difflib",
|
|
4385
|
+
"dis",
|
|
4386
|
+
"doctest",
|
|
4387
|
+
"email",
|
|
4388
|
+
"encodings",
|
|
4389
|
+
"ensurepip",
|
|
4390
|
+
"enum",
|
|
4391
|
+
"errno",
|
|
4392
|
+
"faulthandler",
|
|
4393
|
+
"fcntl",
|
|
4394
|
+
"filecmp",
|
|
4395
|
+
"fileinput",
|
|
4396
|
+
"fnmatch",
|
|
4397
|
+
"fractions",
|
|
4398
|
+
"ftplib",
|
|
4399
|
+
"functools",
|
|
4400
|
+
"gc",
|
|
4401
|
+
"getopt",
|
|
4402
|
+
"getpass",
|
|
4403
|
+
"gettext",
|
|
4404
|
+
"glob",
|
|
4405
|
+
"graphlib",
|
|
4406
|
+
"grp",
|
|
4407
|
+
"gzip",
|
|
4408
|
+
"hashlib",
|
|
4409
|
+
"heapq",
|
|
4410
|
+
"hmac",
|
|
4411
|
+
"html",
|
|
4412
|
+
"http",
|
|
4413
|
+
"imaplib",
|
|
4414
|
+
"importlib",
|
|
4415
|
+
"inspect",
|
|
4416
|
+
"io",
|
|
4417
|
+
"ipaddress",
|
|
4418
|
+
"itertools",
|
|
4419
|
+
"json",
|
|
4420
|
+
"keyword",
|
|
4421
|
+
"locale",
|
|
4422
|
+
"logging",
|
|
4423
|
+
"lzma",
|
|
4424
|
+
"mailbox",
|
|
4425
|
+
"marshal",
|
|
4426
|
+
"math",
|
|
4427
|
+
"mimetypes",
|
|
4428
|
+
"mmap",
|
|
4429
|
+
"msvcrt",
|
|
4430
|
+
"multiprocessing",
|
|
4431
|
+
"operator",
|
|
4432
|
+
"os",
|
|
4433
|
+
"pathlib",
|
|
4434
|
+
"pdb",
|
|
4435
|
+
"pickle",
|
|
4436
|
+
"pickletools",
|
|
4437
|
+
"pkgutil",
|
|
4438
|
+
"platform",
|
|
4439
|
+
"plistlib",
|
|
4440
|
+
"poplib",
|
|
4441
|
+
"posix",
|
|
4442
|
+
"pprint",
|
|
4443
|
+
"profile",
|
|
4444
|
+
"pstats",
|
|
4445
|
+
"pty",
|
|
4446
|
+
"pwd",
|
|
4447
|
+
"py_compile",
|
|
4448
|
+
"pyclbr",
|
|
4449
|
+
"pydoc",
|
|
4450
|
+
"queue",
|
|
4451
|
+
"quopri",
|
|
4452
|
+
"random",
|
|
4453
|
+
"re",
|
|
4454
|
+
"readline",
|
|
4455
|
+
"reprlib",
|
|
4456
|
+
"resource",
|
|
4457
|
+
"rlcompleter",
|
|
4458
|
+
"runpy",
|
|
4459
|
+
"sched",
|
|
4460
|
+
"secrets",
|
|
4461
|
+
"select",
|
|
4462
|
+
"selectors",
|
|
4463
|
+
"shelve",
|
|
4464
|
+
"shlex",
|
|
4465
|
+
"shutil",
|
|
4466
|
+
"signal",
|
|
4467
|
+
"site",
|
|
4468
|
+
"smtplib",
|
|
4469
|
+
"socket",
|
|
4470
|
+
"socketserver",
|
|
4471
|
+
"sqlite3",
|
|
4472
|
+
"ssl",
|
|
4473
|
+
"stat",
|
|
4474
|
+
"statistics",
|
|
4475
|
+
"string",
|
|
4476
|
+
"stringprep",
|
|
4477
|
+
"struct",
|
|
4478
|
+
"subprocess",
|
|
4479
|
+
"symtable",
|
|
4480
|
+
"sys",
|
|
4481
|
+
"sysconfig",
|
|
4482
|
+
"syslog",
|
|
4483
|
+
"tarfile",
|
|
4484
|
+
"telnetlib",
|
|
4485
|
+
"tempfile",
|
|
4486
|
+
"termios",
|
|
4487
|
+
"test",
|
|
4488
|
+
"textwrap",
|
|
4489
|
+
"threading",
|
|
4490
|
+
"time",
|
|
4491
|
+
"timeit",
|
|
4492
|
+
"tkinter",
|
|
4493
|
+
"token",
|
|
4494
|
+
"tokenize",
|
|
4495
|
+
"tomllib",
|
|
4496
|
+
"trace",
|
|
4497
|
+
"traceback",
|
|
4498
|
+
"tracemalloc",
|
|
4499
|
+
"tty",
|
|
4500
|
+
"turtle",
|
|
4501
|
+
"types",
|
|
4502
|
+
"typing",
|
|
4503
|
+
"unicodedata",
|
|
4504
|
+
"unittest",
|
|
4505
|
+
"urllib",
|
|
4506
|
+
"uuid",
|
|
4507
|
+
"venv",
|
|
4508
|
+
"warnings",
|
|
4509
|
+
"wave",
|
|
4510
|
+
"weakref",
|
|
4511
|
+
"webbrowser",
|
|
4512
|
+
"winreg",
|
|
4513
|
+
"winsound",
|
|
4514
|
+
"wsgiref",
|
|
4515
|
+
"xml",
|
|
4516
|
+
"xmlrpc",
|
|
4517
|
+
"zipapp",
|
|
4518
|
+
"zipfile",
|
|
4519
|
+
"zipimport",
|
|
4520
|
+
"zlib",
|
|
4521
|
+
"zoneinfo"
|
|
4522
|
+
]);
|
|
4523
|
+
function resolvePythonSpecifier(fromPath, specifier, trackedPaths) {
|
|
4524
|
+
const dotsMatch = specifier.match(/^\.+/);
|
|
4525
|
+
const level = dotsMatch ? dotsMatch[0].length : 0;
|
|
4526
|
+
if (level === 0 && STDLIB_MODULES.has(specifier)) return null;
|
|
4527
|
+
const segments = (level === 0 ? specifier : specifier.slice(level)).split(".").filter(Boolean);
|
|
4528
|
+
if (segments.length === 0) return null;
|
|
4529
|
+
let dir = posix5.dirname(fromPath);
|
|
4530
|
+
for (let i = 1; i < level; i++) {
|
|
4531
|
+
dir = posix5.dirname(dir);
|
|
4532
|
+
}
|
|
4533
|
+
const joined = posix5.join(dir, ...segments);
|
|
4534
|
+
const moduleFile = `${joined}.py`;
|
|
4535
|
+
if (trackedPaths.has(moduleFile)) return moduleFile;
|
|
4536
|
+
const packageInit = posix5.join(joined, "__init__.py");
|
|
4537
|
+
if (trackedPaths.has(packageInit)) return packageInit;
|
|
4538
|
+
return null;
|
|
4539
|
+
}
|
|
4540
|
+
|
|
4541
|
+
// src/structure/resolve-rust.ts
|
|
4542
|
+
import { posix as posix6 } from "path";
|
|
4543
|
+
function resolveRustModSpecifier(fromPath, name, trackedPaths) {
|
|
4544
|
+
const dir = posix6.dirname(fromPath);
|
|
4545
|
+
const base = posix6.basename(fromPath, ".rs");
|
|
4546
|
+
const moduleDir = base === "mod" || base === "lib" || base === "main" ? dir : posix6.join(dir, base);
|
|
4547
|
+
const sibling = posix6.join(moduleDir, `${name}.rs`);
|
|
4548
|
+
if (trackedPaths.has(sibling)) return sibling;
|
|
4549
|
+
const nested = posix6.join(moduleDir, name, "mod.rs");
|
|
4550
|
+
if (trackedPaths.has(nested)) return nested;
|
|
4551
|
+
return null;
|
|
4552
|
+
}
|
|
4553
|
+
|
|
4163
4554
|
// src/structure/collect.ts
|
|
4164
|
-
var TRACKED_PATHSPECS = ["*.ts", "*.tsx", "*.js", "*.jsx"];
|
|
4555
|
+
var TRACKED_PATHSPECS = ["*.ts", "*.tsx", "*.js", "*.jsx", "*.py", "*.go", "*.rs", "*.java", "*.php"];
|
|
4556
|
+
function resolveTargets(path, content, trackedPaths, goModulePath) {
|
|
4557
|
+
if (path.endsWith(".py")) {
|
|
4558
|
+
return extractPythonImportSpecifiers(content).map((s) => resolvePythonSpecifier(path, s, trackedPaths)).filter((t) => t !== null);
|
|
4559
|
+
}
|
|
4560
|
+
if (path.endsWith(".rs")) {
|
|
4561
|
+
return extractRustModSpecifiers(content).map((s) => resolveRustModSpecifier(path, s, trackedPaths)).filter((t) => t !== null);
|
|
4562
|
+
}
|
|
4563
|
+
if (path.endsWith(".go")) {
|
|
4564
|
+
if (!goModulePath) return [];
|
|
4565
|
+
return extractGoImportSpecifiers(content).flatMap((imp) => resolveGoSpecifier(goModulePath, imp, trackedPaths));
|
|
4566
|
+
}
|
|
4567
|
+
if (path.endsWith(".java")) {
|
|
4568
|
+
return extractJavaImportSpecifiers(content).flatMap((s) => resolveJavaSpecifier(s, trackedPaths));
|
|
4569
|
+
}
|
|
4570
|
+
if (path.endsWith(".php")) {
|
|
4571
|
+
return extractPhpIncludeSpecifiers(content).map((s) => resolvePhpSpecifier(path, s, trackedPaths)).filter((t) => t !== null);
|
|
4572
|
+
}
|
|
4573
|
+
return extractImportSpecifiers(content).map((s) => resolveSpecifier(path, s, trackedPaths)).filter((t) => t !== null);
|
|
4574
|
+
}
|
|
4165
4575
|
async function collectFileEdges(repoRoot) {
|
|
4166
4576
|
const out = await git(repoRoot, ["ls-files", "--", ...TRACKED_PATHSPECS]);
|
|
4167
4577
|
const paths = out.split("\n").map((line) => line.trim().replace(/\\/g, "/")).filter(Boolean);
|
|
4168
4578
|
const trackedPaths = new Set(paths);
|
|
4579
|
+
let goModulePath = null;
|
|
4580
|
+
try {
|
|
4581
|
+
goModulePath = parseGoModulePath(await readFile10(join8(repoRoot, "go.mod"), "utf8"));
|
|
4582
|
+
} catch {
|
|
4583
|
+
goModulePath = null;
|
|
4584
|
+
}
|
|
4169
4585
|
const edges = [];
|
|
4170
4586
|
const seenEdges = /* @__PURE__ */ new Set();
|
|
4171
4587
|
const unreadable = [];
|
|
@@ -4177,10 +4593,9 @@ async function collectFileEdges(repoRoot) {
|
|
|
4177
4593
|
unreadable.push(path);
|
|
4178
4594
|
continue;
|
|
4179
4595
|
}
|
|
4180
|
-
for (const
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
const key = `${path}\0${target}`;
|
|
4596
|
+
for (const target of resolveTargets(path, content, trackedPaths, goModulePath)) {
|
|
4597
|
+
if (target === path) continue;
|
|
4598
|
+
const key = `${path} ${target}`;
|
|
4184
4599
|
if (seenEdges.has(key)) continue;
|
|
4185
4600
|
seenEdges.add(key);
|
|
4186
4601
|
edges.push({ fromPath: path, toPath: target, kind: "import" });
|
|
@@ -5344,6 +5759,7 @@ function formatNode5(node) {
|
|
|
5344
5759
|
|
|
5345
5760
|
// src/cli/commands/scan-structure.ts
|
|
5346
5761
|
import pc17 from "picocolors";
|
|
5762
|
+
var TRACKED_EXTENSIONS = TRACKED_PATHSPECS.map((p) => p.replace("*", "")).join("/");
|
|
5347
5763
|
async function runScanStructure(opts) {
|
|
5348
5764
|
const repo = await readRepoInfo(opts.cwd);
|
|
5349
5765
|
const { edges, filesScanned, unreadable } = await collectFileEdges(repo.root);
|
|
@@ -5363,16 +5779,134 @@ async function runScanStructure(opts) {
|
|
|
5363
5779
|
}
|
|
5364
5780
|
process.stderr.write(
|
|
5365
5781
|
`
|
|
5366
|
-
${pc17.bold(String(edges.length))} edge(s) from ${filesScanned} tracked
|
|
5782
|
+
${pc17.bold(String(edges.length))} edge(s) from ${filesScanned} tracked ${TRACKED_EXTENSIONS} file(s)
|
|
5367
5783
|
`
|
|
5368
5784
|
);
|
|
5369
5785
|
return 0;
|
|
5370
5786
|
}
|
|
5371
5787
|
|
|
5788
|
+
// src/cli/commands/stale.ts
|
|
5789
|
+
import pc18 from "picocolors";
|
|
5790
|
+
|
|
5791
|
+
// src/slm/contradiction.ts
|
|
5792
|
+
var MAX_BODY_CHARS = 1500;
|
|
5793
|
+
var MAX_REASON_CHARS = 200;
|
|
5794
|
+
var CONTRADICTION_INSTRUCTIONS = `You are checking whether a NEWER memory replaces or contradicts an OLDER one, for an AI coding assistant's memory index.
|
|
5795
|
+
|
|
5796
|
+
Answer in exactly this shape:
|
|
5797
|
+
VERDICT: YES or NO
|
|
5798
|
+
REASON: <one line, under 20 words>
|
|
5799
|
+
|
|
5800
|
+
Say YES only if the NEWER memory states something that makes the OLDER one factually wrong or obsolete -- a decision reversed, a bug fixed, a plan abandoned. Say NO if they are about different things, or the newer one only adds detail without contradicting the older one. When unsure, say NO.`;
|
|
5801
|
+
function buildContradictionPrompt(older, newer) {
|
|
5802
|
+
const body = [
|
|
5803
|
+
`OLDER (${older.title}):`,
|
|
5804
|
+
truncate(older.body, MAX_BODY_CHARS),
|
|
5805
|
+
"",
|
|
5806
|
+
`NEWER (${newer.title}):`,
|
|
5807
|
+
truncate(newer.body, MAX_BODY_CHARS)
|
|
5808
|
+
].join("\n");
|
|
5809
|
+
return `${CONTRADICTION_INSTRUCTIONS}
|
|
5810
|
+
|
|
5811
|
+
---
|
|
5812
|
+
|
|
5813
|
+
${body}
|
|
5814
|
+
|
|
5815
|
+
---
|
|
5816
|
+
|
|
5817
|
+
Answer:`;
|
|
5818
|
+
}
|
|
5819
|
+
function parseContradictionVerdict(raw) {
|
|
5820
|
+
const lines = raw.trim().split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
5821
|
+
const verdictLine = lines.find((l) => /^VERDICT:/i.test(l));
|
|
5822
|
+
if (!verdictLine) return null;
|
|
5823
|
+
const verdict = /^VERDICT:\s*(YES|NO)\b/i.exec(verdictLine);
|
|
5824
|
+
if (!verdict) return null;
|
|
5825
|
+
const reasonLine = lines.find((l) => /^REASON:/i.test(l));
|
|
5826
|
+
const reason = reasonLine ? reasonLine.replace(/^REASON:\s*/i, "").trim() : "";
|
|
5827
|
+
return {
|
|
5828
|
+
contradicts: verdict[1].toUpperCase() === "YES",
|
|
5829
|
+
reason: truncate(reason, MAX_REASON_CHARS)
|
|
5830
|
+
};
|
|
5831
|
+
}
|
|
5832
|
+
|
|
5833
|
+
// src/retrieval/contradiction.ts
|
|
5834
|
+
var DEFAULT_LIMIT = 10;
|
|
5835
|
+
var DEFAULT_NEIGHBOR_LIMIT = 25;
|
|
5836
|
+
async function checkContradictions(store, embeddingProvider, slmProvider, projectId, candidates, opts = {}) {
|
|
5837
|
+
const limit = opts.limit ?? DEFAULT_LIMIT;
|
|
5838
|
+
const neighborLimit = opts.neighborLimit ?? DEFAULT_NEIGHBOR_LIMIT;
|
|
5839
|
+
const suggestions = [];
|
|
5840
|
+
for (const candidate of candidates.slice(0, limit)) {
|
|
5841
|
+
const full = store.getNodesByIds([candidate.id])[0];
|
|
5842
|
+
if (!full) continue;
|
|
5843
|
+
const embedding = await embeddingProvider.embed(`${full.title}
|
|
5844
|
+
${full.body}`);
|
|
5845
|
+
if (!embedding) continue;
|
|
5846
|
+
const candidateEpoch = Date.parse(candidate.ts);
|
|
5847
|
+
const nearest = store.vectorSearch(projectId, embedding, neighborLimit + 1).find((hit) => hit.id !== candidate.id && Date.parse(hit.ts) > candidateEpoch);
|
|
5848
|
+
if (!nearest) continue;
|
|
5849
|
+
const reply = await slmProvider.complete(buildContradictionPrompt(full, nearest));
|
|
5850
|
+
if (!reply) continue;
|
|
5851
|
+
const verdict = parseContradictionVerdict(reply);
|
|
5852
|
+
if (!verdict?.contradicts) continue;
|
|
5853
|
+
suggestions.push({
|
|
5854
|
+
candidateId: candidate.id,
|
|
5855
|
+
againstId: nearest.id,
|
|
5856
|
+
againstTitle: nearest.title,
|
|
5857
|
+
reason: verdict.reason
|
|
5858
|
+
});
|
|
5859
|
+
}
|
|
5860
|
+
return suggestions;
|
|
5861
|
+
}
|
|
5862
|
+
|
|
5863
|
+
// src/cli/commands/stale.ts
|
|
5864
|
+
var STALE_DEFAULT_MODEL = DEFAULT_SLM_MODEL;
|
|
5865
|
+
async function runStale(opts) {
|
|
5866
|
+
const { projectId, ws } = await loadContext(opts.cwd);
|
|
5867
|
+
const out = opts.out ?? ((chunk2) => void process.stdout.write(chunk2));
|
|
5868
|
+
const store = MemoryStore.open(ws.dbPath);
|
|
5869
|
+
try {
|
|
5870
|
+
const candidates = store.listStaleCandidates(projectId, { minAgeDays: opts.minAgeDays, limit: opts.limit });
|
|
5871
|
+
if (candidates.length === 0) {
|
|
5872
|
+
out(`${pc18.dim("no stale candidates")} -- no inferred node older than the threshold lacks a successor
|
|
5873
|
+
`);
|
|
5874
|
+
return 0;
|
|
5875
|
+
}
|
|
5876
|
+
let suggestions = [];
|
|
5877
|
+
if (opts.checkContradictions) {
|
|
5878
|
+
suggestions = await checkContradictions(
|
|
5879
|
+
store,
|
|
5880
|
+
new OllamaEmbeddingProvider(),
|
|
5881
|
+
new OllamaChatProvider({ model: opts.model ?? DEFAULT_SLM_MODEL }),
|
|
5882
|
+
projectId,
|
|
5883
|
+
candidates
|
|
5884
|
+
);
|
|
5885
|
+
}
|
|
5886
|
+
const byCandidateId = new Map(suggestions.map((s) => [s.candidateId, s]));
|
|
5887
|
+
out(
|
|
5888
|
+
[
|
|
5889
|
+
`${pc18.bold(String(candidates.length))} stale candidate(s) -- oldest first, none of these were changed:`,
|
|
5890
|
+
...candidates.map((c) => {
|
|
5891
|
+
const line = ` ${pc18.dim(c.id)} ${pc18.yellow(`${c.ageDays}d old`)} [${c.kind}] ${c.title}`;
|
|
5892
|
+
const hit = byCandidateId.get(c.id);
|
|
5893
|
+
return hit ? `${line}
|
|
5894
|
+
${pc18.red("likely superseded by")} ${pc18.dim(hit.againstId)} ${hit.againstTitle} -- ${hit.reason}` : line;
|
|
5895
|
+
}),
|
|
5896
|
+
"",
|
|
5897
|
+
`run ${pc18.bold("nexusmem mark-stale <id> --supersedes <newId>")} on any that are actually wrong`
|
|
5898
|
+
].join("\n").concat("\n")
|
|
5899
|
+
);
|
|
5900
|
+
return 0;
|
|
5901
|
+
} finally {
|
|
5902
|
+
store.close();
|
|
5903
|
+
}
|
|
5904
|
+
}
|
|
5905
|
+
|
|
5372
5906
|
// src/cli/commands/status.ts
|
|
5373
5907
|
import { basename as basename4 } from "path";
|
|
5374
5908
|
import { statSync } from "fs";
|
|
5375
|
-
import
|
|
5909
|
+
import pc19 from "picocolors";
|
|
5376
5910
|
function daySpan(oldest, newest) {
|
|
5377
5911
|
const oldestDay = Date.parse(oldest.slice(0, 10));
|
|
5378
5912
|
const newestDay = Date.parse(newest.slice(0, 10));
|
|
@@ -5428,34 +5962,36 @@ async function runStatus(opts) {
|
|
|
5428
5962
|
const otherProjectIds = store.listOtherProjectIds(projectId);
|
|
5429
5963
|
const otherProjectNodes = store.countProjectNodes(otherProjectIds);
|
|
5430
5964
|
const structure = store.fileEdgeStats(projectId);
|
|
5965
|
+
const staleCount = store.countStaleCandidates(projectId);
|
|
5431
5966
|
const dbBytes = fileSize(ws.dbPath) + fileSize(`${ws.dbPath}-wal`);
|
|
5432
5967
|
const kinds = Object.entries(stats2.byKind).sort((a, b) => b[1] - a[1]).map(([kind, n]) => ` ${String(n).padStart(6)} ${kind}`);
|
|
5433
|
-
const staleProjectWarning = otherProjectIds.length ? `${
|
|
5968
|
+
const staleProjectWarning = otherProjectIds.length ? `${pc19.yellow("stale ")} ${otherProjectIds.length} prior project ${otherProjectIds.length === 1 ? "identity holds" : "identities hold"} ${otherProjectNodes} node(s) \u2014 run ${pc19.bold(
|
|
5434
5969
|
"nexusmem sync --prune-source <name>"
|
|
5435
5970
|
)} to remove stale source data` : "";
|
|
5436
5971
|
out(
|
|
5437
5972
|
[
|
|
5438
|
-
`${
|
|
5439
|
-
`${
|
|
5440
|
-
`${
|
|
5441
|
-
`${
|
|
5442
|
-
`${
|
|
5973
|
+
`${pc19.dim("repo ")} ${repo.root}`,
|
|
5974
|
+
`${pc19.dim("branch ")} ${repo.branch ?? pc19.yellow("(detached)")}`,
|
|
5975
|
+
`${pc19.dim("project ")} ${pc19.cyan(projectId)}`,
|
|
5976
|
+
`${pc19.dim("schema ")} v${schema}${schema === LATEST_SCHEMA_VERSION ? "" : pc19.yellow(` (latest is v${LATEST_SCHEMA_VERSION})`)}`,
|
|
5977
|
+
`${pc19.dim("database")} ${ws.dbPath} ${pc19.dim(`(${humanBytes(dbBytes)})`)}`,
|
|
5443
5978
|
staleProjectWarning,
|
|
5444
5979
|
"",
|
|
5445
|
-
`${
|
|
5980
|
+
`${pc19.bold(String(stats2.total))} node(s)${stats2.total ? ` ${pc19.dim(`${stats2.oldest?.slice(0, 10)} .. ${stats2.newest?.slice(0, 10)}`)}` : ""}`,
|
|
5446
5981
|
...kinds,
|
|
5447
|
-
stats2.total ? ` ${
|
|
5982
|
+
stats2.total ? ` ${pc19.dim(`${stats2.distinctFiles} distinct file path(s)`)}` : "",
|
|
5448
5983
|
"",
|
|
5449
|
-
sources.length ?
|
|
5984
|
+
sources.length ? pc19.dim("sources") : pc19.yellow("no sources synced yet"),
|
|
5450
5985
|
...sources.map((s) => {
|
|
5451
5986
|
const when = s.lastRunAt ? new Date(s.lastRunAt).toISOString().slice(0, 16).replace("T", " ") : "never";
|
|
5452
5987
|
const cursorLabel = s.source === "git" ? s.cursor?.slice(0, 7) ?? "-" : s.cursor ?? "-";
|
|
5453
|
-
return ` ${s.source.padEnd(14)} ${
|
|
5988
|
+
return ` ${s.source.padEnd(14)} ${pc19.dim(`last run ${when}`)} ${pc19.dim(`cursor ${cursorLabel}`)}`;
|
|
5454
5989
|
}),
|
|
5455
5990
|
"",
|
|
5456
|
-
gitCursor && gitCursor !== repo.head ? `${
|
|
5457
|
-
chains.failuresTotal ? `${
|
|
5458
|
-
structure.edges ? `${
|
|
5991
|
+
gitCursor && gitCursor !== repo.head ? `${pc19.yellow("git behind HEAD")} \u2014 run ${pc19.bold("nexusmem sync")}` : "",
|
|
5992
|
+
chains.failuresTotal ? `${pc19.dim("chains ")} ${pc19.bold(String(chains.resolvedTotal))}/${chains.failuresTotal} failure(s) resolved ${pc19.dim(`(${chains.resolvedByRetry} retry, ${chains.resolvedByDiscussion} discussion)`)}${chains.resolvedTotal < chains.failuresTotal ? ` \u2014 run ${pc19.bold("nexusmem sync --link-failures")} to link more` : ""}` : "",
|
|
5993
|
+
structure.edges ? `${pc19.dim("structure")} ${pc19.bold(String(structure.edges))} import edge(s) across ${structure.files} file(s)` : "",
|
|
5994
|
+
staleCount ? `${pc19.dim("aging ")} ${pc19.bold(String(staleCount))} inferred node(s) worth a look \u2014 run ${pc19.bold("nexusmem stale")}` : ""
|
|
5459
5995
|
].filter((line) => line !== "").join("\n").concat("\n")
|
|
5460
5996
|
);
|
|
5461
5997
|
return 0;
|
|
@@ -5478,7 +6014,7 @@ function guard(run) {
|
|
|
5478
6014
|
process.exitCode = await run();
|
|
5479
6015
|
} catch (err) {
|
|
5480
6016
|
if (isExpected(err)) {
|
|
5481
|
-
process.stderr.write(`${
|
|
6017
|
+
process.stderr.write(`${pc20.red("error")} ${err.message}
|
|
5482
6018
|
`);
|
|
5483
6019
|
process.exitCode = 1;
|
|
5484
6020
|
return;
|
|
@@ -5577,6 +6113,20 @@ program.command("mark-stale").description(
|
|
|
5577
6113
|
).argument("<nodeId>", "id of the node to mark stale").requiredOption("--supersedes <newNodeId>", "id of the node that supersedes it").option("-C, --cwd <path>", "repository path", process.cwd()).action(
|
|
5578
6114
|
(nodeId, options) => guard(() => runMarkStale({ cwd: options.cwd, nodeId, supersedesId: options.supersedes }))()
|
|
5579
6115
|
);
|
|
6116
|
+
program.command("stale").description("List inferred nodes old enough to be worth double-checking (writes nothing)").option("-C, --cwd <path>", "repository path", process.cwd()).option("--min-age-days <days>", "only nodes at least this old", (v) => Number.parseFloat(v)).option("-n, --limit <count>", "stop after N candidates", (v) => Number.parseInt(v, 10)).option(
|
|
6117
|
+
"--check-contradictions",
|
|
6118
|
+
"ask the local SLM whether a similar newer node actually contradicts each candidate (needs Ollama)"
|
|
6119
|
+
).option("--model <name>", "Ollama chat model for --check-contradictions", STALE_DEFAULT_MODEL).action(
|
|
6120
|
+
(options) => guard(
|
|
6121
|
+
() => runStale({
|
|
6122
|
+
cwd: options.cwd,
|
|
6123
|
+
minAgeDays: options.minAgeDays,
|
|
6124
|
+
limit: options.limit,
|
|
6125
|
+
checkContradictions: options.checkContradictions,
|
|
6126
|
+
model: options.model
|
|
6127
|
+
})
|
|
6128
|
+
)()
|
|
6129
|
+
);
|
|
5580
6130
|
program.command("projects").description("List the repositories `query --all-projects` would search").option("--prune", "forget registered projects whose database is no longer on disk", false).option("--json", "emit the registry as JSON on stdout", false).action((options) => guard(() => runProjects({ prune: options.prune, json: options.json }))());
|
|
5581
6131
|
program.command("scan-git").description("Preview the MemoryNodes git history would produce (writes nothing)").option("-C, --cwd <path>", "repository path", process.cwd()).option("--since <date>", "only commits newer than this git date expression, e.g. 90.days.ago").option("-n, --limit <count>", "stop after N commits", (v) => Number.parseInt(v, 10)).option("--no-merges", "skip merge commits").option("--min-signal <score>", "drop nodes below this signal", (v) => Number.parseFloat(v), 0).option("--json", "emit MemoryNodes as JSON on stdout", false).action(
|
|
5582
6132
|
(options) => guard(
|
|
@@ -5633,11 +6183,11 @@ program.command("precheck").description("Warn about staged files with unresolved
|
|
|
5633
6183
|
})
|
|
5634
6184
|
)()
|
|
5635
6185
|
);
|
|
5636
|
-
program.command("scan-structure").description("Preview the JS/TS import-graph edges a sync would produce (writes nothing)").option("-C, --cwd <path>", "repository path", process.cwd()).option("--json", "emit edges as JSON on stdout", false).action((options) => guard(() => runScanStructure({ cwd: options.cwd, json: options.json }))());
|
|
6186
|
+
program.command("scan-structure").description("Preview the JS/TS/Python/Go/Rust/Java/PHP import-graph edges a sync would produce (writes nothing)").option("-C, --cwd <path>", "repository path", process.cwd()).option("--json", "emit edges as JSON on stdout", false).action((options) => guard(() => runScanStructure({ cwd: options.cwd, json: options.json }))());
|
|
5637
6187
|
program.command("mcp").description("Start the MCP server (stdio transport) for Claude Desktop, Cursor, Windsurf, etc.").action(() => guard(() => runMcpServer().then(() => 0))());
|
|
5638
6188
|
program.parseAsync(process.argv).catch((err) => {
|
|
5639
6189
|
const message = err instanceof Error ? err.message : String(err);
|
|
5640
|
-
process.stderr.write(`${
|
|
6190
|
+
process.stderr.write(`${pc20.red("error")} ${message}
|
|
5641
6191
|
`);
|
|
5642
6192
|
process.exitCode = 1;
|
|
5643
6193
|
});
|