nexusmem 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/CHANGELOG.md +32 -1
- package/README.md +19 -6
- package/dist/cli/index.js +295 -10
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,36 @@ built from, matched by publish timestamp: `v0.1.0` → `67a4776`, `v0.1.1` → `
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
No unreleased changes yet.
|
|
13
|
+
|
|
14
|
+
## [0.7.0] — 2026-08-21
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `nexusmem stale --check-contradictions`: for each stale candidate, finds the most similar newer
|
|
19
|
+
node (local embedding search) and asks a local SLM (Ollama, `qwen2.5:3b` by default) whether it
|
|
20
|
+
actually contradicts the older one, instead of only surfacing by age. Suggest-only — nothing is
|
|
21
|
+
written, same as plain `stale`. Live-dogfooded against this repo's own real database and Ollama
|
|
22
|
+
instance; found and fixed a real gap along the way (below) before the feature surfaced anything
|
|
23
|
+
useful.
|
|
24
|
+
- Import graph: bare Python imports with no leading dot (`import foo`, `from foo import bar`)
|
|
25
|
+
now resolve to a same-directory sibling file too, alongside the existing relative-dot support.
|
|
26
|
+
Found dogfooding two real local projects: neither used a single PEP 328 relative import, both
|
|
27
|
+
relied entirely on this flat-script style. Guarded by a static list of stdlib module names so a
|
|
28
|
+
bare `import os`/`import queue`/etc. is never mistaken for a same-named local file.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Import graph: a Java wildcard import (`import a.b.*;`) no longer merges files from two
|
|
33
|
+
unrelated packages that happen to share a directory-name suffix (e.g. two Gradle/Maven modules
|
|
34
|
+
each with their own `.../foo`) — it now refuses to guess, same as the single-class-import case.
|
|
35
|
+
- `stale --check-contradictions`'s neighbor search now looks past same-timestamp sibling nodes
|
|
36
|
+
(e.g. the many chunks one long conversation gets split into) to reach genuinely newer content.
|
|
37
|
+
Found live-dogfooding against this repo's own database: the first real candidate's closest 15
|
|
38
|
+
neighbors were all same-conversation siblings sharing its exact timestamp, so the original
|
|
39
|
+
5-neighbor default silently found zero suggestions for every candidate, regardless of what the
|
|
40
|
+
SLM would have said.
|
|
41
|
+
|
|
12
42
|
## [0.6.0] — 2026-08-20
|
|
13
43
|
|
|
14
44
|
### Added
|
|
@@ -451,7 +481,8 @@ First public release.
|
|
|
451
481
|
there is no local-model summarization pass, and the conversation collector has never been audited
|
|
452
482
|
for the stale-node bug that was found and fixed in the docs collector.
|
|
453
483
|
|
|
454
|
-
[Unreleased]: https://github.com/yaminbkk/NexusMem/compare/v0.
|
|
484
|
+
[Unreleased]: https://github.com/yaminbkk/NexusMem/compare/v0.7.0...HEAD
|
|
485
|
+
[0.7.0]: https://github.com/yaminbkk/NexusMem/compare/v0.6.0...v0.7.0
|
|
455
486
|
[0.6.0]: https://github.com/yaminbkk/NexusMem/compare/v0.5.4...v0.6.0
|
|
456
487
|
[0.5.4]: https://github.com/yaminbkk/NexusMem/compare/v0.5.3...v0.5.4
|
|
457
488
|
[0.5.3]: https://github.com/yaminbkk/NexusMem/compare/v0.5.2...v0.5.3
|
package/README.md
CHANGED
|
@@ -290,10 +290,22 @@ nexusmem mark-stale <oldNodeId> --supersedes <newNodeId>
|
|
|
290
290
|
Links `newNodeId` as the replacement for `oldNodeId`. The ranker down-weights the old node from then
|
|
291
291
|
on (it stays queryable, just usually loses to its replacement) — nothing is deleted, unlike `forget`.
|
|
292
292
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
293
|
+
```bash
|
|
294
|
+
nexusmem stale --check-contradictions
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
For each candidate, finds the most similar newer node (local embedding search) and asks a local SLM
|
|
298
|
+
(Ollama, `qwen2.5:3b` by default) whether it actually contradicts the older one — real content
|
|
299
|
+
comparison, not just age. A match is printed as `likely superseded by <id> <title> -- <reason>`
|
|
300
|
+
under the candidate; nothing is written, same as plain `stale`. Needs Ollama running; costs one
|
|
301
|
+
embedding call and, when a plausible newer node exists, one chat completion per candidate (capped at
|
|
302
|
+
10 by default — pass `-n` to raise it).
|
|
303
|
+
|
|
304
|
+
**What this doesn't do:** it is one small model's yes/no judgment on one older/newer pair, not a
|
|
305
|
+
verified fact — treat a match as a lead to check, not a conclusion. It also only ever compares a
|
|
306
|
+
candidate against nodes *found by embedding similarity*; a contradiction from an unrelated-sounding
|
|
307
|
+
node would never surface. Real contradiction detection (comprehensively, not just for the pair the
|
|
308
|
+
vector search happens to surface) is still an open problem.
|
|
297
309
|
|
|
298
310
|
## Where it breaks
|
|
299
311
|
|
|
@@ -361,8 +373,9 @@ just how old one is) is still an open problem.
|
|
|
361
373
|
## Commands
|
|
362
374
|
|
|
363
375
|
`init`, `sync`, `query <text>`, `status` (add `--share` for a plain-text summary worth pasting
|
|
364
|
-
somewhere), `projects`, `mcp`, `forget <value>`, `stale
|
|
365
|
-
`mark-stale <nodeId> --supersedes <newNodeId>`, and
|
|
376
|
+
somewhere), `projects`, `mcp`, `forget <value>`, `stale` (add `--check-contradictions` for a local-SLM
|
|
377
|
+
content check, see above), `mark-stale <nodeId> --supersedes <newNodeId>`, and
|
|
378
|
+
`hook install|remove|status`.
|
|
366
379
|
|
|
367
380
|
There are also five dry-run previews (`scan-git`, `scan-diff`, `scan-shell`, `scan-docs`,
|
|
368
381
|
`scan-conversation`)
|
package/dist/cli/index.js
CHANGED
|
@@ -4216,6 +4216,8 @@ function extractPhpIncludeSpecifiers(source) {
|
|
|
4216
4216
|
|
|
4217
4217
|
// src/structure/extract-python.ts
|
|
4218
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;
|
|
4219
4221
|
function cleanNames(raw) {
|
|
4220
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));
|
|
4221
4223
|
}
|
|
@@ -4234,6 +4236,16 @@ function extractPythonImportSpecifiers(source) {
|
|
|
4234
4236
|
seen.add(dots + name);
|
|
4235
4237
|
}
|
|
4236
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
|
+
}
|
|
4237
4249
|
return [...seen];
|
|
4238
4250
|
}
|
|
4239
4251
|
|
|
@@ -4314,7 +4326,9 @@ function resolveJavaSpecifier(specifier, trackedPaths) {
|
|
|
4314
4326
|
if (segments.length === 0) return [];
|
|
4315
4327
|
if (isWildcard) {
|
|
4316
4328
|
const dirSuffix = segments.join("/");
|
|
4317
|
-
|
|
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() : [];
|
|
4318
4332
|
}
|
|
4319
4333
|
const fileSuffix = `${segments.join("/")}.java`;
|
|
4320
4334
|
const matches = [...trackedPaths].filter((p) => endsAtSegmentBoundary(p, fileSuffix));
|
|
@@ -4335,11 +4349,182 @@ function resolvePhpSpecifier(fromPath, specifier, trackedPaths) {
|
|
|
4335
4349
|
|
|
4336
4350
|
// src/structure/resolve-python.ts
|
|
4337
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
|
+
]);
|
|
4338
4523
|
function resolvePythonSpecifier(fromPath, specifier, trackedPaths) {
|
|
4339
4524
|
const dotsMatch = specifier.match(/^\.+/);
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
const segments = specifier.slice(level).split(".").filter(Boolean);
|
|
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);
|
|
4343
4528
|
if (segments.length === 0) return null;
|
|
4344
4529
|
let dir = posix5.dirname(fromPath);
|
|
4345
4530
|
for (let i = 1; i < level; i++) {
|
|
@@ -5602,6 +5787,81 @@ ${pc17.bold(String(edges.length))} edge(s) from ${filesScanned} tracked ${TRACKE
|
|
|
5602
5787
|
|
|
5603
5788
|
// src/cli/commands/stale.ts
|
|
5604
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;
|
|
5605
5865
|
async function runStale(opts) {
|
|
5606
5866
|
const { projectId, ws } = await loadContext(opts.cwd);
|
|
5607
5867
|
const out = opts.out ?? ((chunk2) => void process.stdout.write(chunk2));
|
|
@@ -5613,12 +5873,26 @@ async function runStale(opts) {
|
|
|
5613
5873
|
`);
|
|
5614
5874
|
return 0;
|
|
5615
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]));
|
|
5616
5887
|
out(
|
|
5617
5888
|
[
|
|
5618
5889
|
`${pc18.bold(String(candidates.length))} stale candidate(s) -- oldest first, none of these were changed:`,
|
|
5619
|
-
...candidates.map(
|
|
5620
|
-
|
|
5621
|
-
|
|
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
|
+
}),
|
|
5622
5896
|
"",
|
|
5623
5897
|
`run ${pc18.bold("nexusmem mark-stale <id> --supersedes <newId>")} on any that are actually wrong`
|
|
5624
5898
|
].join("\n").concat("\n")
|
|
@@ -5839,8 +6113,19 @@ program.command("mark-stale").description(
|
|
|
5839
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(
|
|
5840
6114
|
(nodeId, options) => guard(() => runMarkStale({ cwd: options.cwd, nodeId, supersedesId: options.supersedes }))()
|
|
5841
6115
|
);
|
|
5842
|
-
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)).
|
|
5843
|
-
|
|
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
|
+
)()
|
|
5844
6129
|
);
|
|
5845
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 }))());
|
|
5846
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(
|
|
@@ -5898,7 +6183,7 @@ program.command("precheck").description("Warn about staged files with unresolved
|
|
|
5898
6183
|
})
|
|
5899
6184
|
)()
|
|
5900
6185
|
);
|
|
5901
|
-
program.command("scan-structure").description("Preview the JS/TS/Python 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 }))());
|
|
5902
6187
|
program.command("mcp").description("Start the MCP server (stdio transport) for Claude Desktop, Cursor, Windsurf, etc.").action(() => guard(() => runMcpServer().then(() => 0))());
|
|
5903
6188
|
program.parseAsync(process.argv).catch((err) => {
|
|
5904
6189
|
const message = err instanceof Error ? err.message : String(err);
|