memory-crystal 0.2.0

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 (104) hide show
  1. package/.env.example +20 -0
  2. package/CHANGELOG.md +6 -0
  3. package/LETTERS.md +22 -0
  4. package/LICENSE +21 -0
  5. package/README-ENTERPRISE.md +162 -0
  6. package/README-old.md +275 -0
  7. package/README.md +91 -0
  8. package/RELAY.md +88 -0
  9. package/TECHNICAL.md +379 -0
  10. package/ai/dev-updates/2026-02-25--cc-air--phase2-architecture-pivot.md +70 -0
  11. package/ai/dev-updates/2026-02-25--cc-air--phase2-worker-build.md +72 -0
  12. package/ai/dev-updates/2026-02-26--10-25-16--cc-mini--phase2-implementation.md +49 -0
  13. package/ai/dev-updates/2026-02-27--20-30-00--cc-mini--readme-overhaul-and-public-deploy.md +69 -0
  14. package/ai/notes/2026-02-26--cc-air--notes.md +412 -0
  15. package/ai/notes/2026-02-27--cc-mini--grok-feedback.md +44 -0
  16. package/ai/notes/2026-02-27--cc-mini--lesa-feedback.md +45 -0
  17. package/ai/notes/RESEARCH.md +1185 -0
  18. package/ai/notes/salience-research/README.md +29 -0
  19. package/ai/notes/salience-research/eurosla-salience-review.md +64 -0
  20. package/ai/notes/salience-research/full-research-summary.md +269 -0
  21. package/ai/notes/salience-research/salience-levels-diagram.png +0 -0
  22. package/ai/plan/2026-02-27--cc-mini--qr-pairing-spec.md +203 -0
  23. package/ai/plan/_archive/PLAN.md +194 -0
  24. package/ai/plan/_archive/PRD.md +1014 -0
  25. package/ai/plan/cc-plans-duplicates-from-dot-claude/2026-02-26--cc-mini--phase2-implementation-plan.md +245 -0
  26. package/ai/plan/dev-conventions-note.md +70 -0
  27. package/ai/plan/ldm-os-install-and-boot-architecture.md +285 -0
  28. package/ai/plan/memory-crystal-phase2-plan.md +192 -0
  29. package/ai/plan/memory-system-lay-of-the-land.md +214 -0
  30. package/ai/plan/phase2-ephemeral-relay.md +238 -0
  31. package/ai/plan/readme-first.md +68 -0
  32. package/ai/plan/roadmap.md +159 -0
  33. package/ai/todos/PUNCHLIST.md +44 -0
  34. package/ai/todos/README.md +31 -0
  35. package/ai/todos/inboxes/cc-air/2026-02-26--cc-air--post-relay-todos.md +85 -0
  36. package/ai/todos/inboxes/cc-mini/2026-02-26--cc-mini--phase2-status.md +100 -0
  37. package/ai/todos/inboxes/cc-mini/_archive/TODO.md +25 -0
  38. package/ai/todos/inboxes/parker/2026-02-25--cc-air--setup-checklist.md +139 -0
  39. package/ai/todos/inboxes/parker/2026-02-26--cc-mini--phase2-your-moves.md +72 -0
  40. package/dist/cc-hook.d.ts +1 -0
  41. package/dist/cc-hook.js +349 -0
  42. package/dist/chunk-3VFIJYS4.js +818 -0
  43. package/dist/chunk-52QE3YI3.js +1169 -0
  44. package/dist/chunk-AA3OPP4Z.js +432 -0
  45. package/dist/chunk-D3I3ZSE2.js +411 -0
  46. package/dist/chunk-EKSACBTJ.js +1070 -0
  47. package/dist/chunk-F3Y7EL7K.js +83 -0
  48. package/dist/chunk-JWZXYVET.js +1068 -0
  49. package/dist/chunk-KYVWO6ZM.js +1069 -0
  50. package/dist/chunk-L3VHARQH.js +413 -0
  51. package/dist/chunk-LOVAHSQV.js +411 -0
  52. package/dist/chunk-LQOYCAGG.js +446 -0
  53. package/dist/chunk-MK42FMEG.js +147 -0
  54. package/dist/chunk-NIJCVN3O.js +147 -0
  55. package/dist/chunk-O2UITJGH.js +465 -0
  56. package/dist/chunk-PEK6JH65.js +432 -0
  57. package/dist/chunk-PJ6FFKEX.js +77 -0
  58. package/dist/chunk-PLUBBZYR.js +800 -0
  59. package/dist/chunk-SGL6ISBJ.js +1061 -0
  60. package/dist/chunk-UNHVZB5G.js +411 -0
  61. package/dist/chunk-VAFTWSTE.js +1061 -0
  62. package/dist/chunk-XZ3S56RQ.js +1061 -0
  63. package/dist/chunk-Y72C7F6O.js +148 -0
  64. package/dist/cli.d.ts +1 -0
  65. package/dist/cli.js +325 -0
  66. package/dist/core.d.ts +188 -0
  67. package/dist/core.js +12 -0
  68. package/dist/crypto.d.ts +16 -0
  69. package/dist/crypto.js +18 -0
  70. package/dist/dev-update-SZ2Z4WCQ.js +6 -0
  71. package/dist/ldm.d.ts +17 -0
  72. package/dist/ldm.js +12 -0
  73. package/dist/mcp-server.d.ts +1 -0
  74. package/dist/mcp-server.js +250 -0
  75. package/dist/migrate.d.ts +1 -0
  76. package/dist/migrate.js +89 -0
  77. package/dist/mirror-sync.d.ts +1 -0
  78. package/dist/mirror-sync.js +130 -0
  79. package/dist/openclaw.d.ts +5 -0
  80. package/dist/openclaw.js +349 -0
  81. package/dist/poller.d.ts +1 -0
  82. package/dist/poller.js +272 -0
  83. package/dist/summarize.d.ts +19 -0
  84. package/dist/summarize.js +10 -0
  85. package/dist/worker.js +137 -0
  86. package/openclaw.plugin.json +11 -0
  87. package/package.json +40 -0
  88. package/scripts/migrate-lance-to-sqlite.mjs +217 -0
  89. package/skills/memory/SKILL.md +61 -0
  90. package/src/cc-hook.ts +447 -0
  91. package/src/cli.ts +356 -0
  92. package/src/core.ts +1472 -0
  93. package/src/crypto.ts +113 -0
  94. package/src/dev-update.ts +178 -0
  95. package/src/ldm.ts +117 -0
  96. package/src/mcp-server.ts +274 -0
  97. package/src/migrate.ts +104 -0
  98. package/src/mirror-sync.ts +175 -0
  99. package/src/openclaw.ts +250 -0
  100. package/src/poller.ts +345 -0
  101. package/src/summarize.ts +210 -0
  102. package/src/worker.ts +208 -0
  103. package/tsconfig.json +18 -0
  104. package/wrangler.toml +20 -0
package/src/cc-hook.ts ADDED
@@ -0,0 +1,447 @@
1
+ #!/usr/bin/env node
2
+ // memory-crystal/cc-hook.ts — Claude Code Stop hook handler.
3
+ // Triggered after every Claude Code response. Reads the session JSONL,
4
+ // extracts new turns since last watermark.
5
+ //
6
+ // Two modes:
7
+ // LOCAL: Ingests directly into local crystal (Mini)
8
+ // RELAY: Encrypts and drops at ephemeral relay Worker (Air/remote devices)
9
+ //
10
+ // Usage (Stop hook):
11
+ // Receives JSON on stdin: { transcript_path, session_id, ... }
12
+ //
13
+ // Usage (CLI):
14
+ // node cc-hook.js --on Enable capture
15
+ // node cc-hook.js --off Disable capture
16
+ // node cc-hook.js --status Check state
17
+
18
+ import { Crystal, RemoteCrystal, resolveConfig, createCrystal, type Chunk } from './core.js';
19
+ import { loadRelayKey, encryptJSON } from './crypto.js';
20
+ import { ensureLdm, ldmPaths } from './ldm.js';
21
+ import {
22
+ readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync,
23
+ statSync, openSync, readSync, closeSync, copyFileSync,
24
+ } from 'node:fs';
25
+ import { join, basename, dirname } from 'node:path';
26
+
27
+ const HOME = process.env.HOME || '';
28
+ const CC_AGENT_ID = process.env.CRYSTAL_AGENT_ID || 'cc-mini';
29
+ const RELAY_URL = process.env.CRYSTAL_RELAY_URL || '';
30
+ const RELAY_TOKEN = process.env.CRYSTAL_RELAY_TOKEN || '';
31
+ const OC_DIR = join(HOME, '.openclaw');
32
+ const LDM_DAILY = join(HOME, '.ldm', 'agents', CC_AGENT_ID, 'memory', 'daily');
33
+ const PRIVATE_MODE_PATH = join(OC_DIR, 'memory', 'memory-capture-state.json');
34
+ const WATERMARK_PATH = join(OC_DIR, 'memory', 'cc-capture-watermark.json');
35
+ const CC_ENABLED_PATH = join(OC_DIR, 'memory', 'cc-capture-enabled.json');
36
+
37
+ // ── Mode detection ──
38
+
39
+ type CaptureMode = 'local' | 'relay';
40
+
41
+ function getCaptureMode(): CaptureMode {
42
+ if (RELAY_URL && RELAY_TOKEN) return 'relay';
43
+ return 'local';
44
+ }
45
+
46
+ // ── Private mode (shared with Lēsa's system) ──
47
+
48
+ function isPrivateMode(): boolean {
49
+ try {
50
+ if (existsSync(PRIVATE_MODE_PATH)) {
51
+ const state = JSON.parse(readFileSync(PRIVATE_MODE_PATH, 'utf-8'));
52
+ return state.enabled === false;
53
+ }
54
+ } catch {}
55
+ return false;
56
+ }
57
+
58
+ // ── CC capture on/off switch ──
59
+
60
+ function isCaptureEnabled(): boolean {
61
+ try {
62
+ if (existsSync(CC_ENABLED_PATH)) {
63
+ const state = JSON.parse(readFileSync(CC_ENABLED_PATH, 'utf-8'));
64
+ return state.enabled !== false;
65
+ }
66
+ } catch {}
67
+ return true; // Default: on
68
+ }
69
+
70
+ function setCaptureEnabled(enabled: boolean): void {
71
+ const dir = dirname(CC_ENABLED_PATH);
72
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
73
+ writeFileSync(CC_ENABLED_PATH, JSON.stringify({
74
+ enabled,
75
+ updatedAt: new Date().toISOString(),
76
+ }, null, 2));
77
+ }
78
+
79
+ // ── Watermark ──
80
+
81
+ interface Watermark {
82
+ files: Record<string, { lastByteOffset: number; lastTimestamp: string }>;
83
+ lastRun: string | null;
84
+ }
85
+
86
+ function loadWatermark(): Watermark {
87
+ try {
88
+ if (existsSync(WATERMARK_PATH)) {
89
+ return JSON.parse(readFileSync(WATERMARK_PATH, 'utf-8'));
90
+ }
91
+ } catch {}
92
+ return { files: {}, lastRun: null };
93
+ }
94
+
95
+ function saveWatermark(wm: Watermark): void {
96
+ const dir = dirname(WATERMARK_PATH);
97
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
98
+ wm.lastRun = new Date().toISOString();
99
+ writeFileSync(WATERMARK_PATH, JSON.stringify(wm, null, 2));
100
+ }
101
+
102
+ // ── JSONL parsing ──
103
+
104
+ interface ExtractedMessage {
105
+ role: string;
106
+ text: string;
107
+ timestamp: string;
108
+ sessionId: string;
109
+ }
110
+
111
+ function extractMessages(filePath: string, lastByteOffset: number): {
112
+ messages: ExtractedMessage[];
113
+ newByteOffset: number;
114
+ } {
115
+ const fileSize = statSync(filePath).size;
116
+ if (lastByteOffset >= fileSize) {
117
+ return { messages: [], newByteOffset: fileSize };
118
+ }
119
+
120
+ const fd = openSync(filePath, 'r');
121
+ const bufSize = fileSize - lastByteOffset;
122
+ const buf = Buffer.alloc(bufSize);
123
+ readSync(fd, buf, 0, bufSize, lastByteOffset);
124
+ closeSync(fd);
125
+
126
+ const lines = buf.toString('utf-8').split('\n').filter(Boolean);
127
+ const messages: ExtractedMessage[] = [];
128
+
129
+ for (const line of lines) {
130
+ try {
131
+ const obj = JSON.parse(line);
132
+ if (obj.type !== 'user' && obj.type !== 'assistant') continue;
133
+
134
+ const msg = obj.message;
135
+ if (!msg) continue;
136
+
137
+ let text = '';
138
+ if (typeof msg.content === 'string') {
139
+ text = msg.content;
140
+ } else if (Array.isArray(msg.content)) {
141
+ const parts: string[] = [];
142
+ for (const block of msg.content) {
143
+ if (block.type === 'text' && block.text) parts.push(block.text);
144
+ if (block.type === 'thinking' && block.thinking) parts.push(`[thinking] ${block.thinking}`);
145
+ }
146
+ text = parts.join('\n\n');
147
+ }
148
+
149
+ if (text.length < 20) continue;
150
+
151
+ messages.push({
152
+ role: msg.role || obj.type,
153
+ text,
154
+ timestamp: obj.timestamp || new Date().toISOString(),
155
+ sessionId: obj.sessionId || 'unknown',
156
+ });
157
+ } catch {}
158
+ }
159
+
160
+ return { messages, newByteOffset: fileSize };
161
+ }
162
+
163
+ // ── JSONL transcript archive ──
164
+
165
+ function archiveTranscript(transcriptPath: string, agentId?: string): void {
166
+ try {
167
+ if (isPrivateMode()) return;
168
+ const paths = ensureLdm(agentId);
169
+ const dest = join(paths.transcripts, basename(transcriptPath));
170
+ // Only copy if source is newer than destination (mtime check)
171
+ if (existsSync(dest)) {
172
+ const srcMtime = statSync(transcriptPath).mtimeMs;
173
+ const dstMtime = statSync(dest).mtimeMs;
174
+ if (srcMtime <= dstMtime) return;
175
+ }
176
+ // copyFileSync imported at top of file
177
+ copyFileSync(transcriptPath, dest);
178
+ } catch {} // Non-fatal
179
+ }
180
+
181
+ // archiveTranscript: copies JSONL to ~/.ldm/agents/{id}/transcripts/
182
+ // Called early in main(), after kill-switch checks, before watermark logic.
183
+
184
+ // ── Daily log breadcrumb ──
185
+
186
+ function appendDailyLog(messages: ExtractedMessage[], agentId?: string): void {
187
+ try {
188
+ const paths = ldmPaths(agentId);
189
+ if (!existsSync(paths.root)) return; // LDM not scaffolded
190
+ if (!existsSync(paths.daily)) mkdirSync(paths.daily, { recursive: true });
191
+
192
+ const now = new Date();
193
+ const dateStr = now.toISOString().slice(0, 10);
194
+ const timeStr = now.toLocaleTimeString('en-US', {
195
+ hour: '2-digit', minute: '2-digit', hour12: false,
196
+ timeZone: 'America/Los_Angeles',
197
+ });
198
+ const logPath = join(paths.daily, `${dateStr}.md`);
199
+
200
+ // Extract first user message as snippet
201
+ const userMsg = messages.find(m => m.role === 'user');
202
+ if (!userMsg) return;
203
+ const snippet = userMsg.text.slice(0, 120).replace(/\n/g, ' ').trim();
204
+
205
+ const line = `- **${timeStr}** ${snippet}${userMsg.text.length > 120 ? '...' : ''}\n`;
206
+
207
+ // Create file with header if new
208
+ if (!existsSync(logPath)) {
209
+ writeFileSync(logPath, `# ${dateStr} - CC Daily Log\n\n`);
210
+ }
211
+
212
+ appendFileSync(logPath, line);
213
+ } catch {} // Fail silently
214
+ }
215
+
216
+ // ── Relay mode: encrypt and drop at Worker ──
217
+
218
+ async function dropAtRelay(messages: ExtractedMessage[]): Promise<number> {
219
+ const relayKey = loadRelayKey();
220
+
221
+ // Package messages for relay
222
+ const payload = {
223
+ agent_id: CC_AGENT_ID,
224
+ dropped_at: new Date().toISOString(),
225
+ messages: messages.map(m => ({
226
+ text: m.text,
227
+ role: m.role,
228
+ timestamp: m.timestamp,
229
+ sessionId: m.sessionId,
230
+ })),
231
+ };
232
+
233
+ // Encrypt
234
+ const encrypted = encryptJSON(payload, relayKey);
235
+ const body = JSON.stringify(encrypted);
236
+
237
+ // Drop at Worker with retry
238
+ let retries = 0;
239
+ while (retries < 4) {
240
+ try {
241
+ const resp = await fetch(`${RELAY_URL}/drop/conversations`, {
242
+ method: 'POST',
243
+ headers: {
244
+ 'Authorization': `Bearer ${RELAY_TOKEN}`,
245
+ 'Content-Type': 'application/octet-stream',
246
+ },
247
+ body,
248
+ });
249
+
250
+ if (!resp.ok) {
251
+ const err = await resp.text();
252
+ throw new Error(`Relay drop failed: ${resp.status} ${err}`);
253
+ }
254
+
255
+ const result = await resp.json() as any;
256
+ return messages.length;
257
+ } catch (err: any) {
258
+ retries++;
259
+ if (retries >= 4) throw err;
260
+ const delay = Math.min(1000 * 2 ** retries, 30000);
261
+ process.stderr.write(` [relay retry ${retries}] ${err.message}, waiting ${delay}ms\n`);
262
+ await new Promise(r => setTimeout(r, delay));
263
+ }
264
+ }
265
+ return 0;
266
+ }
267
+
268
+ // ── Local mode: direct ingest with batched retry ──
269
+
270
+ const BATCH_SIZE = 200;
271
+
272
+ async function ingestLocal(messages: ExtractedMessage[]): Promise<number> {
273
+ const config = resolveConfig();
274
+ const crystal = createCrystal(config);
275
+ await crystal.init();
276
+
277
+ // Turn-boundary chunking: one message = one chunk.
278
+ // Only fall back to chunkText() for very long messages (>2000 tokens).
279
+ const maxSingleChunkChars = 2000 * 4;
280
+ const chunks: Chunk[] = [];
281
+ for (const msg of messages) {
282
+ if (msg.text.length <= maxSingleChunkChars) {
283
+ chunks.push({
284
+ text: msg.text,
285
+ role: msg.role as 'user' | 'assistant',
286
+ source_type: 'conversation',
287
+ source_id: `cc:${msg.sessionId}`,
288
+ agent_id: CC_AGENT_ID,
289
+ token_count: Math.ceil(msg.text.length / 4),
290
+ created_at: msg.timestamp,
291
+ });
292
+ } else {
293
+ for (const ct of crystal.chunkText(msg.text)) {
294
+ chunks.push({
295
+ text: ct,
296
+ role: msg.role as 'user' | 'assistant',
297
+ source_type: 'conversation',
298
+ source_id: `cc:${msg.sessionId}`,
299
+ agent_id: CC_AGENT_ID,
300
+ token_count: Math.ceil(ct.length / 4),
301
+ created_at: msg.timestamp,
302
+ });
303
+ }
304
+ }
305
+ }
306
+
307
+ // Batched ingest with retry
308
+ let total = 0;
309
+ for (let i = 0; i < chunks.length; i += BATCH_SIZE) {
310
+ const batch = chunks.slice(i, i + BATCH_SIZE);
311
+ let retries = 0;
312
+ while (retries < 4) {
313
+ try {
314
+ total += await crystal.ingest(batch);
315
+ break;
316
+ } catch (err: any) {
317
+ retries++;
318
+ if (retries >= 4) throw err;
319
+ const delay = Math.min(1000 * 2 ** retries, 30000);
320
+ process.stderr.write(` [retry ${retries}] ${err.message}, waiting ${delay}ms\n`);
321
+ await new Promise(r => setTimeout(r, delay));
322
+ }
323
+ }
324
+ }
325
+ return total;
326
+ }
327
+
328
+ // ── CLI commands ──
329
+
330
+ const args = process.argv.slice(2);
331
+
332
+ if (args.includes('--on')) {
333
+ setCaptureEnabled(true);
334
+ console.log('(*) Claude Code memory capture ON');
335
+ process.exit(0);
336
+ }
337
+
338
+ if (args.includes('--off')) {
339
+ setCaptureEnabled(false);
340
+ console.log('( ) Claude Code memory capture OFF');
341
+ process.exit(0);
342
+ }
343
+
344
+ if (args.includes('--status')) {
345
+ const mode = getCaptureMode();
346
+ console.log(isCaptureEnabled() ? '(*) CC capture: ON' : '( ) CC capture: OFF');
347
+ console.log(isPrivateMode() ? '( ) Private mode: ON (blocks all capture)' : '(*) Private mode: OFF');
348
+ console.log(` Mode: ${mode}${mode === 'relay' ? ` (${RELAY_URL})` : ''}`);
349
+ console.log(` Agent ID: ${CC_AGENT_ID}`);
350
+ process.exit(0);
351
+ }
352
+
353
+ // ── Stop hook handler ──
354
+
355
+ async function main(): Promise<void> {
356
+ // Read hook JSON from stdin
357
+ let input = '';
358
+ for await (const chunk of process.stdin) {
359
+ input += chunk;
360
+ }
361
+
362
+ let hookData: any;
363
+ try {
364
+ hookData = JSON.parse(input);
365
+ } catch {
366
+ process.exit(0);
367
+ }
368
+
369
+ const transcriptPath = hookData.transcript_path;
370
+ if (!transcriptPath || !existsSync(transcriptPath)) process.exit(0);
371
+
372
+ // Kill switches
373
+ if (isPrivateMode() || !isCaptureEnabled()) process.exit(0);
374
+
375
+ // Archive JSONL transcript to LDM (copy if newer)
376
+ archiveTranscript(transcriptPath);
377
+
378
+ const wm = loadWatermark();
379
+ const fileKey = transcriptPath;
380
+
381
+ // First time: seed watermark at current size (skip old history)
382
+ if (!wm.files[fileKey]) {
383
+ const size = statSync(transcriptPath).size;
384
+ wm.files[fileKey] = { lastByteOffset: size, lastTimestamp: new Date().toISOString() };
385
+ saveWatermark(wm);
386
+ process.stderr.write(`[cc-memory-capture] seeded ${basename(transcriptPath)} at ${size} bytes\n`);
387
+ process.exit(0);
388
+ }
389
+
390
+ const lastOffset = wm.files[fileKey].lastByteOffset || 0;
391
+ const { messages, newByteOffset } = extractMessages(transcriptPath, lastOffset);
392
+
393
+ if (messages.length === 0) {
394
+ wm.files[fileKey] = { lastByteOffset: newByteOffset, lastTimestamp: new Date().toISOString() };
395
+ saveWatermark(wm);
396
+ process.exit(0);
397
+ }
398
+
399
+ const totalTokens = messages.reduce((sum, m) => sum + Math.ceil(m.text.length / 4), 0);
400
+
401
+ // Min threshold
402
+ if (totalTokens < 500) {
403
+ wm.files[fileKey] = { lastByteOffset: newByteOffset, lastTimestamp: new Date().toISOString() };
404
+ saveWatermark(wm);
405
+ process.exit(0);
406
+ }
407
+
408
+ const mode = getCaptureMode();
409
+
410
+ try {
411
+ if (mode === 'relay') {
412
+ // Relay mode: encrypt and drop at Worker
413
+ const count = await dropAtRelay(messages);
414
+ process.stderr.write(`[cc-memory-capture] relayed ${count} messages (${totalTokens} tokens) from ${basename(transcriptPath)}\n`);
415
+ } else {
416
+ // Local mode: direct ingest into crystal
417
+ const count = await ingestLocal(messages);
418
+ process.stderr.write(`[cc-memory-capture] ${count} chunks (${totalTokens} tokens) from ${basename(transcriptPath)}\n`);
419
+ }
420
+
421
+ wm.files[fileKey] = { lastByteOffset: newByteOffset, lastTimestamp: new Date().toISOString() };
422
+ saveWatermark(wm);
423
+
424
+ // Append breadcrumb to LDM daily log
425
+ appendDailyLog(messages);
426
+
427
+ // Generate MD session summary (non-fatal)
428
+ try {
429
+ const { generateSessionSummary, writeSummaryFile } = await import('./summarize.js');
430
+ const paths = ldmPaths();
431
+ const summaryMsgs = messages.map(m => ({ role: m.role, text: m.text, timestamp: m.timestamp, sessionId: m.sessionId }));
432
+ const summary = await generateSessionSummary(summaryMsgs);
433
+ const sessionId = messages[0]?.sessionId || 'unknown';
434
+ const agentId = process.env.CRYSTAL_AGENT_ID || 'cc-mini';
435
+ writeSummaryFile(paths.sessions, summary, agentId, sessionId);
436
+ } catch {} // Summary failure is non-fatal
437
+
438
+ // Dev updates disabled (2026-02-28). Was auto-generating files in every repo's
439
+ // ai/ folder after each session. Created noise, not signal. If we bring this back,
440
+ // it should be opt-in per repo, not a blanket scan.
441
+ } catch (err: any) {
442
+ process.stderr.write(`[cc-memory-capture] error: ${err.message}\n`);
443
+ process.exit(1);
444
+ }
445
+ }
446
+
447
+ main();