@pcircle/memesh 4.1.2 → 4.1.4

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 (128) hide show
  1. package/.mcp.json +1 -3
  2. package/README.de.md +12 -1
  3. package/README.es.md +14 -2
  4. package/README.fr.md +14 -2
  5. package/README.ja.md +12 -1
  6. package/README.ko.md +12 -1
  7. package/README.md +17 -2
  8. package/README.pt.md +14 -2
  9. package/README.th.md +14 -2
  10. package/README.vi.md +14 -2
  11. package/README.zh-CN.md +12 -1
  12. package/README.zh-TW.md +12 -1
  13. package/dashboard/dist/index.html +13 -4
  14. package/dist/cli/view-live.d.ts +2 -0
  15. package/dist/cli/view-live.d.ts.map +1 -0
  16. package/dist/cli/view-live.js +2177 -0
  17. package/dist/cli/view-live.js.map +1 -0
  18. package/dist/cli/view.d.ts +0 -1
  19. package/dist/cli/view.d.ts.map +1 -1
  20. package/dist/cli/view.js +0 -2169
  21. package/dist/cli/view.js.map +1 -1
  22. package/dist/core/analytics.d.ts +23 -0
  23. package/dist/core/analytics.d.ts.map +1 -1
  24. package/dist/core/analytics.js +79 -0
  25. package/dist/core/analytics.js.map +1 -1
  26. package/dist/core/config.d.ts +9 -4
  27. package/dist/core/config.d.ts.map +1 -1
  28. package/dist/core/config.js +16 -9
  29. package/dist/core/config.js.map +1 -1
  30. package/dist/core/consolidator.d.ts.map +1 -1
  31. package/dist/core/consolidator.js +1 -0
  32. package/dist/core/consolidator.js.map +1 -1
  33. package/dist/core/demo.d.ts +9 -0
  34. package/dist/core/demo.d.ts.map +1 -0
  35. package/dist/core/demo.js +79 -0
  36. package/dist/core/demo.js.map +1 -0
  37. package/dist/core/doctor.d.ts +2 -1
  38. package/dist/core/doctor.d.ts.map +1 -1
  39. package/dist/core/doctor.js +198 -8
  40. package/dist/core/doctor.js.map +1 -1
  41. package/dist/core/dreamer.d.ts +65 -0
  42. package/dist/core/dreamer.d.ts.map +1 -0
  43. package/dist/core/dreamer.js +477 -0
  44. package/dist/core/dreamer.js.map +1 -0
  45. package/dist/core/embedder.d.ts.map +1 -1
  46. package/dist/core/embedder.js +9 -5
  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 +44 -22
  50. package/dist/core/extractor.js.map +1 -1
  51. package/dist/core/graph.d.ts +1 -0
  52. package/dist/core/graph.d.ts.map +1 -1
  53. package/dist/core/graph.js +8 -2
  54. package/dist/core/graph.js.map +1 -1
  55. package/dist/core/install-hooks.d.ts +41 -0
  56. package/dist/core/install-hooks.d.ts.map +1 -0
  57. package/dist/core/install-hooks.js +181 -0
  58. package/dist/core/install-hooks.js.map +1 -0
  59. package/dist/core/install-id.d.ts +9 -0
  60. package/dist/core/install-id.d.ts.map +1 -0
  61. package/dist/core/install-id.js +51 -0
  62. package/dist/core/install-id.js.map +1 -0
  63. package/dist/core/lesson-engine.d.ts +1 -5
  64. package/dist/core/lesson-engine.d.ts.map +1 -1
  65. package/dist/core/lesson-engine.js +13 -13
  66. package/dist/core/lesson-engine.js.map +1 -1
  67. package/dist/core/llm-validator.d.ts +16 -0
  68. package/dist/core/llm-validator.d.ts.map +1 -0
  69. package/dist/core/llm-validator.js +182 -0
  70. package/dist/core/llm-validator.js.map +1 -0
  71. package/dist/core/operations.d.ts.map +1 -1
  72. package/dist/core/operations.js +6 -2
  73. package/dist/core/operations.js.map +1 -1
  74. package/dist/core/projects.d.ts +14 -0
  75. package/dist/core/projects.d.ts.map +1 -0
  76. package/dist/core/projects.js +70 -0
  77. package/dist/core/projects.js.map +1 -0
  78. package/dist/core/query-expander.js.map +1 -1
  79. package/dist/core/scoring.d.ts +0 -4
  80. package/dist/core/scoring.d.ts.map +1 -1
  81. package/dist/core/scoring.js +3 -10
  82. package/dist/core/scoring.js.map +1 -1
  83. package/dist/core/serializer.d.ts.map +1 -1
  84. package/dist/core/serializer.js +2 -0
  85. package/dist/core/serializer.js.map +1 -1
  86. package/dist/core/signal-scorer.d.ts +9 -0
  87. package/dist/core/signal-scorer.d.ts.map +1 -0
  88. package/dist/core/signal-scorer.js +70 -0
  89. package/dist/core/signal-scorer.js.map +1 -0
  90. package/dist/core/types.d.ts +2 -30
  91. package/dist/core/types.d.ts.map +1 -1
  92. package/dist/core/updater.d.ts +16 -0
  93. package/dist/core/updater.d.ts.map +1 -1
  94. package/dist/core/updater.js +85 -0
  95. package/dist/core/updater.js.map +1 -1
  96. package/dist/core/version-check.d.ts +2 -1
  97. package/dist/core/version-check.d.ts.map +1 -1
  98. package/dist/core/version-check.js +154 -30
  99. package/dist/core/version-check.js.map +1 -1
  100. package/dist/db.d.ts +7 -0
  101. package/dist/db.d.ts.map +1 -1
  102. package/dist/db.js +91 -0
  103. package/dist/db.js.map +1 -1
  104. package/dist/knowledge-graph.d.ts +7 -3
  105. package/dist/knowledge-graph.d.ts.map +1 -1
  106. package/dist/knowledge-graph.js +43 -18
  107. package/dist/knowledge-graph.js.map +1 -1
  108. package/dist/skills-manifest.json +23 -18
  109. package/dist/storage/conflicts.d.ts +4 -1
  110. package/dist/storage/conflicts.d.ts.map +1 -1
  111. package/dist/storage/conflicts.js +5 -2
  112. package/dist/storage/conflicts.js.map +1 -1
  113. package/dist/transports/cli/cli.js +393 -37
  114. package/dist/transports/cli/cli.js.map +1 -1
  115. package/dist/transports/http/server.d.ts.map +1 -1
  116. package/dist/transports/http/server.js +167 -13
  117. package/dist/transports/http/server.js.map +1 -1
  118. package/dist/transports/schemas.d.ts +2 -2
  119. package/hooks/hooks.json +12 -0
  120. package/package.json +16 -6
  121. package/plugin.json +1 -1
  122. package/scripts/hooks/_shared.js +257 -5
  123. package/scripts/hooks/pre-compact.js +19 -9
  124. package/scripts/hooks/session-start.js +364 -15
  125. package/scripts/hooks/session-summary.js +131 -29
  126. package/scripts/hooks/user-prompt-intent.js +159 -0
  127. package/skills/memesh/SKILL.md +9 -5
  128. package/skills/memesh-review/SKILL.md +24 -0
@@ -1,16 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { createRequire } from 'module';
4
+ import { spawn } from 'child_process';
4
5
  import { createHash } from 'crypto';
5
6
  import { homedir } from 'os';
6
7
  import { join, basename } from 'path';
7
- import { existsSync, unlinkSync, rmSync, appendFileSync, chmodSync } from 'fs';
8
+ import { pathToFileURL } from 'url';
9
+ import { existsSync, readFileSync, unlinkSync, rmSync, appendFileSync, chmodSync } from 'fs';
8
10
  import {
9
11
  buildReferenceContext,
10
12
  ensurePrivateDir,
11
13
  getMemeshDir,
12
14
  isAgenticOrchestrationEnabled,
13
15
  isTrustedForAutoContext,
16
+ readUpdateCheckCache,
14
17
  resolvePluginRoot,
15
18
  resolveSessionLimit,
16
19
  writePrivateJson,
@@ -18,16 +21,326 @@ import {
18
21
 
19
22
  const require = createRequire(import.meta.url);
20
23
 
24
+ // Codex round 37: dist/core/install-channel.js is emitted as ESM
25
+ // (the project's tsconfig produces NodeNext modules). On Node 20.x
26
+ // `require()` against an ESM file throws ERR_REQUIRE_ESM, which
27
+ // silently downgraded all install-channel detection to 'unknown' on
28
+ // the supported floor. Pre-load the module via dynamic `import()`
29
+ // at hook startup using a top-level await — once at process init,
30
+ // not on every call. Falls back to null if the dist file is
31
+ // missing (source checkout pre-build) or fails to load.
32
+ let _installChannelMod = null;
33
+ try {
34
+ const _pluginRootForInit = resolvePluginRoot(import.meta.url);
35
+ const _modPath = join(_pluginRootForInit, 'dist/core/install-channel.js');
36
+ if (existsSync(_modPath)) {
37
+ _installChannelMod = await import(pathToFileURL(_modPath).href);
38
+ }
39
+ } catch { /* best-effort — fall through to 'unknown' channel */ }
40
+
21
41
  const dbPath = process.env.MEMESH_DB_PATH || join(homedir(), '.memesh', 'knowledge-graph.db');
22
42
  const memeshDir = getMemeshDir(process.env);
23
43
  const throttlePath = join(memeshDir, 'session-recalled-files.json');
24
44
  const nudgeFlagsDir = join(memeshDir, 'agent-nudge-flags');
25
45
 
46
+ /**
47
+ * Build the strong deprecation warning lines to prepend to the
48
+ * session-start banner when the installed version has been flagged
49
+ * by maintainers (typically a security advisory). Returns an empty
50
+ * array when the cache says nothing to warn about.
51
+ */
52
+ function buildDeprecationBanner(currentVersion, cache) {
53
+ if (!cache || cache.currentVersion !== currentVersion) return [];
54
+ const msg = cache.currentVersionDeprecation;
55
+ if (typeof msg !== 'string' || msg.length === 0) {
56
+ // Partial-failure state ONLY: the version lookup answered but
57
+ // the deprecation sub-call did not. checkSucceeded stays true
58
+ // exactly in that case. A full registry failure (offline,
59
+ // blocked) leaves checkSucceeded=false with a generic lastError,
60
+ // and we must not surface that as a security-style warning —
61
+ // it's just regular "couldn't reach npm". Gate the banner
62
+ // strictly on checkSucceeded=true + lastError populated.
63
+ if (
64
+ cache.checkSucceeded === true
65
+ && typeof cache.lastError === 'string'
66
+ && cache.lastError.length > 0
67
+ ) {
68
+ return [
69
+ '',
70
+ `ℹ️ MeMesh deprecation status unknown for ${currentVersion}: ${cache.lastError}`,
71
+ ` Run: memesh status (retry the lookup once back online)`,
72
+ ];
73
+ }
74
+ return [];
75
+ }
76
+ const lines = [
77
+ '',
78
+ `⚠️ MeMesh ${currentVersion} is DEPRECATED by maintainers.`,
79
+ ` ${msg}`,
80
+ ];
81
+ // Codex round 36: emit a remediation line for EVERY deprecation
82
+ // banner — including the cases where the cached `latestVersion`
83
+ // is null, equal to current, or stale. The previous gate omitted
84
+ // the action line whenever the cache didn't yet show a strictly-
85
+ // newer version, leaving users with a security warning and no
86
+ // follow-up step. doctor / CLI status / dashboard already point
87
+ // at `memesh update` (or channel equivalents) in those uncertain
88
+ // cases, and the session-start banner should match — `npm`
89
+ // resolves @latest at install time, so the command works even
90
+ // when our local cache is uncertain.
91
+ const knownUpgradeTarget = Boolean(
92
+ cache.latestVersion && cache.latestVersion !== currentVersion,
93
+ );
94
+ // Codex round 39: the SessionStart hook reads ONLY cached cache
95
+ // data — there's no fresh lookup happening on this code path.
96
+ // That means `freshness === 'fresh'` (the strict rule the
97
+ // dashboard / `memesh status` use to authoritatively say
98
+ // "no upgrade target yet") can never apply here. Round 38 used a
99
+ // 24h-window heuristic to fire the no-target message anyway, but
100
+ // codex correctly flagged that as suppressing the upgrade hint
101
+ // exactly when a security-advisory fix could ship within the
102
+ // window. Conservative remediation: always recommend
103
+ // `memesh update` (which is a harmless no-op when there's truly
104
+ // no target, and immediately applies a freshly-published fix
105
+ // when there is one). The "no target yet" message remains
106
+ // available in `memesh status` (fresh lookup) and the dashboard
107
+ // (after a Check now click).
108
+ // Tailor the remediation hint to the install channel. `memesh
109
+ // update` and `autoUpdate` only work for npm-global installs;
110
+ // pointing source-checkout / project-local users at those
111
+ // commands is misleading (especially when the deprecation is a
112
+ // security advisory). Detect the channel and suggest the
113
+ // remediation that actually applies.
114
+ let channel = 'unknown';
115
+ try {
116
+ const pluginRoot = resolvePluginRoot(import.meta.url);
117
+ channel = detectInstallChannelHook(pluginRoot);
118
+ } catch { /* best-effort — fall through to generic guidance */ }
119
+
120
+ if (channel === 'npm-global') {
121
+ lines.push(
122
+ knownUpgradeTarget
123
+ ? ` Run: memesh update (or set autoUpdate: memesh config set autoUpdate patch)`
124
+ : ` Run: memesh update (resolves @latest — or set: memesh config set autoUpdate patch)`,
125
+ );
126
+ } else if (channel === 'source-checkout') {
127
+ lines.push(` Source checkout: pull and rebuild (\`git pull && npm install && npm run build\`).`);
128
+ } else if (channel === 'npm-local') {
129
+ // Codex round 30: the cached `latestVersion` may itself be
130
+ // stale (cache TTL is 24h and we're already showing a stale
131
+ // banner). Pinning a specific version risks installing an
132
+ // already-superseded build that's part of the same security
133
+ // advisory. `@latest` always resolves to the registry's
134
+ // current dist-tag at install time, which is the right
135
+ // remediation for a deprecation/security-advisory banner.
136
+ lines.push(
137
+ knownUpgradeTarget
138
+ ? ` Project-local install: run \`npm install @pcircle/memesh@latest\` in this project (cached upgrade target was ${cache.latestVersion}).`
139
+ : ` Project-local install: run \`npm install @pcircle/memesh@latest\` in this project.`,
140
+ );
141
+ } else {
142
+ lines.push(
143
+ knownUpgradeTarget
144
+ ? ` Upgrade via the install path you used: fetch the latest @pcircle/memesh from npm (cached upgrade target was ${cache.latestVersion}).`
145
+ : ` Upgrade via the install path you used: fetch the latest @pcircle/memesh from npm.`,
146
+ );
147
+ }
148
+ return lines;
149
+ }
150
+
151
+ /**
152
+ * Detect the install channel of the running memesh binary by
153
+ * delegating to src/core/install-channel.ts via the dist build. The
154
+ * core helper resolves `npm root -g` so it correctly classifies:
155
+ * - POSIX globals at the default prefix (`/usr/local/lib/...`)
156
+ * - Windows globals (`%AppData%\npm\...`)
157
+ * - Globals at custom prefixes set via `npm config set prefix`
158
+ * - Project-local deps under any directory name (no false-positive
159
+ * 'npm-global' from a path that merely contains `lib`)
160
+ *
161
+ * Earlier hook-side regex heuristics agreed with the core logic on
162
+ * the common cases but disagreed on custom prefixes (false negative
163
+ * → auto-update silently broken) and on project-local deps living
164
+ * under `lib/node_modules/...` (false positive → spawning a global
165
+ * `npm install -g` while the active copy is the local one). Using
166
+ * the dist module here keeps the hook in lockstep with whatever
167
+ * `memesh status` says, paying the one-time `npm root -g` cost
168
+ * (~50-200ms) only on auto-update decision.
169
+ *
170
+ * Returns 'npm-global' | 'npm-local' | 'source-checkout' | 'unknown'.
171
+ * Synchronous + best-effort: any failure in the dist import or the
172
+ * underlying `npm root -g` call returns 'unknown', and the auto-
173
+ * update spawn refuses to fire on 'unknown' so we never run
174
+ * `npm install -g` when we can't confirm it would land where the
175
+ * user expects.
176
+ */
177
+ function detectInstallChannelHook(pluginRoot) {
178
+ if (!_installChannelMod) return 'unknown';
179
+ try {
180
+ return _installChannelMod.getCurrentInstallChannel({ packageRoot: pluginRoot });
181
+ } catch {
182
+ return 'unknown';
183
+ }
184
+ }
185
+
186
+ // Don't fire a fresh-check more often than this. Two parallel
187
+ // session-starts both spawning `memesh status` could otherwise race
188
+ // the cache: a later writer that hits a deprecation-only timeout
189
+ // would overwrite an earlier writer's successful deprecation flag,
190
+ // because each child reads `previous` from the cache *before* its
191
+ // own npm call. The TTL bounds concurrency to one refresh per
192
+ // window per machine, which is enough for the staleness window
193
+ // (24h) to stay accurate.
194
+ const FRESH_CHECK_THROTTLE_MS = 5 * 60 * 1000;
195
+
196
+ function spawnFreshUpdateCheck(installedVersion) {
197
+ try {
198
+ const pluginRoot = resolvePluginRoot(import.meta.url);
199
+ const cliPath = join(pluginRoot, 'dist/transports/cli/cli.js');
200
+ if (!existsSync(cliPath)) return false;
201
+ const fs = require('fs');
202
+ const dir = join(homedir(), '.memesh');
203
+ try { ensurePrivateDir(dir); } catch { /* best-effort */ }
204
+ // Codex round 37: scope the throttle marker to the installed
205
+ // version. The marker was machine-global, so a refresh started
206
+ // by a global 4.1.3 install would suppress refreshes for a
207
+ // sibling project-local 4.1.1 for the next 5 minutes — and the
208
+ // shared cache it wrote would carry version 4.1.3, so the
209
+ // 4.1.1 session would skip its banner because
210
+ // `cache.currentVersion !== currentVersion`. Per-version
211
+ // markers ensure each install gets its own refresh window.
212
+ // Sanitize version for filesystem (semver chars only, no path
213
+ // separators); fall back to 'unknown' if missing.
214
+ const versionTag = typeof installedVersion === 'string'
215
+ && /^[0-9A-Za-z.+-]+$/.test(installedVersion)
216
+ ? installedVersion
217
+ : 'unknown';
218
+ const markerPath = join(dir, `last-fresh-refresh.${versionTag}.lock`);
219
+ // Single-owner claim: O_EXCL atomic create. Codex round 27
220
+ // caught that the previous temp+rename+readback pattern was
221
+ // racy — both peers' renames are destructive, so each could
222
+ // read its own token back and both would spawn a refresh.
223
+ // O_EXCL is the standard POSIX/libuv primitive that lets at
224
+ // most one process succeed. Same pattern as
225
+ // tryAcquireAutoUpdateLock above.
226
+ const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
227
+ const claim = () => {
228
+ try {
229
+ const fd = fs.openSync(
230
+ markerPath,
231
+ fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL,
232
+ 0o600,
233
+ );
234
+ try { fs.writeFileSync(fd, token); } finally { fs.closeSync(fd); }
235
+ return 'won';
236
+ } catch (err) {
237
+ if (err?.code === 'EEXIST') return 'exists';
238
+ return 'error';
239
+ }
240
+ };
241
+ let result = claim();
242
+ if (result === 'exists') {
243
+ // Marker already there. Honor the throttle window: if it's
244
+ // fresh, a peer owns this slot. If it's stale, take it over
245
+ // by removing the marker and retrying ONCE — best-effort,
246
+ // multiple processes may race the unlink but only one can
247
+ // win the subsequent O_EXCL.
248
+ let stat;
249
+ try { stat = fs.statSync(markerPath); } catch { return false; }
250
+ if (Date.now() - stat.mtimeMs < FRESH_CHECK_THROTTLE_MS) {
251
+ return false;
252
+ }
253
+ try { fs.unlinkSync(markerPath); } catch { /* peer already removed */ }
254
+ result = claim();
255
+ }
256
+ if (result !== 'won') return false;
257
+ const child = spawn(
258
+ process.execPath,
259
+ [cliPath, 'status'],
260
+ // windowsHide prevents a console-window flash on every session
261
+ // start on Windows; harmless on POSIX.
262
+ {
263
+ detached: true,
264
+ stdio: 'ignore',
265
+ env: { ...process.env, MEMESH_UPDATE_REFRESH: '1' },
266
+ windowsHide: true,
267
+ },
268
+ );
269
+ child.unref();
270
+ return true;
271
+ } catch {
272
+ return false;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Run the post-banner update tasks: spawn auto-update if policy + cache
278
+ * permit, and always refresh the cache for the next session.
279
+ *
280
+ * Known one-session delay (documented):
281
+ * The very first session after install (or after the cache file is
282
+ * deleted) emits the recall summary BEFORE this finally clause
283
+ * runs, so a freshly-installed deprecated version sees no
284
+ * deprecation banner on session 1. The detached refresh below
285
+ * populates the cache for session 2, where the banner and the
286
+ * security override fire normally. We deliberately don't do an
287
+ * inline synchronous npm fetch here because that would block every
288
+ * cold-cache session-start by ~3s for users whose installed version
289
+ * is healthy — a worse trade for the common case.
290
+ *
291
+ * Idempotent guard: callers should not invoke twice for the same
292
+ * session — duplicated `npm install -g` spawns would race. We use a
293
+ * one-shot flag rather than a no-op-on-second-call lock so a coding
294
+ * mistake produces visible breakage during testing instead of silent
295
+ * over-spawning.
296
+ */
297
+ let __postBannerRan = false;
298
+ function runPostBannerUpdateTasks() {
299
+ if (__postBannerRan) return;
300
+ __postBannerRan = true;
301
+ try {
302
+ let installedVersion = null;
303
+ try {
304
+ const pluginRoot = resolvePluginRoot(import.meta.url);
305
+ const pkg = JSON.parse(readFileSync(join(pluginRoot, 'package.json'), 'utf8'));
306
+ installedVersion = typeof pkg.version === 'string' ? pkg.version : null;
307
+ } catch { /* best-effort */ }
308
+ if (!installedVersion) return;
309
+ // Auto-update spawn moved to Stop hook (v4.1.4) to avoid TOCTOU race
310
+ // where npm install -g overwrites dist/ while peer hooks are still reading it.
311
+ spawnFreshUpdateCheck(installedVersion);
312
+ } catch {
313
+ // Best-effort — never crash the hook on a network or fs hiccup.
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Build a "base message + optional deprecation banner" combined
319
+ * single-line systemMessage payload. Keeps stdout a single JSON
320
+ * object on every empty/no-DB exit path so Claude Code's hook
321
+ * contract holds.
322
+ */
323
+ function combineWithBanner(baseMessage) {
324
+ let lines = [];
325
+ try {
326
+ const pluginRoot = resolvePluginRoot(import.meta.url);
327
+ const pkg = JSON.parse(readFileSync(join(pluginRoot, 'package.json'), 'utf8'));
328
+ const installedVersion = typeof pkg.version === 'string' ? pkg.version : null;
329
+ const cache = readUpdateCheckCache(installedVersion);
330
+ lines = installedVersion ? buildDeprecationBanner(installedVersion, cache) : [];
331
+ } catch {
332
+ // Best-effort — fall through to base message only.
333
+ }
334
+ if (lines.length === 0) return baseMessage;
335
+ return [...lines.filter((l) => l.length > 0), '', baseMessage].join('\n');
336
+ }
337
+
26
338
  let input = '';
27
339
  process.stdin.setEncoding('utf8');
28
340
  process.stdin.on('data', (chunk) => { input += chunk; });
29
341
  process.stdin.on('end', async () => {
30
342
  try {
343
+ try {
31
344
  const data = JSON.parse(input);
32
345
  const projectName = basename(data.cwd || process.cwd());
33
346
 
@@ -48,7 +361,10 @@ process.stdin.on('end', async () => {
48
361
  }
49
362
 
50
363
  if (!existsSync(dbPath)) {
51
- output('MeMesh: No database found. Memories will be created as you work.');
364
+ // Combine deprecation banner (if any) into the same
365
+ // systemMessage so stdout stays a single JSON document. Outer
366
+ // finally runs runPostBannerUpdateTasks().
367
+ output(combineWithBanner('MeMesh: No database found. Memories will be created as you work.'));
52
368
  return;
53
369
  }
54
370
 
@@ -62,7 +378,7 @@ process.stdin.on('end', async () => {
62
378
  "SELECT name FROM sqlite_master WHERE type='table' AND name='entities'"
63
379
  ).get();
64
380
  if (!tableCheck) {
65
- output('MeMesh: Database exists but no memories stored yet.');
381
+ output(combineWithBanner('MeMesh: Database exists but no memories stored yet.'));
66
382
  return;
67
383
  }
68
384
 
@@ -157,8 +473,15 @@ process.stdin.on('end', async () => {
157
473
  }
158
474
  }
159
475
 
160
- // No memories at all — output nothing (don't clutter session)
476
+ // No memories at all — surface only the deprecation banner if
477
+ // active (so a flagged install still warns the user) and skip
478
+ // the rest of the recall-summary work. The outer finally still
479
+ // runs runPostBannerUpdateTasks().
161
480
  if (lines.length === 0) {
481
+ const bannerOnly = combineWithBanner('');
482
+ if (bannerOnly && bannerOnly.trim().length > 0) {
483
+ output(bannerOnly.trim());
484
+ }
162
485
  return;
163
486
  }
164
487
 
@@ -294,14 +617,28 @@ process.stdin.on('end', async () => {
294
617
  // Non-critical — don't break session start
295
618
  }
296
619
 
297
- const hookOutput = {
298
- suppressOutput: true,
299
- hookSpecificOutput: {
300
- hookEventName: 'SessionStart',
301
- additionalContext: buildReferenceContext(memorySummary.split('\n')),
302
- },
303
- };
304
- console.log(JSON.stringify(hookOutput));
620
+ // Deprecation-aware banner. Reads the cache produced by the
621
+ // last `getUpdateCheck` (CLI or background refresh). When the
622
+ // installed version was flagged by maintainers (typically a
623
+ // security advisory), prepend a strong warning so the user sees
624
+ // it on every session start until they upgrade.
625
+ let installedVersion = null;
626
+ try {
627
+ const pluginRoot = resolvePluginRoot(import.meta.url);
628
+ const pkg = JSON.parse(readFileSync(join(pluginRoot, 'package.json'), 'utf8'));
629
+ installedVersion = typeof pkg.version === 'string' ? pkg.version : null;
630
+ } catch {
631
+ // Best-effort — without the version we can't compare to cache.
632
+ }
633
+ const updateCache = readUpdateCheckCache(installedVersion);
634
+ const deprecationLines = installedVersion
635
+ ? buildDeprecationBanner(installedVersion, updateCache)
636
+ : [];
637
+ const memorySummaryWithBanner = deprecationLines.length > 0
638
+ ? [...deprecationLines, '', ...memorySummary.split('\n')].join('\n')
639
+ : memorySummary;
640
+
641
+ output(buildReferenceContext(memorySummaryWithBanner.split('\n')));
305
642
  } finally {
306
643
  db.close();
307
644
  }
@@ -323,9 +660,21 @@ process.stdin.on('end', async () => {
323
660
  } catch {
324
661
  // Non-critical — noise compression failed, will retry next session
325
662
  }
326
- } catch (err) {
327
- // Hooks must never crash Claude Code — but report honestly
328
- console.log(JSON.stringify({ systemMessage: `MeMesh: Session start failed (${err?.message || 'unknown error'}). Memories not loaded.` }));
663
+
664
+ } catch (err) {
665
+ // Hooks must never crash Claude Code but report honestly.
666
+ // Inner catch so the outer finally can still run the post-
667
+ // banner update tasks even when the recall flow blew up.
668
+ console.log(JSON.stringify({ systemMessage: `MeMesh: Session start failed (${err?.message || 'unknown error'}). Memories not loaded.` }));
669
+ }
670
+ } finally {
671
+ // ── Auto-update + cache refresh ──────────────────────────────
672
+ // Outer finally guarantees this runs on every exit path — no-DB
673
+ // short-circuit, empty-DB return, no-memories return, recall
674
+ // happy path, or even a thrown error caught above. The
675
+ // function is single-shot per process so the duplicated
676
+ // late-path call from older versions is now idempotent.
677
+ runPostBannerUpdateTasks();
329
678
  }
330
679
  });
331
680