@pcircle/memesh 4.9.4 → 4.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) 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/AGENTS.md +35 -4
  5. package/README.de.md +3 -3
  6. package/README.md +3 -3
  7. package/README.zh-TW.md +3 -3
  8. package/dashboard/dist/index.html +8 -8
  9. package/dist/cli/view-live.d.ts.map +1 -1
  10. package/dist/cli/view-live.js +0 -427
  11. package/dist/cli/view-live.js.map +1 -1
  12. package/dist/core/briefing-index.d.ts +30 -0
  13. package/dist/core/briefing-index.d.ts.map +1 -0
  14. package/dist/core/briefing-index.js +140 -0
  15. package/dist/core/briefing-index.js.map +1 -0
  16. package/dist/core/briefing.d.ts +4 -0
  17. package/dist/core/briefing.d.ts.map +1 -1
  18. package/dist/core/briefing.js +42 -4
  19. package/dist/core/briefing.js.map +1 -1
  20. package/dist/core/capture-liveness.d.ts +136 -0
  21. package/dist/core/capture-liveness.d.ts.map +1 -0
  22. package/dist/core/capture-liveness.js +447 -0
  23. package/dist/core/capture-liveness.js.map +1 -0
  24. package/dist/core/config.d.ts +1 -0
  25. package/dist/core/config.d.ts.map +1 -1
  26. package/dist/core/config.js +3 -1
  27. package/dist/core/config.js.map +1 -1
  28. package/dist/core/delegation.d.ts +47 -0
  29. package/dist/core/delegation.d.ts.map +1 -0
  30. package/dist/core/delegation.js +174 -0
  31. package/dist/core/delegation.js.map +1 -0
  32. package/dist/core/doctor-fixes.d.ts +1 -0
  33. package/dist/core/doctor-fixes.d.ts.map +1 -1
  34. package/dist/core/doctor-fixes.js +21 -0
  35. package/dist/core/doctor-fixes.js.map +1 -1
  36. package/dist/core/doctor.d.ts +8 -0
  37. package/dist/core/doctor.d.ts.map +1 -1
  38. package/dist/core/doctor.js +148 -3
  39. package/dist/core/doctor.js.map +1 -1
  40. package/dist/core/note-derive.d.ts +14 -0
  41. package/dist/core/note-derive.d.ts.map +1 -0
  42. package/dist/core/note-derive.js +66 -0
  43. package/dist/core/note-derive.js.map +1 -0
  44. package/dist/core/note-ingest.d.ts +34 -0
  45. package/dist/core/note-ingest.d.ts.map +1 -0
  46. package/dist/core/note-ingest.js +445 -0
  47. package/dist/core/note-ingest.js.map +1 -0
  48. package/dist/core/operations.d.ts +10 -1
  49. package/dist/core/operations.d.ts.map +1 -1
  50. package/dist/core/operations.js +129 -14
  51. package/dist/core/operations.js.map +1 -1
  52. package/dist/core/paths.d.ts.map +1 -1
  53. package/dist/core/paths.js.map +1 -1
  54. package/dist/core/schema-export.d.ts.map +1 -1
  55. package/dist/core/schema-export.js +10 -4
  56. package/dist/core/schema-export.js.map +1 -1
  57. package/dist/core/serializer.d.ts.map +1 -1
  58. package/dist/core/serializer.js +3 -0
  59. package/dist/core/serializer.js.map +1 -1
  60. package/dist/core/session-insight.d.ts +44 -0
  61. package/dist/core/session-insight.d.ts.map +1 -0
  62. package/dist/core/session-insight.js +216 -0
  63. package/dist/core/session-insight.js.map +1 -0
  64. package/dist/core/task-state-store.d.ts +4 -0
  65. package/dist/core/task-state-store.d.ts.map +1 -1
  66. package/dist/core/task-state-store.js +18 -5
  67. package/dist/core/task-state-store.js.map +1 -1
  68. package/dist/core/turn-signal.d.ts +22 -0
  69. package/dist/core/turn-signal.d.ts.map +1 -0
  70. package/dist/core/turn-signal.js +89 -0
  71. package/dist/core/turn-signal.js.map +1 -0
  72. package/dist/core/types.d.ts +12 -3
  73. package/dist/core/types.d.ts.map +1 -1
  74. package/dist/core/update-entrypoint.d.ts +19 -0
  75. package/dist/core/update-entrypoint.d.ts.map +1 -0
  76. package/dist/core/update-entrypoint.js +222 -0
  77. package/dist/core/update-entrypoint.js.map +1 -0
  78. package/dist/core/update-notice.d.ts +69 -0
  79. package/dist/core/update-notice.d.ts.map +1 -0
  80. package/dist/core/update-notice.js +188 -0
  81. package/dist/core/update-notice.js.map +1 -0
  82. package/dist/host-runtime/codex-session.d.ts +10 -0
  83. package/dist/host-runtime/codex-session.d.ts.map +1 -1
  84. package/dist/host-runtime/codex-session.js +1 -1
  85. package/dist/host-runtime/codex-session.js.map +1 -1
  86. package/dist/knowledge-graph.d.ts.map +1 -1
  87. package/dist/knowledge-graph.js +24 -3
  88. package/dist/knowledge-graph.js.map +1 -1
  89. package/dist/mcp/THIRD_PARTY_NOTICES.txt +2 -2
  90. package/dist/mcp/server.js +1079 -90
  91. package/dist/mcp/server.js.map +3 -3
  92. package/dist/skills-manifest.json +46 -26
  93. package/dist/transports/cli/cli.d.ts.map +1 -1
  94. package/dist/transports/cli/cli.js +7702 -5764
  95. package/dist/transports/cli/cli.js.map +3 -3
  96. package/dist/transports/http/server.d.ts.map +1 -1
  97. package/dist/transports/http/server.js +38 -27
  98. package/dist/transports/http/server.js.map +1 -1
  99. package/dist/transports/mcp/handlers.d.ts +25 -5
  100. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  101. package/dist/transports/mcp/handlers.js +43 -5
  102. package/dist/transports/mcp/handlers.js.map +1 -1
  103. package/dist/transports/schemas.d.ts +4 -2
  104. package/dist/transports/schemas.d.ts.map +1 -1
  105. package/dist/transports/schemas.js +29 -3
  106. package/dist/transports/schemas.js.map +1 -1
  107. package/docs/platforms/README.md +1 -0
  108. package/docs/platforms/agent-messaging.md +22 -5
  109. package/package.json +14 -4
  110. package/scripts/hooks/_generated/briefing-index.js +147 -0
  111. package/scripts/hooks/_generated/capture-liveness.js +454 -0
  112. package/scripts/hooks/_generated/update-notice.js +195 -0
  113. package/scripts/hooks/_shared.js +302 -14
  114. package/scripts/hooks/_stop-notes.js +504 -0
  115. package/scripts/hooks/auto-update-runner.mjs +22 -0
  116. package/scripts/hooks/decision-nudge.js +32 -5
  117. package/scripts/hooks/guard-check.js +30 -3
  118. package/scripts/hooks/post-commit.js +329 -133
  119. package/scripts/hooks/pre-compact.js +17 -2
  120. package/scripts/hooks/pre-edit-recall.js +32 -1
  121. package/scripts/hooks/session-start.js +361 -78
  122. package/scripts/hooks/session-summary.js +217 -41
  123. package/scripts/hooks/user-prompt-intent.js +89 -19
  124. package/skills/memesh/SKILL.md +20 -8
  125. package/dist/cli/assets/d3.v7.min.js +0 -2
  126. package/dist/core/graph.d.ts +0 -26
  127. package/dist/core/graph.d.ts.map +0 -1
  128. package/dist/core/graph.js +0 -74
  129. package/dist/core/graph.js.map +0 -1
@@ -0,0 +1,454 @@
1
+ // ============================================================================
2
+ // AUTO-GENERATED from src/core/capture-liveness.ts — DO NOT EDIT BY HAND.
3
+ // Regenerate with: npm run build (scripts/generate-hook-core.mjs)
4
+ //
5
+ // Claude Code hooks import this committed copy instead of dist/, so the
6
+ // always-on capture path survives a missing or stale dist/ while staying
7
+ // byte-locked to core — eliminating the hand-mirror drift behind the P0 FTS bug.
8
+ // ============================================================================
9
+ export const HOOK_OUTCOMES_FILENAME = 'hook-outcomes.jsonl';
10
+ export const HOOK_OUTCOMES_PER_HOOK = 20;
11
+ export const HOOK_OUTCOMES_NOT_TRIGGERED_PER_HOOK = 5;
12
+ function windowKeep(entries, maxTriggered, maxNotTriggered) {
13
+ const keep = new Array(entries.length).fill(false);
14
+ const seen = new Map();
15
+ for (let i = entries.length - 1; i >= 0; i--) {
16
+ const { hook, triggered } = entries[i];
17
+ const counts = seen.get(hook) ?? { t: 0, n: 0 };
18
+ seen.set(hook, counts);
19
+ if (triggered) {
20
+ if (++counts.t <= maxTriggered)
21
+ keep[i] = true;
22
+ }
23
+ else if (++counts.n <= maxNotTriggered) {
24
+ keep[i] = true;
25
+ }
26
+ }
27
+ return keep;
28
+ }
29
+ export function isTriggeredRecord(record) {
30
+ if (record.outcome !== 'skipped' || record.reason === undefined)
31
+ return true;
32
+ return !(NOT_TRIGGERED_SKIP_REASONS[record.hook] ?? []).includes(record.reason);
33
+ }
34
+ function readHookOutcomeLines(raw) {
35
+ const entries = [];
36
+ for (const line of raw.split('\n')) {
37
+ const record = parseHookOutcomeLine(line);
38
+ if (record)
39
+ entries.push({ hook: record.hook, triggered: isTriggeredRecord(record), record, line });
40
+ }
41
+ return entries;
42
+ }
43
+ export const HOOK_OUTCOMES_ROTATE_BYTES = 64 * 1024;
44
+ export function serializeHookOutcome(record) {
45
+ return `${JSON.stringify(record)}\n`;
46
+ }
47
+ export function trimHookOutcomeLines(raw, max = HOOK_OUTCOMES_PER_HOOK, maxBytes = HOOK_OUTCOMES_ROTATE_BYTES) {
48
+ const records = readHookOutcomeLines(raw);
49
+ const keep = windowKeep(records, max, HOOK_OUTCOMES_NOT_TRIGGERED_PER_HOOK);
50
+ let kept = records.filter((_, i) => keep[i]).map((r) => r.line);
51
+ let bytes = kept.reduce((n, line) => n + utf8Length(line) + 1, 0);
52
+ if (bytes > maxBytes) {
53
+ const budget = maxBytes / 2;
54
+ const fit = [];
55
+ bytes = 0;
56
+ for (let i = kept.length - 1; i >= 0; i--) {
57
+ const size = utf8Length(kept[i]) + 1;
58
+ if (size > budget)
59
+ continue;
60
+ if (bytes + size > budget)
61
+ break;
62
+ fit.push(kept[i]);
63
+ bytes += size;
64
+ }
65
+ kept = fit.reverse();
66
+ }
67
+ return kept.length ? `${kept.join('\n')}\n` : '';
68
+ }
69
+ function utf8Length(text) {
70
+ let n = 0;
71
+ for (let i = 0; i < text.length; i++) {
72
+ const c = text.charCodeAt(i);
73
+ if (c < 0x80)
74
+ n += 1;
75
+ else if (c < 0x800)
76
+ n += 2;
77
+ else if (c >= 0xd800 && c <= 0xdbff) {
78
+ n += 4;
79
+ i++;
80
+ }
81
+ else
82
+ n += 3;
83
+ }
84
+ return n;
85
+ }
86
+ export const SILENT_HOOK_MIN_RUNS = 5;
87
+ export const CAPTURE_HOOKS = [
88
+ 'post-commit',
89
+ 'session-summary',
90
+ 'pre-compact',
91
+ 'pre-edit-recall',
92
+ 'user-prompt-intent',
93
+ 'decision-nudge',
94
+ 'guard-check',
95
+ 'session-start',
96
+ 'note-ingest',
97
+ 'remember-nudge',
98
+ ];
99
+ export const FAIL_ELIGIBLE_HOOKS = ['session-summary'];
100
+ export const SILENT_ELIGIBLE_HOOKS = ['post-commit', 'session-summary', 'pre-compact'];
101
+ export const SKIP_REASONS = {
102
+ notBash: 'not a Bash tool call',
103
+ notGitCommit: 'not a git commit command',
104
+ commitLineMissing: 'a git commit ran but printed no commit line',
105
+ commitHeadBaseline: 'recorded the repository HEAD baseline; no history was backfilled',
106
+ commitHeadUnchanged: 'a commit-like command completed but repository HEAD did not change',
107
+ commitHeadUnresolvable: 'a commit-like command ran but repository HEAD could not be resolved',
108
+ alreadyCaptured: 'this session was already captured',
109
+ payloadTooLarge: 'payload exceeded the stdin byte cap',
110
+ toolNameAbsent: 'tool_name absent in payload',
111
+ notDecisionTool: 'not a decision-shaped tool call',
112
+ noSessionId: 'no usable session_id in the payload',
113
+ alreadyNudged: 'already nudged for this tool in this session',
114
+ noBashCommand: 'no Bash command in the payload',
115
+ noDatabaseForGuards: 'no database yet — nothing to guard against',
116
+ noGuardMatched: 'no active guard matched this command',
117
+ autoCaptureOff: 'auto-capture is turned off',
118
+ commitCwdAbsent: 'data.cwd absent — cannot resolve project or repo',
119
+ hashNotACommit: 'the hash is not a commit in this repository',
120
+ noSessionOrTranscript: 'neither session_id nor transcript_path in the payload',
121
+ emptyStdin: 'empty stdin',
122
+ cwdAbsent: 'cwd absent in payload — cannot resolve project',
123
+ notAgenticLoop: 'not an agentic loop',
124
+ transcriptPathAbsent: 'transcript_path absent',
125
+ transcriptGone: 'the transcript file named by the payload is gone',
126
+ tooLittleActivity: 'too little activity in the session to be worth saving',
127
+ noRuleMatched: 'no rule matched (no edited file and fewer than 20 tool calls)',
128
+ allMatchedEntitiesArchived: 'every rule that matched targeted an entity the user forget-archived',
129
+ toolInputAbsent: 'tool_input absent in payload',
130
+ noFilePath: 'no file_path in the tool input',
131
+ noDatabaseForRecall: 'no database yet — nothing to recall',
132
+ nothingToRecall: 'no guard matched and nothing to recall for this file',
133
+ noPromptIntent: 'the prompt carried no remember intent and no update decision',
134
+ noMemoryDir: 'no Claude Code memory directory for this project',
135
+ noNoteChanged: 'no note file changed since the last ingestion',
136
+ noteIngesterNotBuilt: 'the note ingester is not built (dist/core/note-ingest.js is missing)',
137
+ noteNothingNew: 'note files were read and nothing new needed storing',
138
+ noteFilesRefused: 'note files were refused and nothing was stored',
139
+ noTranscript: 'no transcript to read',
140
+ trivialTurn: 'trivial turn — too few tool calls since the last Stop',
141
+ noDecisionMove: 'no decision-shaped move since the last Stop',
142
+ memoryWritten: 'a memory was written since the last Stop',
143
+ noteFileChanged: 'a note file changed since the last Stop',
144
+ };
145
+ const KNOWN_SKIP_REASONS = new Set(Object.values(SKIP_REASONS));
146
+ export const UNRECOGNISED_REASON = 'unrecognised reason';
147
+ export function renderableSkipReason(reason) {
148
+ if (reason === undefined)
149
+ return 'unspecified';
150
+ return KNOWN_SKIP_REASONS.has(reason) ? reason : UNRECOGNISED_REASON;
151
+ }
152
+ export function isGitCommitCommand(command) {
153
+ const start = /(?:^|[\s;&|(`/])git(?=\s)/g;
154
+ let m;
155
+ while ((m = start.exec(command)) !== null) {
156
+ const walk = commitFollowsGit(command, m.index + m[0].length);
157
+ if (walk.commit)
158
+ return true;
159
+ if (walk.end > start.lastIndex)
160
+ start.lastIndex = walk.end;
161
+ }
162
+ return false;
163
+ }
164
+ const VALUE_OPTIONS = new Set(['-C', '-c', '--git-dir', '--work-tree', '--namespace']);
165
+ const COMMIT_END = /[\s;&|)`]/;
166
+ const COMMIT_SUBCOMMANDS = ['commit', 'merge', 'cherry-pick', 'revert'];
167
+ function commitFollowsGit(s, i) {
168
+ for (;;) {
169
+ const afterSpace = skipSpace(s, i);
170
+ if (afterSpace === i)
171
+ return { commit: false, end: i };
172
+ i = afterSpace;
173
+ const subcommand = COMMIT_SUBCOMMANDS.find((candidate) => s.startsWith(candidate, i));
174
+ if (subcommand) {
175
+ const next = s[i + subcommand.length];
176
+ return { commit: next === undefined || COMMIT_END.test(next), end: i + subcommand.length };
177
+ }
178
+ if (s[i] !== '-')
179
+ return { commit: false, end: i };
180
+ const optionEnd = tokenEnd(s, i);
181
+ const option = s.slice(i, optionEnd);
182
+ i = optionEnd;
183
+ if (VALUE_OPTIONS.has(option)) {
184
+ const valueStart = skipSpace(s, i);
185
+ if (valueStart === i || valueStart >= s.length)
186
+ return { commit: false, end: valueStart };
187
+ i = valueEnd(s, valueStart);
188
+ }
189
+ }
190
+ }
191
+ function skipSpace(s, i) {
192
+ while (i < s.length && /\s/.test(s[i]))
193
+ i++;
194
+ return i;
195
+ }
196
+ function tokenEnd(s, i) {
197
+ while (i < s.length && !/\s/.test(s[i]))
198
+ i++;
199
+ return i;
200
+ }
201
+ function valueEnd(s, i) {
202
+ const quote = s[i];
203
+ if (quote === '"' || quote === "'") {
204
+ const close = s.indexOf(quote, i + 1);
205
+ return close === -1 ? s.length : close + 1;
206
+ }
207
+ return tokenEnd(s, i);
208
+ }
209
+ export const NOT_TRIGGERED_SKIP_REASONS = {
210
+ 'post-commit': [SKIP_REASONS.notBash, SKIP_REASONS.notGitCommit],
211
+ 'session-summary': [SKIP_REASONS.alreadyCaptured],
212
+ 'note-ingest': [SKIP_REASONS.noNoteChanged],
213
+ 'remember-nudge': [SKIP_REASONS.trivialTurn, SKIP_REASONS.noDecisionMove],
214
+ };
215
+ export const UNCLASSIFIED_SKIP_HOOKS = [
216
+ 'pre-compact',
217
+ 'pre-edit-recall',
218
+ 'user-prompt-intent',
219
+ 'decision-nudge',
220
+ 'guard-check',
221
+ 'session-start',
222
+ ];
223
+ export const NEVER_RAN_GRACE_HOURS = 72;
224
+ export function parseHookOutcomes(raw, limit = HOOK_OUTCOMES_PER_HOOK) {
225
+ if (!raw)
226
+ return { hooks: {} };
227
+ const entries = readHookOutcomeLines(raw);
228
+ const keep = windowKeep(entries, limit, HOOK_OUTCOMES_NOT_TRIGGERED_PER_HOOK);
229
+ const hooks = {};
230
+ entries.forEach(({ hook, record }, i) => {
231
+ if (!keep[i])
232
+ return;
233
+ (hooks[hook] ?? (hooks[hook] = [])).push(record);
234
+ });
235
+ return { hooks };
236
+ }
237
+ export function parseHookOutcomeLine(line) {
238
+ const trimmed = line.trim();
239
+ if (!trimmed)
240
+ return null;
241
+ let parsed;
242
+ try {
243
+ parsed = JSON.parse(trimmed);
244
+ }
245
+ catch {
246
+ return null;
247
+ }
248
+ if (!parsed || typeof parsed !== 'object')
249
+ return null;
250
+ const rec = parsed;
251
+ if (typeof rec.hook !== 'string' || !CAPTURE_HOOKS.includes(rec.hook))
252
+ return null;
253
+ if (typeof rec.at !== 'string')
254
+ return null;
255
+ if (rec.outcome !== 'wrote' && rec.outcome !== 'skipped'
256
+ && rec.outcome !== 'notified' && rec.outcome !== 'error')
257
+ return null;
258
+ const record = {
259
+ hook: rec.hook,
260
+ at: rec.at,
261
+ host: rec.host === 'claude-code' || rec.host === 'codex' ? rec.host : 'unknown',
262
+ outcome: rec.outcome,
263
+ };
264
+ const reason = typeof rec.reason === 'string' ? sanitizeRecordText(rec.reason) : '';
265
+ if (reason)
266
+ record.reason = reason;
267
+ const entity = typeof rec.entity === 'string' ? sanitizeRecordText(rec.entity) : '';
268
+ if (entity)
269
+ record.entity = entity;
270
+ return record;
271
+ }
272
+ export const RECORD_TEXT_MAX = 200;
273
+ export function sanitizeRecordText(text) {
274
+ return text.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, RECORD_TEXT_MAX);
275
+ }
276
+ export function summarizeHookOutcomes(file) {
277
+ const order = [...CAPTURE_HOOKS];
278
+ const names = Object.keys(file.hooks).sort((a, b) => {
279
+ const ai = order.indexOf(a);
280
+ const bi = order.indexOf(b);
281
+ if (ai !== bi)
282
+ return (ai === -1 ? order.length : ai) - (bi === -1 ? order.length : bi);
283
+ return a.localeCompare(b);
284
+ });
285
+ return names.map((hook) => summarizeOne(hook, file.hooks[hook] ?? []));
286
+ }
287
+ function summarizeOne(hook, records) {
288
+ let writes = 0;
289
+ let skips = 0;
290
+ let errors = 0;
291
+ let lastRunAt = null;
292
+ let firstTriggeredAt = null;
293
+ let triggeredRuns = 0;
294
+ let lastWriteAt = null;
295
+ let lastEntity = null;
296
+ let notifies = 0;
297
+ let lastNotifiedAt = null;
298
+ let lastSkipReason = null;
299
+ const skipCounts = new Map();
300
+ const hosts = new Set();
301
+ for (const r of records) {
302
+ hosts.add(r.host);
303
+ if (lastRunAt === null || r.at >= lastRunAt)
304
+ lastRunAt = r.at;
305
+ const triggered = isTriggeredRecord(r);
306
+ if (triggered) {
307
+ triggeredRuns++;
308
+ if (firstTriggeredAt === null || r.at < firstTriggeredAt)
309
+ firstTriggeredAt = r.at;
310
+ }
311
+ if (r.outcome === 'wrote') {
312
+ writes++;
313
+ if (lastWriteAt === null || r.at >= lastWriteAt) {
314
+ lastWriteAt = r.at;
315
+ lastEntity = r.entity ?? null;
316
+ }
317
+ }
318
+ else if (r.outcome === 'notified') {
319
+ notifies++;
320
+ if (lastNotifiedAt === null || r.at >= lastNotifiedAt)
321
+ lastNotifiedAt = r.at;
322
+ }
323
+ else if (r.outcome === 'skipped') {
324
+ skips++;
325
+ lastSkipReason = r.reason === undefined ? null : renderableSkipReason(r.reason);
326
+ if (triggered) {
327
+ const key = renderableSkipReason(r.reason);
328
+ skipCounts.set(key, (skipCounts.get(key) ?? 0) + 1);
329
+ }
330
+ }
331
+ else {
332
+ errors++;
333
+ }
334
+ }
335
+ let dominantSkipReason = null;
336
+ let dominantSkipCount = 0;
337
+ for (const [reason, count] of skipCounts) {
338
+ if (count > dominantSkipCount) {
339
+ dominantSkipCount = count;
340
+ dominantSkipReason = reason;
341
+ }
342
+ }
343
+ const runs = records.length;
344
+ return {
345
+ hook,
346
+ runs,
347
+ triggeredRuns,
348
+ writes,
349
+ skips,
350
+ errors,
351
+ lastRunAt,
352
+ firstTriggeredAt,
353
+ lastWriteAt,
354
+ lastEntity,
355
+ notifies,
356
+ lastNotifiedAt,
357
+ lastSkipReason,
358
+ dominantSkipReason,
359
+ dominantSkipCount,
360
+ hosts: [...hosts].sort(),
361
+ silent: SILENT_ELIGIBLE_HOOKS.includes(hook)
362
+ && triggeredRuns >= SILENT_HOOK_MIN_RUNS
363
+ && writes === 0,
364
+ };
365
+ }
366
+ export function summarizeTypeTrends(rows) {
367
+ return rows
368
+ .map((r) => ({ ...r, stopped: r.prev7 > 0 && r.last7 === 0 }))
369
+ .sort((a, b) => a.type.localeCompare(b.type));
370
+ }
371
+ export function captureLivenessVerdict(input) {
372
+ const withRecords = new Set(input.hooks.filter((h) => h.runs > 0).map((h) => h.hook));
373
+ const graceOver = input.measuringHours !== null &&
374
+ input.measuringHours !== undefined &&
375
+ input.measuringHours > NEVER_RAN_GRACE_HOURS;
376
+ const deadHooks = graceOver
377
+ ? (input.neverRanHooks ?? []).filter((h) => FAIL_ELIGIBLE_HOOKS.includes(h) && !withRecords.has(h)).sort()
378
+ : [];
379
+ const silent = input.hooks.filter((h) => h.silent).sort((a, b) => b.triggeredRuns - a.triggeredRuns);
380
+ const stoppedTypes = input.types.filter((t) => t.stopped);
381
+ let status = 'PASS';
382
+ if (deadHooks.length > 0)
383
+ status = 'FAIL';
384
+ else if (silent.length > 0 || stoppedTypes.length > 0)
385
+ status = 'PASS_WITH_CONCERNS';
386
+ return { status, silentHook: silent[0] ?? null, stoppedTypes, deadHooks };
387
+ }
388
+ export function captureLivenessNotice(verdict) {
389
+ if (verdict.status === 'PASS')
390
+ return null;
391
+ if (verdict.deadHooks.length > 0) {
392
+ const hook = verdict.deadHooks[0];
393
+ return `memesh: the ${hook} hook has never run — \`memesh doctor\` for the reason`;
394
+ }
395
+ const hook = verdict.silentHook;
396
+ if (hook) {
397
+ const since = (hook.firstTriggeredAt ?? '').slice(0, 10) || 'install';
398
+ return `memesh: ${hook.hook} ran ${hook.triggeredRuns} times since ${since} and wrote nothing — \`memesh doctor\` for the reason`;
399
+ }
400
+ const stopped = verdict.stoppedTypes[0];
401
+ if (stopped) {
402
+ return `memesh: nothing of type ${stopped.type} was captured this week (${stopped.prev7} last week) — \`memesh doctor\` for the reason`;
403
+ }
404
+ return null;
405
+ }
406
+ export const GRACE_SESSIONS = 3;
407
+ export const GRACE_HOURS = 24;
408
+ export function parseGraceState(raw) {
409
+ if (!raw)
410
+ return null;
411
+ let parsed;
412
+ try {
413
+ parsed = JSON.parse(raw);
414
+ }
415
+ catch {
416
+ return null;
417
+ }
418
+ if (!parsed || typeof parsed !== 'object')
419
+ return null;
420
+ const rec = parsed;
421
+ if (typeof rec.version !== 'string' || typeof rec.firstSeenAt !== 'string')
422
+ return null;
423
+ const sessions = typeof rec.sessions === 'number' && Number.isFinite(rec.sessions) ? rec.sessions : 0;
424
+ return { version: rec.version, firstSeenAt: rec.firstSeenAt, sessions };
425
+ }
426
+ export function advanceGraceState(previous, version, nowMs) {
427
+ if (!previous || previous.version !== version) {
428
+ return { version, firstSeenAt: new Date(nowMs).toISOString(), sessions: 1 };
429
+ }
430
+ return { ...previous, sessions: previous.sessions + 1 };
431
+ }
432
+ export function graceInEffect(state, nowMs) {
433
+ if (state.sessions <= GRACE_SESSIONS)
434
+ return true;
435
+ const startedMs = Date.parse(state.firstSeenAt);
436
+ if (!Number.isFinite(startedMs))
437
+ return false;
438
+ return nowMs - startedMs < GRACE_HOURS * 60 * 60 * 1000;
439
+ }
440
+ export function detectHookHost(payload, env = {}) {
441
+ if (env.MEMESH_HOOK_HOST === 'claude-code' || env.MEMESH_HOOK_HOST === 'codex') {
442
+ return env.MEMESH_HOOK_HOST;
443
+ }
444
+ if (env.CODEX_HOME || env.CODEX_SANDBOX || env.CODEX_PLUGIN_ROOT)
445
+ return 'codex';
446
+ if (env.CLAUDE_PLUGIN_ROOT || env.CLAUDE_PROJECT_DIR || env.CLAUDECODE)
447
+ return 'claude-code';
448
+ if (payload && typeof payload === 'object') {
449
+ if (typeof payload.transcript_path === 'string' || typeof payload.hook_event_name === 'string') {
450
+ return 'claude-code';
451
+ }
452
+ }
453
+ return 'unknown';
454
+ }
@@ -0,0 +1,195 @@
1
+ // ============================================================================
2
+ // AUTO-GENERATED from src/core/update-notice.ts — DO NOT EDIT BY HAND.
3
+ // Regenerate with: npm run build (scripts/generate-hook-core.mjs)
4
+ //
5
+ // Claude Code hooks import this committed copy instead of dist/, so the
6
+ // always-on capture path survives a missing or stale dist/ while staying
7
+ // byte-locked to core — eliminating the hand-mirror drift behind the P0 FTS bug.
8
+ // ============================================================================
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import { redactUserPaths } from './core-paths.js';
12
+ export const UP_TO_DATE_REFRESH_MS = 60 * 60 * 1000;
13
+ export const UPGRADE_AVAILABLE_REFRESH_MS = 12 * 60 * 60 * 1000;
14
+ export const ANSWER_VALID_MS = 24 * 60 * 60 * 1000;
15
+ export const SNOOZE_LEVEL_MS = [24 * 60 * 60 * 1000, 48 * 60 * 60 * 1000, 7 * 24 * 60 * 60 * 1000];
16
+ const SNOOZE_FILE = 'update-snooze.json';
17
+ const JUST_UPGRADED_FILE = 'just-upgraded.json';
18
+ export function isStrictlyOlder(a, b) {
19
+ const parse = (v) => {
20
+ const [main, ...rest] = String(v).split(/[-+]/);
21
+ const nums = main.split('.').map((s) => Number.parseInt(s, 10));
22
+ return { nums, tail: rest.join('-') };
23
+ };
24
+ const pa = parse(a);
25
+ const pb = parse(b);
26
+ const len = Math.max(pa.nums.length, pb.nums.length);
27
+ for (let i = 0; i < len; i++) {
28
+ const ai = Number.isFinite(pa.nums[i]) ? pa.nums[i] : 0;
29
+ const bi = Number.isFinite(pb.nums[i]) ? pb.nums[i] : 0;
30
+ if (ai !== bi)
31
+ return ai < bi;
32
+ }
33
+ if (pa.tail && !pb.tail)
34
+ return true;
35
+ if (!pa.tail && pb.tail)
36
+ return false;
37
+ return pa.tail < pb.tail;
38
+ }
39
+ function parseIso(value) {
40
+ if (typeof value !== 'string')
41
+ return null;
42
+ const t = Date.parse(value);
43
+ return Number.isNaN(t) ? null : t;
44
+ }
45
+ function readJson(file) {
46
+ try {
47
+ if (!fs.existsSync(file))
48
+ return null;
49
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
50
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ function writePrivateJson(file, value) {
57
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
58
+ fs.writeFileSync(file, JSON.stringify(value, null, 2), { mode: 0o600 });
59
+ try {
60
+ fs.chmodSync(file, 0o600);
61
+ }
62
+ catch { }
63
+ }
64
+ export function readSnooze(dir) {
65
+ const raw = readJson(path.join(dir, SNOOZE_FILE));
66
+ if (!raw)
67
+ return null;
68
+ const { target, level, since } = raw;
69
+ if (typeof target !== 'string' || !target)
70
+ return null;
71
+ if (typeof level !== 'number' || !Number.isInteger(level) || level < 1)
72
+ return null;
73
+ if (parseIso(since) === null)
74
+ return null;
75
+ return { target, level, since: since };
76
+ }
77
+ export function writeSnooze(dir, target, now = new Date()) {
78
+ const previous = readSnooze(dir);
79
+ const level = previous && previous.target === target
80
+ ? Math.min(previous.level + 1, SNOOZE_LEVEL_MS.length)
81
+ : 1;
82
+ const state = { target, level, since: now.toISOString() };
83
+ writePrivateJson(path.join(dir, SNOOZE_FILE), state);
84
+ return state;
85
+ }
86
+ export function clearSnooze(dir) {
87
+ try {
88
+ fs.unlinkSync(path.join(dir, SNOOZE_FILE));
89
+ }
90
+ catch { }
91
+ }
92
+ export function snoozeExpiresAt(state) {
93
+ const since = parseIso(state.since) ?? 0;
94
+ const duration = SNOOZE_LEVEL_MS[Math.min(state.level, SNOOZE_LEVEL_MS.length) - 1];
95
+ return since + duration;
96
+ }
97
+ export function readJustUpgradedMarker(dir) {
98
+ const raw = readJson(path.join(dir, JUST_UPGRADED_FILE));
99
+ if (!raw)
100
+ return null;
101
+ const { from, to, at } = raw;
102
+ if (typeof from !== 'string' || !from || typeof to !== 'string' || !to)
103
+ return null;
104
+ return { from, to, at: typeof at === 'string' ? at : '' };
105
+ }
106
+ export function writeJustUpgradedMarker(dir, from, to, now = new Date()) {
107
+ writePrivateJson(path.join(dir, JUST_UPGRADED_FILE), { from, to, at: now.toISOString() });
108
+ }
109
+ export function clearJustUpgradedMarker(dir) {
110
+ try {
111
+ fs.unlinkSync(path.join(dir, JUST_UPGRADED_FILE));
112
+ }
113
+ catch { }
114
+ }
115
+ export function claimJustUpgradedMarker(dir) {
116
+ const file = path.join(dir, JUST_UPGRADED_FILE);
117
+ const taken = `${file}.claimed-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
118
+ try {
119
+ fs.renameSync(file, taken);
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ const raw = readJson(taken);
125
+ try {
126
+ fs.unlinkSync(taken);
127
+ }
128
+ catch { }
129
+ if (!raw)
130
+ return null;
131
+ const { from, to, at } = raw;
132
+ if (typeof from !== 'string' || !from || typeof to !== 'string' || !to)
133
+ return null;
134
+ return { from, to, at: typeof at === 'string' ? at : '' };
135
+ }
136
+ function boundedReason(raw) {
137
+ const oneLine = redactUserPaths(raw).replace(/[\u0000-\u001f\u007f]+/g, ' ').replace(/\s+/g, ' ').trim();
138
+ return oneLine.length > 160 ? `${oneLine.slice(0, 157)}...` : oneLine;
139
+ }
140
+ function answerIsCurrent(currentVersion, cache, now) {
141
+ if (!cache || cache.currentVersion !== currentVersion)
142
+ return false;
143
+ if (typeof cache.latestVersion !== 'string' || !cache.latestVersion)
144
+ return false;
145
+ const successAt = parseIso(cache.lastSuccessfulCheckAt);
146
+ if (successAt === null)
147
+ return false;
148
+ return now.getTime() - successAt <= ANSWER_VALID_MS;
149
+ }
150
+ export function shouldRefreshUpdateCache(currentVersion, cache, now = new Date()) {
151
+ if (!answerIsCurrent(currentVersion, cache, now))
152
+ return true;
153
+ const successAt = parseIso(cache.lastSuccessfulCheckAt);
154
+ const age = now.getTime() - successAt;
155
+ const upgrade = isStrictlyOlder(currentVersion, cache.latestVersion);
156
+ return age > (upgrade ? UPGRADE_AVAILABLE_REFRESH_MS : UP_TO_DATE_REFRESH_MS);
157
+ }
158
+ export function resolveUpdateNotice(input) {
159
+ const { dir, currentVersion, cache } = input;
160
+ const now = input.now ?? new Date();
161
+ if (input.updateCheckEnabled === false)
162
+ return { kind: 'DISABLED', currentVersion };
163
+ const marker = readJustUpgradedMarker(dir);
164
+ if (marker) {
165
+ if (marker.to === currentVersion) {
166
+ return { kind: 'JUST_UPGRADED', currentVersion, from: marker.from, to: marker.to };
167
+ }
168
+ clearJustUpgradedMarker(dir);
169
+ }
170
+ if (!answerIsCurrent(currentVersion, cache, now)) {
171
+ let reason = 'no update check has completed yet';
172
+ let attempted = false;
173
+ if (cache && cache.currentVersion === currentVersion) {
174
+ attempted = parseIso(cache.lastSuccessfulCheckAt) !== null
175
+ || (typeof cache.lastError === 'string' && cache.lastError.length > 0);
176
+ if (typeof cache.lastError === 'string' && cache.lastError)
177
+ reason = boundedReason(cache.lastError);
178
+ else if (parseIso(cache.lastSuccessfulCheckAt) !== null)
179
+ reason = 'the last successful check is more than a day old';
180
+ }
181
+ return { kind: 'CHECK_FAILED', currentVersion, reason, attempted };
182
+ }
183
+ const latestVersion = cache.latestVersion;
184
+ if (!isStrictlyOlder(currentVersion, latestVersion)) {
185
+ return { kind: 'UP_TO_DATE', currentVersion, latestVersion };
186
+ }
187
+ const snooze = readSnooze(dir);
188
+ if (snooze && snooze.target === latestVersion) {
189
+ const until = snoozeExpiresAt(snooze);
190
+ if (now.getTime() < until) {
191
+ return { kind: 'SNOOZED', currentVersion, latestVersion, until: new Date(until).toISOString(), level: snooze.level };
192
+ }
193
+ }
194
+ return { kind: 'UPGRADE_AVAILABLE', currentVersion, latestVersion };
195
+ }