@pcircle/memesh 4.5.1 → 4.6.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 (202) hide show
  1. package/.claude-plugin/marketplace.json +5 -3
  2. package/.claude-plugin/plugin.json +6 -4
  3. package/AGENTS.md +116 -0
  4. package/README.de.md +141 -48
  5. package/README.md +173 -48
  6. package/README.zh-TW.md +142 -48
  7. package/dashboard/dist/index.html +15 -14
  8. package/dist/cli/view-live.js +3 -3
  9. package/dist/core/analytics.d.ts +9 -0
  10. package/dist/core/analytics.d.ts.map +1 -1
  11. package/dist/core/analytics.js +36 -18
  12. package/dist/core/analytics.js.map +1 -1
  13. package/dist/core/auto-tagger.d.ts.map +1 -1
  14. package/dist/core/auto-tagger.js +4 -9
  15. package/dist/core/auto-tagger.js.map +1 -1
  16. package/dist/core/briefing.d.ts +8 -0
  17. package/dist/core/briefing.d.ts.map +1 -0
  18. package/dist/core/briefing.js +92 -0
  19. package/dist/core/briefing.js.map +1 -0
  20. package/dist/core/capture-flag.d.ts +5 -0
  21. package/dist/core/capture-flag.d.ts.map +1 -0
  22. package/dist/core/capture-flag.js +10 -0
  23. package/dist/core/capture-flag.js.map +1 -0
  24. package/dist/core/config.d.ts +0 -1
  25. package/dist/core/config.d.ts.map +1 -1
  26. package/dist/core/config.js.map +1 -1
  27. package/dist/core/conflict-candidates.d.ts +20 -0
  28. package/dist/core/conflict-candidates.d.ts.map +1 -0
  29. package/dist/core/conflict-candidates.js +79 -0
  30. package/dist/core/conflict-candidates.js.map +1 -0
  31. package/dist/core/conflict-judge.d.ts +47 -0
  32. package/dist/core/conflict-judge.d.ts.map +1 -0
  33. package/dist/core/conflict-judge.js +189 -0
  34. package/dist/core/conflict-judge.js.map +1 -0
  35. package/dist/core/demo.d.ts.map +1 -1
  36. package/dist/core/demo.js +1 -1
  37. package/dist/core/demo.js.map +1 -1
  38. package/dist/core/digest-validator.d.ts.map +1 -1
  39. package/dist/core/digest-validator.js +3 -5
  40. package/dist/core/digest-validator.js.map +1 -1
  41. package/dist/core/doctor.d.ts +2 -0
  42. package/dist/core/doctor.d.ts.map +1 -1
  43. package/dist/core/doctor.js +59 -62
  44. package/dist/core/doctor.js.map +1 -1
  45. package/dist/core/dreamer.d.ts +5 -2
  46. package/dist/core/dreamer.d.ts.map +1 -1
  47. package/dist/core/dreamer.js +329 -25
  48. package/dist/core/dreamer.js.map +1 -1
  49. package/dist/core/embedder.d.ts +8 -4
  50. package/dist/core/embedder.d.ts.map +1 -1
  51. package/dist/core/embedder.js +82 -24
  52. package/dist/core/embedder.js.map +1 -1
  53. package/dist/core/failure-analyzer.d.ts.map +1 -1
  54. package/dist/core/failure-analyzer.js +7 -12
  55. package/dist/core/failure-analyzer.js.map +1 -1
  56. package/dist/core/graph.d.ts +12 -0
  57. package/dist/core/graph.d.ts.map +1 -1
  58. package/dist/core/graph.js +56 -1
  59. package/dist/core/graph.js.map +1 -1
  60. package/dist/core/guards.d.ts +20 -0
  61. package/dist/core/guards.d.ts.map +1 -0
  62. package/dist/core/guards.js +103 -0
  63. package/dist/core/guards.js.map +1 -0
  64. package/dist/core/install-channel.d.ts +1 -1
  65. package/dist/core/install-channel.d.ts.map +1 -1
  66. package/dist/core/install-channel.js +16 -5
  67. package/dist/core/install-channel.js.map +1 -1
  68. package/dist/core/install-hooks.d.ts +5 -0
  69. package/dist/core/install-hooks.d.ts.map +1 -1
  70. package/dist/core/install-hooks.js +0 -0
  71. package/dist/core/install-hooks.js.map +1 -1
  72. package/dist/core/json-utils.d.ts +1 -0
  73. package/dist/core/json-utils.d.ts.map +1 -1
  74. package/dist/core/json-utils.js +19 -10
  75. package/dist/core/json-utils.js.map +1 -1
  76. package/dist/core/kg-backfill.d.ts +5 -2
  77. package/dist/core/kg-backfill.d.ts.map +1 -1
  78. package/dist/core/kg-backfill.js +155 -5
  79. package/dist/core/kg-backfill.js.map +1 -1
  80. package/dist/core/lifecycle.d.ts.map +1 -1
  81. package/dist/core/lifecycle.js +14 -21
  82. package/dist/core/lifecycle.js.map +1 -1
  83. package/dist/core/memory-tool.d.ts.map +1 -1
  84. package/dist/core/memory-tool.js +4 -4
  85. package/dist/core/memory-tool.js.map +1 -1
  86. package/dist/core/operations.d.ts +13 -2
  87. package/dist/core/operations.d.ts.map +1 -1
  88. package/dist/core/operations.js +115 -28
  89. package/dist/core/operations.js.map +1 -1
  90. package/dist/core/prompt-safety.d.ts +1 -0
  91. package/dist/core/prompt-safety.d.ts.map +1 -1
  92. package/dist/core/prompt-safety.js +7 -0
  93. package/dist/core/prompt-safety.js.map +1 -1
  94. package/dist/core/schema-export.d.ts.map +1 -1
  95. package/dist/core/schema-export.js +31 -0
  96. package/dist/core/schema-export.js.map +1 -1
  97. package/dist/core/serializer.d.ts.map +1 -1
  98. package/dist/core/serializer.js +8 -0
  99. package/dist/core/serializer.js.map +1 -1
  100. package/dist/core/setup.d.ts +29 -0
  101. package/dist/core/setup.d.ts.map +1 -0
  102. package/dist/core/setup.js +127 -0
  103. package/dist/core/setup.js.map +1 -0
  104. package/dist/core/task-state-store.d.ts +17 -0
  105. package/dist/core/task-state-store.d.ts.map +1 -0
  106. package/dist/core/task-state-store.js +45 -0
  107. package/dist/core/task-state-store.js.map +1 -0
  108. package/dist/core/task-state.d.ts +19 -0
  109. package/dist/core/task-state.d.ts.map +1 -0
  110. package/dist/core/task-state.js +91 -0
  111. package/dist/core/task-state.js.map +1 -0
  112. package/dist/core/time-utils.d.ts +2 -0
  113. package/dist/core/time-utils.d.ts.map +1 -0
  114. package/dist/core/time-utils.js +14 -0
  115. package/dist/core/time-utils.js.map +1 -0
  116. package/dist/core/title.d.ts +5 -0
  117. package/dist/core/title.d.ts.map +1 -0
  118. package/dist/core/title.js +14 -0
  119. package/dist/core/title.js.map +1 -0
  120. package/dist/core/transcript-source.d.ts.map +1 -1
  121. package/dist/core/transcript-source.js +2 -3
  122. package/dist/core/transcript-source.js.map +1 -1
  123. package/dist/core/types.d.ts +5 -0
  124. package/dist/core/types.d.ts.map +1 -1
  125. package/dist/core/why.d.ts +54 -0
  126. package/dist/core/why.d.ts.map +1 -0
  127. package/dist/core/why.js +168 -0
  128. package/dist/core/why.js.map +1 -0
  129. package/dist/core/work-topology.d.ts +36 -0
  130. package/dist/core/work-topology.d.ts.map +1 -0
  131. package/dist/core/work-topology.js +192 -0
  132. package/dist/core/work-topology.js.map +1 -0
  133. package/dist/db.d.ts +33 -11
  134. package/dist/db.d.ts.map +1 -1
  135. package/dist/db.js +307 -315
  136. package/dist/db.js.map +1 -1
  137. package/dist/knowledge-graph.d.ts +1 -0
  138. package/dist/knowledge-graph.d.ts.map +1 -1
  139. package/dist/knowledge-graph.js +50 -40
  140. package/dist/knowledge-graph.js.map +1 -1
  141. package/dist/skills-manifest.json +62 -22
  142. package/dist/storage/conflicts.d.ts.map +1 -1
  143. package/dist/storage/conflicts.js +2 -7
  144. package/dist/storage/conflicts.js.map +1 -1
  145. package/dist/storage/fts-index.d.ts +4 -2
  146. package/dist/storage/fts-index.d.ts.map +1 -1
  147. package/dist/storage/fts-index.js +16 -4
  148. package/dist/storage/fts-index.js.map +1 -1
  149. package/dist/storage/schema.d.ts +20 -0
  150. package/dist/storage/schema.d.ts.map +1 -0
  151. package/dist/storage/schema.js +274 -0
  152. package/dist/storage/schema.js.map +1 -0
  153. package/dist/storage/sqlite.d.ts.map +1 -1
  154. package/dist/storage/sqlite.js +1 -1
  155. package/dist/storage/sqlite.js.map +1 -1
  156. package/dist/transports/cli/cli.d.ts +1 -4
  157. package/dist/transports/cli/cli.d.ts.map +1 -1
  158. package/dist/transports/cli/cli.js +579 -66
  159. package/dist/transports/cli/cli.js.map +1 -1
  160. package/dist/transports/http/server.d.ts.map +1 -1
  161. package/dist/transports/http/server.js +242 -303
  162. package/dist/transports/http/server.js.map +1 -1
  163. package/dist/transports/mcp/handlers.d.ts +46 -0
  164. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  165. package/dist/transports/mcp/handlers.js +59 -4
  166. package/dist/transports/mcp/handlers.js.map +1 -1
  167. package/dist/transports/schemas.d.ts +29 -10
  168. package/dist/transports/schemas.d.ts.map +1 -1
  169. package/dist/transports/schemas.js +33 -8
  170. package/dist/transports/schemas.js.map +1 -1
  171. package/hooks/hooks.json +10 -0
  172. package/llms-install.md +138 -0
  173. package/package.json +14 -9
  174. package/scripts/hooks/_generated/capture-flag.js +17 -0
  175. package/scripts/hooks/_generated/fts-index.js +16 -4
  176. package/scripts/hooks/_generated/guards.js +110 -0
  177. package/scripts/hooks/_generated/schema.js +281 -0
  178. package/scripts/hooks/_generated/sqlite.js +1 -1
  179. package/scripts/hooks/_generated/task-state.js +98 -0
  180. package/scripts/hooks/_generated/time-utils.js +21 -0
  181. package/scripts/hooks/_generated/title.js +21 -0
  182. package/scripts/hooks/_generated/work-topology.js +199 -0
  183. package/scripts/hooks/_shared.js +197 -480
  184. package/scripts/hooks/guard-check.js +76 -0
  185. package/scripts/hooks/post-commit.js +31 -1
  186. package/scripts/hooks/pre-compact.js +13 -1
  187. package/scripts/hooks/pre-edit-recall.js +158 -120
  188. package/scripts/hooks/session-start.js +169 -82
  189. package/scripts/hooks/session-summary.js +78 -90
  190. package/skills/memesh/SKILL.md +108 -76
  191. package/README.es.md +0 -467
  192. package/README.fr.md +0 -459
  193. package/README.ja.md +0 -467
  194. package/README.ko.md +0 -467
  195. package/README.pt.md +0 -459
  196. package/README.th.md +0 -460
  197. package/README.vi.md +0 -459
  198. package/README.zh-CN.md +0 -466
  199. package/dist/cli/view.d.ts +0 -3
  200. package/dist/cli/view.d.ts.map +0 -1
  201. package/dist/cli/view.js +0 -523
  202. package/dist/cli/view.js.map +0 -1
@@ -1,16 +1,21 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from 'commander';
2
+ import { Command, Option } from 'commander';
3
3
  import { randomBytes } from 'crypto';
4
4
  import fs from 'fs';
5
5
  import path from 'path';
6
6
  import { fileURLToPath } from 'url';
7
- import { openDatabase, closeDatabase, getDatabase, reindexFts, allowVectorIndexRebuild } from '../../db.js';
7
+ import { openDatabase, closeDatabase, getDatabase, reindexFts, readVectorGeneration, generationRowIds, discardVectorGeneration, } from '../../db.js';
8
8
  import { remember, recallWithConflicts, forget, exportMemories, importMemories, learn, reindex, setPinned } from '../../core/operations.js';
9
9
  import { readConfig, writeConfig, maskApiKey, detectCapabilities } from '../../core/config.js';
10
10
  import { MAX_LANGUAGE_LENGTH, languageValueError } from '../../core/output-language.js';
11
- import { getDbPath, redactSecrets, redactUserPaths } from '../../core/paths.js';
11
+ import { getDbPath, getProjectName, homeDir, redactSecrets, redactUserPaths } from '../../core/paths.js';
12
12
  import { flushPendingEmbeddings, canRefillVectorIndex } from '../../core/embedder.js';
13
13
  import { NAMESPACES } from '../../core/types.js';
14
+ import { assembleBriefing } from '../../core/briefing.js';
15
+ import { inspectHosts, allWired } from '../../core/setup.js';
16
+ import { installHooks } from '../../core/install-hooks.js';
17
+ import { getTaskState, setTaskState } from '../../core/task-state-store.js';
18
+ import { TASK_STATE_FIELDS, taskStateLines } from '../../core/task-state.js';
14
19
  async function withDatabase(fn) {
15
20
  openDatabase();
16
21
  try {
@@ -26,13 +31,34 @@ function requireOneOf(value, allowed, flag) {
26
31
  console.error(`Error: ${flag} "${value}" is not valid. Use one of: ${allowed.join(', ')}.`);
27
32
  process.exit(1);
28
33
  }
34
+ function isOnPath(tool) {
35
+ const exts = process.platform === 'win32'
36
+ ? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean)
37
+ : [''];
38
+ for (const dir of (process.env.PATH ?? '').split(path.delimiter)) {
39
+ if (!dir)
40
+ continue;
41
+ for (const ext of exts) {
42
+ try {
43
+ fs.accessSync(path.join(dir, tool + ext), fs.constants.X_OK);
44
+ return true;
45
+ }
46
+ catch { }
47
+ }
48
+ }
49
+ return false;
50
+ }
51
+ function wireUserHooks() {
52
+ const r = installHooks({ pluginRoot: packageRoot, pluginVersion: pkg.version, scope: 'user' });
53
+ return `hooks: added ${r.added}, skipped ${r.skipped} already-installed${r.backupPath ? ` (backup: ${r.backupPath})` : ''}`;
54
+ }
29
55
  const packageJsonPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../package.json');
30
56
  const packageRoot = path.dirname(packageJsonPath);
31
57
  const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
32
58
  const program = new Command();
33
59
  program
34
60
  .name('memesh')
35
- .description('MeMesh — Local memory for Claude Code and MCP coding agents')
61
+ .description('MeMesh — Agentic memory for coding agents')
36
62
  .version(pkg.version)
37
63
  .allowExcessArguments(true)
38
64
  .showSuggestionAfterError(true);
@@ -42,6 +68,7 @@ program
42
68
  .description('Store knowledge as an entity (use flags for explicit form, or positional text for quick capture)')
43
69
  .option('--name <name>', 'Entity name')
44
70
  .option('--type <type>', 'Entity type')
71
+ .option('--title <title>', 'Short human-readable label shown as the headline (name stays the stable machine key)')
45
72
  .option('--obs <observations...>', 'Observations (space-separated)')
46
73
  .option('--tags <tags...>', 'Tags (space-separated)')
47
74
  .option('--namespace <namespace>', 'Namespace: personal, team, or global. On a NEW memory this places it (default personal); on one that already exists it MOVES it out of the scope it is in — omit the flag to leave it alone.')
@@ -60,6 +87,8 @@ program
60
87
  const suffix = randomBytes(3).toString('hex');
61
88
  opts.name = `quick-${date}-${slug || 'note'}-${suffix}`;
62
89
  opts.type = 'note';
90
+ if (!opts.title)
91
+ opts.title = String(text).slice(0, 200);
63
92
  if (!opts.obs || opts.obs.length === 0)
64
93
  opts.obs = [String(text)];
65
94
  else
@@ -85,6 +114,7 @@ program
85
114
  const result = remember({
86
115
  name: opts.name,
87
116
  type: opts.type,
117
+ title: opts.title,
88
118
  observations: opts.obs,
89
119
  tags: opts.tags,
90
120
  namespace: opts.namespace,
@@ -129,7 +159,7 @@ program
129
159
  .action(async (query, opts) => {
130
160
  requireOneOf(opts.namespace, NAMESPACES, '--namespace');
131
161
  await withDatabase(async () => {
132
- const { entities, conflicts } = await recallWithConflicts({
162
+ const { entities, conflicts, retrieval } = await recallWithConflicts({
133
163
  query: query || undefined,
134
164
  tag: opts.tag,
135
165
  limit: parseInt(opts.limit),
@@ -138,12 +168,7 @@ program
138
168
  cross_project: opts.crossProject,
139
169
  });
140
170
  if (opts.json) {
141
- if (conflicts.length > 0) {
142
- console.log(JSON.stringify({ entities, conflicts }));
143
- }
144
- else {
145
- console.log(JSON.stringify(entities));
146
- }
171
+ console.log(JSON.stringify(conflicts.length > 0 ? { entities, retrieval, conflicts } : { entities, retrieval }));
147
172
  }
148
173
  else if (entities.length === 0) {
149
174
  console.log('No results found.');
@@ -173,7 +198,11 @@ program
173
198
  console.log(` ... +${e.observations.length - 3} more`);
174
199
  }
175
200
  }
176
- console.log(`\n${entities.length} result(s)`);
201
+ const truncatedNote = retrieval.truncated ? ' (limit reached — more may exist)' : '';
202
+ console.log(`\n${entities.length} result(s)${truncatedNote}`);
203
+ if (retrieval.degraded) {
204
+ console.log('Warning: semantic search unavailable right now — keyword-only results (degraded). Run `memesh doctor` to see why.');
205
+ }
177
206
  if (conflicts.length > 0) {
178
207
  console.log('\nWarning: Conflicts detected:');
179
208
  for (const c of conflicts) {
@@ -400,6 +429,242 @@ program
400
429
  }
401
430
  });
402
431
  });
432
+ program
433
+ .command('briefing')
434
+ .description('The assembled work topology for a project — task state, decisions, lessons, knowledge, recent activity')
435
+ .option('--project <name>', 'Project name (default: the current directory’s project)')
436
+ .option('--json', 'Output as JSON')
437
+ .action(async (opts) => {
438
+ await withDatabase(() => {
439
+ const result = assembleBriefing(opts.project);
440
+ if (opts.json) {
441
+ console.log(JSON.stringify(result));
442
+ return;
443
+ }
444
+ if (!result.text) {
445
+ console.log(`No memories for "${result.project}" yet.\n` +
446
+ `Capture happens automatically as you work; or set the task state: memesh task --goal "…"`);
447
+ return;
448
+ }
449
+ console.log(result.text);
450
+ });
451
+ });
452
+ const WHY_ABSTENTION_TEXT = {
453
+ git_unavailable: 'git is not installed or not on PATH — commit attribution unavailable.',
454
+ not_a_git_repo: 'Not inside a git repository — commit attribution unavailable.',
455
+ file_not_tracked: 'File is not tracked by git — commit attribution unavailable.',
456
+ history_unreadable: "git could not read this file's history (too much output, too slow, or the repository has no commits yet) — nothing is listed because the question went unanswered, not because no commit touched the file.",
457
+ no_commits_supplied: 'No commit hashes were supplied — only the file-tag half of this answer ran.',
458
+ line_out_of_range: 'That line does not exist in the tracked file.',
459
+ line_uncommitted: 'That line is not committed yet — nothing to attribute.',
460
+ no_commit_entity: 'memesh has no memory of this commit (it predates capture, or was made without hooks / on another machine).',
461
+ no_session_link: 'This commit was captured before commits recorded their session — the session link does not exist.',
462
+ };
463
+ program
464
+ .command('why')
465
+ .description('Explain a file: commits memesh remembers touching it, their sessions, and related memories')
466
+ .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')
469
+ .option('--json', 'Output as JSON')
470
+ .action(async (file, opts) => {
471
+ await withDatabase(async () => {
472
+ const { resolveFileCommits, explainCommits } = await import('../../core/why.js');
473
+ 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
+ }
482
+ const resolved = resolveFileCommits(cwd, file, { line, limit });
483
+ const result = explainCommits(getDatabase(), {
484
+ file,
485
+ commits: resolved.commits,
486
+ project: getProjectName(cwd),
487
+ limit,
488
+ abstentions: resolved.abstention ? [resolved.abstention] : [],
489
+ });
490
+ if (opts.json) {
491
+ console.log(JSON.stringify(result, null, 2));
492
+ return;
493
+ }
494
+ console.log(`why ${result.file} (project: ${result.project ?? 'all'})`);
495
+ for (const code of result.abstentions) {
496
+ console.log(` ! ${WHY_ABSTENTION_TEXT[code] ?? code}`);
497
+ }
498
+ if (result.commits.length === 0 && result.abstentions.length === 0) {
499
+ console.log(' No commits touch this file.');
500
+ }
501
+ for (const c of result.commits) {
502
+ const head = [c.commit.hash.slice(0, 10), c.commit.date, c.commit.subject].filter(Boolean).join(' ');
503
+ console.log(`\n ${head}`);
504
+ if (c.entity) {
505
+ console.log(` memory: ${c.entity.name} [mem:${c.entity.id}]`);
506
+ for (const obs of c.entity.observations.slice(0, 3))
507
+ console.log(` - ${obs}`);
508
+ }
509
+ if (c.session) {
510
+ console.log(` session: ${c.session.session_id}`);
511
+ for (const s of c.session.entities)
512
+ console.log(` - ${s.name} (${s.type})`);
513
+ if (c.session.truncated) {
514
+ console.log(` … first ${c.session.entities.length} of a larger session — more exist`);
515
+ }
516
+ }
517
+ for (const code of c.abstentions) {
518
+ console.log(` ! ${WHY_ABSTENTION_TEXT[code] ?? code}`);
519
+ }
520
+ }
521
+ const fm = result.file_memories.entities;
522
+ if (fm.length > 0) {
523
+ console.log(`\n Related by file tag (file:${result.basename} — associated, not commit-derived):`);
524
+ for (const e of fm)
525
+ console.log(` - ${e.name} (${e.type})${e.title ? `: ${e.title}` : ''}`);
526
+ }
527
+ });
528
+ });
529
+ program
530
+ .command('setup')
531
+ .description('Detect Claude Code / Codex / Gemini on this machine, wire memesh into each, and verify')
532
+ .option('--check', 'Only report wiring status per host; change nothing (exit 1 if a present host is unwired)')
533
+ .option('--yes', 'Apply every wiring action without asking')
534
+ .action(async (opts) => {
535
+ const { spawnSync, execFileSync } = await import('child_process');
536
+ const runSeam = (cmd, args) => {
537
+ try {
538
+ let target = cmd;
539
+ let useShell = false;
540
+ if (process.platform === 'win32') {
541
+ const resolved = execFileSync('where', [cmd], { encoding: 'utf8' }).split(/\r?\n/)[0]?.trim();
542
+ if (resolved) {
543
+ target = resolved;
544
+ useShell = /\.(cmd|bat)$/i.test(resolved);
545
+ }
546
+ }
547
+ const r = spawnSync(target, args, { encoding: 'utf8', shell: useShell });
548
+ return { status: r.status, stderr: r.stderr ?? '' };
549
+ }
550
+ catch (err) {
551
+ return { status: null, stderr: err instanceof Error ? err.message : String(err) };
552
+ }
553
+ };
554
+ const seams = { home: () => homeDir(), isOnPath, run: runSeam };
555
+ const render = (statuses) => {
556
+ for (const st of statuses) {
557
+ if (!st.present) {
558
+ console.log(` ${st.title}: not found (looked for ${st.presenceDetail}) — skipped`);
559
+ continue;
560
+ }
561
+ const mark = st.wired === true ? '✅' : st.wired === false ? '❌' : '❓';
562
+ console.log(`${mark} ${st.title}: ${st.wiredDetail}`);
563
+ }
564
+ };
565
+ let statuses = inspectHosts(seams);
566
+ if (opts.check) {
567
+ render(statuses);
568
+ process.exit(allWired(statuses) ? 0 : 1);
569
+ }
570
+ const pending = statuses.filter((st) => st.present && st.actions.length > 0);
571
+ if (pending.length === 0) {
572
+ render(statuses);
573
+ console.log(allWired(statuses)
574
+ ? '\nEverything present is wired. Nothing to do.'
575
+ : '\nNothing to wire automatically — see the lines above.');
576
+ process.exit(allWired(statuses) ? 0 : 1);
577
+ }
578
+ render(statuses);
579
+ console.log('\nPlanned actions:');
580
+ for (const st of pending)
581
+ for (const a of st.actions) {
582
+ console.log(` • [${st.title}] ${a.label}${a.cmd ? `\n ${a.cmd} ${(a.args ?? []).join(' ')}` : ''}`);
583
+ }
584
+ if (!opts.yes && !process.stdin.isTTY) {
585
+ console.error('\nNot a terminal and --yes not given — nothing was changed. Re-run with: memesh setup --yes');
586
+ process.exit(1);
587
+ }
588
+ const confirmAll = Boolean(opts.yes);
589
+ let rl = null;
590
+ if (!confirmAll) {
591
+ const { createInterface } = await import('node:readline/promises');
592
+ rl = createInterface({ input: process.stdin, output: process.stdout });
593
+ }
594
+ let failed = false;
595
+ for (const st of pending) {
596
+ for (const action of st.actions) {
597
+ if (!confirmAll && rl) {
598
+ const answer = (await rl.question(`\n[${st.title}] ${action.label} — proceed? [y/N] `)).trim().toLowerCase();
599
+ if (answer !== 'y' && answer !== 'yes') {
600
+ console.log(' skipped');
601
+ continue;
602
+ }
603
+ }
604
+ if (action.kind === 'install-hooks') {
605
+ console.log(` ✅ ${wireUserHooks()}`);
606
+ }
607
+ else if (action.cmd) {
608
+ const r = runSeam(action.cmd, action.args ?? []);
609
+ if (r.status === 0)
610
+ console.log(` ✅ done (${action.cmd} ${(action.args ?? []).join(' ')})`);
611
+ else {
612
+ console.error(` ❌ ${action.cmd} exited ${r.status ?? 'without running'}${r.stderr ? `: ${r.stderr.trim()}` : ''}`);
613
+ failed = true;
614
+ }
615
+ }
616
+ }
617
+ }
618
+ rl?.close();
619
+ console.log('\nAfter wiring:');
620
+ statuses = inspectHosts(seams);
621
+ render(statuses);
622
+ process.exit(failed || !allWired(statuses) ? 1 : 0);
623
+ });
624
+ program
625
+ .command('task')
626
+ .description('Show or update where the work stands on this project')
627
+ .option('--project <name>', 'Project name (default: the current directory’s project)')
628
+ .option('--goal <text>', 'What this work is FOR — the outcome being aimed at')
629
+ .option('--next <text>', 'The next concrete step')
630
+ .option('--blocked <text>', 'What is standing in the way (pass "" to clear it once resolved)')
631
+ .option('--done <text>', 'What was just finished')
632
+ .option('--json', 'Output as JSON')
633
+ .action(async (opts) => {
634
+ await withDatabase(() => {
635
+ const patch = {};
636
+ for (const field of TASK_STATE_FIELDS) {
637
+ if (opts[field] !== undefined)
638
+ patch[field] = opts[field];
639
+ }
640
+ if (Object.keys(patch).length === 0) {
641
+ const { project, state } = getTaskState(opts.project);
642
+ if (opts.json) {
643
+ console.log(JSON.stringify({ project, state }));
644
+ return;
645
+ }
646
+ const lines = taskStateLines(state, project);
647
+ if (lines.length === 0) {
648
+ console.log(`Nothing recorded for "${project}" yet.\n` +
649
+ `Set it with: memesh task --goal "…" --next "…"`);
650
+ return;
651
+ }
652
+ console.log(lines.join('\n'));
653
+ return;
654
+ }
655
+ const result = setTaskState({ project: opts.project, patch, sourceHost: 'cli' });
656
+ if (opts.json) {
657
+ console.log(JSON.stringify(result));
658
+ return;
659
+ }
660
+ if (result.changed.length === 0) {
661
+ console.log(`No change — "${result.project}" already said exactly that.`);
662
+ return;
663
+ }
664
+ console.log(`Updated ${result.changed.join(', ')} for "${result.project}".`);
665
+ console.log(taskStateLines(result.state, result.project).join('\n'));
666
+ });
667
+ });
403
668
  const configCmd = program.command('config').description('Manage configuration');
404
669
  configCmd
405
670
  .command('list')
@@ -426,12 +691,12 @@ const ALLOWED_KEYS = new Set([
426
691
  'llm.apiKey',
427
692
  'llm.model',
428
693
  'embedder.provider',
429
- 'embedder.model',
430
694
  'autoUpdate',
431
695
  'sessionLimit',
432
696
  'autoCapture',
433
697
  'llmFallbacks',
434
698
  'language',
699
+ 'transcriptMining',
435
700
  ]);
436
701
  const KEY_VALIDATORS = {
437
702
  'llm.provider': (v) => ['anthropic', 'openai', 'ollama'].includes(v) ? null : `must be one of: anthropic, openai, ollama`,
@@ -498,7 +763,7 @@ function formatConfigValue(key, raw) {
498
763
  return JSON.stringify(raw);
499
764
  return String(raw);
500
765
  }
501
- export function buildConfigListing(config) {
766
+ function buildConfigListing(config) {
502
767
  const rows = [];
503
768
  for (const key of Array.from(ALLOWED_KEYS).sort()) {
504
769
  const raw = getNested(config, key.split('.'));
@@ -555,6 +820,9 @@ configCmd
555
820
  if (canonical === 'autoCapture') {
556
821
  coerced = value === 'true' || value === '1';
557
822
  }
823
+ if (canonical === 'transcriptMining') {
824
+ coerced = value === 'true' || value === '1';
825
+ }
558
826
  const config = readConfig();
559
827
  setNested(config, canonical.split('.'), coerced);
560
828
  writeConfig(config);
@@ -680,6 +948,53 @@ program
680
948
  process.exit(1);
681
949
  }
682
950
  });
951
+ program
952
+ .command('upgrade-plugin')
953
+ .description('Upgrade the Claude Code plugin install (finds and runs its bundled upgrade script)')
954
+ .action(async () => {
955
+ const { spawnSync } = await import('child_process');
956
+ const cacheRoot = path.join(homeDir(), '.claude', 'plugins', 'cache', 'pcircle-memesh', 'memesh');
957
+ let versions = [];
958
+ try {
959
+ versions = fs.readdirSync(cacheRoot, { withFileTypes: true })
960
+ .filter((entry) => entry.isDirectory() && /^\d+\.\d+\.\d+/.test(entry.name))
961
+ .map((entry) => entry.name);
962
+ }
963
+ catch { }
964
+ if (versions.length === 0) {
965
+ console.error('No Claude Code plugin install found (looked in ~/.claude/plugins/cache/pcircle-memesh).');
966
+ console.error('If you installed via npm, upgrade with: memesh update');
967
+ process.exit(1);
968
+ }
969
+ versions.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
970
+ const newest = versions[versions.length - 1];
971
+ const script = path.join(cacheRoot, newest, 'scripts', 'upgrade-plugin.sh');
972
+ if (!fs.existsSync(script)) {
973
+ console.error(`Plugin install found (v${newest}), but it has no scripts/upgrade-plugin.sh — plugin versions before 4.2.5 shipped without it.`);
974
+ console.error('Reinstall once from the Claude Code /plugin UI, or run the npm-global copy directly:');
975
+ console.error(' bash "$(npm prefix -g)/lib/node_modules/@pcircle/memesh/scripts/upgrade-plugin.sh"');
976
+ process.exit(1);
977
+ }
978
+ const installHints = {
979
+ node: 'node is required by the upgrade script. Install Node.js from https://nodejs.org',
980
+ npm: 'npm is required by the upgrade script. It ships with Node.js — reinstall from https://nodejs.org',
981
+ rsync: 'rsync is required by the upgrade script. macOS: already installed; Debian/Ubuntu: sudo apt install rsync',
982
+ };
983
+ const missing = Object.keys(installHints).filter((tool) => !isOnPath(tool));
984
+ if (missing.length > 0) {
985
+ for (const tool of missing)
986
+ console.error(installHints[tool]);
987
+ process.exit(1);
988
+ }
989
+ const run = spawnSync('bash', [script], { stdio: 'inherit' });
990
+ if (run.error) {
991
+ console.error(`Could not run the upgrade script: ${run.error.message}`);
992
+ console.error('bash is required to run it. If bash is available under another name, run it yourself:');
993
+ console.error(` bash ${script}`);
994
+ process.exit(1);
995
+ }
996
+ process.exit(run.status ?? 1);
997
+ });
683
998
  program
684
999
  .command('telemetry')
685
1000
  .description('Show LLM call telemetry (per-flow scorecard for the last N days)')
@@ -765,7 +1080,8 @@ kgCmd
765
1080
  .option('--session-cooccurrence', 'Rule 3: link high-signal orphans co-created in the same session')
766
1081
  .option('--name-tokens', 'Rule 4: link orphans sharing ≥3 name content tokens (or Jaccard ≥ 0.50)')
767
1082
  .option('--min-jaccard <n>', 'Jaccard threshold for name similarity (default 0.50)', parseFloat)
768
- .option('--all-rules', 'Enable all heuristic rules (Rules 1–4)')
1083
+ .option('--all-rules', 'Enable all heuristic rules (Rules 1–5)')
1084
+ .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)')
769
1085
  .option('--reset-idempotency', 'Clear the persistent "already-attempted" orphan cache before running (use after schema changes or to reconsider every orphan)')
770
1086
  .option('--json', 'Output as JSON')
771
1087
  .action(async (opts) => {
@@ -780,6 +1096,7 @@ kgCmd
780
1096
  dryRun: !!opts.dryRun,
781
1097
  includeSessionCooccurrence: allRules || !!opts.sessionCooccurrence,
782
1098
  includeNameTokenSimilarity: allRules || !!opts.nameTokens,
1099
+ includeEvidenceLinks: opts.evidenceLinks !== false,
783
1100
  minNameJaccard: opts.minJaccard,
784
1101
  resetIdempotency: !!opts.resetIdempotency,
785
1102
  };
@@ -819,6 +1136,7 @@ kgCmd
819
1136
  console.log(` project clustering: ${result.byRule.projectClustering}`);
820
1137
  console.log(` session co-occurrence: ${result.byRule.sessionCooccurrence}`);
821
1138
  console.log(` name token similarity: ${result.byRule.nameTokenSimilarity}`);
1139
+ console.log(` evidence links: ${result.byRule.evidenceLinks}`);
822
1140
  if (result.candidatesProposed > result.edgesWritten) {
823
1141
  console.log(` (${result.candidatesProposed - result.edgesWritten} candidates were already-existing edges; INSERT OR IGNORE skipped them.)`);
824
1142
  }
@@ -908,15 +1226,82 @@ program
908
1226
  .option('--probe-http', 'Also probe the local HTTP server health endpoint')
909
1227
  .option('--probe', 'Make one small live call to the configured LLM to confirm it actually answers')
910
1228
  .option('--url <url>', 'Base URL for --probe-http', 'http://127.0.0.1:3737')
1229
+ .option('--fix', 'Apply the whitelisted fixes doctor prescribes (asks per fix; --yes skips asking)')
1230
+ .option('--yes', 'With --fix: apply without asking')
911
1231
  .action(async (opts) => {
912
1232
  const { formatDoctorReport, runDoctor } = await import('../../core/doctor.js');
913
- const result = await runDoctor({
1233
+ let result = await runDoctor({
914
1234
  packageRoot,
915
1235
  packageVersion: pkg.version,
916
1236
  probeHttp: opts.probeHttp,
917
1237
  probeCapabilities: opts.probe,
918
1238
  httpBaseUrl: opts.url,
919
1239
  });
1240
+ if (opts.fix) {
1241
+ const FIX_ACTIONS = {
1242
+ 'install-hooks': wireUserHooks,
1243
+ 'fts-rebuild': () => {
1244
+ openDatabase();
1245
+ try {
1246
+ return `keyword index rebuilt (${reindexFts().entities} entities)`;
1247
+ }
1248
+ finally {
1249
+ closeDatabase();
1250
+ }
1251
+ },
1252
+ 'chmod-db': () => {
1253
+ fs.chmodSync(getDbPath(), 0o600);
1254
+ return `permissions restored: chmod 600 ${getDbPath()}`;
1255
+ },
1256
+ };
1257
+ const fixable = result.checks.filter((c) => c.fixId && (c.status === 'warn' || c.status === 'fail'));
1258
+ if (fixable.length === 0) {
1259
+ console.log('Nothing on the --fix whitelist to apply.');
1260
+ }
1261
+ else {
1262
+ if (!opts.yes && !process.stdin.isTTY) {
1263
+ console.error('Not a terminal and --yes not given — nothing was changed. Re-run with: memesh doctor --fix --yes');
1264
+ process.exit(1);
1265
+ }
1266
+ let rl = null;
1267
+ if (!opts.yes) {
1268
+ const { createInterface } = await import('node:readline/promises');
1269
+ rl = createInterface({ input: process.stdin, output: process.stdout });
1270
+ }
1271
+ for (const check of fixable) {
1272
+ console.log(`\n${check.label}: ${check.summary}`);
1273
+ if (rl) {
1274
+ const answer = (await rl.question(`Apply fix (${check.fixId})? [y/N] `)).trim().toLowerCase();
1275
+ if (answer !== 'y' && answer !== 'yes') {
1276
+ console.log(' skipped');
1277
+ continue;
1278
+ }
1279
+ }
1280
+ try {
1281
+ console.log(` ✅ ${FIX_ACTIONS[check.fixId]()}`);
1282
+ }
1283
+ catch (err) {
1284
+ console.error(` ❌ ${err instanceof Error ? err.message : String(err)}`);
1285
+ }
1286
+ }
1287
+ rl?.close();
1288
+ console.log('\nAfter fixes:');
1289
+ const before = new Map(result.checks.map((c) => [c.id, c.status]));
1290
+ const fixedIds = new Set(fixable.map((c) => c.id));
1291
+ result = await runDoctor({
1292
+ packageRoot,
1293
+ packageVersion: pkg.version,
1294
+ probeHttp: false,
1295
+ probeCapabilities: false,
1296
+ httpBaseUrl: opts.url,
1297
+ });
1298
+ for (const c of result.checks) {
1299
+ const was = before.get(c.id);
1300
+ if (fixedIds.has(c.id) && was && was !== c.status)
1301
+ console.log(` ${c.label}: ${was} → ${c.status}`);
1302
+ }
1303
+ }
1304
+ }
920
1305
  if (opts.json) {
921
1306
  console.log(JSON.stringify(result, null, 2));
922
1307
  }
@@ -1151,7 +1536,8 @@ dreamCmd
1151
1536
  console.log(`${proposals.length} ${opts.status} proposal(s):`);
1152
1537
  console.log('');
1153
1538
  for (const p of proposals) {
1154
- const srcLabel = p.source_kind === 'transcript' ? ' (transcript)' : '';
1539
+ const srcLabel = p.kind === 'relation' ? ' (conflict)'
1540
+ : p.source_kind === 'transcript' ? ' (transcript)' : '';
1155
1541
  console.log(` #${p.id} [${p.project}/${p.cluster_key}]${srcLabel} ${p.source_count} source(s) → "${p.digest_name}"`);
1156
1542
  if (p.digest_observations_preview !== null) {
1157
1543
  console.log(` ${p.digest_observations_preview}`);
@@ -1162,6 +1548,49 @@ dreamCmd
1162
1548
  console.log(`Inspect: memesh dream show <id> | Apply: memesh dream accept <id> | Reject: memesh dream reject <id>`);
1163
1549
  });
1164
1550
  });
1551
+ dreamCmd
1552
+ .command('conflicts')
1553
+ .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))
1555
+ .option('--dry-run', 'Show how many candidates are queued without calling an LLM or writing anything')
1556
+ .action(async (opts) => {
1557
+ await withDatabase(async () => {
1558
+ const cfg = readConfig();
1559
+ const llm = detectCapabilities().llm;
1560
+ if (!llm) {
1561
+ console.error('No LLM configured. Run `memesh config set llm.provider <anthropic|openai|ollama>` first (or set ANTHROPIC_API_KEY / OPENAI_API_KEY).');
1562
+ console.error('LLM is required for `memesh dream conflicts` because "do these two entries disagree" is a semantic judgement, not a rule.');
1563
+ process.exit(1);
1564
+ }
1565
+ const { judgeConflicts, CONFLICT_JUDGE_MAX_PAIRS } = await import('../../core/conflict-judge.js');
1566
+ const { getDatabase } = await import('../../db.js');
1567
+ const result = await judgeConflicts(getDatabase(), llm, {
1568
+ maxPairs: typeof opts.maxPairs === 'number' && !Number.isNaN(opts.maxPairs) ? opts.maxPairs : CONFLICT_JUDGE_MAX_PAIRS,
1569
+ dryRun: !!opts.dryRun,
1570
+ fallbacks: cfg.llmFallbacks,
1571
+ });
1572
+ console.log(`${opts.dryRun ? '[dry-run] ' : ''}Conflict pass complete in ${result.durationMs}ms`);
1573
+ console.log(` candidate pairs available: ${result.candidatesAvailable}`);
1574
+ if (opts.dryRun) {
1575
+ console.log(' (dry run — no LLM called, nothing judged or written)');
1576
+ return;
1577
+ }
1578
+ console.log(` LLM calls: ${result.llmCalls}`);
1579
+ console.log(` judged: ${result.judged} (${result.unrelated} unrelated, remembered so they are never re-bought)`);
1580
+ console.log(` staged: ${result.staged} relation proposal(s)`);
1581
+ if (result.llmFailures > 0) {
1582
+ console.log(` failed: ${result.llmFailures} (unparseable or errored LLM responses; those pairs stay at the head of the candidate list and are retried next run)`);
1583
+ }
1584
+ if (result.aborted) {
1585
+ console.error(` ABORTED after the work above: ${result.aborted}`);
1586
+ process.exit(1);
1587
+ }
1588
+ if (result.staged > 0) {
1589
+ console.log('');
1590
+ console.log('Review: memesh dream list | Apply: memesh dream accept <id> | Reject: memesh dream reject <id>');
1591
+ }
1592
+ });
1593
+ });
1165
1594
  dreamCmd
1166
1595
  .command('show <id>')
1167
1596
  .description('Show a proposal in full — name, type, ALL observations, tags, source — so you can review the whole thing before accepting')
@@ -1183,6 +1612,31 @@ dreamCmd
1183
1612
  console.log(`Proposal #${detail.id} [${detail.project}/${detail.cluster_key}] source: ${detail.source_kind} status: ${detail.status}`);
1184
1613
  console.log(`created: ${detail.created_at}`);
1185
1614
  console.log('');
1615
+ if (detail.kind === 'relation') {
1616
+ const rel = detail.relation;
1617
+ if (!rel) {
1618
+ console.error('relation payload is corrupt — reject this proposal');
1619
+ process.exit(1);
1620
+ }
1621
+ const [fromE, toE] = rel.relation_type === 'supersedes' && rel.direction === 'b_supersedes_a' ? [rel.b, rel.a] : [rel.a, rel.b];
1622
+ console.log(`verdict: ${rel.verdict} (severity: ${rel.severity ?? 'unknown'})`);
1623
+ console.log(`accepting creates: ${fromE?.name} —${rel.relation_type}→ ${toE?.name}`);
1624
+ if (rel.relation_type === 'supersedes') {
1625
+ console.log(` survivor: ${fromE?.name} (the arrow points from the surviving claim to the obsolete one)`);
1626
+ }
1627
+ console.log(`rationale: ${rel.rationale ?? '(none given)'}`);
1628
+ if (rel.recommended_action)
1629
+ console.log(`recommended action: ${rel.recommended_action}`);
1630
+ if (rel.excerpts?.a || rel.excerpts?.b) {
1631
+ console.log(`excerpt A (${rel.a?.name}): ${rel.excerpts?.a ?? ''}`);
1632
+ console.log(`excerpt B (${rel.b?.name}): ${rel.excerpts?.b ?? ''}`);
1633
+ }
1634
+ if (typeof rel.cosine_distance === 'number')
1635
+ console.log(`cosine distance: ${rel.cosine_distance.toFixed(3)}`);
1636
+ console.log('');
1637
+ console.log(`Accept: memesh dream accept ${detail.id} | Reject: memesh dream reject ${detail.id}`);
1638
+ return;
1639
+ }
1186
1640
  console.log(`name: ${detail.digest.name}`);
1187
1641
  console.log(`type: ${detail.digest.type}`);
1188
1642
  console.log(`observations (${detail.digest.observations.length}):`);
@@ -1350,7 +1804,7 @@ program
1350
1804
  }
1351
1805
  }
1352
1806
  body = redactUserPaths(redactSecrets(body));
1353
- const url = `https://github.com/PCIRCLE-AI/memesh-llm-memory/issues/new?title=${encodeURIComponent(`[${typeLabel}] `)}&body=${encodeURIComponent(body)}&labels=${encodeURIComponent(labels)}`;
1807
+ const url = `https://github.com/PCIRCLE-AI/memesh/issues/new?title=${encodeURIComponent(`[${typeLabel}] `)}&body=${encodeURIComponent(body)}&labels=${encodeURIComponent(labels)}`;
1354
1808
  if (opts.open === false) {
1355
1809
  console.log(url);
1356
1810
  return;
@@ -1383,17 +1837,60 @@ program
1383
1837
  .description('Regenerate vector embeddings for all entities (--fts rebuilds the keyword index instead)')
1384
1838
  .option('--namespace <namespace>', 'Reindex only entities in this namespace')
1385
1839
  .option('--fts', 'Rebuild the full-text keyword index instead of the vector index')
1386
- .option('--vectors', 'Also rebuild the vector index at the configured dimension. DESTRUCTIVE: drops every ' +
1387
- 'stored embedding first. Needed only when switching embedding providers.')
1840
+ .option('--discard-generation', 'Throw away a half-built vector index left by an interrupted rebuild, without rebuilding')
1388
1841
  .option('--json', 'Output as JSON')
1842
+ .addOption(new Option('--vectors').hideHelp())
1389
1843
  .action(async (opts) => {
1844
+ if (opts.vectors) {
1845
+ if (opts.json) {
1846
+ console.log(JSON.stringify({
1847
+ refused: true,
1848
+ reason: 'the --vectors flag is retired',
1849
+ fix: 'run plain memesh reindex; to change provider first: memesh config set embedder.provider ollama|openai',
1850
+ indexTouched: false,
1851
+ }));
1852
+ process.exit(1);
1853
+ }
1854
+ console.error('`memesh reindex --vectors` has been retired.');
1855
+ console.error('');
1856
+ console.error('It existed to consent to dropping every stored embedding before a refill.');
1857
+ console.error('A rebuild now happens beside the live index and replaces it only when');
1858
+ console.error('complete, so there is nothing left to consent to.');
1859
+ console.error('');
1860
+ console.error('Run plain `memesh reindex`. To change embedding provider first:');
1861
+ console.error(' memesh config set embedder.provider ollama (or openai)');
1862
+ process.exit(1);
1863
+ }
1390
1864
  requireOneOf(opts.namespace, NAMESPACES, '--namespace');
1865
+ if (opts.discardGeneration) {
1866
+ await withDatabase(async () => {
1867
+ const read = readVectorGeneration();
1868
+ const staged = generationRowIds().size;
1869
+ if (read.state === 'none' && staged === 0) {
1870
+ if (opts.json)
1871
+ console.log(JSON.stringify({ discarded: false, staged: 0 }));
1872
+ else
1873
+ console.log('Nothing to discard: there is no half-built vector index.');
1874
+ return;
1875
+ }
1876
+ const describe = read.state === 'open'
1877
+ ? `${read.info.dimension}-dim, provider ${read.info.provider}, started ${read.info.startedAt}`
1878
+ : read.state === 'unreadable'
1879
+ ? `marker unreadable (${read.detail})`
1880
+ : 'no marker';
1881
+ discardVectorGeneration();
1882
+ if (opts.json) {
1883
+ console.log(JSON.stringify({ discarded: true, staged, generation: read, liveIndexTouched: false }));
1884
+ }
1885
+ else {
1886
+ console.log(`Discarded a half-built vector index: ${staged} staged vectors (${describe}).\n` +
1887
+ ' Your live index was not touched. Run `memesh reindex` to build a new one.');
1888
+ }
1889
+ });
1890
+ return;
1891
+ }
1391
1892
  try {
1392
1893
  if (opts.fts) {
1393
- if (opts.vectors) {
1394
- console.error('❌ --fts and --vectors rebuild different indexes. Run them separately.');
1395
- process.exit(1);
1396
- }
1397
1894
  await withDatabase(async () => {
1398
1895
  const { entities } = reindexFts();
1399
1896
  if (opts.json) {
@@ -1405,68 +1902,84 @@ program
1405
1902
  });
1406
1903
  return;
1407
1904
  }
1408
- if (opts.vectors) {
1409
- if (opts.namespace) {
1410
- console.error('❌ --vectors rebuilds the whole vector index, which is shared by every\n' +
1411
- ' namespace, so it cannot be limited to one. Combining it with\n' +
1412
- ' --namespace would delete the other namespaces\' embeddings and never\n' +
1413
- ' regenerate them. Run `memesh reindex --vectors` on its own, or drop\n' +
1414
- ' --vectors to reindex just this namespace at the current dimension.');
1415
- process.exit(1);
1905
+ if (!opts.namespace && !(await canRefillVectorIndex())) {
1906
+ const embedder = detectCapabilities().embeddings;
1907
+ const written = readConfig().embedder?.provider;
1908
+ const known = embedder === 'openai' || embedder === 'ollama';
1909
+ const invalid = written !== undefined && !known;
1910
+ const unconfigured = written === undefined && !known;
1911
+ const reason = invalid
1912
+ ? `embedder.provider is set to '${written}', which is not a provider MeMesh knows`
1913
+ : unconfigured
1914
+ ? 'no embedding provider is configured, so there is nothing to build vectors with'
1915
+ : 'could not produce a test embedding at the configured vector width';
1916
+ const fix = invalid || unconfigured
1917
+ ? (invalid ? 'Set it to one MeMesh knows: ' : 'Pick one first: ') +
1918
+ '`memesh config set embedder.provider ollama` (local, needs `ollama serve`) ' +
1919
+ 'or `memesh config set embedder.provider openai` (needs OPENAI_API_KEY). Until then, recall ' +
1920
+ 'runs on keyword search, which needs no rebuild.'
1921
+ : 'Check that Ollama is running (or that your OpenAI API key is valid), then run this again. ' +
1922
+ '`memesh doctor` reports which provider is configured.';
1923
+ if (opts.json) {
1924
+ console.log(JSON.stringify({ refused: true, reason, fix, indexTouched: false }));
1416
1925
  }
1417
- if (!(await allowVectorIndexRebuild(getDbPath(), canRefillVectorIndex))) {
1418
- console.error('❌ Could not produce a test embedding at this database\'s vector width, so\n' +
1419
- ' the index was left untouched. Rebuilding it deletes every stored\n' +
1420
- ' embedding, and nothing here could regenerate them.\n' +
1421
- ' Check that Ollama is running (or that your OpenAI API key is valid) —\n' +
1422
- ' then run this again. `memesh doctor` reports which provider is configured.');
1423
- process.exit(1);
1926
+ else {
1927
+ console.error(`❌ Nothing was rebuilt: ${reason}.\n` +
1928
+ ' Your existing index is untouched and still answering queries.\n' +
1929
+ ` ${fix}`);
1424
1930
  }
1931
+ process.exit(1);
1425
1932
  }
1426
1933
  await withDatabase(async () => {
1427
1934
  const result = await reindex({ namespace: opts.namespace });
1428
- const incomplete = result.missingVectors > 0 || result.failed > 0;
1935
+ const incomplete = result.missingVectors > 0
1936
+ || result.failed > 0
1937
+ || result.generationSwapped === false
1938
+ || result.abortedAfter !== null;
1429
1939
  if (opts.json) {
1430
1940
  console.log(JSON.stringify(result));
1431
1941
  }
1432
- else if (incomplete) {
1433
- console.log(`⚠️ Reindex incomplete:`);
1942
+ else {
1943
+ console.log(incomplete ? `⚠️ Reindex incomplete:` : `✅ Reindex complete:`);
1434
1944
  console.log(` Processed: ${result.processed}`);
1435
1945
  console.log(` Embedded: ${result.embedded}`);
1436
1946
  console.log(` Skipped: ${result.skipped}`);
1437
- if (result.missingVectors > 0) {
1438
- console.log(` Still without a vector: ${result.missingVectors}`);
1439
- }
1440
- if (result.failed > 0 && result.missingVectors === 0) {
1441
- console.log(` Could not be regenerated: ${result.failed} ` +
1442
- `(these still hold their previous embedding)`);
1947
+ if (incomplete) {
1948
+ if (result.abortedAfter !== null) {
1949
+ console.log(` Stopped early after ${result.abortedAfter} entities: the provider failed ` +
1950
+ `repeatedly. Everything embedded so far is kept — run this again to continue.`);
1951
+ }
1952
+ if (result.generationSwapped === false) {
1953
+ console.log(` The new index was NOT switched in, so your existing index is untouched ` +
1954
+ `and still answering queries.`);
1955
+ }
1956
+ if (result.missingVectors > 0) {
1957
+ console.log(` Still without a vector: ${result.missingVectors}`);
1958
+ }
1959
+ if (result.failed > 0 && result.missingVectors === 0) {
1960
+ console.log(` Could not be regenerated: ${result.failed} ` +
1961
+ `(these still hold their previous embedding)`);
1962
+ }
1963
+ for (const [outcome, count] of Object.entries(result.outcomes)) {
1964
+ if (outcome !== 'stored' && count > 0)
1965
+ console.log(` ${outcome}: ${count}`);
1966
+ }
1443
1967
  }
1444
- for (const [outcome, count] of Object.entries(result.outcomes)) {
1445
- if (outcome !== 'stored' && count > 0)
1446
- console.log(` ${outcome}: ${count}`);
1968
+ else if (!result.pendingReindexCleared) {
1969
+ console.log(` Note: ${result.missingVectorsDatabaseWide} memories in other namespaces still ` +
1970
+ `have no vector, so the reindex-needed flag stays set.`);
1447
1971
  }
1448
1972
  }
1449
- else if (!result.pendingReindexCleared) {
1450
- console.log(`✅ Reindex complete:`);
1451
- console.log(` Processed: ${result.processed}`);
1452
- console.log(` Embedded: ${result.embedded}`);
1453
- console.log(` Skipped: ${result.skipped}`);
1454
- console.log(` Note: ${result.missingVectorsDatabaseWide} memories in other namespaces still ` +
1455
- `have no vector, so the reindex-needed flag stays set.`);
1456
- }
1457
- else {
1458
- console.log(`✅ Reindex complete:`);
1459
- console.log(` Processed: ${result.processed}`);
1460
- console.log(` Embedded: ${result.embedded}`);
1461
- console.log(` Skipped: ${result.skipped}`);
1462
- }
1463
1973
  if (incomplete)
1464
1974
  process.exitCode = 1;
1465
1975
  });
1466
1976
  }
1467
1977
  catch (err) {
1468
1978
  if (err instanceof Error) {
1469
- console.error(`❌ Reindex failed: ${err.message}`);
1979
+ if (opts.json)
1980
+ console.log(JSON.stringify({ refused: true, reason: err.message, indexTouched: false }));
1981
+ else
1982
+ console.error(`❌ Reindex failed: ${err.message}`);
1470
1983
  process.exit(1);
1471
1984
  }
1472
1985
  throw err;