@pcircle/memesh 4.2.6 → 4.2.8

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 (86) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.de.md +106 -2
  4. package/README.es.md +106 -2
  5. package/README.fr.md +106 -2
  6. package/README.ja.md +106 -2
  7. package/README.ko.md +106 -2
  8. package/README.md +89 -3
  9. package/README.pt.md +106 -2
  10. package/README.th.md +124 -6
  11. package/README.vi.md +106 -2
  12. package/README.zh-CN.md +105 -2
  13. package/README.zh-TW.md +105 -2
  14. package/dashboard/dist/index.html +7 -7
  15. package/dist/cli/view-live.d.ts.map +1 -1
  16. package/dist/cli/view-live.js +3 -1
  17. package/dist/cli/view-live.js.map +1 -1
  18. package/dist/core/analytics.d.ts +0 -31
  19. package/dist/core/analytics.d.ts.map +1 -1
  20. package/dist/core/analytics.js +0 -59
  21. package/dist/core/analytics.js.map +1 -1
  22. package/dist/core/config.d.ts +0 -1
  23. package/dist/core/config.d.ts.map +1 -1
  24. package/dist/core/config.js +25 -4
  25. package/dist/core/config.js.map +1 -1
  26. package/dist/core/digest-validator.d.ts +1 -1
  27. package/dist/core/digest-validator.d.ts.map +1 -1
  28. package/dist/core/digest-validator.js +7 -2
  29. package/dist/core/digest-validator.js.map +1 -1
  30. package/dist/core/doctor.d.ts +6 -0
  31. package/dist/core/doctor.d.ts.map +1 -1
  32. package/dist/core/doctor.js +121 -5
  33. package/dist/core/doctor.js.map +1 -1
  34. package/dist/core/dreamer.d.ts.map +1 -1
  35. package/dist/core/dreamer.js.map +1 -1
  36. package/dist/core/embedder.d.ts +1 -0
  37. package/dist/core/embedder.d.ts.map +1 -1
  38. package/dist/core/embedder.js +14 -2
  39. package/dist/core/embedder.js.map +1 -1
  40. package/dist/core/extractor.d.ts.map +1 -1
  41. package/dist/core/extractor.js +10 -1
  42. package/dist/core/extractor.js.map +1 -1
  43. package/dist/core/failure-analyzer.d.ts.map +1 -1
  44. package/dist/core/failure-analyzer.js +16 -2
  45. package/dist/core/failure-analyzer.js.map +1 -1
  46. package/dist/core/install-hooks.d.ts +6 -0
  47. package/dist/core/install-hooks.d.ts.map +1 -1
  48. package/dist/core/install-hooks.js +37 -0
  49. package/dist/core/install-hooks.js.map +1 -1
  50. package/dist/core/llm-telemetry.d.ts +12 -0
  51. package/dist/core/llm-telemetry.d.ts.map +1 -1
  52. package/dist/core/llm-telemetry.js +21 -3
  53. package/dist/core/llm-telemetry.js.map +1 -1
  54. package/dist/core/operations.d.ts +5 -0
  55. package/dist/core/operations.d.ts.map +1 -1
  56. package/dist/core/operations.js +16 -0
  57. package/dist/core/operations.js.map +1 -1
  58. package/dist/core/paths.d.ts +2 -0
  59. package/dist/core/paths.d.ts.map +1 -1
  60. package/dist/core/paths.js +43 -0
  61. package/dist/core/paths.js.map +1 -1
  62. package/dist/core/project-tags.d.ts +20 -0
  63. package/dist/core/project-tags.d.ts.map +1 -0
  64. package/dist/core/project-tags.js +42 -0
  65. package/dist/core/project-tags.js.map +1 -0
  66. package/dist/core/schema-export.d.ts.map +1 -1
  67. package/dist/core/schema-export.js +17 -1
  68. package/dist/core/schema-export.js.map +1 -1
  69. package/dist/core/skill-usage-log.d.ts +1 -1
  70. package/dist/core/skill-usage-log.d.ts.map +1 -1
  71. package/dist/core/skill-usage-log.js +2 -2
  72. package/dist/core/skill-usage-log.js.map +1 -1
  73. package/dist/core/verifier.d.ts.map +1 -1
  74. package/dist/core/verifier.js +1 -6
  75. package/dist/core/verifier.js.map +1 -1
  76. package/dist/skills-manifest.json +10 -10
  77. package/dist/transports/cli/cli.js +156 -9
  78. package/dist/transports/cli/cli.js.map +1 -1
  79. package/dist/transports/http/server.d.ts.map +1 -1
  80. package/dist/transports/http/server.js +7 -1
  81. package/dist/transports/http/server.js.map +1 -1
  82. package/package.json +1 -1
  83. package/scripts/hooks/_shared.js +79 -3
  84. package/scripts/hooks/pre-compact.js +32 -8
  85. package/scripts/hooks/session-start.js +168 -17
  86. package/scripts/hooks/session-summary.js +137 -23
@@ -14,6 +14,7 @@ import {
14
14
  decideAutoUpdateHook,
15
15
  getMemeshDirFromDbPath,
16
16
  getProjectName,
17
+ importFromPluginRoot,
17
18
  isAutoCaptureEnabled,
18
19
  openHookDb,
19
20
  readUpdateCheckCache,
@@ -125,11 +126,23 @@ function parseTranscript(transcriptPath) {
125
126
  // the prior version) that signalled review fatigue more than
126
127
  // working logic.
127
128
  } catch {
128
- // Skip malformed JSONL lines
129
+ // Skip malformed JSONL lines — benign, per-line, deliberately not traced.
129
130
  }
130
131
  }
131
- } catch {
132
- // Transcript unreadable return empty results
132
+ } catch (err) {
133
+ // The transcript file itself could not be read, which empties this
134
+ // session's entire capture — filesEdited/errors/toolCallCount all return
135
+ // zero, so downstream `toolCallCount < 3` bails and no session insight,
136
+ // failure analysis or lesson is produced. An absent file is the normal
137
+ // "not written yet" case; anything else is a real fault worth a trace.
138
+ if (err?.code !== 'ENOENT') {
139
+ try {
140
+ process.stderr.write(
141
+ `[memesh session-summary] transcript ${transcriptPath} unreadable ` +
142
+ `(${err?.message || err}); session capture skipped this run.\n`,
143
+ );
144
+ } catch { /* stderr must never throw */ }
145
+ }
133
146
  }
134
147
 
135
148
  return { filesEdited: [...filesEdited], bashCommands, errorsEncountered, toolCallCount };
@@ -163,7 +176,6 @@ process.stdin.on('end', async () => {
163
176
  const sessionId = inputData.session_id || 'unknown';
164
177
  const transcriptPath = inputData.transcript_path;
165
178
  const cwd = inputData.cwd || process.cwd();
166
- const stopReason = inputData.stop_reason || 'unknown';
167
179
  // Default-allow: when Claude Code's Stop payload omits
168
180
  // `was_in_agentic_loop` (it has been silently absent in production
169
181
  // for an unknown number of releases — symptom: zero session-insight
@@ -173,8 +185,18 @@ process.stdin.on('end', async () => {
173
185
  // (default-deny) and the hook silently never captured anything.
174
186
  const wasAgenticLoop = inputData.was_in_agentic_loop !== false;
175
187
 
176
- // Guards: skip low-signal sessions
177
- if (stopReason === 'user_interrupt') return exit0();
188
+ // Guards: skip low-signal sessions.
189
+ //
190
+ // A `stop_reason === 'user_interrupt'` guard used to live here, but
191
+ // Claude Code's Stop payload carries no `stop_reason` field — verified
192
+ // against the shipped cli.js bundle, whose Stop input is
193
+ // `{...base, hook_event_name:"Stop", stop_hook_active}` with no such key
194
+ // (the `stop_reason` that appears in the bundle is the Anthropic API
195
+ // message field, not a hook input). So the guard read `undefined`, was
196
+ // always false, and never skipped anything — a filter that looked active
197
+ // but did nothing, the exact sibling of the `was_in_agentic_loop` absence
198
+ // above. Removed; the `toolCallCount < 3` check below is the real
199
+ // low-signal filter.
178
200
  if (!wasAgenticLoop) return exit0();
179
201
  // Trace why we're skipping. Two failure modes:
180
202
  // (a) transcript_path absent — schema flip, Claude Code stopped
@@ -251,6 +273,24 @@ process.stdin.on('end', async () => {
251
273
  // Build and store session memories
252
274
  const baseTags = ['source:auto-capture', `session:${sessionId}`, `project:${projectName}`];
253
275
 
276
+ // Producer for pre-edit-recall's Strategy 1 (`file:<name>` tag lookup).
277
+ // That read path queries both the full basename and the extension-less
278
+ // form (`file:auth.ts` OR `file:auth`), but nothing ever WROTE these
279
+ // tags — on every real DB the query returned zero rows and the strategy
280
+ // was dead. Emitting both forms here lights it up: a memory captured while
281
+ // editing a file becomes findable the next time that file is edited.
282
+ // filesEdited already holds basenames (see parseTranscript).
283
+ function fileTagsFor(files) {
284
+ const tags = new Set();
285
+ for (const f of files) {
286
+ if (!f) continue;
287
+ tags.add(`file:${f}`);
288
+ const noExt = f.replace(/\.[^.]+$/, '');
289
+ if (noExt && noExt !== f) tags.add(`file:${noExt}`);
290
+ }
291
+ return [...tags];
292
+ }
293
+
254
294
  const insertEntity = db.prepare('INSERT OR IGNORE INTO entities (name, type) VALUES (?, ?)');
255
295
  const selectEntity = db.prepare('SELECT id FROM entities WHERE name = ?');
256
296
  const insertObs = db.prepare('INSERT INTO observations (entity_id, content) VALUES (?, ?)');
@@ -273,7 +313,7 @@ process.stdin.on('end', async () => {
273
313
  `Session edited ${filesEdited.length} file(s): ${filesEdited.join(', ')}`,
274
314
  `Total tool calls: ${toolCallCount}`,
275
315
  ],
276
- baseTags
316
+ [...baseTags, ...fileTagsFor(filesEdited)]
277
317
  );
278
318
  }
279
319
 
@@ -286,7 +326,7 @@ process.stdin.on('end', async () => {
286
326
  `Fixed ${errorsEncountered.length} error(s) by editing ${filesEdited.join(', ')}`,
287
327
  ...errorsEncountered.slice(0, 3).map(e => `Error: ${e.slice(0, 100)}`),
288
328
  ],
289
- [...baseTags, 'type:bugfix']
329
+ [...baseTags, 'type:bugfix', ...fileTagsFor(filesEdited)]
290
330
  );
291
331
  }
292
332
 
@@ -366,15 +406,16 @@ process.stdin.on('end', async () => {
366
406
  // Check if recall_hits column exists (v4.0+ migration)
367
407
  const colCheck = db.prepare("PRAGMA table_info(entities)").all();
368
408
  if (colCheck.some(c => c.name === 'recall_hits')) {
369
- // Build a lowercase transcript text for matching
370
- let transcriptText = readFileSync(transcriptPath, 'utf8').toLowerCase();
371
-
372
- // FIX: Exclude injected context from hit detection to avoid pollution
373
- // Remove the memorySummary that was injected at session start
374
- const injectedContext = (injectedData.injectedContext || '').toLowerCase();
375
- if (injectedContext) {
376
- transcriptText = transcriptText.replace(injectedContext, '');
377
- }
409
+ // Drop the records Claude Code created FROM our own hook
410
+ // output before matching. One SessionStart injection lands in
411
+ // the transcript 2+ times (hook_success + hook_additional_context),
412
+ // so any count-based discount depends on guessing an
413
+ // undocumented internal get it wrong and every entity scores
414
+ // a hit instead of a miss. Structural removal is copy-count
415
+ // and encoding independent.
416
+ const sessionText = stripHookEchoes(readFileSync(transcriptPath, 'utf8')).toLowerCase();
417
+
418
+ // Hit/miss decision lives in `isRecallHit` (exported, unit-tested).
378
419
 
379
420
  const updateHit = db.prepare(
380
421
  'UPDATE entities SET recall_hits = COALESCE(recall_hits, 0) + 1 WHERE id = ?'
@@ -387,7 +428,7 @@ process.stdin.on('end', async () => {
387
428
  const name = (entityNames[i] || '').toLowerCase();
388
429
  // Skip very short names to avoid false positives
389
430
  if (name.length < 4) continue;
390
- if (transcriptText.includes(name)) {
431
+ if (isRecallHit(sessionText, name)) {
391
432
  updateHit.run(entityIds[i]);
392
433
  } else {
393
434
  updateMiss.run(entityIds[i]);
@@ -417,13 +458,13 @@ process.stdin.on('end', async () => {
417
458
  // F5: derive pluginRoot strictly from this file's location.
418
459
  // See `resolvePluginRoot` for the full reasoning.
419
460
  const pluginRoot = resolvePluginRoot(import.meta.url);
420
- const configMod = await import(join(pluginRoot, 'dist/core/config.js'));
461
+ const configMod = await importFromPluginRoot(pluginRoot, 'dist/core/config.js');
421
462
  const config = configMod.readConfig();
422
463
 
423
464
  if (config.llm) {
424
- const { openDatabase, closeDatabase } = await import(join(pluginRoot, 'dist/db.js'));
425
- const { analyzeFailure } = await import(join(pluginRoot, 'dist/core/failure-analyzer.js'));
426
- const { createLesson } = await import(join(pluginRoot, 'dist/core/lesson-engine.js'));
465
+ const { openDatabase, closeDatabase } = await importFromPluginRoot(pluginRoot, 'dist/db.js');
466
+ const { analyzeFailure } = await importFromPluginRoot(pluginRoot, 'dist/core/failure-analyzer.js');
467
+ const { createLesson } = await importFromPluginRoot(pluginRoot, 'dist/core/lesson-engine.js');
427
468
 
428
469
  openDatabase();
429
470
  try {
@@ -452,7 +493,7 @@ process.stdin.on('end', async () => {
452
493
  // logic and dream-history.json schema.
453
494
  try {
454
495
  const pluginRoot = resolvePluginRoot(import.meta.url);
455
- const configMod = await import(join(pluginRoot, 'dist/core/config.js'));
496
+ const configMod = await importFromPluginRoot(pluginRoot, 'dist/core/config.js');
456
497
  const config = configMod.readConfig();
457
498
  maybeTriggerDream(projectName, config, pluginRoot);
458
499
  } catch (dreamErr) {
@@ -647,6 +688,79 @@ function countEpisodicEntities(projectName) {
647
688
  * the detached background runner. Pure side effect — no return value
648
689
  * used by callers.
649
690
  */
691
+ /**
692
+ * Attachment record types Claude Code uses to persist a hook's own output
693
+ * into the transcript. Anything memesh injected reaches the transcript
694
+ * through one of these, so they must be removed before asking "did the
695
+ * session reference this memory?".
696
+ *
697
+ * Verified against Claude Code v2.1.19: ONE SessionStart injection lands in
698
+ * the transcript at least twice — once as `hook_success` (carrying the raw
699
+ * hook stdout) and once as `hook_additional_context` (the parsed payload).
700
+ */
701
+ const HOOK_ECHO_ATTACHMENT_TYPES = new Set([
702
+ 'hook_success',
703
+ 'hook_additional_context',
704
+ 'hook_system_message',
705
+ ]);
706
+
707
+ /**
708
+ * Remove memesh's own injected text from a raw JSONL transcript.
709
+ *
710
+ * Counting occurrences and subtracting the injected copies does NOT work:
711
+ * it depends on knowing exactly how many times Claude Code echoes a hook
712
+ * payload, which is an undocumented internal that has already been observed
713
+ * at 2+ copies (and 16 in one real transcript). Guessing that constant is
714
+ * how "every entity is a miss" becomes "every entity is a hit" — equally
715
+ * useless, and invisible to a hand-built test fixture.
716
+ *
717
+ * Dropping the hook-echo records structurally is independent of both the
718
+ * copy count and the JSON escaping.
719
+ */
720
+ export function stripHookEchoes(rawTranscript) {
721
+ const kept = [];
722
+ for (const line of String(rawTranscript ?? '').split('\n')) {
723
+ if (!line.trim()) continue;
724
+ let entry;
725
+ try {
726
+ entry = JSON.parse(line);
727
+ } catch {
728
+ // Unparseable line: keep it. Losing a line can only cause a false
729
+ // MISS (we under-count references), which is the safe direction —
730
+ // it never manufactures a hit the session did not earn.
731
+ kept.push(line);
732
+ continue;
733
+ }
734
+ const type = entry?.attachment?.type ?? entry?.type;
735
+ if (typeof type === 'string' && HOOK_ECHO_ATTACHMENT_TYPES.has(type)) continue;
736
+ kept.push(line);
737
+ }
738
+ return kept.join('\n');
739
+ }
740
+
741
+ /**
742
+ * Did the session actually USE the memory named `name`, or does the name only
743
+ * appear because memesh injected it at session start?
744
+ *
745
+ * The caller passes `sessionText` with memesh's own SessionStart injection
746
+ * already stripped structurally (see `stripHookEchoes` — matches on
747
+ * `attachment.type`, so it is independent of JSON escaping and of how many
748
+ * times Claude Code echoes one injection). That removal is what stops an
749
+ * injected name from scoring a false hit; once the echo is gone, a plain
750
+ * substring match is the whole test.
751
+ *
752
+ * (This replaced an earlier `transcript.replace(injectedBlob, '')` + match,
753
+ * which silently failed on JSON-encoded transcripts and scored every entity a
754
+ * hit — see the callsite comment.)
755
+ *
756
+ * Self-contained for its unit tests: lowercases both sides and ignores names
757
+ * shorter than 4 chars (too generic to match reliably).
758
+ */
759
+ export function isRecallHit(sessionText, name) {
760
+ if (!name || name.length < 4) return false;
761
+ return String(sessionText ?? '').toLowerCase().includes(String(name).toLowerCase());
762
+ }
763
+
650
764
  export function maybeTriggerDream(projectName, config, pluginRoot) {
651
765
  dreamTrigTrace('enter', { projectName, hasLlm: Boolean(config?.llm) });
652
766
  if (!projectName || projectName === 'unknown') {