nexusmem 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -1
- package/README.md +12 -2
- package/dist/cli/index.js +178 -37
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,21 @@ built from, matched by publish timestamp: `v0.1.0` → `67a4776`, `v0.1.1` → `
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.5.1] — 2026-08-17
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`nexusmem forget --export <path>` / `forget --import <path>`: carry a deny-list across a clone or
|
|
17
|
+
restore.** `.nexusmem/` is gitignored by design, so `deny_list` never traveled with `git clone`/
|
|
18
|
+
`git push` — confirmed live 2026-08-17 that a fresh clone of the exact same repo resurrected a value
|
|
19
|
+
already forgotten elsewhere, with zero deny-list protection, because git history (what a fresh `sync`
|
|
20
|
+
re-derives from) is fully portable while the deny-list that would have blocked it was not. `--export`
|
|
21
|
+
writes the active entries to a plaintext JSON file (loudly warned as exactly as sensitive as the
|
|
22
|
+
values it holds — never meant for git, moved through whatever secure channel the user already
|
|
23
|
+
trusts); `--import` re-applies each new entry through `forget` itself, so an imported value is
|
|
24
|
+
deleted from the new checkout's nodes too, not just blocked going forward. Same dry-run-by-default /
|
|
25
|
+
`--yes` convention as the rest of `forget`. See `docs/forget-mechanism.md`.
|
|
26
|
+
|
|
12
27
|
## [0.5.0] — 2026-08-17
|
|
13
28
|
|
|
14
29
|
### Added
|
|
@@ -337,7 +352,8 @@ First public release.
|
|
|
337
352
|
there is no local-model summarization pass, and the conversation collector has never been audited
|
|
338
353
|
for the stale-node bug that was found and fixed in the docs collector.
|
|
339
354
|
|
|
340
|
-
[Unreleased]: https://github.com/yaminbkk/NexusMem/compare/v0.5.
|
|
355
|
+
[Unreleased]: https://github.com/yaminbkk/NexusMem/compare/v0.5.1...HEAD
|
|
356
|
+
[0.5.1]: https://github.com/yaminbkk/NexusMem/compare/v0.5.0...v0.5.1
|
|
341
357
|
[0.5.0]: https://github.com/yaminbkk/NexusMem/compare/v0.4.0...v0.5.0
|
|
342
358
|
[0.4.0]: https://github.com/yaminbkk/NexusMem/compare/v0.3.3...v0.4.0
|
|
343
359
|
[0.3.3]: https://github.com/yaminbkk/NexusMem/compare/v0.3.2...v0.3.3
|
package/README.md
CHANGED
|
@@ -322,6 +322,15 @@ optimizing, and it is somebody else's process.
|
|
|
322
322
|
ran against (plus that repo's stale prior identities, same scope `--prune-source` already uses). A
|
|
323
323
|
value that leaked into shell history from several repositories needs `forget` run once per repo —
|
|
324
324
|
there is no shared, machine-wide deny-list across every project you have synced.
|
|
325
|
+
- **A deny-list doesn't survive a clone or restore on its own.** `.nexusmem/` is gitignored by design,
|
|
326
|
+
so `deny_list` never travels with `git clone`/`git push` — while git history itself, the thing a
|
|
327
|
+
fresh `sync` re-derives from, is fully portable and copied by every clone. A teammate's fresh
|
|
328
|
+
checkout, a new machine, or a restored backup starts with zero protection: the forgotten value comes
|
|
329
|
+
right back on the first sync. Confirmed live 2026-08-17, not just a theoretical read of the code.
|
|
330
|
+
`forget --export <path>` / `forget --import <path>` close this: export writes the active entries to
|
|
331
|
+
a plaintext JSON file you move through a channel you control (never git — the file is exactly as
|
|
332
|
+
sensitive as the value it holds), and import re-applies them in the new checkout, deleting any
|
|
333
|
+
copies that already synced back in. It is deliberately manual, not automatic on every `sync`.
|
|
325
334
|
|
|
326
335
|
## Commands
|
|
327
336
|
|
|
@@ -344,8 +353,9 @@ is the finer-grained complement — it deletes every node matching one exact str
|
|
|
344
353
|
pattern) *and* writes a standing deny-list entry so the value can never be re-ingested, even by a
|
|
345
354
|
later `sync --rebuild` re-reading the append-only shell-hook log or a full transcript scan. Every
|
|
346
355
|
removal leaves a hash-only tombstone, never the forgotten content itself. Both are dry-run by
|
|
347
|
-
default; `--yes` confirms.
|
|
348
|
-
|
|
356
|
+
default; `--yes` confirms. `forget --list` shows active entries; `forget --export <path>` /
|
|
357
|
+
`forget --import <path>` carry them to another checkout of the same repo (see the limitation above).
|
|
358
|
+
See [`docs/forget-mechanism.md`](docs/forget-mechanism.md) for why this exists.
|
|
349
359
|
|
|
350
360
|
## Recall across projects
|
|
351
361
|
|
package/dist/cli/index.js
CHANGED
|
@@ -726,6 +726,12 @@ function insertDenyListEntry(db, input) {
|
|
|
726
726
|
createdAt
|
|
727
727
|
};
|
|
728
728
|
}
|
|
729
|
+
function denyListEntryExists(db, projectId, input) {
|
|
730
|
+
const row = db.prepare(
|
|
731
|
+
`SELECT 1 FROM deny_list WHERE project_id = ? AND match_type = ? AND pattern = ? AND ignore_case = ? LIMIT 1`
|
|
732
|
+
).get(projectId, input.matchType, input.pattern, input.ignoreCase ? 1 : 0);
|
|
733
|
+
return row !== void 0;
|
|
734
|
+
}
|
|
729
735
|
function matchableText(node) {
|
|
730
736
|
return `${node.title}
|
|
731
737
|
${node.body}
|
|
@@ -748,7 +754,9 @@ function firstMatchingEntry(entries, node) {
|
|
|
748
754
|
}
|
|
749
755
|
|
|
750
756
|
// src/cli/commands/forget.ts
|
|
757
|
+
import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
751
758
|
import pc from "picocolors";
|
|
759
|
+
import { z as z2 } from "zod";
|
|
752
760
|
|
|
753
761
|
// src/store/store.ts
|
|
754
762
|
import Database from "better-sqlite3";
|
|
@@ -1348,6 +1356,59 @@ var MemoryStore = class _MemoryStore {
|
|
|
1348
1356
|
listDenyList(projectId) {
|
|
1349
1357
|
return listDenyListEntries(this.db, projectId);
|
|
1350
1358
|
}
|
|
1359
|
+
/**
|
|
1360
|
+
* What `importDenyList` with these same entries would do, without writing
|
|
1361
|
+
* anything -- `forget --import`'s dry-run default, same convention as
|
|
1362
|
+
* `previewForget`.
|
|
1363
|
+
*/
|
|
1364
|
+
previewImportDenyList(projectId, otherProjectIds, entries) {
|
|
1365
|
+
return entries.map((input) => {
|
|
1366
|
+
validatePattern(input);
|
|
1367
|
+
if (denyListEntryExists(this.db, projectId, input)) {
|
|
1368
|
+
return { matchType: input.matchType, pattern: input.pattern, alreadyPresent: true, wouldRemove: 0 };
|
|
1369
|
+
}
|
|
1370
|
+
const preview = this.previewForget(projectId, otherProjectIds, input);
|
|
1371
|
+
return {
|
|
1372
|
+
matchType: input.matchType,
|
|
1373
|
+
pattern: input.pattern,
|
|
1374
|
+
alreadyPresent: false,
|
|
1375
|
+
wouldRemove: preview.reduce((sum, p) => sum + p.count, 0)
|
|
1376
|
+
};
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Re-apply a previously-exported deny-list against this project.
|
|
1381
|
+
*
|
|
1382
|
+
* This is the fix for `forget`'s per-checkout gap: `deny_list` lives in
|
|
1383
|
+
* `.nexusmem/memory.db`, which is gitignored and never travels with `git
|
|
1384
|
+
* clone`/`git push`, while the things a fresh `sync` re-derives from --
|
|
1385
|
+
* git history and the user-home shell-hook log -- both travel or persist
|
|
1386
|
+
* independently of any one checkout. A fresh clone or a restored backup
|
|
1387
|
+
* starts with an empty deny_list and no memory of what was forgotten. See
|
|
1388
|
+
* docs/forget-mechanism.md.
|
|
1389
|
+
*
|
|
1390
|
+
* Entries already active (same matchType+pattern+ignoreCase) are left
|
|
1391
|
+
* untouched. Every new one goes through `forget` itself, so an imported
|
|
1392
|
+
* value is deleted from this checkout's nodes too, not just blocked going
|
|
1393
|
+
* forward -- exactly what running `nexusmem forget <value>` fresh in this
|
|
1394
|
+
* checkout would have done.
|
|
1395
|
+
*/
|
|
1396
|
+
importDenyList(projectId, otherProjectIds, entries) {
|
|
1397
|
+
let imported = 0;
|
|
1398
|
+
let skipped = 0;
|
|
1399
|
+
let removedNodes = 0;
|
|
1400
|
+
for (const input of entries) {
|
|
1401
|
+
validatePattern(input);
|
|
1402
|
+
if (denyListEntryExists(this.db, projectId, input)) {
|
|
1403
|
+
skipped += 1;
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
const result = this.forget(projectId, otherProjectIds, input);
|
|
1407
|
+
imported += 1;
|
|
1408
|
+
removedNodes += result.removed;
|
|
1409
|
+
}
|
|
1410
|
+
return { imported, skipped, removedNodes };
|
|
1411
|
+
}
|
|
1351
1412
|
/**
|
|
1352
1413
|
* Replace this project's entire `file_edges` snapshot in one transaction.
|
|
1353
1414
|
*
|
|
@@ -1509,6 +1570,17 @@ async function loadContext(cwd) {
|
|
|
1509
1570
|
}
|
|
1510
1571
|
|
|
1511
1572
|
// src/cli/commands/forget.ts
|
|
1573
|
+
var ExportPayloadSchema = z2.object({
|
|
1574
|
+
version: z2.literal(1),
|
|
1575
|
+
entries: z2.array(
|
|
1576
|
+
z2.object({
|
|
1577
|
+
matchType: z2.enum(["literal", "regex"]),
|
|
1578
|
+
pattern: z2.string(),
|
|
1579
|
+
ignoreCase: z2.boolean(),
|
|
1580
|
+
reason: z2.string().nullable()
|
|
1581
|
+
})
|
|
1582
|
+
)
|
|
1583
|
+
});
|
|
1512
1584
|
async function runForget(opts) {
|
|
1513
1585
|
const { ws, projectId } = await loadContext(opts.cwd);
|
|
1514
1586
|
const out = opts.out ?? ((chunk2) => void process.stdout.write(chunk2));
|
|
@@ -1532,8 +1604,75 @@ async function runForget(opts) {
|
|
|
1532
1604
|
);
|
|
1533
1605
|
return 0;
|
|
1534
1606
|
}
|
|
1607
|
+
if (opts.export) {
|
|
1608
|
+
const entries = store.listDenyList(projectId);
|
|
1609
|
+
const payload = {
|
|
1610
|
+
version: 1,
|
|
1611
|
+
entries: entries.map((e) => ({ matchType: e.matchType, pattern: e.pattern, ignoreCase: e.ignoreCase, reason: e.reason }))
|
|
1612
|
+
};
|
|
1613
|
+
await writeFile4(opts.export, `${JSON.stringify(payload, null, 2)}
|
|
1614
|
+
`, "utf8");
|
|
1615
|
+
out(
|
|
1616
|
+
[
|
|
1617
|
+
`${pc.green("exported")} ${entries.length} deny-list entrie(s) to ${opts.export}`,
|
|
1618
|
+
pc.yellow(
|
|
1619
|
+
"this file contains the raw forgotten value(s) in plaintext -- store it somewhere secure (password manager, encrypted note) and never commit it to git or share it publicly."
|
|
1620
|
+
),
|
|
1621
|
+
""
|
|
1622
|
+
].join("\n")
|
|
1623
|
+
);
|
|
1624
|
+
return 0;
|
|
1625
|
+
}
|
|
1626
|
+
if (opts.import) {
|
|
1627
|
+
let raw;
|
|
1628
|
+
try {
|
|
1629
|
+
raw = await readFile4(opts.import, "utf8");
|
|
1630
|
+
} catch (err) {
|
|
1631
|
+
throw new DenyListError(`could not read ${opts.import}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1632
|
+
}
|
|
1633
|
+
let parsed;
|
|
1634
|
+
try {
|
|
1635
|
+
parsed = JSON.parse(raw);
|
|
1636
|
+
} catch (err) {
|
|
1637
|
+
throw new DenyListError(`${opts.import} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
1638
|
+
}
|
|
1639
|
+
const validated = ExportPayloadSchema.safeParse(parsed);
|
|
1640
|
+
if (!validated.success) {
|
|
1641
|
+
throw new DenyListError(
|
|
1642
|
+
`${opts.import} is not a valid deny-list export: ${validated.error.issues.map((i) => i.message).join("; ")}`
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
const entries = validated.data.entries;
|
|
1646
|
+
const otherProjectIds2 = store.listOtherProjectIds(projectId);
|
|
1647
|
+
if (!opts.yes) {
|
|
1648
|
+
const preview = store.previewImportDenyList(projectId, otherProjectIds2, entries);
|
|
1649
|
+
const toImport = preview.filter((p) => !p.alreadyPresent);
|
|
1650
|
+
const totalRemove = toImport.reduce((sum, p) => sum + p.wouldRemove, 0);
|
|
1651
|
+
if (toImport.length === 0) {
|
|
1652
|
+
out(`${pc.dim("forget --import")} all ${preview.length} entrie(s) in ${opts.import} are already active -- nothing to do
|
|
1653
|
+
`);
|
|
1654
|
+
return 0;
|
|
1655
|
+
}
|
|
1656
|
+
const describe = (p) => ` ${p.matchType === "regex" ? pc.dim("/") + p.pattern + pc.dim("/") : JSON.stringify(p.pattern)}: ${p.wouldRemove} node(s)`;
|
|
1657
|
+
out(
|
|
1658
|
+
[
|
|
1659
|
+
`${pc.yellow("would import")} ${toImport.length} new deny-list entrie(s)${preview.length > toImport.length ? ` (${preview.length - toImport.length} already active)` : ""}, removing ${totalRemove} node(s):`,
|
|
1660
|
+
...toImport.map(describe),
|
|
1661
|
+
pc.dim("re-run with --yes to permanently deny-list these value(s) and delete matching node(s) -- this cannot be undone"),
|
|
1662
|
+
""
|
|
1663
|
+
].join("\n")
|
|
1664
|
+
);
|
|
1665
|
+
return 0;
|
|
1666
|
+
}
|
|
1667
|
+
const result2 = store.importDenyList(projectId, otherProjectIds2, entries);
|
|
1668
|
+
out(
|
|
1669
|
+
`${pc.green("imported")} ${result2.imported} deny-list entrie(s)${result2.skipped > 0 ? ` (${result2.skipped} already active)` : ""}, ${result2.removedNodes} node(s) deleted
|
|
1670
|
+
`
|
|
1671
|
+
);
|
|
1672
|
+
return 0;
|
|
1673
|
+
}
|
|
1535
1674
|
if (!opts.value) {
|
|
1536
|
-
throw new DenyListError("a value is required (or pass --list
|
|
1675
|
+
throw new DenyListError("a value is required (or pass --list/--export/--import)");
|
|
1537
1676
|
}
|
|
1538
1677
|
const input = {
|
|
1539
1678
|
matchType: opts.regex ? "regex" : "literal",
|
|
@@ -1663,20 +1802,20 @@ import pc4 from "picocolors";
|
|
|
1663
1802
|
|
|
1664
1803
|
// src/config/registry.ts
|
|
1665
1804
|
import { existsSync as existsSync2 } from "fs";
|
|
1666
|
-
import { mkdir as mkdir4, readFile as
|
|
1805
|
+
import { mkdir as mkdir4, readFile as readFile5, rename, writeFile as writeFile5 } from "fs/promises";
|
|
1667
1806
|
import { join as join5 } from "path";
|
|
1668
|
-
import { z as
|
|
1669
|
-
var ENTRY_SCHEMA =
|
|
1670
|
-
projectId:
|
|
1671
|
-
root:
|
|
1672
|
-
dbPath:
|
|
1673
|
-
originUrl:
|
|
1807
|
+
import { z as z3 } from "zod";
|
|
1808
|
+
var ENTRY_SCHEMA = z3.object({
|
|
1809
|
+
projectId: z3.string().min(1),
|
|
1810
|
+
root: z3.string().min(1),
|
|
1811
|
+
dbPath: z3.string().min(1),
|
|
1812
|
+
originUrl: z3.string().nullable().default(null),
|
|
1674
1813
|
/** Epoch ms of the last `init`/`sync` that recorded this entry. */
|
|
1675
|
-
lastSeenAt:
|
|
1814
|
+
lastSeenAt: z3.number().int().nonnegative()
|
|
1676
1815
|
});
|
|
1677
|
-
var REGISTRY_SCHEMA =
|
|
1678
|
-
version:
|
|
1679
|
-
projects:
|
|
1816
|
+
var REGISTRY_SCHEMA = z3.object({
|
|
1817
|
+
version: z3.literal(1),
|
|
1818
|
+
projects: z3.array(ENTRY_SCHEMA).default([])
|
|
1680
1819
|
});
|
|
1681
1820
|
function registryPath() {
|
|
1682
1821
|
return join5(globalWorkspaceDir(), "projects.json");
|
|
@@ -1684,7 +1823,7 @@ function registryPath() {
|
|
|
1684
1823
|
async function readRegistry() {
|
|
1685
1824
|
let raw;
|
|
1686
1825
|
try {
|
|
1687
|
-
raw = await
|
|
1826
|
+
raw = await readFile5(registryPath(), "utf8");
|
|
1688
1827
|
} catch {
|
|
1689
1828
|
return [];
|
|
1690
1829
|
}
|
|
@@ -1724,7 +1863,7 @@ async function writeRegistry(projects) {
|
|
|
1724
1863
|
const path = registryPath();
|
|
1725
1864
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
1726
1865
|
await mkdir4(globalWorkspaceDir(), { recursive: true });
|
|
1727
|
-
await
|
|
1866
|
+
await writeFile5(tmp, `${JSON.stringify({ version: 1, projects }, null, 2)}
|
|
1728
1867
|
`, "utf8");
|
|
1729
1868
|
await rename(tmp, path);
|
|
1730
1869
|
}
|
|
@@ -1849,7 +1988,7 @@ ${pc5.yellow(`${missing.length} registered project(s) have no database on disk`)
|
|
|
1849
1988
|
// src/mcp/server.ts
|
|
1850
1989
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1851
1990
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1852
|
-
import { z as
|
|
1991
|
+
import { z as z4 } from "zod";
|
|
1853
1992
|
|
|
1854
1993
|
// src/mcp/tools.ts
|
|
1855
1994
|
import { basename as basename3 } from "path";
|
|
@@ -3378,7 +3517,7 @@ function collectShellHistory(entries, projectId, opts = {}) {
|
|
|
3378
3517
|
}
|
|
3379
3518
|
|
|
3380
3519
|
// src/conversation/claude-code-reader.ts
|
|
3381
|
-
import { readFile as
|
|
3520
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
3382
3521
|
import { basename as basename2 } from "path";
|
|
3383
3522
|
|
|
3384
3523
|
// src/conversation/paths.ts
|
|
@@ -3471,14 +3610,14 @@ async function collectClaudeCodeTranscripts(repoRoot) {
|
|
|
3471
3610
|
const files = await listTranscriptFiles(repoRoot);
|
|
3472
3611
|
const turns = [];
|
|
3473
3612
|
for (const file of files) {
|
|
3474
|
-
const raw = await
|
|
3613
|
+
const raw = await readFile6(file, "utf8");
|
|
3475
3614
|
turns.push(...parseClaudeCodeTranscript(raw, { sessionId: basename2(file, ".jsonl") }));
|
|
3476
3615
|
}
|
|
3477
3616
|
return turns;
|
|
3478
3617
|
}
|
|
3479
3618
|
|
|
3480
3619
|
// src/docs/read.ts
|
|
3481
|
-
import { readFile as
|
|
3620
|
+
import { readFile as readFile7, stat } from "fs/promises";
|
|
3482
3621
|
import { join as join7 } from "path";
|
|
3483
3622
|
var DEFAULT_PATHSPECS = ["*.md"];
|
|
3484
3623
|
async function listDocFiles(repoRoot, opts = {}) {
|
|
@@ -3496,7 +3635,7 @@ async function readDocFiles(repoRoot, opts = {}) {
|
|
|
3496
3635
|
let content;
|
|
3497
3636
|
let mtime;
|
|
3498
3637
|
try {
|
|
3499
|
-
[content, { mtime }] = await Promise.all([
|
|
3638
|
+
[content, { mtime }] = await Promise.all([readFile7(absPath, "utf8"), stat(absPath)]);
|
|
3500
3639
|
} catch {
|
|
3501
3640
|
unreadable.push(path);
|
|
3502
3641
|
continue;
|
|
@@ -3508,10 +3647,10 @@ async function readDocFiles(repoRoot, opts = {}) {
|
|
|
3508
3647
|
|
|
3509
3648
|
// src/shell/detect.ts
|
|
3510
3649
|
import { existsSync as existsSync4 } from "fs";
|
|
3511
|
-
import { readFile as
|
|
3650
|
+
import { readFile as readFile9, stat as stat2 } from "fs/promises";
|
|
3512
3651
|
|
|
3513
3652
|
// src/shell/hook-log.ts
|
|
3514
|
-
import { appendFile, mkdir as mkdir5, readFile as
|
|
3653
|
+
import { appendFile, mkdir as mkdir5, readFile as readFile8 } from "fs/promises";
|
|
3515
3654
|
import { dirname as dirname4 } from "path";
|
|
3516
3655
|
function parseHookLogLine(line) {
|
|
3517
3656
|
const trimmed = line.trim();
|
|
@@ -3536,7 +3675,7 @@ function parseHookLogLine(line) {
|
|
|
3536
3675
|
async function readHookLog(path, fromLine) {
|
|
3537
3676
|
let raw;
|
|
3538
3677
|
try {
|
|
3539
|
-
raw = await
|
|
3678
|
+
raw = await readFile8(path, "utf8");
|
|
3540
3679
|
} catch {
|
|
3541
3680
|
return { entries: [], totalLines: fromLine };
|
|
3542
3681
|
}
|
|
@@ -3670,7 +3809,7 @@ function hookEntryToRaw(e) {
|
|
|
3670
3809
|
}
|
|
3671
3810
|
async function tryReadScrapeSource(path, parse, tailLines) {
|
|
3672
3811
|
if (!existsSync4(path)) return null;
|
|
3673
|
-
const [raw, stats] = await Promise.all([
|
|
3812
|
+
const [raw, stats] = await Promise.all([readFile9(path, "utf8"), stat2(path)]);
|
|
3674
3813
|
return parse(raw, stats.mtimeMs, { tailLines });
|
|
3675
3814
|
}
|
|
3676
3815
|
async function collectAvailableShellHistory(opts = {}) {
|
|
@@ -3824,7 +3963,7 @@ function reconcileProjectId(db, oldProjectId, newProjectId) {
|
|
|
3824
3963
|
}
|
|
3825
3964
|
|
|
3826
3965
|
// src/structure/collect.ts
|
|
3827
|
-
import { readFile as
|
|
3966
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
3828
3967
|
import { join as join8 } from "path";
|
|
3829
3968
|
|
|
3830
3969
|
// src/structure/extract.ts
|
|
@@ -3901,7 +4040,7 @@ async function collectFileEdges(repoRoot) {
|
|
|
3901
4040
|
for (const path of paths) {
|
|
3902
4041
|
let content;
|
|
3903
4042
|
try {
|
|
3904
|
-
content = await
|
|
4043
|
+
content = await readFile10(join8(repoRoot, path), "utf8");
|
|
3905
4044
|
} catch {
|
|
3906
4045
|
unreadable.push(path);
|
|
3907
4046
|
continue;
|
|
@@ -4453,10 +4592,10 @@ function createServer() {
|
|
|
4453
4592
|
title: "Search remembered project history",
|
|
4454
4593
|
description: "Search a NexusMem-tracked repository's remembered history: git commits, code diffs (the patch of each changed file), shell commands, tracked markdown docs, and (if enabled) conversation transcripts and per-session summaries. Returns a token-budgeted, ranked context block -- not raw search results.",
|
|
4455
4594
|
inputSchema: {
|
|
4456
|
-
projectRoot:
|
|
4457
|
-
query:
|
|
4458
|
-
budget:
|
|
4459
|
-
allProjects:
|
|
4595
|
+
projectRoot: z4.string().describe("Absolute path to the repository root"),
|
|
4596
|
+
query: z4.string().describe("Free-text question or search terms"),
|
|
4597
|
+
budget: z4.number().int().positive().optional().describe("Max tokens in the returned context block. Default 2000."),
|
|
4598
|
+
allProjects: z4.boolean().optional().describe(
|
|
4460
4599
|
"Search every repository NexusMem has been run in on this machine, not just projectRoot. Use when the answer may live in a different project (a pattern solved elsewhere, a tool that failed the same way before). Each result is tagged with its repository."
|
|
4461
4600
|
)
|
|
4462
4601
|
}
|
|
@@ -4483,10 +4622,10 @@ function createServer() {
|
|
|
4483
4622
|
title: "Sync remembered history",
|
|
4484
4623
|
description: "Ingest new git, diff, shell, docs and (if enabled) conversation history for a NexusMem-tracked repository into its local database. Pass pruneSource or pruneStaleShell instead to delete a dead source's nodes (e.g. the pre-hook shell scrape) rather than syncing -- dry-run unless yes is also true, since this is an irreversible full wipe of that source.",
|
|
4485
4624
|
inputSchema: {
|
|
4486
|
-
projectRoot:
|
|
4487
|
-
pruneSource:
|
|
4488
|
-
pruneStaleShell:
|
|
4489
|
-
yes:
|
|
4625
|
+
projectRoot: z4.string().describe("Absolute path to the repository root"),
|
|
4626
|
+
pruneSource: z4.string().optional().describe('Delete every node from this exact source (e.g. "shell:pwsh") instead of syncing'),
|
|
4627
|
+
pruneStaleShell: z4.boolean().optional().describe("Shortcut for pruneSource on shell:pwsh, shell:bash and shell:zsh at once -- the dead pre-hook scrape sources"),
|
|
4628
|
+
yes: z4.boolean().optional().describe("Confirms the delete. Without it, pruneSource/pruneStaleShell only report the matching count.")
|
|
4490
4629
|
}
|
|
4491
4630
|
},
|
|
4492
4631
|
async ({ projectRoot, pruneSource, pruneStaleShell, yes }) => {
|
|
@@ -4500,7 +4639,7 @@ function createServer() {
|
|
|
4500
4639
|
title: "Show what is remembered",
|
|
4501
4640
|
description: "Report how many nodes NexusMem currently remembers for a repository, broken down by kind and source.",
|
|
4502
4641
|
inputSchema: {
|
|
4503
|
-
projectRoot:
|
|
4642
|
+
projectRoot: z4.string().describe("Absolute path to the repository root")
|
|
4504
4643
|
}
|
|
4505
4644
|
},
|
|
4506
4645
|
async ({ projectRoot }) => {
|
|
@@ -4517,8 +4656,8 @@ function createServer() {
|
|
|
4517
4656
|
title: "List recently remembered items",
|
|
4518
4657
|
description: "List the most recently remembered items for a NexusMem-tracked repository -- git commits, code diffs, shell commands, tracked docs, and (if enabled) conversation transcripts and session summaries -- newest first. Chronological, not relevance-ranked: use search_memory instead for a specific question.",
|
|
4519
4658
|
inputSchema: {
|
|
4520
|
-
projectRoot:
|
|
4521
|
-
limit:
|
|
4659
|
+
projectRoot: z4.string().describe("Absolute path to the repository root"),
|
|
4660
|
+
limit: z4.number().int().positive().optional().describe("Max items to return, newest first. Default 20.")
|
|
4522
4661
|
}
|
|
4523
4662
|
},
|
|
4524
4663
|
async ({ projectRoot, limit }) => {
|
|
@@ -5254,7 +5393,7 @@ program.command("query").description("Search remembered history and print a toke
|
|
|
5254
5393
|
);
|
|
5255
5394
|
program.command("forget").description(
|
|
5256
5395
|
"Permanently deny-list a value: deletes matching nodes now and blocks it from ever being re-ingested (irreversible)"
|
|
5257
|
-
).argument("[value]", "exact text to forget (see --regex); omit with --list").option("-C, --cwd <path>", "repository path", process.cwd()).option("--regex", "treat <value> as a regular expression instead of a literal substring", false).option("--ignore-case", "case-insensitive match", false).option("--reason <text>", "free-text note stored with the deny-list entry").option("--list", "list active deny-list entries instead of forgetting a new value", false).option("--yes", "confirm the irreversible delete + deny-list write", false).action(
|
|
5396
|
+
).argument("[value]", "exact text to forget (see --regex); omit with --list").option("-C, --cwd <path>", "repository path", process.cwd()).option("--regex", "treat <value> as a regular expression instead of a literal substring", false).option("--ignore-case", "case-insensitive match", false).option("--reason <text>", "free-text note stored with the deny-list entry").option("--list", "list active deny-list entries instead of forgetting a new value", false).option("--export <path>", "write this project's deny-list to a JSON file, for --import in another checkout").option("--import <path>", "re-apply a deny-list JSON file (from --export) against this project").option("--yes", "confirm the irreversible delete + deny-list write", false).action(
|
|
5258
5397
|
(value, options) => guard(
|
|
5259
5398
|
() => runForget({
|
|
5260
5399
|
cwd: options.cwd,
|
|
@@ -5263,6 +5402,8 @@ program.command("forget").description(
|
|
|
5263
5402
|
ignoreCase: options.ignoreCase,
|
|
5264
5403
|
reason: options.reason,
|
|
5265
5404
|
list: options.list,
|
|
5405
|
+
export: options.export,
|
|
5406
|
+
import: options.import,
|
|
5266
5407
|
yes: options.yes
|
|
5267
5408
|
})
|
|
5268
5409
|
)()
|