@pcircle/memesh 4.8.5 → 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/mcp.json +6 -4
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +60 -14
- package/README.de.md +83 -556
- package/README.md +83 -582
- package/README.zh-TW.md +84 -573
- package/dashboard/dist/index.html +10 -10
- package/dist/cli/view-live.d.ts.map +1 -1
- package/dist/cli/view-live.js +154 -384
- package/dist/cli/view-live.js.map +1 -1
- package/dist/core/agent-router.d.ts +11 -10
- package/dist/core/agent-router.d.ts.map +1 -1
- package/dist/core/agent-router.js +29 -14
- package/dist/core/agent-router.js.map +1 -1
- package/dist/core/agent-scope-id.d.ts.map +1 -1
- package/dist/core/agent-scope-id.js.map +1 -1
- package/dist/core/analytics.d.ts.map +1 -1
- package/dist/core/analytics.js.map +1 -1
- package/dist/core/config.d.ts +4 -40
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +75 -141
- package/dist/core/config.js.map +1 -1
- package/dist/core/demo.d.ts.map +1 -1
- package/dist/core/demo.js +6 -6
- package/dist/core/demo.js.map +1 -1
- 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 +2 -7
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +57 -229
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/dreamer.d.ts +32 -47
- package/dist/core/dreamer.d.ts.map +1 -1
- package/dist/core/dreamer.js +211 -703
- package/dist/core/dreamer.js.map +1 -1
- package/dist/core/install-channel.d.ts.map +1 -1
- package/dist/core/install-channel.js +4 -47
- package/dist/core/install-channel.js.map +1 -1
- package/dist/core/install-id.d.ts.map +1 -1
- package/dist/core/install-id.js.map +1 -1
- package/dist/core/kg-backfill.d.ts.map +1 -1
- package/dist/core/kg-backfill.js.map +1 -1
- package/dist/core/lesson-engine.d.ts +0 -5
- package/dist/core/lesson-engine.d.ts.map +1 -1
- package/dist/core/lesson-engine.js +0 -25
- package/dist/core/lesson-engine.js.map +1 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +54 -48
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core/memory-tool.d.ts.map +1 -1
- package/dist/core/memory-tool.js +17 -17
- package/dist/core/memory-tool.js.map +1 -1
- package/dist/core/operations.d.ts +2 -26
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +11 -246
- package/dist/core/operations.js.map +1 -1
- package/dist/core/paths.d.ts +4 -1
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +88 -14
- package/dist/core/paths.js.map +1 -1
- package/dist/core/product-improvements.js +2 -2
- package/dist/core/product-improvements.js.map +1 -1
- package/dist/core/schema-export.d.ts.map +1 -1
- package/dist/core/schema-export.js +11 -1
- package/dist/core/schema-export.js.map +1 -1
- package/dist/core/semver.d.ts +7 -0
- package/dist/core/semver.d.ts.map +1 -0
- package/dist/core/semver.js +49 -0
- package/dist/core/semver.js.map +1 -0
- package/dist/core/serializer.d.ts.map +1 -1
- package/dist/core/serializer.js +69 -57
- package/dist/core/serializer.js.map +1 -1
- package/dist/core/signal-scorer.d.ts.map +1 -1
- package/dist/core/signal-scorer.js.map +1 -1
- package/dist/core/transcript-extractor.d.ts +1 -85
- package/dist/core/transcript-extractor.d.ts.map +1 -1
- package/dist/core/transcript-extractor.js +5 -364
- package/dist/core/transcript-extractor.js.map +1 -1
- package/dist/core/transcript-source.d.ts +22 -6
- package/dist/core/transcript-source.d.ts.map +1 -1
- package/dist/core/transcript-source.js +108 -69
- package/dist/core/transcript-source.js.map +1 -1
- package/dist/core/types.d.ts +1 -17
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/version-check.d.ts.map +1 -1
- package/dist/core/version-check.js +11 -1
- package/dist/core/version-check.js.map +1 -1
- package/dist/db.d.ts +0 -34
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +3 -287
- package/dist/db.js.map +1 -1
- package/dist/host-runtime/acp.d.ts.map +1 -1
- package/dist/host-runtime/acp.js +2 -2
- package/dist/host-runtime/acp.js.map +1 -1
- package/dist/host-runtime/claude.d.ts.map +1 -1
- package/dist/host-runtime/claude.js +9 -7
- package/dist/host-runtime/claude.js.map +1 -1
- package/dist/host-runtime/codex-session.d.ts +9 -1
- package/dist/host-runtime/codex-session.d.ts.map +1 -1
- package/dist/host-runtime/codex-session.js +474 -29
- package/dist/host-runtime/codex-session.js.map +1 -1
- package/dist/host-runtime/codex.d.ts.map +1 -1
- package/dist/host-runtime/codex.js +2 -2
- package/dist/host-runtime/codex.js.map +1 -1
- package/dist/host-runtime/config.d.ts +1 -0
- package/dist/host-runtime/config.d.ts.map +1 -1
- package/dist/host-runtime/config.js +4 -0
- package/dist/host-runtime/config.js.map +1 -1
- package/dist/host-runtime/router-client.d.ts.map +1 -1
- package/dist/host-runtime/router-client.js +62 -15
- package/dist/host-runtime/router-client.js.map +1 -1
- package/dist/host-runtime/router.js +2 -2
- package/dist/host-runtime/router.js.map +1 -1
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +84 -55
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/mcp/THIRD_PARTY_NOTICES.txt +217 -0
- package/dist/mcp/server.js +30714 -38
- package/dist/mcp/server.js.map +6 -1
- package/dist/skills-manifest.json +24 -24
- package/dist/storage/entity-index.d.ts +0 -1
- package/dist/storage/entity-index.d.ts.map +1 -1
- package/dist/storage/entity-index.js +0 -7
- package/dist/storage/entity-index.js.map +1 -1
- package/dist/storage/fts-index.d.ts.map +1 -1
- package/dist/storage/fts-index.js +6 -7
- package/dist/storage/fts-index.js.map +1 -1
- package/dist/storage/graph-repairs.d.ts +0 -5
- package/dist/storage/graph-repairs.d.ts.map +1 -1
- package/dist/storage/graph-repairs.js +4 -91
- package/dist/storage/graph-repairs.js.map +1 -1
- package/dist/storage/schema.d.ts +1 -1
- package/dist/storage/schema.d.ts.map +1 -1
- package/dist/storage/schema.js +1 -2
- package/dist/storage/schema.js.map +1 -1
- package/dist/storage/sqlite.d.ts +0 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js.map +1 -1
- 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 +16 -8
- 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 +60751 -2408
- package/dist/transports/cli/cli.js.map +6 -1
- package/dist/transports/http/retired-routes.js +1 -1
- package/dist/transports/http/retired-routes.js.map +1 -1
- package/dist/transports/http/server.d.ts.map +1 -1
- package/dist/transports/http/server.js +45 -233
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts +130 -4
- package/dist/transports/mcp/handlers.d.ts.map +1 -1
- package/dist/transports/mcp/handlers.js +56 -7
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/dist/transports/schemas.d.ts +52 -0
- package/dist/transports/schemas.d.ts.map +1 -1
- package/dist/transports/schemas.js +39 -0
- package/dist/transports/schemas.js.map +1 -1
- package/docs/platforms/README.md +5 -4
- package/docs/platforms/agent-messaging.md +134 -61
- package/hooks/hooks.json +13 -2
- package/llms-install.md +62 -30
- package/package.json +4 -5
- package/scripts/hooks/_generated/core-paths.js +88 -14
- package/scripts/hooks/_generated/fts-index.js +6 -7
- package/scripts/hooks/_generated/schema.js +1 -2
- package/scripts/hooks/_shared.js +164 -21
- package/scripts/hooks/session-start.js +98 -10
- package/scripts/hooks/session-summary.js +34 -376
- package/scripts/hooks/user-prompt-intent.js +71 -11
- package/skills/memesh/SKILL.md +19 -14
- package/skills/memesh-review/SKILL.md +7 -6
- package/dist/core/auto-tagger.d.ts +0 -10
- package/dist/core/auto-tagger.d.ts.map +0 -1
- package/dist/core/auto-tagger.js +0 -63
- package/dist/core/auto-tagger.js.map +0 -1
- package/dist/core/conflict-candidates.d.ts +0 -20
- package/dist/core/conflict-candidates.d.ts.map +0 -1
- package/dist/core/conflict-candidates.js +0 -71
- package/dist/core/conflict-candidates.js.map +0 -1
- package/dist/core/conflict-judge.d.ts +0 -58
- package/dist/core/conflict-judge.d.ts.map +0 -1
- package/dist/core/conflict-judge.js +0 -189
- package/dist/core/conflict-judge.js.map +0 -1
- package/dist/core/digest-validator.d.ts +0 -18
- package/dist/core/digest-validator.d.ts.map +0 -1
- package/dist/core/digest-validator.js +0 -85
- package/dist/core/digest-validator.js.map +0 -1
- package/dist/core/embedder.d.ts +0 -20
- package/dist/core/embedder.d.ts.map +0 -1
- package/dist/core/embedder.js +0 -245
- package/dist/core/embedder.js.map +0 -1
- package/dist/core/failure-analyzer.d.ts +0 -19
- package/dist/core/failure-analyzer.d.ts.map +0 -1
- package/dist/core/failure-analyzer.js +0 -83
- package/dist/core/failure-analyzer.js.map +0 -1
- package/dist/core/json-utils.d.ts +0 -3
- package/dist/core/json-utils.d.ts.map +0 -1
- package/dist/core/json-utils.js +0 -46
- package/dist/core/json-utils.js.map +0 -1
- package/dist/core/llm-client.d.ts +0 -22
- package/dist/core/llm-client.d.ts.map +0 -1
- package/dist/core/llm-client.js +0 -203
- package/dist/core/llm-client.js.map +0 -1
- package/dist/core/llm-telemetry.d.ts +0 -47
- package/dist/core/llm-telemetry.d.ts.map +0 -1
- package/dist/core/llm-telemetry.js +0 -117
- package/dist/core/llm-telemetry.js.map +0 -1
- package/dist/core/llm-validator.d.ts +0 -20
- package/dist/core/llm-validator.d.ts.map +0 -1
- package/dist/core/llm-validator.js +0 -231
- package/dist/core/llm-validator.js.map +0 -1
- package/dist/core/ollama-host.d.ts +0 -6
- package/dist/core/ollama-host.d.ts.map +0 -1
- package/dist/core/ollama-host.js +0 -62
- package/dist/core/ollama-host.js.map +0 -1
- package/dist/core/output-language.d.ts +0 -6
- package/dist/core/output-language.d.ts.map +0 -1
- package/dist/core/output-language.js +0 -25
- package/dist/core/output-language.js.map +0 -1
- package/dist/core/prompt-safety.d.ts +0 -4
- package/dist/core/prompt-safety.d.ts.map +0 -1
- package/dist/core/prompt-safety.js +0 -20
- package/dist/core/prompt-safety.js.map +0 -1
- package/dist/storage/vector-index.d.ts +0 -3
- package/dist/storage/vector-index.d.ts.map +0 -1
- package/dist/storage/vector-index.js +0 -13
- package/dist/storage/vector-index.js.map +0 -1
|
@@ -4,26 +4,9 @@
|
|
|
4
4
|
// Extracts knowledge from completed Claude Code sessions
|
|
5
5
|
// and stores as session-insight entities in MeMesh.
|
|
6
6
|
//
|
|
7
|
-
// THIS HOOK STAYS, and that was an open question rather than an assumption.
|
|
8
|
-
// `dream run --from-transcripts` reads the same sessions from their raw JSONL
|
|
9
|
-
// and does not depend on this hook having fired, so the obvious next step was
|
|
10
|
-
// to retire the hook as redundant. Measured first, on a real graph
|
|
11
|
-
// (2026-08-09, 214 active entities, ollama nomic-embed-text, L2 over the same
|
|
12
|
-
// `name + observations` text the runtime embeds):
|
|
13
|
-
//
|
|
14
|
-
// every transcript-mined memory -> its nearest hook-captured entity
|
|
15
|
-
// min 0.784 p25 0.821 p50 0.865 max 0.946
|
|
16
|
-
// within 0.55: 0 of 47. within 0.70: 0 of 47.
|
|
17
|
-
//
|
|
18
|
-
// Nothing the transcript miner produced came within 0.78 of anything this hook
|
|
19
|
-
// recorded. They are not two views of the same material: this hook records what
|
|
20
|
-
// HAPPENED (files touched, commands run, commits), the miner extracts what was
|
|
21
|
-
// DECIDED and what was LEARNED. Retiring either one loses a whole category.
|
|
22
|
-
|
|
23
7
|
import { createRequire } from 'module';
|
|
24
8
|
import { basename, join } from 'path';
|
|
25
|
-
import { existsSync, readFileSync
|
|
26
|
-
import { spawn } from 'child_process';
|
|
9
|
+
import { existsSync, readFileSync } from 'fs';
|
|
27
10
|
import { pathToFileURL } from 'url';
|
|
28
11
|
import {
|
|
29
12
|
AUTO_CAPTURE_TAG,
|
|
@@ -32,7 +15,7 @@ import {
|
|
|
32
15
|
extractCitedMemoryIds,
|
|
33
16
|
getMemeshDirFromDbPath,
|
|
34
17
|
getProjectName,
|
|
35
|
-
|
|
18
|
+
findAutoUpdateConsent,
|
|
36
19
|
isAutoCaptureEnabled,
|
|
37
20
|
openHookDb,
|
|
38
21
|
readUpdateCheckCache,
|
|
@@ -47,25 +30,18 @@ import {
|
|
|
47
30
|
|
|
48
31
|
const require = createRequire(import.meta.url);
|
|
49
32
|
|
|
50
|
-
|
|
51
|
-
// Same pattern as session-start.js: async ESM import at process init,
|
|
52
|
-
// falls back to null if dist is missing (source checkout pre-build).
|
|
53
|
-
let _installChannelMod = null;
|
|
33
|
+
let installChannel = null;
|
|
54
34
|
try {
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
if (existsSync(
|
|
58
|
-
|
|
35
|
+
const pluginRoot = resolvePluginRoot(import.meta.url);
|
|
36
|
+
const modulePath = join(pluginRoot, 'dist/core/install-channel.js');
|
|
37
|
+
if (existsSync(modulePath)) {
|
|
38
|
+
installChannel = await import(pathToFileURL(modulePath).href);
|
|
59
39
|
}
|
|
60
|
-
} catch {
|
|
40
|
+
} catch {
|
|
41
|
+
// Best-effort: source checkouts may not have built dist output yet.
|
|
42
|
+
}
|
|
61
43
|
|
|
62
|
-
|
|
63
|
-
* Run auto-update at Stop hook: reads cache, evaluates policy, and dispatches
|
|
64
|
-
* the detached updater runner if warranted. Runs after all session work
|
|
65
|
-
* completes, avoiding the TOCTOU race where install would overwrite dist/
|
|
66
|
-
* mid-session.
|
|
67
|
-
*/
|
|
68
|
-
async function runAutoUpdateAtStop() {
|
|
44
|
+
async function runAutoUpdateAtStop(sessionId) {
|
|
69
45
|
try {
|
|
70
46
|
const pluginRoot = resolvePluginRoot(import.meta.url);
|
|
71
47
|
const pkg = JSON.parse(readFileSync(join(pluginRoot, 'package.json'), 'utf8'));
|
|
@@ -75,11 +51,16 @@ async function runAutoUpdateAtStop() {
|
|
|
75
51
|
const cache = readUpdateCheckCache(installedVersion);
|
|
76
52
|
const policy = resolveAutoUpdatePolicy(process.env);
|
|
77
53
|
const decision = decideAutoUpdateHook(installedVersion, cache, policy);
|
|
78
|
-
|
|
79
|
-
|
|
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);
|
|
80
61
|
}
|
|
81
62
|
} catch {
|
|
82
|
-
// Best-effort
|
|
63
|
+
// Best-effort: update failures must never break session capture.
|
|
83
64
|
}
|
|
84
65
|
}
|
|
85
66
|
|
|
@@ -183,13 +164,9 @@ function parseTranscript(transcriptPath) {
|
|
|
183
164
|
const text = typeof block.content === 'string'
|
|
184
165
|
? block.content
|
|
185
166
|
: JSON.stringify(block.content);
|
|
186
|
-
// Same reason as the bash branch
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
// header — the ordinary shape of an auth error — would be stored
|
|
190
|
-
// and then transmitted off the machine. Redacted once here, at
|
|
191
|
-
// the point the text enters the process, so every downstream use
|
|
192
|
-
// inherits it.
|
|
167
|
+
// Same reason as the bash branch: a failed request may echo its
|
|
168
|
+
// own Authorization header. Redact once, where text enters the
|
|
169
|
+
// process, so every downstream use inherits it.
|
|
193
170
|
errorsEncountered.push(redactSecrets(text).slice(0, 200));
|
|
194
171
|
}
|
|
195
172
|
}
|
|
@@ -235,6 +212,7 @@ let input = '';
|
|
|
235
212
|
process.stdin.setEncoding('utf8');
|
|
236
213
|
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
237
214
|
process.stdin.on('end', async () => {
|
|
215
|
+
let sessionId = 'unknown';
|
|
238
216
|
try {
|
|
239
217
|
if (!input.trim()) return exit0();
|
|
240
218
|
|
|
@@ -255,7 +233,7 @@ process.stdin.on('end', async () => {
|
|
|
255
233
|
return exit0();
|
|
256
234
|
}
|
|
257
235
|
|
|
258
|
-
|
|
236
|
+
sessionId = inputData.session_id || 'unknown';
|
|
259
237
|
const transcriptPath = inputData.transcript_path;
|
|
260
238
|
|
|
261
239
|
// `cwd` decides the project tag, and the project tag decides which
|
|
@@ -338,33 +316,12 @@ process.stdin.on('end', async () => {
|
|
|
338
316
|
// healthy exit, so it MUST stamp (see stampHookRunOnly).
|
|
339
317
|
if (toolCallCount < 3) { stampHookRunOnly(process.env, 'session-summary'); return exit0(); }
|
|
340
318
|
|
|
341
|
-
// Hoisted to outer-try scope so the LLM failure-analysis block
|
|
342
|
-
// below (which runs AFTER db.close()) can reference it. Earlier
|
|
343
|
-
// version defined projectName inside the inner try-finally and the
|
|
344
|
-
// LLM path threw `projectName is not defined` silently — caught by
|
|
345
|
-
// the LLM try/catch but logged to stderr. Result: lesson_learned
|
|
346
|
-
// creation never actually happened in production.
|
|
347
319
|
const projectName = getProjectName(cwd);
|
|
348
320
|
|
|
349
321
|
// Open DB via shared helper — applies SCHEMA_SQL + status migration.
|
|
350
322
|
// { fts: true } guarantees the entities_fts table exists so captureEntity()
|
|
351
323
|
// can keep it in sync — session-insight memories must be FTS-recallable.
|
|
352
324
|
//
|
|
353
|
-
// sqlite-vec is NOT loaded here, and used to be. The comment said it was
|
|
354
|
-
// needed "for embedding-aware recall-effectiveness tracking" — but this
|
|
355
|
-
// hook runs exactly two statements, `PRAGMA table_info(entities)` and
|
|
356
|
-
// `SELECT id FROM entities WHERE name = ?`, and `captureEntity` in
|
|
357
|
-
// _shared.js touches no vectors either. Nothing here has ever used the
|
|
358
|
-
// extension.
|
|
359
|
-
//
|
|
360
|
-
// It was not free. sqlite-vec ships its engine as a per-platform file
|
|
361
|
-
// through optionalDependencies, and on a platform it does not publish the
|
|
362
|
-
// load threw — past the `require` guard, which never fired because the JS
|
|
363
|
-
// wrapper resolves fine and the throw happens later inside
|
|
364
|
-
// `sqliteVec.load()`. Measured with the platform binary hidden: the whole
|
|
365
|
-
// Stop capture vanished (0 entities against a control run's 1) and the
|
|
366
|
-
// user got a `Require stack:` dump on stderr. An extension nobody calls
|
|
367
|
-
// was silently costing every session on those platforms its memory.
|
|
368
325
|
const { db } = openHookDb(process.env, { fts: true });
|
|
369
326
|
let writeFailed = false;
|
|
370
327
|
try {
|
|
@@ -389,6 +346,10 @@ process.stdin.on('end', async () => {
|
|
|
389
346
|
).get(`session-${sessionId}-files`, `session-${sessionId}-fixes`, `session-${sessionId}-summary`);
|
|
390
347
|
if (alreadyCaptured) {
|
|
391
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);
|
|
392
353
|
return exit0();
|
|
393
354
|
}
|
|
394
355
|
|
|
@@ -628,69 +589,18 @@ process.stdin.on('end', async () => {
|
|
|
628
589
|
db.close();
|
|
629
590
|
}
|
|
630
591
|
|
|
631
|
-
// ── LLM-powered failure analysis (Level 1 only) ──────────────────────
|
|
632
|
-
// Runs AFTER the hook's own DB is closed.
|
|
633
|
-
// Uses the core module's DB singleton (openDatabase/closeDatabase).
|
|
634
|
-
// Wrapped in its own try/catch — never blocks rule-based extraction.
|
|
635
|
-
if (errorsEncountered.length > 0 && filesEdited.length > 0) {
|
|
636
|
-
try {
|
|
637
|
-
// F5: derive pluginRoot strictly from this file's location.
|
|
638
|
-
// See `resolvePluginRoot` for the full reasoning.
|
|
639
|
-
const pluginRoot = resolvePluginRoot(import.meta.url);
|
|
640
|
-
const configMod = await importFromPluginRoot(pluginRoot, 'dist/core/config.js');
|
|
641
|
-
const config = configMod.readConfig();
|
|
642
|
-
|
|
643
|
-
if (config.llm) {
|
|
644
|
-
const { openDatabase, closeDatabase } = await importFromPluginRoot(pluginRoot, 'dist/db.js');
|
|
645
|
-
const { analyzeFailure } = await importFromPluginRoot(pluginRoot, 'dist/core/failure-analyzer.js');
|
|
646
|
-
const { createLesson } = await importFromPluginRoot(pluginRoot, 'dist/core/lesson-engine.js');
|
|
647
|
-
|
|
648
|
-
openDatabase();
|
|
649
|
-
try {
|
|
650
|
-
// Pass cross-provider failover chain so a stale Anthropic key
|
|
651
|
-
// doesn't silently disable Stop-hook lesson generation.
|
|
652
|
-
const lesson = await analyzeFailure(errorsEncountered, filesEdited, config.llm, { fallbacks: config.llmFallbacks });
|
|
653
|
-
if (lesson) {
|
|
654
|
-
createLesson(lesson, projectName);
|
|
655
|
-
}
|
|
656
|
-
} finally {
|
|
657
|
-
closeDatabase();
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
} catch (llmErr) {
|
|
661
|
-
// LLM analysis failed — rule-based extraction already captured the session.
|
|
662
|
-
// Log to stderr so config issues (e.g. invalid API key) are visible.
|
|
663
|
-
try { process.stderr.write(`[memesh] LLM failure analysis skipped: ${llmErr?.message || llmErr}\n`); } catch {}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// Auto-trigger dream — solves the "Insights tab is empty for users
|
|
668
|
-
// who don't know `memesh dream run` exists" problem. Throttled to
|
|
669
|
-
// once per project per 24h, gated by minimum activity threshold.
|
|
670
|
-
// Background-detached spawn so the hook exits immediately even if
|
|
671
|
-
// the LLM call takes 30-60s. See `maybeTriggerDream` for the gate
|
|
672
|
-
// logic and dream-history.json schema.
|
|
673
|
-
try {
|
|
674
|
-
const pluginRoot = resolvePluginRoot(import.meta.url);
|
|
675
|
-
const configMod = await importFromPluginRoot(pluginRoot, 'dist/core/config.js');
|
|
676
|
-
const config = configMod.readConfig();
|
|
677
|
-
maybeTriggerDream(projectName, config, pluginRoot);
|
|
678
|
-
} catch (dreamErr) {
|
|
679
|
-
try { process.stderr.write(`[memesh] dream auto-trigger skipped: ${dreamErr?.message || dreamErr}\n`); } catch {}
|
|
680
|
-
}
|
|
681
592
|
} catch (err) {
|
|
682
593
|
// Never crash Claude Code — leave a trace for debugging.
|
|
683
594
|
//
|
|
684
|
-
// Every error is traced
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
// could only ever hide a real error from here on.
|
|
595
|
+
// Every error is traced. A retired suppression sentinel once hid setup
|
|
596
|
+
// failures from this hook; with that branch gone, real capture errors stay
|
|
597
|
+
// visible without crashing the host session.
|
|
688
598
|
try { process.stderr.write(`[memesh session-summary] ${err?.message || err}\n`); } catch {}
|
|
689
599
|
}
|
|
690
600
|
|
|
691
|
-
//
|
|
692
|
-
//
|
|
693
|
-
await runAutoUpdateAtStop();
|
|
601
|
+
// Update only after all session work so installed files cannot change while
|
|
602
|
+
// this hook is still reading them.
|
|
603
|
+
await runAutoUpdateAtStop(sessionId);
|
|
694
604
|
|
|
695
605
|
// Emit NOTHING on success — not `{"suppressOutput": true}`.
|
|
696
606
|
//
|
|
@@ -711,173 +621,6 @@ process.stdin.on('end', async () => {
|
|
|
711
621
|
function exit0() {
|
|
712
622
|
process.exit(0);
|
|
713
623
|
}
|
|
714
|
-
|
|
715
|
-
// =============================================================================
|
|
716
|
-
// Dream auto-trigger (Phase 2 / Phase 3 background runner)
|
|
717
|
-
// =============================================================================
|
|
718
|
-
//
|
|
719
|
-
// Without an automated trigger, `memesh dream` only runs when the user
|
|
720
|
-
// types it into a terminal — and most users never read the docs that
|
|
721
|
-
// far. Result: Insights tab stays empty and the KG accumulates 89.7%
|
|
722
|
-
// orphan rate (the maintainer's own observation on this DB).
|
|
723
|
-
//
|
|
724
|
-
// This trigger fires at the END of every Stop hook (after rule-based
|
|
725
|
-
// session capture + optional LLM failure analysis). It:
|
|
726
|
-
// 1. Loads ~/.memesh/dream-history.json — a per-project record of
|
|
727
|
-
// the last dream run timestamp + outcome.
|
|
728
|
-
// 2. Throttles: skip if < THROTTLE_HOURS since the project's last
|
|
729
|
-
// run, even if the previous run produced zero proposals.
|
|
730
|
-
// 3. Activity gate: skip if the project has < MIN_EPISODIC episodic
|
|
731
|
-
// entities to draw from in the last WINDOW_DAYS days.
|
|
732
|
-
// 4. LLM gate: skip if no LLM provider configured (Phase 2 needs
|
|
733
|
-
// Smart Mode — Phase 3 patterns same).
|
|
734
|
-
// 5. Spawns `node <pluginRoot>/dist/transports/cli/cli.js dream run
|
|
735
|
-
// --project <name> --max-llm-calls 2 --window-days 14` as a
|
|
736
|
-
// detached child process. Stdout/stderr go to a per-project log
|
|
737
|
-
// under ~/.memesh/dream-runs/ so the user can `tail -f` to see
|
|
738
|
-
// progress without the hook blocking.
|
|
739
|
-
// 6. Records the run start in dream-history.json BEFORE spawning so
|
|
740
|
-
// a long-running spawn doesn't get re-triggered on the next
|
|
741
|
-
// Stop within the same window.
|
|
742
|
-
//
|
|
743
|
-
// The spawned process inherits config from disk, including the
|
|
744
|
-
// `llmFallbacks` chain wired in commit 883abd4d, so a primary
|
|
745
|
-
// outage falls through to Ollama automatically.
|
|
746
|
-
|
|
747
|
-
const DREAM_THROTTLE_HOURS = 24;
|
|
748
|
-
const DREAM_MIN_EPISODIC = 10;
|
|
749
|
-
const DREAM_WINDOW_DAYS = 14;
|
|
750
|
-
const DREAM_MAX_LLM_CALLS = 2;
|
|
751
|
-
const DREAM_HISTORY_BASENAME = 'dream-history.json';
|
|
752
|
-
const DREAM_LOG_DIRNAME = 'dream-runs';
|
|
753
|
-
|
|
754
|
-
const DREAM_EPISODIC_TYPES = [
|
|
755
|
-
'commit',
|
|
756
|
-
'session_keypoint',
|
|
757
|
-
'session-insight',
|
|
758
|
-
'workflow_checkpoint',
|
|
759
|
-
'weekly-summary',
|
|
760
|
-
'weekly_summary',
|
|
761
|
-
];
|
|
762
|
-
|
|
763
|
-
// Debug-trace gate. Set MEMESH_DREAM_TRIGGER_DEBUG=1 to emit a stderr
|
|
764
|
-
// breadcrumb at every gate decision in maybeTriggerDream. Contract:
|
|
765
|
-
// [memesh dream-trigger] <stage>=<value>...
|
|
766
|
-
// Matches one line per stage (resolve, llm-gate, throttle-gate,
|
|
767
|
-
// activity-gate, history-write, spawn). Stable across releases — if
|
|
768
|
-
// you rename a stage, update the matching test fixture too.
|
|
769
|
-
function dreamTrigTrace(stage, fields) {
|
|
770
|
-
if (process.env.MEMESH_DREAM_TRIGGER_DEBUG !== '1') return;
|
|
771
|
-
try {
|
|
772
|
-
const parts = Object.entries(fields || {}).map(([k, v]) => {
|
|
773
|
-
if (v === undefined) return `${k}=<undef>`;
|
|
774
|
-
if (v === null) return `${k}=<null>`;
|
|
775
|
-
return `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`;
|
|
776
|
-
});
|
|
777
|
-
process.stderr.write(`[memesh dream-trigger] ${stage} ${parts.join(' ')}\n`);
|
|
778
|
-
} catch {}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
function dreamHistoryPath() {
|
|
782
|
-
// Test isolation: tests set MEMESH_DB_PATH to a tmp file, expecting
|
|
783
|
-
// sibling state (history, logs) to land beside it. Without honouring
|
|
784
|
-
// that, real-home `~/.memesh/dream-history.json` would be polluted
|
|
785
|
-
// by every test run. Same precedence as memesh's other state files —
|
|
786
|
-
// see `getMemeshDirFromDbPath` in `_shared.js`.
|
|
787
|
-
//
|
|
788
|
-
// Windows note: on Windows under execFileSync, env vars propagate as
|
|
789
|
-
// plain strings (no canonicalisation). If a caller passed MEMESH_DIR
|
|
790
|
-
// with mixed separators (e.g. `C:/Users/.../tmp/.memesh`), join() will
|
|
791
|
-
// happily mix `/` and `\`. Node fs accepts either — the only divergence
|
|
792
|
-
// is in pure string comparisons, which we don't do here. The trace
|
|
793
|
-
// below shows the resolved value verbatim so a Windows diagnosis run
|
|
794
|
-
// can confirm what actually arrived.
|
|
795
|
-
// The helper IS the precedence (MEMESH_DB_PATH > MEMESH_DIR > home).
|
|
796
|
-
// A hand-rolled version here inverted it (MEMESH_DIR won over
|
|
797
|
-
// MEMESH_DB_PATH), so with both set, dream history landed in a different
|
|
798
|
-
// directory than every sibling state file — plus a dead home-fallback
|
|
799
|
-
// branch, since the helper always returns a string.
|
|
800
|
-
const dir = getMemeshDirFromDbPath();
|
|
801
|
-
dreamTrigTrace('resolve', {
|
|
802
|
-
src: process.env.MEMESH_DB_PATH ? 'db-path' : (process.env.MEMESH_DIR ? 'env' : 'home'),
|
|
803
|
-
MEMESH_DIR: process.env.MEMESH_DIR,
|
|
804
|
-
MEMESH_DB_PATH: process.env.MEMESH_DB_PATH,
|
|
805
|
-
dir,
|
|
806
|
-
platform: process.platform,
|
|
807
|
-
});
|
|
808
|
-
return { dir, historyFile: join(dir, DREAM_HISTORY_BASENAME), logDir: join(dir, DREAM_LOG_DIRNAME) };
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
function readDreamHistory() {
|
|
812
|
-
try {
|
|
813
|
-
const { historyFile } = dreamHistoryPath();
|
|
814
|
-
if (!existsSync(historyFile)) return {};
|
|
815
|
-
const raw = readFileSync(historyFile, 'utf8');
|
|
816
|
-
const parsed = JSON.parse(raw);
|
|
817
|
-
return (parsed && typeof parsed === 'object') ? parsed : {};
|
|
818
|
-
} catch { return {}; }
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
function writeDreamHistory(history) {
|
|
822
|
-
try {
|
|
823
|
-
const { dir, historyFile } = dreamHistoryPath();
|
|
824
|
-
mkdirSync(dir, { recursive: true });
|
|
825
|
-
writeFileSync(historyFile, JSON.stringify(history, null, 2));
|
|
826
|
-
dreamTrigTrace('history-write', { historyFile, ok: true, projects: Object.keys(history) });
|
|
827
|
-
} catch (err) {
|
|
828
|
-
dreamTrigTrace('history-write', { ok: false, err: err?.message || String(err) });
|
|
829
|
-
try { process.stderr.write(`[memesh dream-history write] ${err?.message || err}\n`); } catch {}
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
/**
|
|
834
|
-
* Count episodic entities for the given project over the configured
|
|
835
|
-
* window. Only issues a SELECT, but the handle is openHookDb's ordinary
|
|
836
|
-
* read-write one (it runs the schema/migration chain) — there is no
|
|
837
|
-
* read-only variant on the hook side, and this call site must not stamp
|
|
838
|
-
* the heartbeat (recordHookRun is per-hook-exit, never per-open).
|
|
839
|
-
*/
|
|
840
|
-
function countEpisodicEntities(projectName) {
|
|
841
|
-
let handle;
|
|
842
|
-
try {
|
|
843
|
-
handle = openHookDb();
|
|
844
|
-
// { db, dbPath } — don't call db.prepare() on the wrapper itself (an
|
|
845
|
-
// earlier version did, and silently skipped the gate, defeating the
|
|
846
|
-
// trigger's whole purpose).
|
|
847
|
-
const db = handle.db;
|
|
848
|
-
const since = new Date(Date.now() - DREAM_WINDOW_DAYS * 86400000).toISOString();
|
|
849
|
-
const types = DREAM_EPISODIC_TYPES.map(() => '?').join(',');
|
|
850
|
-
// Project-membership is determined by the `project:<name>` tag only.
|
|
851
|
-
// The previous implementation also OR'd on `e.name LIKE 'project-%'`
|
|
852
|
-
// as a fallback for legacy entities, but that branch over-counts when
|
|
853
|
-
// two projects share a name prefix (e.g. counting `memesh` would
|
|
854
|
-
// sweep in `memesh-cloud-keypoint-*` entities). Post-v3.0 episodic
|
|
855
|
-
// entities all carry the project tag — the auto-tagger writes it on
|
|
856
|
-
// every session_keypoint / commit / session_insight / session_lesson
|
|
857
|
-
// / failure_pattern / decision_anchor — so the tag-only path is the
|
|
858
|
-
// accurate signal. See v4.2.1 CHANGELOG known-limitations note.
|
|
859
|
-
const sql = `SELECT COUNT(DISTINCT e.id) AS n
|
|
860
|
-
FROM entities e
|
|
861
|
-
INNER JOIN tags t ON t.entity_id = e.id AND t.tag = ?
|
|
862
|
-
WHERE e.status = 'active'
|
|
863
|
-
AND e.type IN (${types})
|
|
864
|
-
AND e.created_at >= ?`;
|
|
865
|
-
const projectTag = `project:${projectName}`;
|
|
866
|
-
const row = db.prepare(sql).get(projectTag, ...DREAM_EPISODIC_TYPES, since);
|
|
867
|
-
return row?.n ?? 0;
|
|
868
|
-
} catch (err) {
|
|
869
|
-
try { process.stderr.write(`[memesh dream-trigger count] ${err?.message || err}\n`); } catch {}
|
|
870
|
-
return 0;
|
|
871
|
-
} finally {
|
|
872
|
-
try { handle?.db?.close(); } catch {}
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
/**
|
|
877
|
-
* Decide whether to fire dream for `projectName` and, if so, spawn
|
|
878
|
-
* the detached background runner. Pure side effect — no return value
|
|
879
|
-
* used by callers.
|
|
880
|
-
*/
|
|
881
624
|
/**
|
|
882
625
|
* Attachment record types Claude Code uses to persist a hook's own output
|
|
883
626
|
* into the transcript. Anything memesh injected reaches the transcript
|
|
@@ -927,88 +670,3 @@ export function stripHookEchoes(rawTranscript) {
|
|
|
927
670
|
}
|
|
928
671
|
return kept.join('\n');
|
|
929
672
|
}
|
|
930
|
-
|
|
931
|
-
export function maybeTriggerDream(projectName, config, pluginRoot) {
|
|
932
|
-
dreamTrigTrace('enter', { projectName, hasLlm: Boolean(config?.llm) });
|
|
933
|
-
if (!projectName || projectName === 'unknown') {
|
|
934
|
-
dreamTrigTrace('exit', { reason: 'no-project-name', projectName });
|
|
935
|
-
return;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// Phase 2 & 3 both need a configured LLM. Without it the dreamer's
|
|
939
|
-
// first action is to push `{reason: 'no LLM configured'}` into
|
|
940
|
-
// skipped[] and exit, which would still bump our throttle clock
|
|
941
|
-
// for no value — so gate here.
|
|
942
|
-
if (!config?.llm) {
|
|
943
|
-
dreamTrigTrace('exit', { reason: 'llm-gate-fail' });
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
|
-
dreamTrigTrace('llm-gate', { ok: true });
|
|
947
|
-
|
|
948
|
-
const history = readDreamHistory();
|
|
949
|
-
const last = history[projectName];
|
|
950
|
-
if (last?.last_run_iso) {
|
|
951
|
-
const ageMs = Date.now() - new Date(last.last_run_iso).getTime();
|
|
952
|
-
if (Number.isFinite(ageMs) && ageMs < DREAM_THROTTLE_HOURS * 3600 * 1000) {
|
|
953
|
-
dreamTrigTrace('exit', { reason: 'throttle-gate-fail', ageMs, last_run_iso: last.last_run_iso });
|
|
954
|
-
return; // throttled
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
dreamTrigTrace('throttle-gate', { ok: true, prior_last_run_iso: last?.last_run_iso });
|
|
958
|
-
|
|
959
|
-
const episodicCount = countEpisodicEntities(projectName);
|
|
960
|
-
if (episodicCount < DREAM_MIN_EPISODIC) {
|
|
961
|
-
dreamTrigTrace('exit', { reason: 'activity-gate-fail', episodicCount, threshold: DREAM_MIN_EPISODIC });
|
|
962
|
-
return;
|
|
963
|
-
}
|
|
964
|
-
dreamTrigTrace('activity-gate', { ok: true, episodicCount });
|
|
965
|
-
|
|
966
|
-
// Activity gate passed — record start BEFORE spawning so we don't
|
|
967
|
-
// re-trigger on any subsequent Stop within the window even if the
|
|
968
|
-
// child takes 30-60s.
|
|
969
|
-
history[projectName] = {
|
|
970
|
-
last_run_iso: new Date().toISOString(),
|
|
971
|
-
last_episodic_count: episodicCount,
|
|
972
|
-
last_window_days: DREAM_WINDOW_DAYS,
|
|
973
|
-
};
|
|
974
|
-
writeDreamHistory(history);
|
|
975
|
-
|
|
976
|
-
// Spawn detached so the hook exits immediately. Stdio routes to a
|
|
977
|
-
// per-project log file so the user can inspect dream output without
|
|
978
|
-
// the hook blocking on the LLM call.
|
|
979
|
-
try {
|
|
980
|
-
const { dir, logDir } = dreamHistoryPath();
|
|
981
|
-
mkdirSync(logDir, { recursive: true });
|
|
982
|
-
const safeProj = projectName.replace(/[^A-Za-z0-9_-]/g, '_').slice(0, 64);
|
|
983
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
984
|
-
const logFile = join(logDir, `${safeProj}-${ts}.log`);
|
|
985
|
-
|
|
986
|
-
// Append a header so a tail -f shows context for the run.
|
|
987
|
-
appendFileSync(logFile, `[memesh dream] ${new Date().toISOString()} project=${projectName} episodic_count=${episodicCount} window=${DREAM_WINDOW_DAYS}d max_llm_calls=${DREAM_MAX_LLM_CALLS}\n`);
|
|
988
|
-
|
|
989
|
-
const cliPath = join(pluginRoot, 'dist/transports/cli/cli.js');
|
|
990
|
-
if (!existsSync(cliPath)) {
|
|
991
|
-
appendFileSync(logFile, `[memesh dream] cli.js missing at ${cliPath}, skipping\n`);
|
|
992
|
-
return;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
const args = [
|
|
996
|
-
cliPath, 'dream', 'run',
|
|
997
|
-
'--project', projectName,
|
|
998
|
-
'--max-llm-calls', String(DREAM_MAX_LLM_CALLS),
|
|
999
|
-
'--window-days', String(DREAM_WINDOW_DAYS),
|
|
1000
|
-
];
|
|
1001
|
-
|
|
1002
|
-
const logFd = require('fs').openSync(logFile, 'a');
|
|
1003
|
-
const child = spawn(process.execPath, args, {
|
|
1004
|
-
detached: true,
|
|
1005
|
-
stdio: ['ignore', logFd, logFd],
|
|
1006
|
-
env: { ...process.env, MEMESH_DIR: dir },
|
|
1007
|
-
});
|
|
1008
|
-
child.unref();
|
|
1009
|
-
dreamTrigTrace('spawn', { ok: true, pid: child.pid, logFile, MEMESH_DIR: dir });
|
|
1010
|
-
} catch (err) {
|
|
1011
|
-
dreamTrigTrace('spawn', { ok: false, err: err?.message || String(err) });
|
|
1012
|
-
try { process.stderr.write(`[memesh dream-trigger spawn] ${err?.message || err}\n`); } catch {}
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
@@ -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) {
|