@pcircle/memesh 4.1.7 → 4.2.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 (131) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.de.md +48 -10
  4. package/README.es.md +82 -17
  5. package/README.fr.md +49 -11
  6. package/README.ja.md +83 -17
  7. package/README.ko.md +86 -20
  8. package/README.md +12 -7
  9. package/README.pt.md +48 -10
  10. package/README.th.md +46 -8
  11. package/README.vi.md +48 -10
  12. package/README.zh-CN.md +83 -17
  13. package/README.zh-TW.md +110 -44
  14. package/dashboard/dist/index.html +9 -9
  15. package/dist/cli/view-live.js +2 -2
  16. package/dist/cli/view.d.ts.map +1 -1
  17. package/dist/cli/view.js +15 -12
  18. package/dist/cli/view.js.map +1 -1
  19. package/dist/core/analytics.d.ts +17 -0
  20. package/dist/core/analytics.d.ts.map +1 -1
  21. package/dist/core/analytics.js +40 -0
  22. package/dist/core/analytics.js.map +1 -1
  23. package/dist/core/auto-tagger.d.ts +7 -2
  24. package/dist/core/auto-tagger.d.ts.map +1 -1
  25. package/dist/core/auto-tagger.js +12 -4
  26. package/dist/core/auto-tagger.js.map +1 -1
  27. package/dist/core/config.d.ts +2 -0
  28. package/dist/core/config.d.ts.map +1 -1
  29. package/dist/core/config.js +19 -11
  30. package/dist/core/config.js.map +1 -1
  31. package/dist/core/consolidator.d.ts.map +1 -1
  32. package/dist/core/consolidator.js +13 -4
  33. package/dist/core/consolidator.js.map +1 -1
  34. package/dist/core/digest-validator.d.ts +18 -0
  35. package/dist/core/digest-validator.d.ts.map +1 -0
  36. package/dist/core/digest-validator.js +79 -0
  37. package/dist/core/digest-validator.js.map +1 -0
  38. package/dist/core/doctor.d.ts.map +1 -1
  39. package/dist/core/doctor.js +28 -11
  40. package/dist/core/doctor.js.map +1 -1
  41. package/dist/core/dreamer.d.ts +8 -1
  42. package/dist/core/dreamer.d.ts.map +1 -1
  43. package/dist/core/dreamer.js +68 -14
  44. package/dist/core/dreamer.js.map +1 -1
  45. package/dist/core/embedder.d.ts.map +1 -1
  46. package/dist/core/embedder.js +2 -2
  47. package/dist/core/embedder.js.map +1 -1
  48. package/dist/core/extractor.d.ts.map +1 -1
  49. package/dist/core/extractor.js +2 -1
  50. package/dist/core/extractor.js.map +1 -1
  51. package/dist/core/failure-analyzer.d.ts +6 -1
  52. package/dist/core/failure-analyzer.d.ts.map +1 -1
  53. package/dist/core/failure-analyzer.js +10 -2
  54. package/dist/core/failure-analyzer.js.map +1 -1
  55. package/dist/core/install-hooks.d.ts.map +1 -1
  56. package/dist/core/install-hooks.js +1 -7
  57. package/dist/core/install-hooks.js.map +1 -1
  58. package/dist/core/install-id.d.ts.map +1 -1
  59. package/dist/core/install-id.js +2 -3
  60. package/dist/core/install-id.js.map +1 -1
  61. package/dist/core/kg-backfill.d.ts +39 -0
  62. package/dist/core/kg-backfill.d.ts.map +1 -0
  63. package/dist/core/kg-backfill.js +362 -0
  64. package/dist/core/kg-backfill.js.map +1 -0
  65. package/dist/core/llm-client.d.ts +13 -0
  66. package/dist/core/llm-client.d.ts.map +1 -1
  67. package/dist/core/llm-client.js +63 -3
  68. package/dist/core/llm-client.js.map +1 -1
  69. package/dist/core/llm-telemetry.d.ts +35 -0
  70. package/dist/core/llm-telemetry.d.ts.map +1 -0
  71. package/dist/core/llm-telemetry.js +96 -0
  72. package/dist/core/llm-telemetry.js.map +1 -0
  73. package/dist/core/llm-validator.d.ts.map +1 -1
  74. package/dist/core/llm-validator.js +3 -3
  75. package/dist/core/llm-validator.js.map +1 -1
  76. package/dist/core/operations.d.ts.map +1 -1
  77. package/dist/core/operations.js +4 -35
  78. package/dist/core/operations.js.map +1 -1
  79. package/dist/core/paths.d.ts +6 -0
  80. package/dist/core/paths.d.ts.map +1 -0
  81. package/dist/core/paths.js +27 -0
  82. package/dist/core/paths.js.map +1 -0
  83. package/dist/core/prompt-safety.d.ts.map +1 -1
  84. package/dist/core/prompt-safety.js.map +1 -1
  85. package/dist/core/scoring.d.ts +5 -0
  86. package/dist/core/scoring.d.ts.map +1 -1
  87. package/dist/core/scoring.js +8 -0
  88. package/dist/core/scoring.js.map +1 -1
  89. package/dist/core/serializer.js +1 -1
  90. package/dist/core/serializer.js.map +1 -1
  91. package/dist/core/skill-usage-log.js +2 -2
  92. package/dist/core/skill-usage-log.js.map +1 -1
  93. package/dist/core/types.d.ts +2 -2
  94. package/dist/core/types.d.ts.map +1 -1
  95. package/dist/core/verifier.d.ts.map +1 -1
  96. package/dist/core/verifier.js +4 -4
  97. package/dist/core/verifier.js.map +1 -1
  98. package/dist/core/version-check.d.ts.map +1 -1
  99. package/dist/core/version-check.js +4 -3
  100. package/dist/core/version-check.js.map +1 -1
  101. package/dist/db.d.ts.map +1 -1
  102. package/dist/db.js +71 -14
  103. package/dist/db.js.map +1 -1
  104. package/dist/knowledge-graph.d.ts +1 -1
  105. package/dist/knowledge-graph.d.ts.map +1 -1
  106. package/dist/knowledge-graph.js +1 -1
  107. package/dist/knowledge-graph.js.map +1 -1
  108. package/dist/skills-manifest.json +16 -16
  109. package/dist/storage/fts-index.js +1 -1
  110. package/dist/storage/fts-index.js.map +1 -1
  111. package/dist/transports/cli/cli.js +128 -6
  112. package/dist/transports/cli/cli.js.map +1 -1
  113. package/dist/transports/http/server.d.ts.map +1 -1
  114. package/dist/transports/http/server.js +204 -25
  115. package/dist/transports/http/server.js.map +1 -1
  116. package/dist/transports/mcp/handlers.d.ts +1 -1
  117. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  118. package/dist/transports/mcp/handlers.js +1 -1
  119. package/dist/transports/mcp/handlers.js.map +1 -1
  120. package/package.json +2 -2
  121. package/scripts/hooks/_shared.js +177 -14
  122. package/scripts/hooks/post-commit.js +50 -8
  123. package/scripts/hooks/pre-bash-orchestration-nudge.js +8 -3
  124. package/scripts/hooks/pre-compact.js +13 -16
  125. package/scripts/hooks/pre-edit-recall.js +28 -13
  126. package/scripts/hooks/session-start.js +194 -184
  127. package/scripts/hooks/session-summary.js +376 -41
  128. package/dist/core/query-expander.d.ts +0 -4
  129. package/dist/core/query-expander.d.ts.map +0 -1
  130. package/dist/core/query-expander.js +0 -53
  131. package/dist/core/query-expander.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pcircle/memesh",
3
- "version": "4.1.7",
3
+ "version": "4.2.1",
4
4
  "description": "MeMesh — Local memory for Claude Code and MCP coding agents. One SQLite file, zero cloud required.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -22,7 +22,7 @@
22
22
  "LICENSE"
23
23
  ],
24
24
  "scripts": {
25
- "build": "tsc && node scripts/copy-cli-assets.mjs && node scripts/set-executable-bits.mjs && node scripts/generate-skills-manifest.mjs && npm run build:dashboard && node scripts/smoke-test.mjs",
25
+ "build": "node scripts/check-schema-drift.mjs && tsc && node scripts/copy-cli-assets.mjs && node scripts/set-executable-bits.mjs && node scripts/generate-skills-manifest.mjs && npm run build:dashboard && node scripts/smoke-test.mjs",
26
26
  "build:dashboard": "node scripts/build-dashboard.mjs",
27
27
  "test": "vitest",
28
28
  "test:e2e-dashboard": "node scripts/dashboard-e2e-smoke.mjs",
@@ -2,11 +2,26 @@ import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync,
2
2
  import { spawn } from 'child_process';
3
3
  import { createRequire } from 'module';
4
4
  import { homedir } from 'os';
5
- import { dirname, join } from 'path';
5
+ import { basename, dirname, join } from 'path';
6
6
  import { fileURLToPath } from 'url';
7
7
 
8
8
  const require = createRequire(import.meta.url);
9
9
 
10
+ // =============================================================================
11
+ // Path helpers — MIRROR of src/core/paths.ts
12
+ // =============================================================================
13
+ //
14
+ // Hooks cannot import from `dist/` (the F5 security boundary — `dist/` may
15
+ // be stale or absent at hook execution time), so the path-resolution logic
16
+ // is duplicated here. The contract MUST stay in lockstep with
17
+ // `src/core/paths.ts`. Any change to the function shapes / precedence
18
+ // rules in that file MUST be reflected here too.
19
+ //
20
+ // Unlike the schema duplication (which has a build-time diff guard via
21
+ // `scripts/check-schema-drift.mjs`), these helpers are short enough that
22
+ // human review at code-review time is sufficient. If they grow, add a
23
+ // programmatic guard.
24
+
10
25
  /**
11
26
  * Return the user's home directory, honoring HOME env var first.
12
27
  *
@@ -17,10 +32,71 @@ const require = createRequire(import.meta.url);
17
32
  * across platforms. Production users on Windows almost never set HOME,
18
33
  * so this falls through to os.homedir() as before.
19
34
  *
35
+ * Mirror of: src/core/paths.ts → homeDir()
36
+ *
20
37
  * @returns {string}
21
38
  */
22
39
  function homeDir() {
23
- return process.env.HOME ?? homedir();
40
+ // Mirror src/core/paths.ts homeDir() — same three-step fallback to
41
+ // handle HOME="" environments. `os.homedir()` itself reads HOME on
42
+ // POSIX, so HOME="" makes it return "". `os.userInfo().homedir`
43
+ // reads pw_dir via getpwuid syscall, bypassing env vars entirely.
44
+ const home = process.env.HOME;
45
+ if (home && home.length > 0) return home;
46
+ const fromOs = homedir();
47
+ if (fromOs && fromOs.length > 0) return fromOs;
48
+ // userInfo is the final defence — re-import here to keep the
49
+ // top-of-file `import { homedir } from 'os'` line stable.
50
+ return require('os').userInfo().homedir;
51
+ }
52
+
53
+ /**
54
+ * Resolve the memesh data directory.
55
+ *
56
+ * Precedence: MEMESH_DIR env var > <home>/.memesh.
57
+ * Mirror of: src/core/paths.ts → memeshDir()
58
+ *
59
+ * No-arg to mirror the core helper exactly. Earlier drafts accepted a
60
+ * custom `env` parameter for symmetry with `getMemeshDirFromDbPath(env)`,
61
+ * but the inner `homeDir()` only ever read `process.env.HOME`, so a
62
+ * caller that passed `{HOME: '/tmp/x'}` would be silently ignored — a
63
+ * footgun. Tests redirect via `process.env.HOME`; that's the supported
64
+ * extension point.
65
+ *
66
+ * @returns {string}
67
+ */
68
+ export function memeshDir() {
69
+ return process.env.MEMESH_DIR ?? join(homeDir(), '.memesh');
70
+ }
71
+
72
+ /**
73
+ * Resolve the active memesh DB path.
74
+ *
75
+ * Precedence: MEMESH_DB_PATH env var > <memeshDir>/knowledge-graph.db.
76
+ * Mirror of: src/core/paths.ts → getDbPath() — no-arg, see memeshDir().
77
+ *
78
+ * @returns {string}
79
+ */
80
+ export function getDbPath() {
81
+ return process.env.MEMESH_DB_PATH ?? join(memeshDir(), 'knowledge-graph.db');
82
+ }
83
+
84
+ /**
85
+ * Derive the project name from a working directory.
86
+ *
87
+ * Hooks historically used `basename(data.cwd || process.cwd())`. Core
88
+ * had two variants (`basename(context.cwd)` and `basename(process.cwd())`).
89
+ * This helper unifies the contract — explicit cwd wins, falls through to
90
+ * process.cwd() — matching the most permissive caller's behaviour.
91
+ *
92
+ * Mirror of: src/core/paths.ts → getProjectName()
93
+ *
94
+ * @param {string|null|undefined} [cwdInput]
95
+ * @returns {string}
96
+ */
97
+ export function getProjectName(cwdInput) {
98
+ const cwd = cwdInput && cwdInput.length > 0 ? cwdInput : process.cwd();
99
+ return basename(cwd);
24
100
  }
25
101
 
26
102
  /**
@@ -64,7 +140,7 @@ export function resolvePluginRoot(metaUrl) {
64
140
  * @returns {Record<string, any>}
65
141
  */
66
142
  export function readHookConfig(_env = process.env) {
67
- const path = join(homeDir(), '.memesh', 'config.json');
143
+ const path = join(memeshDir(), 'config.json');
68
144
  if (!existsSync(path)) return {};
69
145
  try {
70
146
  const raw = readFileSync(path, 'utf8');
@@ -210,6 +286,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_entity_tag_unique ON tags(entity_id,
210
286
  CREATE INDEX IF NOT EXISTS idx_observations_entity ON observations(entity_id);
211
287
  CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity_id);
212
288
  CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity_id);
289
+ CREATE INDEX IF NOT EXISTS idx_entities_type_created ON entities(type, created_at);
213
290
  `;
214
291
 
215
292
  // FTS5 virtual table — separate so hooks that don't need it stay lean.
@@ -233,32 +310,118 @@ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
233
310
  * @param {boolean} [opts.fts=false] - Also create the FTS5 virtual table.
234
311
  * @returns {{ db: any, dbPath: string }}
235
312
  */
313
+ // Cached lookup for the better-sqlite3 native module. Plugin-marketplace
314
+ // installs ship the tarball without node_modules, so the cache copy of the
315
+ // hook scripts cannot resolve the binding. tryRequireBetterSqlite() returns
316
+ // null in that scenario and lets each caller silent-skip — callers paired
317
+ // with a working dev/npm-global registration still produce output.
318
+ let _cachedDatabaseCtor;
319
+ export function tryRequireBetterSqlite() {
320
+ // Test-only seam: force the "native module unavailable" branch so
321
+ // tests can exercise the silent-skip path that plugin-marketplace
322
+ // cache installs hit. Production callers never set this var.
323
+ if (process.env.MEMESH_TEST_FORCE_MISSING_NATIVE === '1') return null;
324
+ if (_cachedDatabaseCtor !== undefined) return _cachedDatabaseCtor;
325
+ try {
326
+ _cachedDatabaseCtor = require('better-sqlite3');
327
+ } catch {
328
+ _cachedDatabaseCtor = null;
329
+ }
330
+ return _cachedDatabaseCtor;
331
+ }
332
+
236
333
  export function openHookDb(env = process.env, opts = {}) {
237
- const dbPath = env.MEMESH_DB_PATH || join(homeDir(), '.memesh', 'knowledge-graph.db');
238
- const dbDir = env.MEMESH_DB_PATH ? dirname(env.MEMESH_DB_PATH) : join(homeDir(), '.memesh');
334
+ const Database = tryRequireBetterSqlite();
335
+ if (!Database) return null;
336
+
337
+ // Path helpers read process.env directly (no-arg). The `env` parameter
338
+ // is kept on this signature for backward compatibility with callers
339
+ // and is consulted directly only for the DB-PATH dirname check below
340
+ // — that's the one place where a caller's custom env should win over
341
+ // process.env (e.g. the test harness redirects MEMESH_DB_PATH per test).
342
+ const dbPath = env.MEMESH_DB_PATH ?? getDbPath();
343
+ const dbDir = env.MEMESH_DB_PATH ? dirname(env.MEMESH_DB_PATH) : memeshDir();
239
344
  if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
240
345
 
241
- const Database = require('better-sqlite3');
242
346
  const db = new Database(dbPath);
243
347
  db.pragma('journal_mode = WAL');
244
348
  db.pragma('foreign_keys = ON');
245
349
  db.exec(SCHEMA_SQL);
246
350
  if (opts.fts) db.exec(FTS_SQL);
247
351
 
352
+ // Apply the full migration chain — keep in lockstep with src/db.ts.
353
+ // Conditional ALTER TABLE blocks ARE idempotent within a single process,
354
+ // but two hook processes can race: each reads `colNames` from its own
355
+ // PRAGMA snapshot, so both see "column missing" and both run ALTER —
356
+ // the second one throws SQLITE_ERROR: duplicate column name. Each ALTER
357
+ // is wrapped in safeAlter() which treats that specific error as the
358
+ // expected no-op outcome (a peer beat us to it). Any other error
359
+ // re-throws so we don't paper over real bugs.
360
+ //
361
+ // Earlier this helper applied ONLY the v2.11->v2.12 status migration.
362
+ // That left a hook-only-touched DB at v2.12 even though core was at
363
+ // v4.0+, so session-start fell back to `ORDER BY id DESC` (degraded
364
+ // ranking) until the CLI/MCP/HTTP first opened the DB and finished
365
+ // the chain. Backfilled here so write-path hooks produce the same
366
+ // schema state as core.
367
+ const safeAlter = (sql) => {
368
+ try {
369
+ db.exec(sql);
370
+ } catch (e) {
371
+ if (!/duplicate column name/i.test(e?.message || '')) throw e;
372
+ // Peer hook process won the race; column already exists. Idempotent.
373
+ }
374
+ };
248
375
  const cols = db.prepare("PRAGMA table_info(entities)").all();
249
- if (!cols.some((c) => c.name === 'status')) {
250
- db.exec("ALTER TABLE entities ADD COLUMN status TEXT NOT NULL DEFAULT 'active'");
376
+ const colNames = new Set(cols.map((c) => c.name));
377
+
378
+ // v2.11 -> v2.12: status
379
+ if (!colNames.has('status')) {
380
+ safeAlter("ALTER TABLE entities ADD COLUMN status TEXT NOT NULL DEFAULT 'active'");
251
381
  db.exec("CREATE INDEX IF NOT EXISTS idx_entities_status ON entities(status)");
252
382
  }
253
383
 
384
+ // v2.14 -> v2.15: scoring + temporal-validity columns
385
+ if (!colNames.has('access_count')) {
386
+ safeAlter("ALTER TABLE entities ADD COLUMN access_count INTEGER DEFAULT 0");
387
+ safeAlter("ALTER TABLE entities ADD COLUMN last_accessed_at TIMESTAMP");
388
+ safeAlter("ALTER TABLE entities ADD COLUMN confidence REAL DEFAULT 1.0");
389
+ safeAlter("ALTER TABLE entities ADD COLUMN valid_from TIMESTAMP");
390
+ safeAlter("ALTER TABLE entities ADD COLUMN valid_until TIMESTAMP");
391
+ }
392
+
393
+ // v3.0.0-rc -> v3.0.0: namespace
394
+ if (!colNames.has('namespace')) {
395
+ safeAlter("ALTER TABLE entities ADD COLUMN namespace TEXT DEFAULT 'personal'");
396
+ db.exec("CREATE INDEX IF NOT EXISTS idx_entities_namespace ON entities(namespace)");
397
+ }
398
+
399
+ // v4.0.0: recall effectiveness counters
400
+ if (!colNames.has('recall_hits')) {
401
+ safeAlter("ALTER TABLE entities ADD COLUMN recall_hits INTEGER DEFAULT 0");
402
+ safeAlter("ALTER TABLE entities ADD COLUMN recall_misses INTEGER DEFAULT 0");
403
+ }
404
+
254
405
  return { db, dbPath };
255
406
  }
256
407
 
257
408
  const PRIVATE_DIR_MODE = 0o700;
258
409
  const PRIVATE_FILE_MODE = 0o600;
259
410
 
260
- export function getMemeshDir(env = process.env) {
261
- return env.MEMESH_DB_PATH ? dirname(env.MEMESH_DB_PATH) : join(homeDir(), '.memesh');
411
+ /**
412
+ * Resolve the directory containing the active DB file.
413
+ *
414
+ * When MEMESH_DB_PATH is set, returns its parent directory (used for
415
+ * sibling files next to the DB). Otherwise returns memeshDir().
416
+ * Mirror of: src/core/paths.ts → getMemeshDirFromDbPath()
417
+ *
418
+ * Renamed from the legacy `getMemeshDir` to match the core helper —
419
+ * sibling helper `memeshDir()` returns the GLOBAL data directory, so a
420
+ * second function called `getMemeshDir` was confusing. Callers updated
421
+ * in lockstep.
422
+ */
423
+ export function getMemeshDirFromDbPath() {
424
+ return process.env.MEMESH_DB_PATH ? dirname(process.env.MEMESH_DB_PATH) : memeshDir();
262
425
  }
263
426
 
264
427
  export function ensurePrivateDir(dirPath) {
@@ -332,7 +495,7 @@ export function readUpdateCheckCache(installedVersion) {
332
495
  && /^[0-9A-Za-z.+-]+$/.test(installedVersion)
333
496
  ? installedVersion
334
497
  : 'unknown';
335
- const cachePath = join(homeDir(), '.memesh', `update-check.${versionTag}.json`);
498
+ const cachePath = join(memeshDir(), `update-check.${versionTag}.json`);
336
499
  try {
337
500
  if (!existsSync(cachePath)) return null;
338
501
  const parsed = JSON.parse(readFileSync(cachePath, 'utf8'));
@@ -391,7 +554,7 @@ export function decideAutoUpdateHook(currentVersion, cache, policy) {
391
554
 
392
555
  export function logAutoUpdate(line) {
393
556
  try {
394
- const dir = getMemeshDir(process.env);
557
+ const dir = getMemeshDirFromDbPath();
395
558
  ensurePrivateDir(dir);
396
559
  const path = join(dir, 'auto-update.log');
397
560
  appendFileSync(path, `[${new Date().toISOString()}] ${line}\n`);
@@ -405,7 +568,7 @@ export const AUTO_UPDATE_LOCK_TTL_MS = 10 * 60 * 1000;
405
568
 
406
569
  export function tryAcquireAutoUpdateLock(version) {
407
570
  try {
408
- const dir = join(homeDir(), '.memesh');
571
+ const dir = memeshDir();
409
572
  ensurePrivateDir(dir);
410
573
  const lockPath = join(dir, 'auto-update.lock');
411
574
  const fs = require('fs');
@@ -490,7 +653,7 @@ export function spawnAutoUpdate(version, deprecationOverride, installChannelMod)
490
653
  );
491
654
  return { state: 'in-progress' };
492
655
  }
493
- const dir = getMemeshDir(process.env);
656
+ const dir = getMemeshDirFromDbPath();
494
657
  ensurePrivateDir(dir);
495
658
  const logPath = join(dir, 'auto-update.log');
496
659
  let fd = -1;
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { execFileSync } from 'child_process';
4
- import { basename } from 'path';
5
- import { openHookDb } from './_shared.js';
4
+ import { getProjectName, openHookDb } from './_shared.js';
6
5
 
7
6
  let input = '';
8
7
  process.stdin.setEncoding('utf8');
@@ -11,11 +10,37 @@ process.stdin.on('end', () => {
11
10
  try {
12
11
  const data = JSON.parse(input);
13
12
 
14
- // Only process Bash tool outputs
13
+ // tool_name absent is a schema-flip signal (Claude Code has done
14
+ // tool_name renames historically — e.g. tool_use/tool_result
15
+ // nesting). Trace to surface the rename day-1 instead of months
16
+ // of silent dropout (the bug shape that bit `tool_output` and
17
+ // `was_in_agentic_loop`).
18
+ if (data.tool_name === undefined) {
19
+ try { process.stderr.write(`[memesh post-commit] tool_name absent in payload (keys: ${Object.keys(data).join(',')}); skipping\n`); } catch {}
20
+ return exit0();
21
+ }
15
22
  if (data.tool_name !== 'Bash') return exit0();
16
- const toolOutput = typeof data.tool_output === 'string'
17
- ? data.tool_output
18
- : JSON.stringify(data.tool_output || '');
23
+
24
+ // Claude Code's PostToolUse hook payload has had two field-name shapes:
25
+ // legacy `tool_output: <string>` and current
26
+ // `tool_response: { stdout, stderr, interrupted, isError }`. Prefer the
27
+ // current shape; fall back to legacy so unit-test fixtures (which use
28
+ // tool_output) keep working. This block existed only as the legacy
29
+ // branch — once Claude Code unified on tool_response the hook silently
30
+ // stopped seeing any output and never wrote commit entities again.
31
+ const tr = data.tool_response;
32
+ let toolOutput = '';
33
+ if (typeof tr === 'string') {
34
+ toolOutput = tr;
35
+ } else if (tr && typeof tr === 'object') {
36
+ const stdout = typeof tr.stdout === 'string' ? tr.stdout : '';
37
+ const stderr = typeof tr.stderr === 'string' ? tr.stderr : '';
38
+ toolOutput = stdout + (stderr ? '\n' + stderr : '');
39
+ } else if (typeof data.tool_output === 'string') {
40
+ toolOutput = data.tool_output;
41
+ } else if (data.tool_output != null) {
42
+ toolOutput = JSON.stringify(data.tool_output);
43
+ }
19
44
 
20
45
  // Detect git commit in output
21
46
  // Pattern: [branch hash] commit message
@@ -27,11 +52,28 @@ process.stdin.on('end', () => {
27
52
 
28
53
  const commitHash = commitMatch[1];
29
54
  const commitMsg = commitMatch[2];
30
- const projectName = basename(data.cwd || process.cwd());
55
+
56
+ // `data.cwd` MUST be present for project-tag and `git show` below
57
+ // to be correct. If absent, falling through to process.cwd() (the
58
+ // hook process's launch dir, unspecified for PostToolUse Bash)
59
+ // would either run git show against a different repo (silent
60
+ // corruption) or write the wrong project tag. Skip + trace
61
+ // instead — better to miss one commit than to tag it wrong.
62
+ if (!data.cwd) {
63
+ try { process.stderr.write(`[memesh post-commit] data.cwd absent — cannot resolve project / repo; skipping commit ${commitHash}\n`); } catch {}
64
+ return exit0();
65
+ }
66
+ const projectName = getProjectName(data.cwd);
31
67
 
32
68
  // Open DB via shared helper — applies SCHEMA_SQL + status migration.
33
69
  // Pass fts:true so the FTS5 entity-search index is also available.
34
- const { db } = openHookDb(process.env, { fts: true });
70
+ // Returns null when the better-sqlite3 native module is unavailable
71
+ // (e.g. plugin-marketplace cache install with no node_modules); in
72
+ // that case silently skip — a sibling registered hook copy with
73
+ // proper deps still records the commit.
74
+ const handle = openHookDb(process.env, { fts: true });
75
+ if (!handle) return;
76
+ const { db } = handle;
35
77
  try {
36
78
  const entityName = `commit-${commitHash}`;
37
79
 
@@ -13,9 +13,9 @@
13
13
 
14
14
  import { join } from 'path';
15
15
  import { openSync, closeSync } from 'fs';
16
- import { ensurePrivateDir, getMemeshDir, isAgenticOrchestrationEnabled } from './_shared.js';
16
+ import { ensurePrivateDir, getMemeshDirFromDbPath, isAgenticOrchestrationEnabled } from './_shared.js';
17
17
 
18
- const memeshDir = getMemeshDir(process.env);
18
+ const memeshDir = getMemeshDirFromDbPath();
19
19
  const FLAGS_DIR = join(memeshDir, 'agent-nudge-flags');
20
20
 
21
21
  // ---------------------------------------------------------------------------
@@ -83,7 +83,12 @@ process.stdin.on('end', () => {
83
83
  if (!isAgenticOrchestrationEnabled(process.env)) return pass();
84
84
 
85
85
  const data = JSON.parse(input);
86
- const command = (data.tool_input?.command ?? '').trim();
86
+ if (!data.tool_input) {
87
+ // Schema-flip signal: trace so a rename surfaces day-1.
88
+ try { process.stderr.write(`[memesh pre-bash-nudge] tool_input absent (keys: ${Object.keys(data).join(',')}); skipping\n`); } catch {}
89
+ return pass();
90
+ }
91
+ const command = (data.tool_input.command ?? '').trim();
87
92
 
88
93
  if (!command) return pass();
89
94
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { basename } from 'path';
4
4
  import { existsSync, readFileSync } from 'fs';
5
- import { isAutoCaptureEnabled, openHookDb } from './_shared.js';
5
+ import { getProjectName, isAutoCaptureEnabled, openHookDb } from './_shared.js';
6
6
 
7
7
  // Timeout guard: always exit within 10 seconds
8
8
  const TIMEOUT_MS = 10000;
@@ -27,7 +27,7 @@ process.stdin.on('end', () => {
27
27
  const transcriptPath = data.transcript_path || '';
28
28
  const cwd = data.cwd || process.cwd();
29
29
  const reason = data.reason || 'auto';
30
- const projectName = basename(cwd);
30
+ const projectName = getProjectName(cwd);
31
31
 
32
32
  // Parse transcript to gather insights
33
33
  let toolCallCount = 0;
@@ -40,7 +40,12 @@ process.stdin.on('end', () => {
40
40
  if (!line.trim()) continue;
41
41
  try {
42
42
  const entry = JSON.parse(line);
43
- // Current format: {type:'assistant', message:{content:[{type:'tool_use',...}]}}
43
+ // Claude Code transcript format: {type:'assistant', message:{content:[{type:'tool_use',...}]}}
44
+ // The earlier legacy branch (entry.role === 'assistant', entry.content
45
+ // at top level) was confirmed dead code via real-transcript audit:
46
+ // no entry in production transcripts ever shipped that shape.
47
+ // Removed to avoid confusion between two parsers that read
48
+ // different fields for the same logical event.
44
49
  if (entry.type === 'assistant' && Array.isArray(entry.message?.content)) {
45
50
  for (const block of entry.message.content) {
46
51
  if (block.type !== 'tool_use') continue;
@@ -52,18 +57,6 @@ process.stdin.on('end', () => {
52
57
  }
53
58
  }
54
59
  }
55
- // Legacy format: {role:'assistant', content:[{type:'tool_use',...}]}
56
- if (entry.role === 'assistant' && Array.isArray(entry.content)) {
57
- for (const block of entry.content) {
58
- if (block.type !== 'tool_use') continue;
59
- toolCallCount++;
60
- const name = block.name || '';
61
- if (name === 'Edit' || name === 'Write' || name === 'MultiEdit') {
62
- const filePath = block.input?.file_path || block.input?.path || '';
63
- if (filePath) editedFiles.add(basename(filePath));
64
- }
65
- }
66
- }
67
60
  } catch {
68
61
  // Skip malformed lines
69
62
  }
@@ -84,7 +77,11 @@ process.stdin.on('end', () => {
84
77
 
85
78
  // Open DB via shared helper — applies SCHEMA_SQL + status migration.
86
79
  // FTS5 needed for the entity-search index updates below.
87
- const { db } = openHookDb(process.env, { fts: true });
80
+ // Returns null on plugin-marketplace cache installs without node_modules;
81
+ // silently skip in that case (sibling registered copy handles it).
82
+ const handle = openHookDb(process.env, { fts: true });
83
+ if (!handle) return;
84
+ const { db } = handle;
88
85
  try {
89
86
  // Upsert entity
90
87
  const insertResult = db.prepare('INSERT OR IGNORE INTO entities (name, type) VALUES (?, ?)').run(entityName, 'session-summary');
@@ -4,22 +4,21 @@
4
4
  // When editing a file, checks if MeMesh has relevant memories
5
5
  // and injects them as context. Throttled: max 1 recall per file per session.
6
6
 
7
- import { createRequire } from 'module';
8
- import { homedir } from 'os';
9
- import { join, basename } from 'path';
7
+ import { basename, join } from 'path';
10
8
  import { existsSync, readFileSync } from 'fs';
11
9
  import {
12
10
  buildReferenceContext,
13
11
  ensurePrivateDir,
14
- getMemeshDir,
12
+ getDbPath,
13
+ getMemeshDirFromDbPath,
14
+ getProjectName,
15
15
  isTrustedForAutoContext,
16
+ tryRequireBetterSqlite,
16
17
  writePrivateJson,
17
18
  } from './_shared.js';
18
19
 
19
- const require = createRequire(import.meta.url);
20
-
21
- const dbPath = process.env.MEMESH_DB_PATH || join(homedir(), '.memesh', 'knowledge-graph.db');
22
- const memeshDir = getMemeshDir(process.env);
20
+ const dbPath = getDbPath();
21
+ const memeshDir = getMemeshDirFromDbPath();
23
22
  const THROTTLE_FILE = join(memeshDir, 'session-recalled-files.json');
24
23
  const MAX_RESULTS = 3;
25
24
 
@@ -29,7 +28,13 @@ process.stdin.on('data', (chunk) => { input += chunk; });
29
28
  process.stdin.on('end', () => {
30
29
  try {
31
30
  const data = JSON.parse(input);
32
- const toolInput = data.tool_input || {};
31
+ if (!data.tool_input) {
32
+ // Schema-flip signal — Claude Code has renamed `tool_input` for
33
+ // transcript blocks before. Trace so the rename surfaces day-1.
34
+ try { process.stderr.write(`[memesh pre-edit-recall] tool_input absent (keys: ${Object.keys(data).join(',')}); skipping\n`); } catch {}
35
+ return pass();
36
+ }
37
+ const toolInput = data.tool_input;
33
38
  const filePath = toolInput.file_path || toolInput.path || '';
34
39
 
35
40
  // Only process if we have a file path
@@ -38,7 +43,7 @@ process.stdin.on('end', () => {
38
43
  }
39
44
 
40
45
  // Get project name from cwd for project-scoped filtering
41
- const projectName = basename(data.cwd || process.cwd());
46
+ const projectName = getProjectName(data.cwd);
42
47
 
43
48
  // Throttle: skip if we already recalled for this file
44
49
  const fileKey = filePath.toLowerCase();
@@ -58,7 +63,12 @@ process.stdin.on('end', () => {
58
63
 
59
64
  if (!existsSync(dbPath)) return pass();
60
65
 
61
- const Database = require('better-sqlite3');
66
+ // tryRequireBetterSqlite() returns null on plugin-marketplace cache
67
+ // installs that ship without node_modules; pass-through silently in
68
+ // that case so a sibling registered hook copy can still inject
69
+ // recall context.
70
+ const Database = tryRequireBetterSqlite();
71
+ if (!Database) return pass();
62
72
  const db = new Database(dbPath, { readonly: true });
63
73
  try {
64
74
 
@@ -155,8 +165,13 @@ process.stdin.on('end', () => {
155
165
  } finally {
156
166
  db.close();
157
167
  }
158
- } catch {
159
- // Never crash Claude Code
168
+ } catch (err) {
169
+ // Never crash Claude Code, but trace — peer hooks (post-commit,
170
+ // pre-compact, session-summary) all stderr-trace their outer
171
+ // catches. Without a trace here, a typo in any prepare statement
172
+ // would silently break continuous recall on every Edit/Write
173
+ // tool call indefinitely.
174
+ try { process.stderr.write(`[memesh pre-edit-recall] ${err?.message || err}\n`); } catch {}
160
175
  pass();
161
176
  }
162
177
  });