@pcircle/memesh 4.6.1 → 4.7.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.
Files changed (120) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.de.md +22 -11
  4. package/README.md +22 -11
  5. package/README.zh-TW.md +21 -11
  6. package/dashboard/dist/index.html +6 -6
  7. package/dist/core/analytics.d.ts.map +1 -1
  8. package/dist/core/analytics.js +1 -1
  9. package/dist/core/analytics.js.map +1 -1
  10. package/dist/core/citation-rule.d.ts +24 -0
  11. package/dist/core/citation-rule.d.ts.map +1 -0
  12. package/dist/core/citation-rule.js +71 -0
  13. package/dist/core/citation-rule.js.map +1 -0
  14. package/dist/core/conflict-candidates.d.ts.map +1 -1
  15. package/dist/core/conflict-candidates.js +4 -12
  16. package/dist/core/conflict-candidates.js.map +1 -1
  17. package/dist/core/conflict-judge.d.ts +11 -0
  18. package/dist/core/conflict-judge.d.ts.map +1 -1
  19. package/dist/core/conflict-judge.js +1 -1
  20. package/dist/core/conflict-judge.js.map +1 -1
  21. package/dist/core/demo.d.ts.map +1 -1
  22. package/dist/core/demo.js +1 -1
  23. package/dist/core/demo.js.map +1 -1
  24. package/dist/core/digest-validator.d.ts.map +1 -1
  25. package/dist/core/digest-validator.js +3 -3
  26. package/dist/core/digest-validator.js.map +1 -1
  27. package/dist/core/doctor.d.ts.map +1 -1
  28. package/dist/core/doctor.js +100 -16
  29. package/dist/core/doctor.js.map +1 -1
  30. package/dist/core/dreamer.d.ts.map +1 -1
  31. package/dist/core/dreamer.js +10 -6
  32. package/dist/core/dreamer.js.map +1 -1
  33. package/dist/core/install-channel.d.ts +2 -1
  34. package/dist/core/install-channel.d.ts.map +1 -1
  35. package/dist/core/install-channel.js +11 -3
  36. package/dist/core/install-channel.js.map +1 -1
  37. package/dist/core/install-hooks.d.ts +6 -0
  38. package/dist/core/install-hooks.d.ts.map +1 -1
  39. package/dist/core/install-hooks.js +0 -0
  40. package/dist/core/install-hooks.js.map +1 -1
  41. package/dist/core/kg-backfill.d.ts.map +1 -1
  42. package/dist/core/kg-backfill.js +1 -1
  43. package/dist/core/kg-backfill.js.map +1 -1
  44. package/dist/core/lesson-engine.d.ts.map +1 -1
  45. package/dist/core/lesson-engine.js +3 -3
  46. package/dist/core/lesson-engine.js.map +1 -1
  47. package/dist/core/lifecycle.js +2 -2
  48. package/dist/core/llm-client.d.ts.map +1 -1
  49. package/dist/core/llm-client.js +7 -3
  50. package/dist/core/llm-client.js.map +1 -1
  51. package/dist/core/llm-telemetry.d.ts.map +1 -1
  52. package/dist/core/llm-telemetry.js +5 -2
  53. package/dist/core/llm-telemetry.js.map +1 -1
  54. package/dist/core/operations.d.ts +2 -0
  55. package/dist/core/operations.d.ts.map +1 -1
  56. package/dist/core/operations.js +2 -2
  57. package/dist/core/operations.js.map +1 -1
  58. package/dist/core/paths.d.ts.map +1 -1
  59. package/dist/core/paths.js +3 -2
  60. package/dist/core/paths.js.map +1 -1
  61. package/dist/core/patterns.d.ts +0 -5
  62. package/dist/core/patterns.d.ts.map +1 -1
  63. package/dist/core/patterns.js +1 -43
  64. package/dist/core/patterns.js.map +1 -1
  65. package/dist/core/schema-export.js +1 -1
  66. package/dist/core/schema-export.js.map +1 -1
  67. package/dist/core/serializer.d.ts.map +1 -1
  68. package/dist/core/serializer.js +47 -9
  69. package/dist/core/serializer.js.map +1 -1
  70. package/dist/core/signal-scorer.d.ts +0 -1
  71. package/dist/core/signal-scorer.d.ts.map +1 -1
  72. package/dist/core/signal-scorer.js +12 -8
  73. package/dist/core/signal-scorer.js.map +1 -1
  74. package/dist/core/transcript-extractor.d.ts +2 -0
  75. package/dist/core/transcript-extractor.d.ts.map +1 -1
  76. package/dist/core/transcript-extractor.js +10 -5
  77. package/dist/core/transcript-extractor.js.map +1 -1
  78. package/dist/core/types.d.ts +10 -0
  79. package/dist/core/types.d.ts.map +1 -1
  80. package/dist/core/updater.d.ts.map +1 -1
  81. package/dist/core/updater.js +1 -1
  82. package/dist/core/updater.js.map +1 -1
  83. package/dist/db.d.ts.map +1 -1
  84. package/dist/db.js +1 -1
  85. package/dist/db.js.map +1 -1
  86. package/dist/knowledge-graph.d.ts +3 -1
  87. package/dist/knowledge-graph.d.ts.map +1 -1
  88. package/dist/knowledge-graph.js +44 -28
  89. package/dist/knowledge-graph.js.map +1 -1
  90. package/dist/skills-manifest.json +20 -15
  91. package/dist/storage/schema.d.ts.map +1 -1
  92. package/dist/storage/schema.js +18 -22
  93. package/dist/storage/schema.js.map +1 -1
  94. package/dist/storage/vector-index.d.ts.map +1 -1
  95. package/dist/storage/vector-index.js +10 -4
  96. package/dist/storage/vector-index.js.map +1 -1
  97. package/dist/transports/cli/cli.js +105 -45
  98. package/dist/transports/cli/cli.js.map +1 -1
  99. package/dist/transports/http/server.d.ts.map +1 -1
  100. package/dist/transports/http/server.js +56 -1
  101. package/dist/transports/http/server.js.map +1 -1
  102. package/dist/transports/mcp/handlers.d.ts +2 -2
  103. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  104. package/dist/transports/mcp/handlers.js +15 -15
  105. package/dist/transports/mcp/handlers.js.map +1 -1
  106. package/dist/transports/schemas.d.ts +0 -1
  107. package/dist/transports/schemas.d.ts.map +1 -1
  108. package/dist/transports/schemas.js +2 -2
  109. package/dist/transports/schemas.js.map +1 -1
  110. package/hooks/hooks.json +4 -2
  111. package/llms-install.md +28 -0
  112. package/package.json +3 -2
  113. package/scripts/hooks/_generated/citation-rule.js +78 -0
  114. package/scripts/hooks/_generated/core-paths.js +3 -2
  115. package/scripts/hooks/_generated/schema.js +18 -22
  116. package/scripts/hooks/_shared.js +73 -4
  117. package/scripts/hooks/pre-compact.js +14 -7
  118. package/scripts/hooks/session-start.js +61 -2
  119. package/scripts/hooks/session-summary.js +41 -4
  120. package/scripts/upgrade-plugin.sh +9 -1
@@ -17,14 +17,56 @@ import { installHooks } from '../../core/install-hooks.js';
17
17
  import { getTaskState, setTaskState } from '../../core/task-state-store.js';
18
18
  import { TASK_STATE_FIELDS, taskStateLines } from '../../core/task-state.js';
19
19
  async function withDatabase(fn) {
20
- openDatabase();
20
+ try {
21
+ openDatabase();
22
+ }
23
+ catch (err) {
24
+ const message = err instanceof Error ? err.message : String(err);
25
+ console.error(`Error: memesh cannot open its database.`);
26
+ console.error(` ${message}`);
27
+ console.error(` Run \`memesh doctor\` — it names the file, the likely cause and the way back.`);
28
+ process.exit(1);
29
+ }
21
30
  try {
22
31
  return await fn();
23
32
  }
24
33
  finally {
34
+ await flushPendingEmbeddings();
25
35
  closeDatabase();
26
36
  }
27
37
  }
38
+ function unitFraction(flag) {
39
+ return (value) => {
40
+ const parsed = Number(value);
41
+ if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
42
+ console.error(`Error: ${flag} needs a number between 0 and 1, not "${value}".`);
43
+ process.exit(1);
44
+ }
45
+ return parsed;
46
+ };
47
+ }
48
+ function nonEmpty(flag) {
49
+ return (value) => {
50
+ if (value.trim() === '') {
51
+ console.error(`Error: ${flag} needs some text. An empty value is not a selector.`);
52
+ process.exit(1);
53
+ }
54
+ return value;
55
+ };
56
+ }
57
+ function proposalId(raw) {
58
+ return wholeNumber('<id>')(raw);
59
+ }
60
+ function wholeNumber(flag, min = 1) {
61
+ return (value) => {
62
+ const parsed = Number(value);
63
+ if (!Number.isInteger(parsed) || parsed < min) {
64
+ console.error(`Error: ${flag} needs a whole number of ${min} or more, not "${value}".`);
65
+ process.exit(1);
66
+ }
67
+ return parsed;
68
+ };
69
+ }
28
70
  function requireOneOf(value, allowed, flag) {
29
71
  if (value === undefined || allowed.includes(value))
30
72
  return;
@@ -143,7 +185,6 @@ program
143
185
  }
144
186
  if (result.relationErrors?.length)
145
187
  process.exitCode = 1;
146
- await flushPendingEmbeddings();
147
188
  });
148
189
  });
149
190
  program
@@ -151,7 +192,7 @@ program
151
192
  .description('Search stored knowledge')
152
193
  .argument('[query]', 'Search query')
153
194
  .option('--tag <tag>', 'Filter by tag')
154
- .option('--limit <n>', 'Max results', '20')
195
+ .option('--limit <n>', 'Max results', wholeNumber('--limit'), 20)
155
196
  .option('--include-archived', 'Include archived entities')
156
197
  .option('--namespace <namespace>', 'Filter by namespace: personal, team, or global')
157
198
  .option('--cross-project', 'Search across all project tags (ignores --tag filter)')
@@ -162,7 +203,7 @@ program
162
203
  const { entities, conflicts, retrieval } = await recallWithConflicts({
163
204
  query: query || undefined,
164
205
  tag: opts.tag,
165
- limit: parseInt(opts.limit),
206
+ limit: opts.limit,
166
207
  include_archived: opts.includeArchived,
167
208
  namespace: opts.namespace,
168
209
  cross_project: opts.crossProject,
@@ -216,7 +257,7 @@ program
216
257
  .command('forget')
217
258
  .description('Archive an entity or remove an observation (soft-delete, recoverable)')
218
259
  .requiredOption('--name <name>', 'Entity name')
219
- .option('--observation <text>', 'Remove specific observation only')
260
+ .option('--observation <text>', 'Remove specific observation only', nonEmpty('--observation'))
220
261
  .option('--json', 'Output as JSON')
221
262
  .option('--confirm', '[deprecated, no-op] forget is a soft archive — no confirmation needed')
222
263
  .action(async (opts) => {
@@ -234,7 +275,7 @@ program
234
275
  else if (result.observation_removed) {
235
276
  console.log(`✂️ Removed observation (${result.remaining_observations} remaining)`);
236
277
  }
237
- else if (opts.observation && result.entity_found) {
278
+ else if (opts.observation !== undefined && result.entity_found) {
238
279
  console.log(`Entity "${opts.name}" has no observation matching that text (${result.remaining_observations} observation(s) present).`);
239
280
  console.log(`See them with: memesh recall "${opts.name}" --json`);
240
281
  process.exitCode = 1;
@@ -316,7 +357,7 @@ program
316
357
  .description('Export memories as JSON. Defaults to stdout (pipe-friendly); use `-o <file>` to write directly.')
317
358
  .option('--tag <tag>', 'Export only entities with this tag')
318
359
  .option('--namespace <ns>', 'Export only from this namespace (personal, team, global)')
319
- .option('--limit <n>', 'Max entities to export', '1000')
360
+ .option('--limit <n>', 'Max entities to export', wholeNumber('--limit'), 1000)
320
361
  .option('-o, --out <file>', 'Write JSON to <file> instead of stdout. Parent directory must exist.')
321
362
  .action(async (opts) => {
322
363
  requireOneOf(opts.namespace, NAMESPACES, '--namespace');
@@ -324,7 +365,7 @@ program
324
365
  const result = exportMemories({
325
366
  tag: opts.tag,
326
367
  namespace: opts.namespace,
327
- limit: parseInt(opts.limit),
368
+ limit: opts.limit,
328
369
  });
329
370
  const json = JSON.stringify(result, null, 2);
330
371
  if (opts.out) {
@@ -340,6 +381,10 @@ program
340
381
  else {
341
382
  console.log(json);
342
383
  }
384
+ if (result.truncated) {
385
+ process.stderr.write(`⚠️ This is NOT the whole graph — ${result.entity_count} entities is the --limit, and there are more.\n`
386
+ + ` For a full backup, raise it: memesh export --limit 100000${opts.out ? ` -o ${opts.out}` : ''}\n`);
387
+ }
343
388
  });
344
389
  });
345
390
  program
@@ -395,6 +440,11 @@ program
395
440
  process.exit(1);
396
441
  }
397
442
  console.log(`Imported: ${result.imported}, Skipped: ${result.skipped}, Appended: ${result.appended}`);
443
+ if (result.skipped_relations.length > 0) {
444
+ console.error(`Note: ${result.skipped_relations.length} relation(s) not restored — the target is not in this bundle:\n `
445
+ + `${result.skipped_relations.join('\n ')}`);
446
+ console.error(` Export those entities too (widen --limit, or drop --tag/--namespace) to keep the links.`);
447
+ }
398
448
  if (result.errors.length > 0) {
399
449
  console.error(`Errors:\n ${result.errors.join('\n ')}`);
400
450
  process.exitCode = 1;
@@ -464,21 +514,15 @@ program
464
514
  .command('why')
465
515
  .description('Explain a file: commits memesh remembers touching it, their sessions, and related memories')
466
516
  .argument('<file>', 'File path (relative to the current directory or absolute)')
467
- .option('--line <n>', 'Attribute one line via git blame instead of file history')
468
- .option('--limit <n>', 'Max commits to inspect', '10')
517
+ .option('--line <n>', 'Attribute one line via git blame instead of file history', wholeNumber('--line'))
518
+ .option('--limit <n>', 'Max commits to inspect', wholeNumber('--limit'), 10)
469
519
  .option('--json', 'Output as JSON')
470
520
  .action(async (file, opts) => {
471
521
  await withDatabase(async () => {
472
522
  const { resolveFileCommits, explainCommits } = await import('../../core/why.js');
473
523
  const cwd = process.cwd();
474
- const limit = parseInt(opts.limit, 10);
475
- const line = opts.line !== undefined ? parseInt(opts.line, 10) : undefined;
476
- for (const [flag, value] of [['--limit', limit], ['--line', line]]) {
477
- if (value !== undefined && (!Number.isInteger(value) || value < 1)) {
478
- console.error(`Error: ${flag} needs a whole number of 1 or more.`);
479
- process.exit(1);
480
- }
481
- }
524
+ const limit = opts.limit;
525
+ const line = opts.line;
482
526
  const resolved = resolveFileCommits(cwd, file, { line, limit });
483
527
  const result = explainCommits(getDatabase(), {
484
528
  file,
@@ -631,7 +675,7 @@ program
631
675
  .option('--done <text>', 'What was just finished')
632
676
  .option('--json', 'Output as JSON')
633
677
  .action(async (opts) => {
634
- await withDatabase(() => {
678
+ await withDatabase(async () => {
635
679
  const patch = {};
636
680
  for (const field of TASK_STATE_FIELDS) {
637
681
  if (opts[field] !== undefined)
@@ -813,8 +857,9 @@ configCmd
813
857
  }
814
858
  }
815
859
  let coerced = value;
816
- if (canonical === 'sessionLimit')
817
- coerced = parseInt(value, 10);
860
+ if (canonical === 'sessionLimit') {
861
+ coerced = wholeNumber('sessionLimit')(value);
862
+ }
818
863
  if (canonical === 'llmFallbacks')
819
864
  coerced = JSON.parse(value);
820
865
  if (canonical === 'autoCapture') {
@@ -898,13 +943,13 @@ program
898
943
  program
899
944
  .command('serve')
900
945
  .description('Start the HTTP API server and web dashboard')
901
- .option('--port <port>', 'Port number', '3737')
946
+ .option('--port <port>', 'Port number', wholeNumber('--port', 0), 3737)
902
947
  .option('--host <host>', 'Host to bind', '127.0.0.1')
903
948
  .option('--allow-remote', 'Permit binding to a non-loopback host. Pair it with --host; on a non-loopback bind a bearer token is generated and REQUIRED for every /v1 request, and the startup output says where it lives. On the default loopback host this flag changes nothing.')
904
949
  .action(async (opts) => {
905
950
  const { startServer } = await import('../http/server.js');
906
951
  try {
907
- startServer(opts.host, parseInt(opts.port, 10), { allowRemote: opts.allowRemote, autoUpdateCheck: true });
952
+ startServer(opts.host, opts.port, { allowRemote: opts.allowRemote, autoUpdateCheck: true });
908
953
  }
909
954
  catch (err) {
910
955
  console.error(`MeMesh: ${err instanceof Error ? err.message : String(err)}`);
@@ -998,16 +1043,10 @@ program
998
1043
  program
999
1044
  .command('telemetry')
1000
1045
  .description('Show LLM call telemetry (per-flow scorecard for the last N days)')
1001
- .option('--window <days>', 'Look-back window in days (default 30)', (v) => parseInt(v, 10), 30)
1002
- .option('--prune <days>', 'Delete rows older than N days BEFORE rendering (closes v4.2.0 retention gap)', (v) => parseInt(v, 10))
1046
+ .option('--window <days>', 'Look-back window in days (default 30)', wholeNumber('--window'), 30)
1047
+ .option('--prune <days>', 'Delete rows older than N days BEFORE rendering (closes v4.2.0 retention gap)', wholeNumber('--prune', 0))
1003
1048
  .option('--json', 'Output as JSON')
1004
1049
  .action(async (opts) => {
1005
- for (const [flag, value] of [['--window', opts.window], ['--prune', opts.prune]]) {
1006
- if (value !== undefined && !Number.isFinite(value)) {
1007
- console.error(`Error: ${flag} needs a number of days.`);
1008
- process.exit(1);
1009
- }
1010
- }
1011
1050
  await withDatabase(async () => {
1012
1051
  const { summariseTelemetry, pruneTelemetry } = await import('../../core/llm-telemetry.js');
1013
1052
  let pruneResult = null;
@@ -1074,12 +1113,12 @@ kgCmd
1074
1113
  .description('Propose / apply heuristic relations to connect orphan entities (no LLM)')
1075
1114
  .option('--project <name>', 'Restrict to one project')
1076
1115
  .option('--dry-run', 'Show proposals without writing (default off — use to preview)')
1077
- .option('--max-per-source <n>', 'Max edges per orphan (default 3)', (v) => parseInt(v, 10), 3)
1078
- .option('--min-shared-tags <n>', 'Min shared topical tags to gate co-occurrence rule (default 2)', (v) => parseInt(v, 10), 2)
1116
+ .option('--max-per-source <n>', 'Max edges per orphan (default 3)', wholeNumber('--max-per-source'), 3)
1117
+ .option('--min-shared-tags <n>', 'Min shared topical tags to gate co-occurrence rule (default 2)', wholeNumber('--min-shared-tags'), 2)
1079
1118
  .option('--include-archived', 'Also process archived entities')
1080
1119
  .option('--session-cooccurrence', 'Rule 3: link high-signal orphans co-created in the same session')
1081
1120
  .option('--name-tokens', 'Rule 4: link orphans sharing ≥3 name content tokens (or Jaccard ≥ 0.50)')
1082
- .option('--min-jaccard <n>', 'Jaccard threshold for name similarity (default 0.50)', parseFloat)
1121
+ .option('--min-jaccard <n>', 'Jaccard threshold for name similarity (default 0.50)', unitFraction('--min-jaccard'))
1083
1122
  .option('--all-rules', 'Enable all heuristic rules (Rules 1–5)')
1084
1123
  .option('--no-evidence-links', 'Disable Rule 5: evidence → work-item links via shared session id (on by default — these edges feed the graph\'s evidence badges)')
1085
1124
  .option('--reset-idempotency', 'Clear the persistent "already-attempted" orphan cache before running (use after schema changes or to reconsider every orphan)')
@@ -1324,12 +1363,12 @@ dreamCmd
1324
1363
  .description('Run a dream pass — propose digests for clusters of compactable entities')
1325
1364
  .option('--project <name>', 'Restrict to one project')
1326
1365
  .option('--dry-run', 'Compute proposals without writing to dream_proposals')
1327
- .option('--max-llm-calls <n>', 'Hard cap on LLM calls (default 100)', (v) => parseInt(v, 10))
1328
- .option('--window-days <n>', 'Look-back window in days (default 56 = 8 weeks)', (v) => parseInt(v, 10))
1366
+ .option('--max-llm-calls <n>', 'Hard cap on LLM calls (default 100)', wholeNumber('--max-llm-calls'))
1367
+ .option('--window-days <n>', 'Look-back window in days (default 56 = 8 weeks)', wholeNumber('--window-days'))
1329
1368
  .option('--validate', 'Run a second LLM pass to cross-check each digest against its sources (doubles LLM calls per proposal; surfaces under flow=digest_validator in `memesh telemetry`)')
1330
1369
  .option('--from-transcripts', 'EXPERIMENTAL: mine Claude Code session transcripts for this project (decisions/lessons/facts hidden in the conversation) and STAGE them as proposals for `dream accept`, instead of clustering existing entities. Scoped to the current project only — --project does not apply here. With --dry-run, lists sessions and conversation-turn counts without calling an LLM.')
1331
1370
  .option('--if-due', 'For a scheduler (cron/launchd): only mine if `transcriptMining` is enabled in config AND at least --min-interval-hours have passed since this project was last mined; otherwise exit 0 doing nothing. Lets one frequently-firing entry self-throttle. Only meaningful with --from-transcripts.')
1332
- .option('--min-interval-hours <n>', 'With --if-due: minimum hours between mined runs for this project (default 24).', (v) => parseInt(v, 10))
1371
+ .option('--min-interval-hours <n>', 'With --if-due: minimum hours between mined runs for this project (default 24).', wholeNumber('--min-interval-hours'))
1333
1372
  .action(async (opts) => {
1334
1373
  if (opts.fromTranscripts) {
1335
1374
  const windowDays = typeof opts.windowDays === 'number' && !Number.isNaN(opts.windowDays) ? opts.windowDays : 3;
@@ -1406,6 +1445,10 @@ dreamCmd
1406
1445
  console.log(` LLM call failures: ${result.llmFailures} (sessions not mined — retry when the provider is reachable)`);
1407
1446
  if (result.parseFailures > 0)
1408
1447
  console.log(` unparsable replies: ${result.parseFailures} (chunk reply not valid JSON — likely truncated; those candidates were lost, retry)`);
1448
+ if (result.cappedTurns > 0) {
1449
+ console.log(` per-turn cap: ${result.cappedTurns} turn(s) were analysed only in part `
1450
+ + `(the first 4,000 characters); the rest of each was not sent`);
1451
+ }
1409
1452
  if (result.truncatedTurns > 0) {
1410
1453
  console.log(` size-cap truncation: ${result.truncatedTurns} conversation turn(s) beyond the cap were NOT analysed`);
1411
1454
  for (const t of result.truncatedSessions) {
@@ -1477,9 +1520,9 @@ dreamCmd
1477
1520
  .description('Run pattern detector — surface emerging patterns/conventions/repeated mistakes per project (Phase 3)')
1478
1521
  .option('--project <name>', 'Restrict to one project (default: all projects)')
1479
1522
  .option('--dry-run', 'Compute proposals without writing to dream_proposals')
1480
- .option('--max-llm-calls <n>', 'Hard cap on LLM calls (default 10)', (v) => parseInt(v, 10))
1481
- .option('--window-days <n>', 'Look-back window in days (default 30)', (v) => parseInt(v, 10))
1482
- .option('--min-signal <n>', 'Minimum signal_score to include in scan (default 0.3)', (v) => parseFloat(v))
1523
+ .option('--max-llm-calls <n>', 'Hard cap on LLM calls (default 10)', wholeNumber('--max-llm-calls'))
1524
+ .option('--window-days <n>', 'Look-back window in days (default 30)', wholeNumber('--window-days'))
1525
+ .option('--min-signal <n>', 'Minimum signal_score to include in scan (default 0.3)', unitFraction('--min-signal'))
1483
1526
  .action(async (opts) => {
1484
1527
  await withDatabase(async () => {
1485
1528
  const { runPatternDetector } = await import('../../core/dreamer.js');
@@ -1551,7 +1594,7 @@ dreamCmd
1551
1594
  dreamCmd
1552
1595
  .command('conflicts')
1553
1596
  .description('Judge semantically-close memory pairs for contradiction / supersession / duplication (LLM) and stage relation proposals for review')
1554
- .option('--max-pairs <n>', 'Judge at most N of the tightest candidate pairs this run (default 20)', (v) => parseInt(v, 10))
1597
+ .option('--max-pairs <n>', 'Judge at most N of the tightest candidate pairs this run (default 20)', wholeNumber('--max-pairs'))
1555
1598
  .option('--dry-run', 'Show how many candidates are queued without calling an LLM or writing anything')
1556
1599
  .action(async (opts) => {
1557
1600
  await withDatabase(async () => {
@@ -1599,7 +1642,7 @@ dreamCmd
1599
1642
  await withDatabase(async () => {
1600
1643
  const { getProposalDetail } = await import('../../core/dreamer.js');
1601
1644
  const { getDatabase } = await import('../../db.js');
1602
- const detail = getProposalDetail(getDatabase(), parseInt(id, 10));
1645
+ const detail = getProposalDetail(getDatabase(), proposalId(id));
1603
1646
  if (!detail) {
1604
1647
  console.error(`proposal #${id} not found`);
1605
1648
  console.error('See ids with: memesh dream list');
@@ -1660,14 +1703,13 @@ dreamCmd
1660
1703
  const kg = new KnowledgeGraph(getDatabase());
1661
1704
  let result;
1662
1705
  try {
1663
- result = applyProposal(getDatabase(), parseInt(id, 10), kg);
1706
+ result = applyProposal(getDatabase(), proposalId(id), kg);
1664
1707
  }
1665
1708
  catch (err) {
1666
1709
  console.error(err instanceof Error ? err.message : String(err));
1667
1710
  console.error('See pending ids with: memesh dream list');
1668
1711
  process.exit(1);
1669
1712
  }
1670
- await flushPendingEmbeddings();
1671
1713
  console.log(`Applied proposal #${result.proposalId}`);
1672
1714
  console.log(` digest entity: ${result.digestEntityName}`);
1673
1715
  console.log(` sources archived: ${result.sourcesArchived}`);
@@ -1682,7 +1724,7 @@ dreamCmd
1682
1724
  const { rejectProposal } = await import('../../core/dreamer.js');
1683
1725
  const { getDatabase } = await import('../../db.js');
1684
1726
  try {
1685
- rejectProposal(getDatabase(), parseInt(id, 10), opts.reason);
1727
+ rejectProposal(getDatabase(), proposalId(id), opts.reason);
1686
1728
  }
1687
1729
  catch (err) {
1688
1730
  console.error(err instanceof Error ? err.message : String(err));
@@ -1716,6 +1758,15 @@ program
1716
1758
  console.log('Hooks are active. Verify with: memesh doctor');
1717
1759
  console.log('');
1718
1760
  console.log('If you really want a second copy in ~/.claude/settings.json on top of the plugin, re-run with --force-over-plugin. (Not recommended — every session-start / Stop / PreToolUse event will fire memesh\'s hooks twice.)');
1761
+ if (result.citationRule.action === 'foreign-file') {
1762
+ console.log('');
1763
+ console.log(`WARNING: the citation contract was NOT installed — a file memesh did not write already exists at ${result.citationRule.path}.`);
1764
+ console.log(' memesh will not overwrite it. Move or rename that file and re-run, or add the contract to it by hand.');
1765
+ console.log(' Until then, memesh cannot tell whether the memories it injects are ever used.');
1766
+ }
1767
+ else if (result.citationRule.action !== 'unchanged') {
1768
+ console.log(`Citation contract ${result.citationRule.action}: ${result.citationRule.path}`);
1769
+ }
1719
1770
  return;
1720
1771
  }
1721
1772
  console.log(`${opts.dryRun ? '[dry-run] ' : ''}Settings: ${result.settingsPath}`);
@@ -1725,6 +1776,15 @@ program
1725
1776
  }
1726
1777
  if (result.backupPath)
1727
1778
  console.log(`Backup: ${result.backupPath}`);
1779
+ if (result.citationRule.action === 'foreign-file') {
1780
+ console.log('');
1781
+ console.log(`WARNING: the citation contract was NOT installed — a file memesh did not write already exists at ${result.citationRule.path}.`);
1782
+ console.log(' memesh will not overwrite it. Move or rename that file and re-run, or add the contract to it by hand.');
1783
+ console.log(' Until then, memesh cannot tell whether the memories it injects are ever used.');
1784
+ }
1785
+ else if (result.citationRule.action !== 'unchanged') {
1786
+ console.log(`Citation contract ${result.citationRule.action}: ${result.citationRule.path}`);
1787
+ }
1728
1788
  if (result.conflicts.length > 0) {
1729
1789
  console.log('');
1730
1790
  console.log('Note: memesh hooks now coexist with the following pre-existing entries:');