@pcircle/memesh 4.9.0 → 4.9.3

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 (47) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.de.md +1 -1
  5. package/README.md +2 -2
  6. package/README.zh-TW.md +2 -2
  7. package/dashboard/dist/index.html +10 -10
  8. package/dist/core/doctor-fixes.d.ts +19 -0
  9. package/dist/core/doctor-fixes.d.ts.map +1 -0
  10. package/dist/core/doctor-fixes.js +104 -0
  11. package/dist/core/doctor-fixes.js.map +1 -0
  12. package/dist/core/doctor.d.ts +1 -1
  13. package/dist/core/doctor.d.ts.map +1 -1
  14. package/dist/core/doctor.js +3 -3
  15. package/dist/core/doctor.js.map +1 -1
  16. package/dist/core/operations.d.ts.map +1 -1
  17. package/dist/core/operations.js +2 -2
  18. package/dist/core/operations.js.map +1 -1
  19. package/dist/core/schema-export.js +1 -1
  20. package/dist/core/schema-export.js.map +1 -1
  21. package/dist/knowledge-graph.d.ts.map +1 -1
  22. package/dist/knowledge-graph.js +30 -10
  23. package/dist/knowledge-graph.js.map +1 -1
  24. package/dist/mcp/THIRD_PARTY_NOTICES.txt +2 -2
  25. package/dist/mcp/server.js +45 -16
  26. package/dist/mcp/server.js.map +1 -1
  27. package/dist/skills-manifest.json +12 -12
  28. package/dist/transports/agent-messaging.d.ts +4 -0
  29. package/dist/transports/agent-messaging.d.ts.map +1 -1
  30. package/dist/transports/agent-messaging.js +12 -2
  31. package/dist/transports/agent-messaging.js.map +1 -1
  32. package/dist/transports/cli/cli.d.ts.map +1 -1
  33. package/dist/transports/cli/cli.js +60735 -1770
  34. package/dist/transports/cli/cli.js.map +6 -1
  35. package/dist/transports/http/server.d.ts.map +1 -1
  36. package/dist/transports/http/server.js +32 -1
  37. package/dist/transports/http/server.js.map +1 -1
  38. package/dist/transports/mcp/handlers.d.ts +3 -3
  39. package/dist/transports/mcp/handlers.js +3 -3
  40. package/dist/transports/mcp/handlers.js.map +1 -1
  41. package/docs/platforms/agent-messaging.md +10 -1
  42. package/hooks/hooks.json +1 -1
  43. package/package.json +2 -2
  44. package/scripts/hooks/_shared.js +150 -1
  45. package/scripts/hooks/session-start.js +90 -4
  46. package/scripts/hooks/session-summary.js +17 -4
  47. package/scripts/hooks/user-prompt-intent.js +71 -11
@@ -26,6 +26,10 @@ import {
26
26
  memeshDir as memeshHomeDir,
27
27
  parseTaskState,
28
28
  readRepoState,
29
+ readAutoUpdateConsent,
30
+ claimUpdatePrompt,
31
+ finalizeUpdatePromptClaim,
32
+ readUpdatePromptClaim,
29
33
  readUpdateCheckCache,
30
34
  repoStateLines,
31
35
  resolvePluginRoot,
@@ -34,6 +38,7 @@ import {
34
38
  homeDir,
35
39
  taskStateName,
36
40
  writeCitationRule,
41
+ writeAutoUpdateConsent,
37
42
  writePrivateJson,
38
43
  } from './_shared.js';
39
44
  import { MemeshDatabase } from './_generated/sqlite.js';
@@ -326,6 +331,43 @@ function detectInstallChannelHook(pluginRoot) {
326
331
  }
327
332
  }
328
333
 
334
+ function buildUpdateConsentPrompt(sessionId, currentVersion, cache, channel) {
335
+ if (!sessionId || sessionId === 'unknown' || !cache || cache.currentVersion !== currentVersion) return null;
336
+ if (!cache.latestVersion || !isStrictlyOlder(currentVersion, cache.latestVersion)) return null;
337
+ const existing = readAutoUpdateConsent(sessionId, currentVersion, cache.latestVersion, channel);
338
+ if (existing?.decision) return null;
339
+ // Claim the session-level notice before emitting it. A resumed hook or a
340
+ // concurrent host process (including a second host channel) sees the
341
+ // atomic claim and does not duplicate the ask.
342
+ if (readUpdatePromptClaim(sessionId, currentVersion, cache.latestVersion)
343
+ || !claimUpdatePrompt(sessionId, currentVersion, cache.latestVersion, channel)) return null;
344
+ const target = channel === 'plugin-marketplace'
345
+ ? 'the installed marketplace plugin'
346
+ : channel === 'npm-global' ? 'the global memesh installation' : 'this MeMesh installation';
347
+ if (channel !== 'npm-global') {
348
+ const pluginRoot = resolvePluginRoot(import.meta.url);
349
+ const action = channel === 'plugin-marketplace'
350
+ ? pluginUpgradeLine(pluginRoot)
351
+ : channel === 'source-checkout'
352
+ ? ' Source checkout: pull and rebuild (`git pull && npm install && npm run build`).'
353
+ : channel === 'npm-local'
354
+ ? ' Project-local install: run `npm install @pcircle/memesh@latest` in the project that installed it.'
355
+ : ' Update it through the tool or package manager that installed MeMesh.';
356
+ return {
357
+ system: `\nℹ️ MeMesh ${cache.latestVersion} is available (you're on ${currentVersion}) for ${target}. This installation cannot be upgraded automatically from this session.\n${action}`,
358
+ context: `MeMesh ${cache.latestVersion} is available for ${target}, but this channel has no safe in-session installer. Show the user the channel-specific update action and do not claim that an Upgrade reply will install it.`,
359
+ };
360
+ }
361
+ // The npm-global channel is the only hook-owned installer. Record a
362
+ // channel-specific pending consent for the Stop hook after the global
363
+ // session notice has been claimed.
364
+ if (!writeAutoUpdateConsent(sessionId, currentVersion, cache.latestVersion, channel, 'pending')) return null;
365
+ return {
366
+ system: `\nℹ️ MeMesh ${cache.latestVersion} is available (you're on ${currentVersion}) for ${target}. Reply “Upgrade” to install it, or “Not now” to skip for this session.`,
367
+ context: `MeMesh update consent is pending for this session. Ask the user whether to upgrade from ${currentVersion} to ${cache.latestVersion} for the ${target}. Wait for an explicit Upgrade or Not now response; do not install without affirmative consent.`,
368
+ };
369
+ }
370
+
329
371
  /**
330
372
  * Which plugin runtime owns this copy: 'claude-code', 'codex', or null.
331
373
  *
@@ -503,6 +545,15 @@ function runPostBannerUpdateTasks() {
503
545
  if (__postBannerRan) return;
504
546
  __postBannerRan = true;
505
547
  try {
548
+ // A first-use session has no database yet. Starting the detached
549
+ // `memesh status` refresh in that state makes status create/migrate the
550
+ // database while the next SessionStart may already be opening it
551
+ // read-only. SQLite can expose that window as a partially-created schema
552
+ // (for example, `entities` exists while `tags` does not), turning a
553
+ // harmless update notice into "memories not loaded". The next session
554
+ // after the first capture will refresh the cache once the database is
555
+ // fully established; consent itself was already emitted above.
556
+ if (!existsSync(dbPath)) return;
506
557
  let installedVersion = null;
507
558
  try {
508
559
  const pluginRoot = resolvePluginRoot(import.meta.url);
@@ -679,7 +730,21 @@ process.stdin.on('end', async () => {
679
730
  // With no database there is nothing to recall either — the warning IS
680
731
  // the whole truth, and "memories will be created as you work" would
681
732
  // contradict it one line later.
682
- output(combineWithBanner(captureWarning ?? '◉ MeMesh ready · no database yet, memories will be created as you work'));
733
+ const emptySummary = combineWithBanner(captureWarning ?? '◉ MeMesh ready · no database yet, memories will be created as you work');
734
+ let consent = null;
735
+ let consentVersion = null;
736
+ let consentCache = null;
737
+ try {
738
+ const pluginRoot = resolvePluginRoot(import.meta.url);
739
+ const pkg = JSON.parse(readFileSync(join(pluginRoot, 'package.json'), 'utf8'));
740
+ consentVersion = typeof pkg.version === 'string' ? pkg.version : null;
741
+ consentCache = readUpdateCheckCache(consentVersion);
742
+ const channel = detectInstallChannelHook(pluginRoot);
743
+ consent = buildUpdateConsentPrompt(data.session_id, consentVersion, consentCache, channel);
744
+ } catch { /* best-effort */ }
745
+ output(consent ? `${consent.system}\n${emptySummary}` : emptySummary,
746
+ consent ? `${consent.context}\n\n${workPackageGuidance}` : workPackageGuidance);
747
+ if (consent) finalizeUpdatePromptClaim(data.session_id, consentVersion, consentCache?.latestVersion);
683
748
  return;
684
749
  }
685
750
 
@@ -1150,20 +1215,41 @@ process.stdin.on('end', async () => {
1150
1215
  }
1151
1216
  const updateCache = readUpdateCheckCache(installedVersion);
1152
1217
  let bannerLines = [];
1218
+ let updateConsentContext = null;
1153
1219
  if (installedVersion) {
1154
1220
  const deprecation = buildDeprecationBanner(installedVersion, updateCache);
1155
1221
  if (deprecation.length > 0) {
1156
1222
  bannerLines = deprecation;
1157
1223
  } else {
1158
- bannerLines = buildUpdateAvailableBanner(installedVersion, updateCache,
1159
- () => detectInstallChannelHook(resolvePluginRoot(import.meta.url)));
1224
+ const channel = detectInstallChannelHook(resolvePluginRoot(import.meta.url));
1225
+ const consent = buildUpdateConsentPrompt(data.session_id, installedVersion, updateCache, channel);
1226
+ if (consent) {
1227
+ // The first-use notice is the authoritative update message for
1228
+ // this session. Do not append the softer 24h banner as well; a
1229
+ // source/plugin user would otherwise see two contradictory
1230
+ // update instructions in one SessionStart payload.
1231
+ bannerLines = [consent.system];
1232
+ updateConsentContext = consent.context;
1233
+ } else if (readUpdatePromptClaim(data.session_id, installedVersion, updateCache?.latestVersion)) {
1234
+ // A first-use notice was already shown (and may have been
1235
+ // answered by UserPromptSubmit). Suppress the routine banner for
1236
+ // the rest of this session as well.
1237
+ bannerLines = [];
1238
+ } else {
1239
+ bannerLines = buildUpdateAvailableBanner(installedVersion, updateCache, () => channel);
1240
+ }
1160
1241
  }
1161
1242
  }
1162
1243
  const finalMessage = bannerLines.length > 0
1163
1244
  ? [...bannerLines.filter(l => l.length > 0), '', summary].join('\n')
1164
1245
  : summary;
1165
1246
 
1166
- output(withCaptureWarning(finalMessage), memoryContext);
1247
+ output(withCaptureWarning(finalMessage), updateConsentContext
1248
+ ? `${updateConsentContext}\n\n${memoryContext}`
1249
+ : memoryContext);
1250
+ if (updateConsentContext) {
1251
+ finalizeUpdatePromptClaim(data.session_id, installedVersion, updateCache?.latestVersion);
1252
+ }
1167
1253
 
1168
1254
  // Pre-read the noise-compression throttle on the handle we already
1169
1255
  // hold. compressWeeklyNoise() re-checks under its own connection, but
@@ -15,6 +15,7 @@ import {
15
15
  extractCitedMemoryIds,
16
16
  getMemeshDirFromDbPath,
17
17
  getProjectName,
18
+ findAutoUpdateConsent,
18
19
  isAutoCaptureEnabled,
19
20
  openHookDb,
20
21
  readUpdateCheckCache,
@@ -40,7 +41,7 @@ try {
40
41
  // Best-effort: source checkouts may not have built dist output yet.
41
42
  }
42
43
 
43
- async function runAutoUpdateAtStop() {
44
+ async function runAutoUpdateAtStop(sessionId) {
44
45
  try {
45
46
  const pluginRoot = resolvePluginRoot(import.meta.url);
46
47
  const pkg = JSON.parse(readFileSync(join(pluginRoot, 'package.json'), 'utf8'));
@@ -50,7 +51,14 @@ async function runAutoUpdateAtStop() {
50
51
  const cache = readUpdateCheckCache(installedVersion);
51
52
  const policy = resolveAutoUpdatePolicy(process.env);
52
53
  const decision = decideAutoUpdateHook(installedVersion, cache, policy);
53
- if (decision.run) await spawnAutoUpdate(decision.latest, installChannel);
54
+ let channel = 'unknown';
55
+ try { channel = installChannel?.getCurrentInstallChannel({ packageRoot: pluginRoot }) ?? 'unknown'; } catch { /* best-effort */ }
56
+ const consent = decision.run
57
+ ? findAutoUpdateConsent(sessionId, installedVersion, decision.latest, channel)
58
+ : null;
59
+ if (decision.run && consent?.decision === 'approved') {
60
+ await spawnAutoUpdate(decision.latest, installChannel);
61
+ }
54
62
  } catch {
55
63
  // Best-effort: update failures must never break session capture.
56
64
  }
@@ -204,6 +212,7 @@ let input = '';
204
212
  process.stdin.setEncoding('utf8');
205
213
  process.stdin.on('data', (chunk) => { input += chunk; });
206
214
  process.stdin.on('end', async () => {
215
+ let sessionId = 'unknown';
207
216
  try {
208
217
  if (!input.trim()) return exit0();
209
218
 
@@ -224,7 +233,7 @@ process.stdin.on('end', async () => {
224
233
  return exit0();
225
234
  }
226
235
 
227
- const sessionId = inputData.session_id || 'unknown';
236
+ sessionId = inputData.session_id || 'unknown';
228
237
  const transcriptPath = inputData.transcript_path;
229
238
 
230
239
  // `cwd` decides the project tag, and the project tag decides which
@@ -337,6 +346,10 @@ process.stdin.on('end', async () => {
337
346
  ).get(`session-${sessionId}-files`, `session-${sessionId}-fixes`, `session-${sessionId}-summary`);
338
347
  if (alreadyCaptured) {
339
348
  recordHookRun(db, 'session-summary');
349
+ // A duplicate capture is still a completed Stop lifecycle. Update
350
+ // consent is session-scoped and must not be skipped merely because
351
+ // the same transcript was observed twice (a common host retry).
352
+ await runAutoUpdateAtStop(sessionId);
340
353
  return exit0();
341
354
  }
342
355
 
@@ -587,7 +600,7 @@ process.stdin.on('end', async () => {
587
600
 
588
601
  // Update only after all session work so installed files cannot change while
589
602
  // this hook is still reading them.
590
- await runAutoUpdateAtStop();
603
+ await runAutoUpdateAtStop(sessionId);
591
604
 
592
605
  // Emit NOTHING on success — not `{"suppressOutput": true}`.
593
606
  //
@@ -19,7 +19,62 @@
19
19
  // Gated by `autoCapture` flag (same as other memesh write hooks).
20
20
 
21
21
  import { pathToFileURL } from 'url';
22
- import { isAutoCaptureEnabled } from './_shared.js';
22
+ import {
23
+ findAutoUpdateConsent,
24
+ isAutoCaptureEnabled,
25
+ parseAutoUpdateConsent,
26
+ readUpdateCheckCache,
27
+ resolvePluginRoot,
28
+ writeAutoUpdateConsent,
29
+ } from './_shared.js';
30
+ import { join } from 'path';
31
+ import { existsSync, readFileSync } from 'fs';
32
+
33
+ let installChannelMod = null;
34
+ try {
35
+ const pluginRoot = resolvePluginRoot(import.meta.url);
36
+ const modulePath = join(pluginRoot, 'dist/core/install-channel.js');
37
+ if (existsSync(modulePath)) installChannelMod = await import(pathToFileURL(modulePath).href);
38
+ } catch {
39
+ // Source checkouts without a build remain non-blocking and cannot self-update.
40
+ }
41
+
42
+ function currentInstallChannel() {
43
+ try {
44
+ return installChannelMod?.getCurrentInstallChannel({
45
+ packageRoot: resolvePluginRoot(import.meta.url),
46
+ }) ?? 'unknown';
47
+ } catch {
48
+ return 'unknown';
49
+ }
50
+ }
51
+
52
+ function currentInstalledVersion() {
53
+ try {
54
+ const pkg = JSON.parse(readFileSync(join(resolvePluginRoot(import.meta.url), 'package.json'), 'utf8'));
55
+ return typeof pkg.version === 'string' ? pkg.version : null;
56
+ } catch { return null; }
57
+ }
58
+
59
+ function recordUpdateConsent(sessionId, prompt) {
60
+ const current = currentInstalledVersion();
61
+ if (!current || !sessionId) return null;
62
+ const channel = currentInstallChannel();
63
+ // Only npm-global has a hook-owned installer. Other channels receive an
64
+ // actionable notice at SessionStart and must not turn an "Upgrade" word
65
+ // into a misleading approval marker for a different installation path.
66
+ if (channel !== 'npm-global') return null;
67
+ const cache = readUpdateCheckCache(current);
68
+ const latest = cache?.latestVersion;
69
+ if (typeof latest !== 'string' || !latest) return null;
70
+ const pending = findAutoUpdateConsent(sessionId, current, latest, channel);
71
+ if (!pending || !['pending'].includes(pending.decision)) return null;
72
+ const decision = parseAutoUpdateConsent(prompt);
73
+ if (!decision) return null;
74
+ return writeAutoUpdateConsent(
75
+ sessionId, current, latest, pending.channel ?? 'unknown', decision,
76
+ ) ? decision : null;
77
+ }
23
78
 
24
79
  // Patterns compiled at module load — invalid regex MUST fail loudly. Do
25
80
  // NOT move into a try block "for safety": a regex compile error is a
@@ -121,8 +176,6 @@ if (isMainModule) {
121
176
  process.stdin.on('data', (chunk) => { input += chunk; });
122
177
  process.stdin.on('end', () => {
123
178
  try {
124
- if (!isAutoCaptureEnabled(process.env)) return process.exit(0);
125
-
126
179
  // Distinguish empty stdin (legitimate degenerate event) from malformed
127
180
  // input (protocol drift). Both stay non-blocking, but only malformed
128
181
  // input is logged — empty is normal, garbage indicates a real bug.
@@ -142,14 +195,21 @@ if (isMainModule) {
142
195
  // we accept either name to survive a similar rename. If both are absent
143
196
  // or non-string, detectRememberIntent's type guard returns false safely.
144
197
  const prompt = data.prompt ?? data.user_prompt ?? '';
145
- if (!detectRememberIntent(prompt)) return process.exit(0);
146
-
147
- const out = {
148
- hookSpecificOutput: {
149
- hookEventName: 'UserPromptSubmit',
150
- additionalContext: buildHint(),
151
- },
152
- };
198
+ const updateDecision = recordUpdateConsent(data.session_id, prompt);
199
+ const rememberIntent = detectRememberIntent(prompt);
200
+ if (!rememberIntent && !updateDecision) return process.exit(0);
201
+ // Update consent is a user-authorized control decision, not memory
202
+ // capture; it must still be recorded when auto-capture is disabled.
203
+ if (!isAutoCaptureEnabled(process.env) && !updateDecision) return process.exit(0);
204
+
205
+ const contexts = [];
206
+ if (updateDecision) {
207
+ contexts.push(updateDecision === 'approved'
208
+ ? 'The user explicitly approved the MeMesh upgrade. The Stop hook may now update the consented installation.'
209
+ : 'The user declined the MeMesh upgrade for this session. Do not install it or ask again in this session.');
210
+ }
211
+ if (rememberIntent) contexts.push(buildHint());
212
+ const out = { hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: contexts.join('\n\n') } };
153
213
  process.stdout.write(JSON.stringify(out));
154
214
  process.exit(0);
155
215
  } catch (err) {