clawvault 1.8.4 β†’ 1.9.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.
package/bin/clawvault.js CHANGED
@@ -696,6 +696,44 @@ program
696
696
  }
697
697
  });
698
698
 
699
+ // === OBSERVE ===
700
+ program
701
+ .command('observe')
702
+ .description('Observe session files and build observational memory')
703
+ .option('--watch <path>', 'Watch session file or directory')
704
+ .option('--threshold <n>', 'Compression token threshold', '30000')
705
+ .option('--reflect-threshold <n>', 'Reflection token threshold', '40000')
706
+ .option('--model <model>', 'LLM model override')
707
+ .option('--compress <file>', 'One-shot compression for a conversation file')
708
+ .option('--daemon', 'Run in detached background mode')
709
+ .option('-v, --vault <path>', 'Vault path')
710
+ .action(async (options) => {
711
+ try {
712
+ const { observeCommand } = await import('../dist/commands/observe.js');
713
+ const threshold = Number.parseInt(options.threshold, 10);
714
+ const reflectThreshold = Number.parseInt(options.reflectThreshold, 10);
715
+ if (Number.isNaN(threshold) || threshold <= 0) {
716
+ throw new Error(`Invalid --threshold value: ${options.threshold}`);
717
+ }
718
+ if (Number.isNaN(reflectThreshold) || reflectThreshold <= 0) {
719
+ throw new Error(`Invalid --reflect-threshold value: ${options.reflectThreshold}`);
720
+ }
721
+
722
+ await observeCommand({
723
+ watch: options.watch,
724
+ threshold,
725
+ reflectThreshold,
726
+ model: options.model,
727
+ compress: options.compress,
728
+ daemon: options.daemon,
729
+ vaultPath: resolveVaultPath(options.vault)
730
+ });
731
+ } catch (err) {
732
+ console.error(chalk.red(`Error: ${err.message}`));
733
+ process.exit(1);
734
+ }
735
+ });
736
+
699
737
  // === SESSION-RECAP ===
700
738
  program
701
739
  .command('session-recap <sessionKey>')
@@ -0,0 +1,841 @@
1
+ // src/commands/observe.ts
2
+ import * as fs4 from "fs";
3
+ import * as path4 from "path";
4
+ import { spawn } from "child_process";
5
+
6
+ // src/observer/observer.ts
7
+ import * as fs2 from "fs";
8
+ import * as path2 from "path";
9
+
10
+ // src/observer/compressor.ts
11
+ var DATE_HEADING_RE = /^##\s+(\d{4}-\d{2}-\d{2})\s*$/;
12
+ var OBSERVATION_LINE_RE = /^(πŸ”΄|🟑|🟒)\s+(.+)$/u;
13
+ var CRITICAL_RE = /\b(decid(?:e|ed|ing|ion)|error|fail(?:ed|ure)?|prefer(?:ence)?|block(?:ed|er)?|must|required?|urgent)\b/i;
14
+ var NOTABLE_RE = /\b(context|pattern|architecture|approach|trade[- ]?off|milestone|notable)\b/i;
15
+ var Compressor = class {
16
+ model;
17
+ now;
18
+ fetchImpl;
19
+ constructor(options = {}) {
20
+ this.model = options.model;
21
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
22
+ this.fetchImpl = options.fetchImpl ?? fetch;
23
+ }
24
+ async compress(messages, existingObservations) {
25
+ const cleanedMessages = messages.map((message) => message.trim()).filter(Boolean);
26
+ if (cleanedMessages.length === 0) {
27
+ return existingObservations.trim();
28
+ }
29
+ const prompt = this.buildPrompt(cleanedMessages, existingObservations);
30
+ const provider = this.resolveProvider();
31
+ if (provider) {
32
+ try {
33
+ const llmOutput = provider === "anthropic" ? await this.callAnthropic(prompt) : await this.callOpenAI(prompt);
34
+ const normalized = this.normalizeLlmOutput(llmOutput);
35
+ if (normalized) {
36
+ return this.mergeObservations(existingObservations, normalized);
37
+ }
38
+ } catch {
39
+ }
40
+ }
41
+ const fallback = this.fallbackCompression(cleanedMessages);
42
+ return this.mergeObservations(existingObservations, fallback);
43
+ }
44
+ resolveProvider() {
45
+ if (process.env.ANTHROPIC_API_KEY) {
46
+ return "anthropic";
47
+ }
48
+ if (process.env.OPENAI_API_KEY) {
49
+ return "openai";
50
+ }
51
+ return null;
52
+ }
53
+ buildPrompt(messages, existingObservations) {
54
+ return [
55
+ "You are an observer that compresses raw AI session messages into durable observations.",
56
+ "",
57
+ "Rules:",
58
+ "- Output markdown only.",
59
+ "- Group observations by date heading: ## YYYY-MM-DD",
60
+ "- Each line must follow: <emoji> <HH:MM> <observation>",
61
+ "- Priority emojis: \u{1F534} critical, \u{1F7E1} notable, \u{1F7E2} info",
62
+ "- Mark decisions, errors, user preferences, and blockers as \u{1F534}",
63
+ "- Keep observations concise and factual.",
64
+ "- Avoid duplicates when possible.",
65
+ "",
66
+ "Existing observations (may be empty):",
67
+ existingObservations.trim() || "(none)",
68
+ "",
69
+ "Raw messages:",
70
+ ...messages.map((message, index) => `[${index + 1}] ${message}`),
71
+ "",
72
+ "Return only the updated observation markdown."
73
+ ].join("\n");
74
+ }
75
+ async callAnthropic(prompt) {
76
+ const apiKey = process.env.ANTHROPIC_API_KEY;
77
+ if (!apiKey) {
78
+ return "";
79
+ }
80
+ const response = await this.fetchImpl("https://api.anthropic.com/v1/messages", {
81
+ method: "POST",
82
+ headers: {
83
+ "content-type": "application/json",
84
+ "x-api-key": apiKey,
85
+ "anthropic-version": "2023-06-01"
86
+ },
87
+ body: JSON.stringify({
88
+ model: this.model ?? "claude-3-5-haiku-latest",
89
+ temperature: 0.1,
90
+ max_tokens: 1400,
91
+ messages: [{ role: "user", content: prompt }]
92
+ })
93
+ });
94
+ if (!response.ok) {
95
+ throw new Error(`Anthropic request failed (${response.status})`);
96
+ }
97
+ const payload = await response.json();
98
+ return payload.content?.filter((part) => part.type === "text" && part.text).map((part) => part.text).join("\n").trim() ?? "";
99
+ }
100
+ async callOpenAI(prompt) {
101
+ const apiKey = process.env.OPENAI_API_KEY;
102
+ if (!apiKey) {
103
+ return "";
104
+ }
105
+ const response = await this.fetchImpl("https://api.openai.com/v1/chat/completions", {
106
+ method: "POST",
107
+ headers: {
108
+ "content-type": "application/json",
109
+ authorization: `Bearer ${apiKey}`
110
+ },
111
+ body: JSON.stringify({
112
+ model: this.model ?? "gpt-4o-mini",
113
+ temperature: 0.1,
114
+ messages: [
115
+ { role: "system", content: "You transform session logs into concise observations." },
116
+ { role: "user", content: prompt }
117
+ ]
118
+ })
119
+ });
120
+ if (!response.ok) {
121
+ throw new Error(`OpenAI request failed (${response.status})`);
122
+ }
123
+ const payload = await response.json();
124
+ return payload.choices?.[0]?.message?.content?.trim() ?? "";
125
+ }
126
+ normalizeLlmOutput(output) {
127
+ if (!output.trim()) {
128
+ return "";
129
+ }
130
+ const cleaned = output.replace(/^```(?:markdown)?\s*/i, "").replace(/\s*```$/, "").trim();
131
+ const lines = cleaned.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
132
+ const hasObservationLine = lines.some((line) => OBSERVATION_LINE_RE.test(line));
133
+ if (!hasObservationLine) {
134
+ return "";
135
+ }
136
+ const hasDateHeading = lines.some((line) => DATE_HEADING_RE.test(line));
137
+ if (hasDateHeading) {
138
+ return cleaned;
139
+ }
140
+ const today = this.formatDate(this.now());
141
+ return `## ${today}
142
+
143
+ ${cleaned}`;
144
+ }
145
+ fallbackCompression(messages) {
146
+ const sections = /* @__PURE__ */ new Map();
147
+ const seen = /* @__PURE__ */ new Set();
148
+ for (const message of messages) {
149
+ const normalized = this.normalizeText(message);
150
+ if (!normalized) continue;
151
+ const date = this.extractDate(message) ?? this.formatDate(this.now());
152
+ const time = this.extractTime(message) ?? this.formatTime(this.now());
153
+ const priority = this.inferPriority(normalized);
154
+ const line = `${time} ${normalized}`;
155
+ const dedupeKey = `${date}|${priority}|${this.normalizeText(line)}`;
156
+ if (seen.has(dedupeKey)) continue;
157
+ seen.add(dedupeKey);
158
+ const bucket = sections.get(date) ?? [];
159
+ bucket.push({ priority, content: line });
160
+ sections.set(date, bucket);
161
+ }
162
+ if (sections.size === 0) {
163
+ const date = this.formatDate(this.now());
164
+ sections.set(date, [{ priority: "\u{1F7E2}", content: `${this.formatTime(this.now())} Processed session updates.` }]);
165
+ }
166
+ return this.renderSections(sections);
167
+ }
168
+ mergeObservations(existing, incoming) {
169
+ const existingSections = this.parseSections(existing);
170
+ const incomingSections = this.parseSections(incoming);
171
+ if (incomingSections.size === 0) {
172
+ return existing.trim();
173
+ }
174
+ if (existingSections.size === 0) {
175
+ return this.renderSections(incomingSections);
176
+ }
177
+ for (const [date, lines] of incomingSections.entries()) {
178
+ const current = existingSections.get(date) ?? [];
179
+ current.push(...lines);
180
+ existingSections.set(date, current);
181
+ }
182
+ return this.renderSections(existingSections);
183
+ }
184
+ parseSections(markdown) {
185
+ const sections = /* @__PURE__ */ new Map();
186
+ let currentDate = null;
187
+ for (const rawLine of markdown.split(/\r?\n/)) {
188
+ const dateMatch = rawLine.match(DATE_HEADING_RE);
189
+ if (dateMatch) {
190
+ currentDate = dateMatch[1];
191
+ if (!sections.has(currentDate)) {
192
+ sections.set(currentDate, []);
193
+ }
194
+ continue;
195
+ }
196
+ if (!currentDate) continue;
197
+ const lineMatch = rawLine.match(OBSERVATION_LINE_RE);
198
+ if (!lineMatch) continue;
199
+ const bucket = sections.get(currentDate) ?? [];
200
+ bucket.push({
201
+ priority: lineMatch[1],
202
+ content: lineMatch[2].trim()
203
+ });
204
+ sections.set(currentDate, bucket);
205
+ }
206
+ return sections;
207
+ }
208
+ renderSections(sections) {
209
+ const chunks = [];
210
+ const sortedDates = [...sections.keys()].sort((a, b) => a.localeCompare(b));
211
+ for (const date of sortedDates) {
212
+ const lines = sections.get(date) ?? [];
213
+ if (lines.length === 0) continue;
214
+ chunks.push(`## ${date}`);
215
+ chunks.push("");
216
+ for (const line of lines) {
217
+ chunks.push(`${line.priority} ${line.content}`);
218
+ }
219
+ chunks.push("");
220
+ }
221
+ return chunks.join("\n").trim();
222
+ }
223
+ inferPriority(text) {
224
+ if (CRITICAL_RE.test(text)) return "\u{1F534}";
225
+ if (NOTABLE_RE.test(text)) return "\u{1F7E1}";
226
+ return "\u{1F7E2}";
227
+ }
228
+ normalizeText(text) {
229
+ return text.replace(/\s+/g, " ").replace(/\[[^\]]+\]/g, "").trim().slice(0, 280);
230
+ }
231
+ extractDate(text) {
232
+ const match = text.match(/\b(\d{4}-\d{2}-\d{2})\b/);
233
+ return match?.[1] ?? null;
234
+ }
235
+ extractTime(text) {
236
+ const match = text.match(/\b([01]\d|2[0-3]):([0-5]\d)\b/);
237
+ if (!match) {
238
+ return null;
239
+ }
240
+ return `${match[1]}:${match[2]}`;
241
+ }
242
+ formatDate(date) {
243
+ return date.toISOString().split("T")[0];
244
+ }
245
+ formatTime(date) {
246
+ return date.toISOString().slice(11, 16);
247
+ }
248
+ };
249
+
250
+ // src/observer/reflector.ts
251
+ var DATE_HEADING_RE2 = /^##\s+(\d{4}-\d{2}-\d{2})\s*$/;
252
+ var OBSERVATION_LINE_RE2 = /^(πŸ”΄|🟑|🟒)\s+(.+)$/u;
253
+ var Reflector = class {
254
+ now;
255
+ constructor(options = {}) {
256
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
257
+ }
258
+ reflect(observations) {
259
+ const sections = this.parseSections(observations);
260
+ if (sections.size === 0) {
261
+ return observations.trim();
262
+ }
263
+ const cutoff = this.buildCutoffDate();
264
+ const dedupeKeys = [];
265
+ const reflected = /* @__PURE__ */ new Map();
266
+ const dates = [...sections.keys()].sort((a, b) => b.localeCompare(a));
267
+ for (const date of dates) {
268
+ const sectionDate = this.parseDate(date);
269
+ const olderThanCutoff = sectionDate ? sectionDate.getTime() < cutoff.getTime() : false;
270
+ const lines = sections.get(date) ?? [];
271
+ const kept = [];
272
+ for (const line of lines) {
273
+ if (line.priority === "\u{1F534}") {
274
+ kept.push(line);
275
+ continue;
276
+ }
277
+ if (line.priority === "\u{1F7E2}" && olderThanCutoff) {
278
+ continue;
279
+ }
280
+ const key = this.normalizeText(line.content);
281
+ const isDuplicate = dedupeKeys.some((existing) => this.isSimilar(existing, key));
282
+ if (isDuplicate) {
283
+ continue;
284
+ }
285
+ dedupeKeys.push(key);
286
+ kept.push(line);
287
+ }
288
+ if (kept.length > 0) {
289
+ reflected.set(date, kept);
290
+ }
291
+ }
292
+ return this.renderSections(reflected);
293
+ }
294
+ buildCutoffDate() {
295
+ const cutoff = new Date(this.now());
296
+ cutoff.setHours(0, 0, 0, 0);
297
+ cutoff.setDate(cutoff.getDate() - 7);
298
+ return cutoff;
299
+ }
300
+ parseDate(date) {
301
+ const parsed = /* @__PURE__ */ new Date(`${date}T00:00:00.000Z`);
302
+ if (Number.isNaN(parsed.getTime())) {
303
+ return null;
304
+ }
305
+ return parsed;
306
+ }
307
+ parseSections(markdown) {
308
+ const sections = /* @__PURE__ */ new Map();
309
+ let currentDate = null;
310
+ for (const rawLine of markdown.split(/\r?\n/)) {
311
+ const dateMatch = rawLine.match(DATE_HEADING_RE2);
312
+ if (dateMatch) {
313
+ currentDate = dateMatch[1];
314
+ if (!sections.has(currentDate)) {
315
+ sections.set(currentDate, []);
316
+ }
317
+ continue;
318
+ }
319
+ if (!currentDate) continue;
320
+ const lineMatch = rawLine.match(OBSERVATION_LINE_RE2);
321
+ if (!lineMatch) continue;
322
+ const bucket = sections.get(currentDate) ?? [];
323
+ bucket.push({
324
+ priority: lineMatch[1],
325
+ content: lineMatch[2].trim()
326
+ });
327
+ sections.set(currentDate, bucket);
328
+ }
329
+ return sections;
330
+ }
331
+ renderSections(sections) {
332
+ const chunks = [];
333
+ const dates = [...sections.keys()].sort((a, b) => a.localeCompare(b));
334
+ for (const date of dates) {
335
+ const lines = sections.get(date) ?? [];
336
+ if (lines.length === 0) continue;
337
+ chunks.push(`## ${date}`);
338
+ chunks.push("");
339
+ for (const line of lines) {
340
+ chunks.push(`${line.priority} ${line.content}`);
341
+ }
342
+ chunks.push("");
343
+ }
344
+ return chunks.join("\n").trim();
345
+ }
346
+ normalizeText(text) {
347
+ return text.toLowerCase().replace(/\s+/g, " ").replace(/[^\w\s:.-]/g, "").trim();
348
+ }
349
+ isSimilar(a, b) {
350
+ if (a === b) return true;
351
+ if (a.length >= 24 && (a.includes(b) || b.includes(a))) {
352
+ return true;
353
+ }
354
+ return false;
355
+ }
356
+ };
357
+
358
+ // src/observer/router.ts
359
+ import * as fs from "fs";
360
+ import * as path from "path";
361
+ var CATEGORY_PATTERNS = [
362
+ {
363
+ category: "decisions",
364
+ patterns: [
365
+ /\b(decid(?:e|ed|ing|ion)|chose|picked|went with|selected|opted)\b/i,
366
+ /\b(decision|trade[- ]?off|alternative|rationale)\b/i
367
+ ]
368
+ },
369
+ {
370
+ category: "lessons",
371
+ patterns: [
372
+ /\b(learn(?:ed|ing|t)|lesson|mistake|insight|realized|discovered)\b/i,
373
+ /\b(note to self|remember|important|don'?t forget|never again)\b/i
374
+ ]
375
+ },
376
+ {
377
+ category: "people",
378
+ patterns: [
379
+ /\b(said|asked|told|mentioned|emailed|called|messaged|met with)\b/i,
380
+ /\b(client|partner|team|colleague|contact)\b/i
381
+ ]
382
+ },
383
+ {
384
+ category: "preferences",
385
+ patterns: [
386
+ /\b(prefer(?:s|red|ence)?|like(?:s|d)?|want(?:s|ed)?|style|convention)\b/i,
387
+ /\b(always use|never use|default to)\b/i
388
+ ]
389
+ },
390
+ {
391
+ category: "commitments",
392
+ patterns: [
393
+ /\b(promised|committed|deadline|due|scheduled|will do|agreed to)\b/i,
394
+ /\b(todo|task|action item|follow[- ]?up)\b/i
395
+ ]
396
+ },
397
+ {
398
+ category: "projects",
399
+ patterns: [
400
+ /\b(deployed|shipped|launched|released|merged|built|created)\b/i,
401
+ /\b(project|repo|service|api|feature|bug fix)\b/i
402
+ ]
403
+ }
404
+ ];
405
+ var OBSERVATION_LINE_RE3 = /^(πŸ”΄|🟑|🟒)\s+(\d{2}:\d{2})?\s*(.+)$/u;
406
+ var DATE_HEADING_RE3 = /^##\s+(\d{4}-\d{2}-\d{2})\s*$/;
407
+ var Router = class {
408
+ vaultPath;
409
+ constructor(vaultPath) {
410
+ this.vaultPath = path.resolve(vaultPath);
411
+ }
412
+ /**
413
+ * Takes observation markdown and routes items to appropriate vault categories.
414
+ * Only routes πŸ”΄ and 🟑 items β€” 🟒 stays only in observations.
415
+ * Returns a summary of what was routed where.
416
+ */
417
+ route(observationMarkdown) {
418
+ const items = this.parseObservations(observationMarkdown);
419
+ const routed = [];
420
+ for (const item of items) {
421
+ if (item.priority === "\u{1F7E2}") continue;
422
+ const category = this.categorize(item.content);
423
+ if (!category) continue;
424
+ const routedItem = { category, title: item.title, content: item.content, priority: item.priority, date: item.date };
425
+ routed.push(routedItem);
426
+ this.appendToCategory(category, routedItem);
427
+ }
428
+ const summary = this.buildSummary(routed);
429
+ return { routed, summary };
430
+ }
431
+ parseObservations(markdown) {
432
+ const results = [];
433
+ let currentDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
434
+ for (const line of markdown.split(/\r?\n/)) {
435
+ const dateMatch = line.match(DATE_HEADING_RE3);
436
+ if (dateMatch) {
437
+ currentDate = dateMatch[1];
438
+ continue;
439
+ }
440
+ const obsMatch = line.match(OBSERVATION_LINE_RE3);
441
+ if (!obsMatch) continue;
442
+ const priority = obsMatch[1];
443
+ const content = obsMatch[3].trim();
444
+ const title = content.slice(0, 80).replace(/[^a-zA-Z0-9\s-]/g, "").trim();
445
+ results.push({ priority, content, date: currentDate, title });
446
+ }
447
+ return results;
448
+ }
449
+ categorize(content) {
450
+ for (const { category, patterns } of CATEGORY_PATTERNS) {
451
+ if (patterns.some((p) => p.test(content))) {
452
+ return category;
453
+ }
454
+ }
455
+ return null;
456
+ }
457
+ appendToCategory(category, item) {
458
+ const categoryDir = path.join(this.vaultPath, category);
459
+ fs.mkdirSync(categoryDir, { recursive: true });
460
+ const filePath = path.join(categoryDir, `${item.date}.md`);
461
+ const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8").trim() : "";
462
+ if (existing.includes(item.content)) return;
463
+ const entry = `- ${item.priority} ${item.content}`;
464
+ const header = existing ? "" : `# ${category} \u2014 ${item.date}
465
+ `;
466
+ const newContent = existing ? `${existing}
467
+ ${entry}
468
+ ` : `${header}
469
+ ${entry}
470
+ `;
471
+ fs.writeFileSync(filePath, newContent, "utf-8");
472
+ }
473
+ buildSummary(routed) {
474
+ if (routed.length === 0) return "No items routed to vault categories.";
475
+ const byCat = /* @__PURE__ */ new Map();
476
+ for (const item of routed) {
477
+ byCat.set(item.category, (byCat.get(item.category) ?? 0) + 1);
478
+ }
479
+ const parts = [...byCat.entries()].map(([cat, count]) => `${cat}: ${count}`);
480
+ return `Routed ${routed.length} observations \u2192 ${parts.join(", ")}`;
481
+ }
482
+ };
483
+
484
+ // src/observer/observer.ts
485
+ var Observer = class {
486
+ vaultPath;
487
+ observationsDir;
488
+ tokenThreshold;
489
+ reflectThreshold;
490
+ compressor;
491
+ reflector;
492
+ now;
493
+ router;
494
+ pendingMessages = [];
495
+ observationsCache = "";
496
+ lastRoutingSummary = "";
497
+ constructor(vaultPath, options = {}) {
498
+ this.vaultPath = path2.resolve(vaultPath);
499
+ this.observationsDir = path2.join(this.vaultPath, "observations");
500
+ this.tokenThreshold = options.tokenThreshold ?? 3e4;
501
+ this.reflectThreshold = options.reflectThreshold ?? 4e4;
502
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
503
+ this.compressor = options.compressor ?? new Compressor({ model: options.model, now: this.now });
504
+ this.reflector = options.reflector ?? new Reflector({ now: this.now });
505
+ this.router = new Router(vaultPath);
506
+ fs2.mkdirSync(this.observationsDir, { recursive: true });
507
+ this.observationsCache = this.readTodayObservations();
508
+ }
509
+ async processMessages(messages) {
510
+ const incoming = messages.map((message) => message.trim()).filter(Boolean);
511
+ if (incoming.length === 0) {
512
+ return;
513
+ }
514
+ this.pendingMessages.push(...incoming);
515
+ const buffered = this.pendingMessages.join("\n");
516
+ if (this.estimateTokens(buffered) < this.tokenThreshold) {
517
+ return;
518
+ }
519
+ const todayPath = this.getObservationPath(this.now());
520
+ const existing = this.readObservationFile(todayPath);
521
+ const compressed = (await this.compressor.compress(this.pendingMessages, existing)).trim();
522
+ this.pendingMessages = [];
523
+ if (!compressed) {
524
+ return;
525
+ }
526
+ this.writeObservationFile(todayPath, compressed);
527
+ this.observationsCache = compressed;
528
+ const { summary } = this.router.route(compressed);
529
+ if (summary) {
530
+ this.lastRoutingSummary = summary;
531
+ }
532
+ await this.reflectIfNeeded();
533
+ }
534
+ /**
535
+ * Force-flush pending messages regardless of threshold.
536
+ * Call this on session end to capture everything.
537
+ */
538
+ async flush() {
539
+ if (this.pendingMessages.length === 0) {
540
+ return { observations: this.observationsCache, routingSummary: this.lastRoutingSummary };
541
+ }
542
+ const todayPath = this.getObservationPath(this.now());
543
+ const existing = this.readObservationFile(todayPath);
544
+ const compressed = (await this.compressor.compress(this.pendingMessages, existing)).trim();
545
+ this.pendingMessages = [];
546
+ if (compressed) {
547
+ this.writeObservationFile(todayPath, compressed);
548
+ this.observationsCache = compressed;
549
+ const { summary } = this.router.route(compressed);
550
+ this.lastRoutingSummary = summary;
551
+ await this.reflectIfNeeded();
552
+ }
553
+ return { observations: this.observationsCache, routingSummary: this.lastRoutingSummary };
554
+ }
555
+ getObservations() {
556
+ this.observationsCache = this.readTodayObservations();
557
+ return this.observationsCache;
558
+ }
559
+ estimateTokens(input) {
560
+ return Math.ceil(input.length / 4);
561
+ }
562
+ getObservationPath(date) {
563
+ const datePart = date.toISOString().split("T")[0];
564
+ return path2.join(this.observationsDir, `${datePart}.md`);
565
+ }
566
+ readTodayObservations() {
567
+ const todayPath = this.getObservationPath(this.now());
568
+ return this.readObservationFile(todayPath);
569
+ }
570
+ readObservationFile(filePath) {
571
+ if (!fs2.existsSync(filePath)) {
572
+ return "";
573
+ }
574
+ return fs2.readFileSync(filePath, "utf-8").trim();
575
+ }
576
+ writeObservationFile(filePath, content) {
577
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
578
+ fs2.writeFileSync(filePath, `${content.trim()}
579
+ `, "utf-8");
580
+ }
581
+ getObservationFiles() {
582
+ if (!fs2.existsSync(this.observationsDir)) {
583
+ return [];
584
+ }
585
+ return fs2.readdirSync(this.observationsDir).filter((name) => name.endsWith(".md")).sort((a, b) => a.localeCompare(b)).map((name) => path2.join(this.observationsDir, name));
586
+ }
587
+ readObservationCorpus() {
588
+ const files = this.getObservationFiles();
589
+ if (files.length === 0) {
590
+ return "";
591
+ }
592
+ return files.map((filePath) => this.readObservationFile(filePath)).filter(Boolean).join("\n\n");
593
+ }
594
+ async reflectIfNeeded() {
595
+ const corpus = this.readObservationCorpus();
596
+ if (this.estimateTokens(corpus) < this.reflectThreshold) {
597
+ return;
598
+ }
599
+ for (const filePath of this.getObservationFiles()) {
600
+ const current = this.readObservationFile(filePath);
601
+ if (!current) continue;
602
+ const reflected = this.reflector.reflect(current).trim();
603
+ if (!reflected) {
604
+ fs2.rmSync(filePath, { force: true });
605
+ continue;
606
+ }
607
+ this.writeObservationFile(filePath, reflected);
608
+ }
609
+ this.observationsCache = this.readTodayObservations();
610
+ }
611
+ };
612
+
613
+ // src/observer/watcher.ts
614
+ import * as fs3 from "fs";
615
+ import * as path3 from "path";
616
+ import chokidar from "chokidar";
617
+ var SessionWatcher = class {
618
+ watchPath;
619
+ observer;
620
+ ignoreInitial;
621
+ watcher = null;
622
+ fileOffsets = /* @__PURE__ */ new Map();
623
+ processingQueue = Promise.resolve();
624
+ constructor(watchPath, observer, options = {}) {
625
+ this.watchPath = path3.resolve(watchPath);
626
+ this.observer = observer;
627
+ this.ignoreInitial = options.ignoreInitial ?? false;
628
+ }
629
+ async start() {
630
+ if (!fs3.existsSync(this.watchPath)) {
631
+ throw new Error(`Watch path does not exist: ${this.watchPath}`);
632
+ }
633
+ this.watcher = chokidar.watch(this.watchPath, {
634
+ persistent: true,
635
+ ignoreInitial: this.ignoreInitial,
636
+ awaitWriteFinish: {
637
+ stabilityThreshold: 120,
638
+ pollInterval: 30
639
+ }
640
+ });
641
+ const enqueue = (changedPath) => {
642
+ this.processingQueue = this.processingQueue.then(() => this.consumeFile(changedPath)).catch(() => void 0);
643
+ };
644
+ this.watcher.on("add", enqueue);
645
+ this.watcher.on("change", enqueue);
646
+ this.watcher.on("unlink", (deletedPath) => {
647
+ this.fileOffsets.delete(path3.resolve(deletedPath));
648
+ });
649
+ }
650
+ async stop() {
651
+ await this.watcher?.close();
652
+ this.watcher = null;
653
+ }
654
+ async consumeFile(filePath) {
655
+ const resolved = path3.resolve(filePath);
656
+ if (!fs3.existsSync(resolved)) {
657
+ return;
658
+ }
659
+ const stats = fs3.statSync(resolved);
660
+ if (!stats.isFile()) {
661
+ return;
662
+ }
663
+ const previousOffset = this.fileOffsets.get(resolved) ?? 0;
664
+ const startOffset = stats.size < previousOffset ? 0 : previousOffset;
665
+ if (stats.size <= startOffset) {
666
+ this.fileOffsets.set(resolved, stats.size);
667
+ return;
668
+ }
669
+ const bytesToRead = stats.size - startOffset;
670
+ const buffer = Buffer.alloc(bytesToRead);
671
+ const fd = fs3.openSync(resolved, "r");
672
+ try {
673
+ fs3.readSync(fd, buffer, 0, bytesToRead, startOffset);
674
+ } finally {
675
+ fs3.closeSync(fd);
676
+ }
677
+ this.fileOffsets.set(resolved, stats.size);
678
+ const chunk = buffer.toString("utf-8");
679
+ const messages = chunk.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
680
+ if (messages.length === 0) {
681
+ return;
682
+ }
683
+ await this.observer.processMessages(messages);
684
+ }
685
+ };
686
+
687
+ // src/commands/observe.ts
688
+ var VAULT_CONFIG_FILE = ".clawvault.json";
689
+ function findVaultRoot(startPath) {
690
+ let current = path4.resolve(startPath);
691
+ while (true) {
692
+ if (fs4.existsSync(path4.join(current, VAULT_CONFIG_FILE))) {
693
+ return current;
694
+ }
695
+ const parent = path4.dirname(current);
696
+ if (parent === current) return null;
697
+ current = parent;
698
+ }
699
+ }
700
+ function resolveVaultPath(explicitPath) {
701
+ if (explicitPath) {
702
+ return path4.resolve(explicitPath);
703
+ }
704
+ if (process.env.CLAWVAULT_PATH) {
705
+ return path4.resolve(process.env.CLAWVAULT_PATH);
706
+ }
707
+ const discovered = findVaultRoot(process.cwd());
708
+ if (!discovered) {
709
+ throw new Error("No ClawVault found. Set CLAWVAULT_PATH or use --vault.");
710
+ }
711
+ return discovered;
712
+ }
713
+ function parsePositiveInteger(raw, optionName) {
714
+ const parsed = Number.parseInt(raw, 10);
715
+ if (!Number.isFinite(parsed) || parsed <= 0) {
716
+ throw new Error(`Invalid ${optionName}: ${raw}`);
717
+ }
718
+ return parsed;
719
+ }
720
+ function buildDaemonArgs(options) {
721
+ const cliPath = process.argv[1];
722
+ if (!cliPath) {
723
+ throw new Error("Unable to resolve CLI script path for daemon mode.");
724
+ }
725
+ const args = [cliPath, "observe"];
726
+ if (options.watch) {
727
+ args.push("--watch", options.watch);
728
+ }
729
+ if (options.threshold) {
730
+ args.push("--threshold", String(options.threshold));
731
+ }
732
+ if (options.reflectThreshold) {
733
+ args.push("--reflect-threshold", String(options.reflectThreshold));
734
+ }
735
+ if (options.model) {
736
+ args.push("--model", options.model);
737
+ }
738
+ if (options.vaultPath) {
739
+ args.push("--vault", options.vaultPath);
740
+ }
741
+ return args;
742
+ }
743
+ async function runOneShotCompression(observer, sourceFile, vaultPath) {
744
+ const resolved = path4.resolve(sourceFile);
745
+ if (!fs4.existsSync(resolved) || !fs4.statSync(resolved).isFile()) {
746
+ throw new Error(`Conversation file not found: ${resolved}`);
747
+ }
748
+ const raw = fs4.readFileSync(resolved, "utf-8");
749
+ const messages = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
750
+ await observer.processMessages(messages.length > 0 ? messages : [raw]);
751
+ const { observations, routingSummary } = await observer.flush();
752
+ const datePart = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
753
+ const outputPath = path4.join(vaultPath, "observations", `${datePart}.md`);
754
+ console.log(`Observations updated: ${outputPath}`);
755
+ if (routingSummary) {
756
+ console.log(routingSummary);
757
+ }
758
+ }
759
+ async function watchSessions(observer, watchPath) {
760
+ const watcher = new SessionWatcher(watchPath, observer);
761
+ await watcher.start();
762
+ console.log(`Watching session updates: ${watchPath}`);
763
+ await new Promise((resolve5) => {
764
+ const shutdown = async () => {
765
+ process.off("SIGINT", onSigInt);
766
+ process.off("SIGTERM", onSigTerm);
767
+ await watcher.stop();
768
+ resolve5();
769
+ };
770
+ const onSigInt = () => {
771
+ void shutdown();
772
+ };
773
+ const onSigTerm = () => {
774
+ void shutdown();
775
+ };
776
+ process.once("SIGINT", onSigInt);
777
+ process.once("SIGTERM", onSigTerm);
778
+ });
779
+ }
780
+ async function observeCommand(options) {
781
+ if (options.compress && options.daemon) {
782
+ throw new Error("--compress cannot be combined with --daemon.");
783
+ }
784
+ const vaultPath = resolveVaultPath(options.vaultPath);
785
+ const observer = new Observer(vaultPath, {
786
+ tokenThreshold: options.threshold,
787
+ reflectThreshold: options.reflectThreshold,
788
+ model: options.model
789
+ });
790
+ if (options.compress) {
791
+ await runOneShotCompression(observer, options.compress, vaultPath);
792
+ return;
793
+ }
794
+ let watchPath = options.watch ? path4.resolve(options.watch) : "";
795
+ if (!watchPath && options.daemon) {
796
+ watchPath = path4.join(vaultPath, "sessions");
797
+ }
798
+ if (!watchPath) {
799
+ throw new Error("Either --watch or --compress must be provided.");
800
+ }
801
+ if (!fs4.existsSync(watchPath)) {
802
+ if (options.daemon && !options.watch) {
803
+ fs4.mkdirSync(watchPath, { recursive: true });
804
+ } else {
805
+ throw new Error(`Watch path does not exist: ${watchPath}`);
806
+ }
807
+ }
808
+ if (options.daemon) {
809
+ const daemonArgs = buildDaemonArgs({ ...options, watch: watchPath, vaultPath });
810
+ const child = spawn(process.execPath, daemonArgs, {
811
+ detached: true,
812
+ stdio: "ignore"
813
+ });
814
+ child.unref();
815
+ console.log(`Observer daemon started (pid: ${child.pid})`);
816
+ return;
817
+ }
818
+ await watchSessions(observer, watchPath);
819
+ }
820
+ function registerObserveCommand(program) {
821
+ program.command("observe").description("Observe session files and build observational memory").option("--watch <path>", "Watch session file or directory").option("--threshold <n>", "Compression token threshold", "30000").option("--reflect-threshold <n>", "Reflection token threshold", "40000").option("--model <model>", "LLM model override").option("--compress <file>", "One-shot compression for a conversation file").option("--daemon", "Run in detached background mode").option("-v, --vault <path>", "Vault path").action(async (rawOptions) => {
822
+ await observeCommand({
823
+ watch: rawOptions.watch,
824
+ threshold: parsePositiveInteger(rawOptions.threshold, "threshold"),
825
+ reflectThreshold: parsePositiveInteger(rawOptions.reflectThreshold, "reflect-threshold"),
826
+ model: rawOptions.model,
827
+ compress: rawOptions.compress,
828
+ daemon: rawOptions.daemon,
829
+ vaultPath: rawOptions.vault
830
+ });
831
+ });
832
+ }
833
+
834
+ export {
835
+ Compressor,
836
+ Reflector,
837
+ Observer,
838
+ SessionWatcher,
839
+ observeCommand,
840
+ registerObserveCommand
841
+ };
@@ -1,3 +1,6 @@
1
+ import {
2
+ formatAge
3
+ } from "../chunk-7ZRP733D.js";
1
4
  import {
2
5
  ClawVault,
3
6
  findVault
@@ -9,9 +12,6 @@ import {
9
12
  scanVaultLinks
10
13
  } from "../chunk-4VQTUVH7.js";
11
14
  import "../chunk-J7ZWCI2C.js";
12
- import {
13
- formatAge
14
- } from "../chunk-7ZRP733D.js";
15
15
 
16
16
  // src/commands/doctor.ts
17
17
  import * as fs from "fs";
@@ -0,0 +1,15 @@
1
+ import { Command } from 'commander';
2
+
3
+ interface ObserveCommandOptions {
4
+ watch?: string;
5
+ threshold?: number;
6
+ reflectThreshold?: number;
7
+ model?: string;
8
+ compress?: string;
9
+ daemon?: boolean;
10
+ vaultPath?: string;
11
+ }
12
+ declare function observeCommand(options: ObserveCommandOptions): Promise<void>;
13
+ declare function registerObserveCommand(program: Command): void;
14
+
15
+ export { type ObserveCommandOptions, observeCommand, registerObserveCommand };
@@ -0,0 +1,8 @@
1
+ import {
2
+ observeCommand,
3
+ registerObserveCommand
4
+ } from "../chunk-NSPPVMWC.js";
5
+ export {
6
+ observeCommand,
7
+ registerObserveCommand
8
+ };
@@ -1,3 +1,6 @@
1
+ import {
2
+ clearDirtyFlag
3
+ } from "../chunk-MZZJLQNQ.js";
1
4
  import {
2
5
  autoSyncOnHandoff
3
6
  } from "../chunk-BBPSJL6H.js";
@@ -7,9 +10,6 @@ import {
7
10
  import {
8
11
  qmdUpdate
9
12
  } from "../chunk-MIIXBNO3.js";
10
- import {
11
- clearDirtyFlag
12
- } from "../chunk-MZZJLQNQ.js";
13
13
 
14
14
  // src/commands/sleep.ts
15
15
  import * as fs from "fs";
@@ -1,3 +1,6 @@
1
+ import {
2
+ formatAge
3
+ } from "../chunk-7ZRP733D.js";
1
4
  import {
2
5
  ClawVault
3
6
  } from "../chunk-3HFB7EMU.js";
@@ -9,9 +12,6 @@ import {
9
12
  scanVaultLinks
10
13
  } from "../chunk-4VQTUVH7.js";
11
14
  import "../chunk-J7ZWCI2C.js";
12
- import {
13
- formatAge
14
- } from "../chunk-7ZRP733D.js";
15
15
 
16
16
  // src/commands/status.ts
17
17
  import * as fs from "fs";
@@ -1,7 +1,3 @@
1
- import {
2
- ClawVault
3
- } from "../chunk-3HFB7EMU.js";
4
- import "../chunk-MIIXBNO3.js";
5
1
  import {
6
2
  recover
7
3
  } from "../chunk-MILVYUPK.js";
@@ -9,6 +5,10 @@ import {
9
5
  clearDirtyFlag
10
6
  } from "../chunk-MZZJLQNQ.js";
11
7
  import "../chunk-7ZRP733D.js";
8
+ import {
9
+ ClawVault
10
+ } from "../chunk-3HFB7EMU.js";
11
+ import "../chunk-MIIXBNO3.js";
12
12
 
13
13
  // src/commands/wake.ts
14
14
  import * as path from "path";
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
+ import { Command } from 'commander';
1
2
  import { V as VaultConfig, S as StoreOptions, D as Document, a as SearchOptions, b as SearchResult, c as SyncOptions, d as SyncResult, C as Category, M as MemoryType, H as HandoffDocument, e as SessionRecap } from './types-DMU3SuAV.js';
2
3
  export { f as DEFAULT_CATEGORIES, g as DEFAULT_CONFIG, h as MEMORY_TYPES, T as TYPE_TO_CATEGORY, i as VaultMeta } from './types-DMU3SuAV.js';
3
4
  export { setupCommand } from './commands/setup.js';
4
5
  export { ContextEntry, ContextFormat, ContextOptions, ContextResult, buildContext, contextCommand, formatContextMarkdown } from './commands/context.js';
6
+ export { ObserveCommandOptions, observeCommand, registerObserveCommand } from './commands/observe.js';
5
7
  export { SessionRecapFormat, SessionRecapOptions, SessionRecapResult, SessionTurn, buildSessionRecap, formatSessionRecapMarkdown, sessionRecapCommand } from './commands/session-recap.js';
6
8
  export { TemplateVariables, buildTemplateVariables, renderTemplate } from './lib/template-engine.js';
7
9
 
@@ -261,6 +263,112 @@ declare function extractWikiLinks(content: string): string[];
261
263
  */
262
264
  declare function extractTags(content: string): string[];
263
265
 
266
+ interface ObserverCompressor {
267
+ compress(messages: string[], existingObservations: string): Promise<string>;
268
+ }
269
+ interface ObserverReflector {
270
+ reflect(observations: string): string;
271
+ }
272
+ interface ObserverOptions {
273
+ tokenThreshold?: number;
274
+ reflectThreshold?: number;
275
+ model?: string;
276
+ compressor?: ObserverCompressor;
277
+ reflector?: ObserverReflector;
278
+ now?: () => Date;
279
+ }
280
+ declare class Observer {
281
+ private readonly vaultPath;
282
+ private readonly observationsDir;
283
+ private readonly tokenThreshold;
284
+ private readonly reflectThreshold;
285
+ private readonly compressor;
286
+ private readonly reflector;
287
+ private readonly now;
288
+ private readonly router;
289
+ private pendingMessages;
290
+ private observationsCache;
291
+ private lastRoutingSummary;
292
+ constructor(vaultPath: string, options?: ObserverOptions);
293
+ processMessages(messages: string[]): Promise<void>;
294
+ /**
295
+ * Force-flush pending messages regardless of threshold.
296
+ * Call this on session end to capture everything.
297
+ */
298
+ flush(): Promise<{
299
+ observations: string;
300
+ routingSummary: string;
301
+ }>;
302
+ getObservations(): string;
303
+ private estimateTokens;
304
+ private getObservationPath;
305
+ private readTodayObservations;
306
+ private readObservationFile;
307
+ private writeObservationFile;
308
+ private getObservationFiles;
309
+ private readObservationCorpus;
310
+ private reflectIfNeeded;
311
+ }
312
+
313
+ interface CompressorOptions {
314
+ model?: string;
315
+ now?: () => Date;
316
+ fetchImpl?: typeof fetch;
317
+ }
318
+ declare class Compressor {
319
+ private readonly model?;
320
+ private readonly now;
321
+ private readonly fetchImpl;
322
+ constructor(options?: CompressorOptions);
323
+ compress(messages: string[], existingObservations: string): Promise<string>;
324
+ private resolveProvider;
325
+ private buildPrompt;
326
+ private callAnthropic;
327
+ private callOpenAI;
328
+ private normalizeLlmOutput;
329
+ private fallbackCompression;
330
+ private mergeObservations;
331
+ private parseSections;
332
+ private renderSections;
333
+ private inferPriority;
334
+ private normalizeText;
335
+ private extractDate;
336
+ private extractTime;
337
+ private formatDate;
338
+ private formatTime;
339
+ }
340
+
341
+ interface ReflectorOptions {
342
+ now?: () => Date;
343
+ }
344
+ declare class Reflector {
345
+ private readonly now;
346
+ constructor(options?: ReflectorOptions);
347
+ reflect(observations: string): string;
348
+ private buildCutoffDate;
349
+ private parseDate;
350
+ private parseSections;
351
+ private renderSections;
352
+ private normalizeText;
353
+ private isSimilar;
354
+ }
355
+
356
+ interface SessionWatcherOptions {
357
+ ignoreInitial?: boolean;
358
+ }
359
+ declare class SessionWatcher {
360
+ private readonly watchPath;
361
+ private readonly observer;
362
+ private readonly ignoreInitial;
363
+ private watcher;
364
+ private fileOffsets;
365
+ private processingQueue;
366
+ constructor(watchPath: string, observer: Observer, options?: SessionWatcherOptions);
367
+ start(): Promise<void>;
368
+ stop(): Promise<void>;
369
+ private consumeFile;
370
+ }
371
+
264
372
  /**
265
373
  * ClawVault 🐘 β€” An Elephant Never Forgets
266
374
  *
@@ -288,5 +396,6 @@ declare function extractTags(content: string): string[];
288
396
  */
289
397
 
290
398
  declare const VERSION: string;
399
+ declare function registerCommanderCommands(program: Command): Command;
291
400
 
292
- export { Category, ClawVault, Document, HandoffDocument, MemoryType, QMD_INSTALL_COMMAND, QMD_INSTALL_URL, QmdUnavailableError, SearchEngine, SearchOptions, SearchResult, SessionRecap, StoreOptions, SyncOptions, SyncResult, VERSION, VaultConfig, createVault, extractTags, extractWikiLinks, findVault, hasQmd, qmdEmbed, qmdUpdate };
401
+ export { Category, ClawVault, Compressor, type CompressorOptions, Document, HandoffDocument, MemoryType, Observer, type ObserverCompressor, type ObserverOptions, type ObserverReflector, QMD_INSTALL_COMMAND, QMD_INSTALL_URL, QmdUnavailableError, Reflector, type ReflectorOptions, SearchEngine, SearchOptions, SearchResult, SessionRecap, SessionWatcher, type SessionWatcherOptions, StoreOptions, SyncOptions, SyncResult, VERSION, VaultConfig, createVault, extractTags, extractWikiLinks, findVault, hasQmd, qmdEmbed, qmdUpdate, registerCommanderCommands };
package/dist/index.js CHANGED
@@ -36,6 +36,14 @@ import {
36
36
  qmdEmbed,
37
37
  qmdUpdate
38
38
  } from "./chunk-MIIXBNO3.js";
39
+ import {
40
+ Compressor,
41
+ Observer,
42
+ Reflector,
43
+ SessionWatcher,
44
+ observeCommand,
45
+ registerObserveCommand
46
+ } from "./chunk-NSPPVMWC.js";
39
47
 
40
48
  // src/index.ts
41
49
  import * as fs from "fs";
@@ -49,15 +57,23 @@ function readPackageVersion() {
49
57
  }
50
58
  }
51
59
  var VERSION = readPackageVersion();
60
+ function registerCommanderCommands(program) {
61
+ registerObserveCommand(program);
62
+ return program;
63
+ }
52
64
  export {
53
65
  ClawVault,
66
+ Compressor,
54
67
  DEFAULT_CATEGORIES,
55
68
  DEFAULT_CONFIG,
56
69
  MEMORY_TYPES,
70
+ Observer,
57
71
  QMD_INSTALL_COMMAND,
58
72
  QMD_INSTALL_URL,
59
73
  QmdUnavailableError,
74
+ Reflector,
60
75
  SearchEngine,
76
+ SessionWatcher,
61
77
  TYPE_TO_CATEGORY,
62
78
  VERSION,
63
79
  buildContext,
@@ -71,8 +87,11 @@ export {
71
87
  formatContextMarkdown,
72
88
  formatSessionRecapMarkdown,
73
89
  hasQmd,
90
+ observeCommand,
74
91
  qmdEmbed,
75
92
  qmdUpdate,
93
+ registerCommanderCommands,
94
+ registerObserveCommand,
76
95
  renderTemplate,
77
96
  sessionRecapCommand,
78
97
  setupCommand
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawvault",
3
- "version": "1.8.4",
3
+ "version": "1.9.0",
4
4
  "description": "ClawVaultβ„’ - 🐘 An elephant never forgets. Structured memory for OpenClaw agents. Context death resilience, Obsidian-compatible markdown, local semantic search.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -29,7 +29,7 @@
29
29
  ]
30
30
  },
31
31
  "scripts": {
32
- "build": "tsup src/index.ts src/commands/entities.ts src/commands/link.ts src/commands/checkpoint.ts src/commands/recover.ts src/commands/status.ts src/commands/template.ts src/commands/setup.ts src/commands/context.ts src/commands/session-recap.ts src/commands/wake.ts src/commands/sleep.ts src/commands/doctor.ts src/commands/shell-init.ts src/commands/repair-session.ts src/commands/cloud.ts src/lib/entity-index.ts src/lib/auto-linker.ts src/lib/config.ts src/lib/template-engine.ts src/lib/session-utils.ts src/lib/session-repair.ts --format esm --dts --clean",
32
+ "build": "tsup src/index.ts src/commands/entities.ts src/commands/link.ts src/commands/checkpoint.ts src/commands/recover.ts src/commands/status.ts src/commands/template.ts src/commands/setup.ts src/commands/context.ts src/commands/observe.ts src/commands/session-recap.ts src/commands/wake.ts src/commands/sleep.ts src/commands/doctor.ts src/commands/shell-init.ts src/commands/repair-session.ts src/commands/cloud.ts src/lib/entity-index.ts src/lib/auto-linker.ts src/lib/config.ts src/lib/template-engine.ts src/lib/session-utils.ts src/lib/session-repair.ts --format esm --dts --clean",
33
33
  "dev": "tsup src/index.ts src/commands/*.ts src/lib/*.ts --format esm --dts --watch",
34
34
  "lint": "eslint src",
35
35
  "typecheck": "tsc --noEmit",