@pcircle/memesh 4.6.0 → 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 (92) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +21 -0
  4. package/README.de.md +12 -13
  5. package/README.md +12 -14
  6. package/README.zh-TW.md +12 -13
  7. package/dashboard/dist/index.html +14 -13
  8. package/dist/core/analytics.d.ts +9 -0
  9. package/dist/core/analytics.d.ts.map +1 -1
  10. package/dist/core/analytics.js +36 -18
  11. package/dist/core/analytics.js.map +1 -1
  12. package/dist/core/briefing.d.ts.map +1 -1
  13. package/dist/core/briefing.js +1 -0
  14. package/dist/core/briefing.js.map +1 -1
  15. package/dist/core/config.d.ts +0 -1
  16. package/dist/core/config.d.ts.map +1 -1
  17. package/dist/core/config.js.map +1 -1
  18. package/dist/core/demo.d.ts.map +1 -1
  19. package/dist/core/demo.js +1 -1
  20. package/dist/core/demo.js.map +1 -1
  21. package/dist/core/doctor.d.ts.map +1 -1
  22. package/dist/core/doctor.js +25 -6
  23. package/dist/core/doctor.js.map +1 -1
  24. package/dist/core/dreamer.d.ts +2 -2
  25. package/dist/core/dreamer.d.ts.map +1 -1
  26. package/dist/core/dreamer.js +221 -0
  27. package/dist/core/dreamer.js.map +1 -1
  28. package/dist/core/embedder.d.ts +4 -1
  29. package/dist/core/embedder.d.ts.map +1 -1
  30. package/dist/core/embedder.js +67 -17
  31. package/dist/core/embedder.js.map +1 -1
  32. package/dist/core/graph.d.ts +12 -0
  33. package/dist/core/graph.d.ts.map +1 -1
  34. package/dist/core/graph.js +56 -1
  35. package/dist/core/graph.js.map +1 -1
  36. package/dist/core/guards.d.ts +20 -0
  37. package/dist/core/guards.d.ts.map +1 -0
  38. package/dist/core/guards.js +103 -0
  39. package/dist/core/guards.js.map +1 -0
  40. package/dist/core/kg-backfill.d.ts +5 -1
  41. package/dist/core/kg-backfill.d.ts.map +1 -1
  42. package/dist/core/kg-backfill.js +155 -2
  43. package/dist/core/kg-backfill.js.map +1 -1
  44. package/dist/core/operations.d.ts +13 -2
  45. package/dist/core/operations.d.ts.map +1 -1
  46. package/dist/core/operations.js +93 -15
  47. package/dist/core/operations.js.map +1 -1
  48. package/dist/core/serializer.d.ts.map +1 -1
  49. package/dist/core/serializer.js +8 -0
  50. package/dist/core/serializer.js.map +1 -1
  51. package/dist/core/types.d.ts +1 -0
  52. package/dist/core/types.d.ts.map +1 -1
  53. package/dist/core/why.d.ts +54 -0
  54. package/dist/core/why.d.ts.map +1 -0
  55. package/dist/core/why.js +168 -0
  56. package/dist/core/why.js.map +1 -0
  57. package/dist/core/work-topology.d.ts +3 -0
  58. package/dist/core/work-topology.d.ts.map +1 -1
  59. package/dist/core/work-topology.js +11 -2
  60. package/dist/core/work-topology.js.map +1 -1
  61. package/dist/db.d.ts +31 -4
  62. package/dist/db.d.ts.map +1 -1
  63. package/dist/db.js +163 -31
  64. package/dist/db.js.map +1 -1
  65. package/dist/skills-manifest.json +29 -19
  66. package/dist/storage/sqlite.d.ts.map +1 -1
  67. package/dist/storage/sqlite.js +1 -1
  68. package/dist/storage/sqlite.js.map +1 -1
  69. package/dist/transports/cli/cli.js +198 -61
  70. package/dist/transports/cli/cli.js.map +1 -1
  71. package/dist/transports/http/server.d.ts.map +1 -1
  72. package/dist/transports/http/server.js +43 -5
  73. package/dist/transports/http/server.js.map +1 -1
  74. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  75. package/dist/transports/mcp/handlers.js +2 -2
  76. package/dist/transports/mcp/handlers.js.map +1 -1
  77. package/dist/transports/schemas.d.ts +8 -0
  78. package/dist/transports/schemas.d.ts.map +1 -1
  79. package/dist/transports/schemas.js +7 -0
  80. package/dist/transports/schemas.js.map +1 -1
  81. package/hooks/hooks.json +10 -0
  82. package/package.json +1 -1
  83. package/scripts/hooks/_generated/guards.js +110 -0
  84. package/scripts/hooks/_generated/sqlite.js +1 -1
  85. package/scripts/hooks/_generated/work-topology.js +11 -2
  86. package/scripts/hooks/_shared.js +77 -4
  87. package/scripts/hooks/guard-check.js +76 -0
  88. package/scripts/hooks/post-commit.js +27 -0
  89. package/scripts/hooks/pre-edit-recall.js +157 -121
  90. package/scripts/hooks/session-start.js +34 -23
  91. package/scripts/hooks/session-summary.js +43 -90
  92. package/skills/memesh/SKILL.md +11 -0
@@ -1,14 +1,14 @@
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, homeDir, 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
14
  import { assembleBriefing } from '../../core/briefing.js';
@@ -159,7 +159,7 @@ program
159
159
  .action(async (query, opts) => {
160
160
  requireOneOf(opts.namespace, NAMESPACES, '--namespace');
161
161
  await withDatabase(async () => {
162
- const { entities, conflicts } = await recallWithConflicts({
162
+ const { entities, conflicts, retrieval } = await recallWithConflicts({
163
163
  query: query || undefined,
164
164
  tag: opts.tag,
165
165
  limit: parseInt(opts.limit),
@@ -168,12 +168,7 @@ program
168
168
  cross_project: opts.crossProject,
169
169
  });
170
170
  if (opts.json) {
171
- if (conflicts.length > 0) {
172
- console.log(JSON.stringify({ entities, conflicts }));
173
- }
174
- else {
175
- console.log(JSON.stringify(entities));
176
- }
171
+ console.log(JSON.stringify(conflicts.length > 0 ? { entities, retrieval, conflicts } : { entities, retrieval }));
177
172
  }
178
173
  else if (entities.length === 0) {
179
174
  console.log('No results found.');
@@ -203,7 +198,11 @@ program
203
198
  console.log(` ... +${e.observations.length - 3} more`);
204
199
  }
205
200
  }
206
- 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
+ }
207
206
  if (conflicts.length > 0) {
208
207
  console.log('\nWarning: Conflicts detected:');
209
208
  for (const c of conflicts) {
@@ -450,6 +449,83 @@ program
450
449
  console.log(result.text);
451
450
  });
452
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
+ });
453
529
  program
454
530
  .command('setup')
455
531
  .description('Detect Claude Code / Codex / Gemini on this machine, wire memesh into each, and verify')
@@ -615,7 +691,6 @@ const ALLOWED_KEYS = new Set([
615
691
  'llm.apiKey',
616
692
  'llm.model',
617
693
  'embedder.provider',
618
- 'embedder.model',
619
694
  'autoUpdate',
620
695
  'sessionLimit',
621
696
  'autoCapture',
@@ -1005,7 +1080,8 @@ kgCmd
1005
1080
  .option('--session-cooccurrence', 'Rule 3: link high-signal orphans co-created in the same session')
1006
1081
  .option('--name-tokens', 'Rule 4: link orphans sharing ≥3 name content tokens (or Jaccard ≥ 0.50)')
1007
1082
  .option('--min-jaccard <n>', 'Jaccard threshold for name similarity (default 0.50)', parseFloat)
1008
- .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)')
1009
1085
  .option('--reset-idempotency', 'Clear the persistent "already-attempted" orphan cache before running (use after schema changes or to reconsider every orphan)')
1010
1086
  .option('--json', 'Output as JSON')
1011
1087
  .action(async (opts) => {
@@ -1020,6 +1096,7 @@ kgCmd
1020
1096
  dryRun: !!opts.dryRun,
1021
1097
  includeSessionCooccurrence: allRules || !!opts.sessionCooccurrence,
1022
1098
  includeNameTokenSimilarity: allRules || !!opts.nameTokens,
1099
+ includeEvidenceLinks: opts.evidenceLinks !== false,
1023
1100
  minNameJaccard: opts.minJaccard,
1024
1101
  resetIdempotency: !!opts.resetIdempotency,
1025
1102
  };
@@ -1059,6 +1136,7 @@ kgCmd
1059
1136
  console.log(` project clustering: ${result.byRule.projectClustering}`);
1060
1137
  console.log(` session co-occurrence: ${result.byRule.sessionCooccurrence}`);
1061
1138
  console.log(` name token similarity: ${result.byRule.nameTokenSimilarity}`);
1139
+ console.log(` evidence links: ${result.byRule.evidenceLinks}`);
1062
1140
  if (result.candidatesProposed > result.edgesWritten) {
1063
1141
  console.log(` (${result.candidatesProposed - result.edgesWritten} candidates were already-existing edges; INSERT OR IGNORE skipped them.)`);
1064
1142
  }
@@ -1759,17 +1837,60 @@ program
1759
1837
  .description('Regenerate vector embeddings for all entities (--fts rebuilds the keyword index instead)')
1760
1838
  .option('--namespace <namespace>', 'Reindex only entities in this namespace')
1761
1839
  .option('--fts', 'Rebuild the full-text keyword index instead of the vector index')
1762
- .option('--vectors', 'Also rebuild the vector index at the configured dimension. DESTRUCTIVE: drops every ' +
1763
- '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')
1764
1841
  .option('--json', 'Output as JSON')
1842
+ .addOption(new Option('--vectors').hideHelp())
1765
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
+ }
1766
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
+ }
1767
1892
  try {
1768
1893
  if (opts.fts) {
1769
- if (opts.vectors) {
1770
- console.error('❌ --fts and --vectors rebuild different indexes. Run them separately.');
1771
- process.exit(1);
1772
- }
1773
1894
  await withDatabase(async () => {
1774
1895
  const { entities } = reindexFts();
1775
1896
  if (opts.json) {
@@ -1781,68 +1902,84 @@ program
1781
1902
  });
1782
1903
  return;
1783
1904
  }
1784
- if (opts.vectors) {
1785
- if (opts.namespace) {
1786
- console.error('❌ --vectors rebuilds the whole vector index, which is shared by every\n' +
1787
- ' namespace, so it cannot be limited to one. Combining it with\n' +
1788
- ' --namespace would delete the other namespaces\' embeddings and never\n' +
1789
- ' regenerate them. Run `memesh reindex --vectors` on its own, or drop\n' +
1790
- ' --vectors to reindex just this namespace at the current dimension.');
1791
- 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 }));
1792
1925
  }
1793
- if (!(await allowVectorIndexRebuild(getDbPath(), canRefillVectorIndex))) {
1794
- console.error('❌ Could not produce a test embedding at this database\'s vector width, so\n' +
1795
- ' the index was left untouched. Rebuilding it deletes every stored\n' +
1796
- ' embedding, and nothing here could regenerate them.\n' +
1797
- ' Check that Ollama is running (or that your OpenAI API key is valid) —\n' +
1798
- ' then run this again. `memesh doctor` reports which provider is configured.');
1799
- 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}`);
1800
1930
  }
1931
+ process.exit(1);
1801
1932
  }
1802
1933
  await withDatabase(async () => {
1803
1934
  const result = await reindex({ namespace: opts.namespace });
1804
- 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;
1805
1939
  if (opts.json) {
1806
1940
  console.log(JSON.stringify(result));
1807
1941
  }
1808
- else if (incomplete) {
1809
- console.log(`⚠️ Reindex incomplete:`);
1942
+ else {
1943
+ console.log(incomplete ? `⚠️ Reindex incomplete:` : `✅ Reindex complete:`);
1810
1944
  console.log(` Processed: ${result.processed}`);
1811
1945
  console.log(` Embedded: ${result.embedded}`);
1812
1946
  console.log(` Skipped: ${result.skipped}`);
1813
- if (result.missingVectors > 0) {
1814
- console.log(` Still without a vector: ${result.missingVectors}`);
1815
- }
1816
- if (result.failed > 0 && result.missingVectors === 0) {
1817
- console.log(` Could not be regenerated: ${result.failed} ` +
1818
- `(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
+ }
1819
1967
  }
1820
- for (const [outcome, count] of Object.entries(result.outcomes)) {
1821
- if (outcome !== 'stored' && count > 0)
1822
- 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.`);
1823
1971
  }
1824
1972
  }
1825
- else if (!result.pendingReindexCleared) {
1826
- console.log(`✅ Reindex complete:`);
1827
- console.log(` Processed: ${result.processed}`);
1828
- console.log(` Embedded: ${result.embedded}`);
1829
- console.log(` Skipped: ${result.skipped}`);
1830
- console.log(` Note: ${result.missingVectorsDatabaseWide} memories in other namespaces still ` +
1831
- `have no vector, so the reindex-needed flag stays set.`);
1832
- }
1833
- else {
1834
- console.log(`✅ Reindex complete:`);
1835
- console.log(` Processed: ${result.processed}`);
1836
- console.log(` Embedded: ${result.embedded}`);
1837
- console.log(` Skipped: ${result.skipped}`);
1838
- }
1839
1973
  if (incomplete)
1840
1974
  process.exitCode = 1;
1841
1975
  });
1842
1976
  }
1843
1977
  catch (err) {
1844
1978
  if (err instanceof Error) {
1845
- 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}`);
1846
1983
  process.exit(1);
1847
1984
  }
1848
1985
  throw err;