@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.de.md +1 -1
- package/README.md +2 -2
- package/README.zh-TW.md +2 -2
- package/dashboard/dist/index.html +10 -10
- package/dist/core/doctor-fixes.d.ts +19 -0
- package/dist/core/doctor-fixes.d.ts.map +1 -0
- package/dist/core/doctor-fixes.js +104 -0
- package/dist/core/doctor-fixes.js.map +1 -0
- package/dist/core/doctor.d.ts +1 -1
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +3 -3
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +2 -2
- package/dist/core/operations.js.map +1 -1
- package/dist/core/schema-export.js +1 -1
- package/dist/core/schema-export.js.map +1 -1
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +30 -10
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/mcp/THIRD_PARTY_NOTICES.txt +2 -2
- package/dist/mcp/server.js +45 -16
- package/dist/mcp/server.js.map +1 -1
- package/dist/skills-manifest.json +12 -12
- package/dist/transports/agent-messaging.d.ts +4 -0
- package/dist/transports/agent-messaging.d.ts.map +1 -1
- package/dist/transports/agent-messaging.js +12 -2
- package/dist/transports/agent-messaging.js.map +1 -1
- package/dist/transports/cli/cli.d.ts.map +1 -1
- package/dist/transports/cli/cli.js +60735 -1770
- package/dist/transports/cli/cli.js.map +6 -1
- package/dist/transports/http/server.d.ts.map +1 -1
- package/dist/transports/http/server.js +32 -1
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts +3 -3
- package/dist/transports/mcp/handlers.js +3 -3
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/docs/platforms/agent-messaging.md +10 -1
- package/hooks/hooks.json +1 -1
- package/package.json +2 -2
- package/scripts/hooks/_shared.js +150 -1
- package/scripts/hooks/session-start.js +90 -4
- package/scripts/hooks/session-summary.js +17 -4
- 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
|
-
|
|
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
|
-
|
|
1159
|
-
|
|
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),
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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) {
|