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 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.0...HEAD
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. See [`docs/forget-mechanism.md`](docs/forget-mechanism.md) for why this
348
- exists.
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 to see active deny-list entries)");
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 readFile4, rename, writeFile as writeFile4 } from "fs/promises";
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 z2 } from "zod";
1669
- var ENTRY_SCHEMA = z2.object({
1670
- projectId: z2.string().min(1),
1671
- root: z2.string().min(1),
1672
- dbPath: z2.string().min(1),
1673
- originUrl: z2.string().nullable().default(null),
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: z2.number().int().nonnegative()
1814
+ lastSeenAt: z3.number().int().nonnegative()
1676
1815
  });
1677
- var REGISTRY_SCHEMA = z2.object({
1678
- version: z2.literal(1),
1679
- projects: z2.array(ENTRY_SCHEMA).default([])
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 readFile4(registryPath(), "utf8");
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 writeFile4(tmp, `${JSON.stringify({ version: 1, projects }, null, 2)}
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 z3 } from "zod";
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 readFile5 } from "fs/promises";
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 readFile5(file, "utf8");
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 readFile6, stat } from "fs/promises";
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([readFile6(absPath, "utf8"), stat(absPath)]);
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 readFile8, stat as stat2 } from "fs/promises";
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 readFile7 } from "fs/promises";
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 readFile7(path, "utf8");
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([readFile8(path, "utf8"), stat2(path)]);
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 readFile9 } from "fs/promises";
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 readFile9(join8(repoRoot, path), "utf8");
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: z3.string().describe("Absolute path to the repository root"),
4457
- query: z3.string().describe("Free-text question or search terms"),
4458
- budget: z3.number().int().positive().optional().describe("Max tokens in the returned context block. Default 2000."),
4459
- allProjects: z3.boolean().optional().describe(
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: z3.string().describe("Absolute path to the repository root"),
4487
- pruneSource: z3.string().optional().describe('Delete every node from this exact source (e.g. "shell:pwsh") instead of syncing'),
4488
- pruneStaleShell: z3.boolean().optional().describe("Shortcut for pruneSource on shell:pwsh, shell:bash and shell:zsh at once -- the dead pre-hook scrape sources"),
4489
- yes: z3.boolean().optional().describe("Confirms the delete. Without it, pruneSource/pruneStaleShell only report the matching count.")
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: z3.string().describe("Absolute path to the repository root")
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: z3.string().describe("Absolute path to the repository root"),
4521
- limit: z3.number().int().positive().optional().describe("Max items to return, newest first. Default 20.")
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
  )()