clawvault 1.11.0 → 1.11.2

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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Observer,
3
3
  parseSessionFile
4
- } from "./chunk-RPMQZZPE.js";
4
+ } from "./chunk-KOF3HYSL.js";
5
5
 
6
6
  // src/commands/observe.ts
7
7
  import * as fs2 from "fs";
@@ -34,6 +34,7 @@ var Compressor = class {
34
34
  return this.mergeObservations(existingObservations, fallback);
35
35
  }
36
36
  resolveProvider() {
37
+ if (process.env.CLAWVAULT_NO_LLM) return null;
37
38
  if (process.env.ANTHROPIC_API_KEY) {
38
39
  return "anthropic";
39
40
  }
@@ -47,21 +48,33 @@ var Compressor = class {
47
48
  }
48
49
  buildPrompt(messages, existingObservations) {
49
50
  return [
50
- "You are an observer that compresses raw AI session messages into durable observations.",
51
+ "You are an observer that compresses raw AI session messages into durable, human-meaningful observations.",
51
52
  "",
52
53
  "Rules:",
53
54
  "- Output markdown only.",
54
55
  "- Group observations by date heading: ## YYYY-MM-DD",
55
56
  "- Each line must follow: <emoji> <HH:MM> <observation>",
56
57
  "- Priority emojis: \u{1F534} critical, \u{1F7E1} notable, \u{1F7E2} info",
57
- "- \u{1F534} for: decisions between alternatives, errors/failures, blockers, deadlines with explicit dates, breaking changes",
58
- "- \u{1F7E1} for: preferences, architecture discussions, trade-offs, milestones, people interactions, notable context, routine deadlines",
59
- "- \u{1F7E2} for: routine updates, deployments, builds, general info",
60
- "- Preferences are \u{1F7E1} unless they indicate a breaking decision between alternatives.",
61
- "- Each distinct error type or failure must be its own observation line; do not merge different errors.",
62
- "- If multiple different errors occurred, list each separately with its specific error message.",
63
- "- Keep observations concise and factual.",
64
- "- Avoid duplicates when possible.",
58
+ "- \u{1F534} for: decisions between alternatives, blockers, deadlines with explicit dates, breaking changes, commitments made to people",
59
+ "- \u{1F7E1} for: preferences, architecture discussions, trade-offs, milestones, people interactions, notable context",
60
+ "- \u{1F7E2} for: completed tasks, deployments, builds, general progress",
61
+ "",
62
+ "QUALITY FILTERS (important):",
63
+ "- DO NOT observe: CLI errors, command failures, tool output parsing issues, retry attempts, debug logs.",
64
+ " These are transient noise, not memories. Only observe errors if they represent a BLOCKER or an unresolved problem.",
65
+ '- DO NOT observe: "acknowledged the conversation", "said okay", routine confirmations.',
66
+ '- MERGE related events into single observations. If 5 images were generated, say "Generated 5 images for X" not 5 separate lines.',
67
+ '- MERGE retry sequences: "Tried X, failed, tried Y, succeeded" \u2192 "Resolved X using Y (after initial failure)"',
68
+ '- Prefer OUTCOMES over PROCESSES: "Deployed v1.2 to Railway" not "Started deploy... build finished... deploy succeeded"',
69
+ "",
70
+ "AGENT ATTRIBUTION:",
71
+ '- If the transcript shows multiple speakers/agents, prefix observations with who did it: "Pedro asked...", "Clawdious deployed...", "Zeca generated..."',
72
+ "- If only one agent is acting, attribution is optional.",
73
+ "",
74
+ "COMMITMENT FORMAT (when someone promises/agrees to something):",
75
+ '- Use: "\u{1F534} HH:MM [COMMITMENT] <who> committed to <what> by <when>" (include deadline if mentioned)',
76
+ "",
77
+ "Keep observations concise and factual. Aim for signal, not completeness.",
65
78
  "",
66
79
  "Existing observations (may be empty):",
67
80
  existingObservations.trim() || "(none)",
@@ -537,10 +550,57 @@ var Router = class {
537
550
  normalizeForDedup(content) {
538
551
  return content.replace(/^\d{2}:\d{2}\s+/, "").replace(/\[\[[^\]]*\]\]/g, (m) => m.replace(/\[\[|\]\]/g, "")).replace(/\s+/g, " ").trim().toLowerCase();
539
552
  }
540
- appendToCategory(category, item) {
553
+ /**
554
+ * Extract entity slug from observation content for people/projects routing.
555
+ * Returns null if no entity can be identified.
556
+ */
557
+ extractEntitySlug(content, category) {
558
+ if (category !== "people" && category !== "projects") return null;
559
+ if (category === "people") {
560
+ const patterns = [
561
+ /(?:talked to|met with|spoke with|chatted with|discussed with|emailed|called|messaged)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)/,
562
+ /([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\s+(?:said|asked|told|mentioned|from|at)\b/,
563
+ /\b(?:client|partner|colleague|contact)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)/i
564
+ ];
565
+ for (const pattern of patterns) {
566
+ const match = content.match(pattern);
567
+ if (match?.[1]) return this.toSlug(match[1]);
568
+ }
569
+ }
570
+ if (category === "projects") {
571
+ const patterns = [
572
+ /(?:deployed|shipped|launched|released|built|created|working on)\s+([A-Z][a-zA-Z0-9-]+)/,
573
+ /"([^"]+)"\s+(?:project|repo|service)/i
574
+ ];
575
+ for (const pattern of patterns) {
576
+ const match = content.match(pattern);
577
+ if (match?.[1]) return this.toSlug(match[1]);
578
+ }
579
+ }
580
+ return null;
581
+ }
582
+ toSlug(name) {
583
+ return name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
584
+ }
585
+ /**
586
+ * Resolve the file path for a routed item.
587
+ * For people/projects: entity-slug subfolder with date file (e.g., people/pedro/2026-02-12.md)
588
+ * For other categories: category/date.md
589
+ */
590
+ resolveFilePath(category, item) {
591
+ const entitySlug = this.extractEntitySlug(item.content, category);
592
+ if (entitySlug) {
593
+ const entityDir = path.join(this.vaultPath, category, entitySlug);
594
+ fs.mkdirSync(entityDir, { recursive: true });
595
+ return path.join(entityDir, `${item.date}.md`);
596
+ }
541
597
  const categoryDir = path.join(this.vaultPath, category);
542
598
  fs.mkdirSync(categoryDir, { recursive: true });
543
- const filePath = path.join(categoryDir, `${item.date}.md`);
599
+ return path.join(categoryDir, `${item.date}.md`);
600
+ }
601
+ appendToCategory(category, item) {
602
+ const filePath = this.resolveFilePath(category, item);
603
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
544
604
  const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8").trim() : "";
545
605
  const normalizedNew = this.normalizeForDedup(item.content);
546
606
  const existingLines = existing.split(/\r?\n/);
@@ -559,7 +619,9 @@ var Router = class {
559
619
  }
560
620
  const linkedContent = this.addWikiLinks(item.content);
561
621
  const entry = `- ${item.priority} ${linkedContent}`;
562
- const header = existing ? "" : `# ${category} \u2014 ${item.date}
622
+ const entitySlug = this.extractEntitySlug(item.content, category);
623
+ const headerLabel = entitySlug ? `${category}/${entitySlug}` : category;
624
+ const header = existing ? "" : `# ${headerLabel} \u2014 ${item.date}
563
625
  `;
564
626
  const newContent = existing ? `${existing}
565
627
  ${entry}
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  observeCommand,
3
3
  registerObserveCommand
4
- } from "../chunk-5VYRT2XZ.js";
5
- import "../chunk-RPMQZZPE.js";
4
+ } from "../chunk-GZOBCUVO.js";
5
+ import "../chunk-KOF3HYSL.js";
6
6
  export {
7
7
  observeCommand,
8
8
  registerObserveCommand
@@ -7,7 +7,7 @@ import {
7
7
  import {
8
8
  Observer,
9
9
  parseSessionFile
10
- } from "../chunk-RPMQZZPE.js";
10
+ } from "../chunk-KOF3HYSL.js";
11
11
  import {
12
12
  clearDirtyFlag
13
13
  } from "../chunk-MZZJLQNQ.js";
@@ -6,6 +6,8 @@ interface WakeOptions {
6
6
  vaultPath: string;
7
7
  handoffLimit?: number;
8
8
  brief?: boolean;
9
+ /** Skip LLM executive summary generation (useful for tests/offline) */
10
+ noSummary?: boolean;
9
11
  }
10
12
  interface WakeResult {
11
13
  recovery: RecoveryInfo;
@@ -43,22 +43,35 @@ function formatDateKey(date) {
43
43
  }
44
44
  function readRecentObservationHighlights(vaultPath) {
45
45
  const now = /* @__PURE__ */ new Date();
46
- const yesterday = new Date(now);
47
- yesterday.setDate(now.getDate() - 1);
48
- const dateKeys = [formatDateKey(now), formatDateKey(yesterday)];
49
46
  const highlights = [];
50
- for (const dateKey of dateKeys) {
47
+ for (let daysAgo = 0; daysAgo < 7; daysAgo++) {
48
+ const date = new Date(now);
49
+ date.setDate(now.getDate() - daysAgo);
50
+ const dateKey = formatDateKey(date);
51
51
  const filePath = path.join(vaultPath, "observations", `${dateKey}.md`);
52
52
  if (!fs.existsSync(filePath)) continue;
53
53
  const content = fs.readFileSync(filePath, "utf-8");
54
+ const dayRed = [];
55
+ const dayYellow = [];
54
56
  for (const line of content.split(/\r?\n/)) {
55
57
  const match = line.trim().match(OBSERVATION_HIGHLIGHT_RE);
56
58
  if (!match?.[2]) continue;
57
- highlights.push({
59
+ const item = {
58
60
  date: dateKey,
59
61
  priority: match[1],
60
62
  text: match[2].trim()
61
- });
63
+ };
64
+ if (item.priority === "\u{1F534}") dayRed.push(item);
65
+ else dayYellow.push(item);
66
+ }
67
+ if (daysAgo === 0) {
68
+ highlights.push(...dayRed, ...dayYellow);
69
+ } else if (daysAgo === 1) {
70
+ highlights.push(...dayRed, ...dayYellow.slice(0, 5));
71
+ } else if (daysAgo <= 3) {
72
+ highlights.push(...dayRed);
73
+ } else {
74
+ highlights.push(...dayRed.slice(0, 3));
62
75
  }
63
76
  }
64
77
  return highlights;
@@ -113,6 +126,56 @@ function formatRecentObservations(highlights) {
113
126
  }
114
127
  return lines.join("\n").trim();
115
128
  }
129
+ async function generateExecutiveSummary(recovery, recap, highlights) {
130
+ if (process.env.CLAWVAULT_NO_LLM || process.env.VITEST) return null;
131
+ const apiKey = process.env.GEMINI_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY;
132
+ if (!apiKey) return null;
133
+ const redItems = highlights.filter((h) => h.priority === "\u{1F534}").map((h) => h.text).slice(0, 10);
134
+ const yellowItems = highlights.filter((h) => h.priority === "\u{1F7E1}").map((h) => h.text).slice(0, 5);
135
+ const projects = recap.activeProjects.slice(0, 8);
136
+ const commitments = recap.pendingCommitments.slice(0, 5);
137
+ const lastWork = recovery.checkpoint?.workingOn || recap.recentHandoffs[0]?.workingOn?.join(", ") || "";
138
+ const blockers = recovery.checkpoint?.blocked || recap.recentHandoffs[0]?.blocked?.join(", ") || "";
139
+ const nextSteps = recap.recentHandoffs[0]?.nextSteps?.join(", ") || "";
140
+ const prompt = [
141
+ "You are a chief of staff briefing an AI agent waking up for a new session.",
142
+ "Write a 3-5 sentence executive summary answering: What matters RIGHT NOW?",
143
+ "Be direct and specific. No headers, no bullets \u2014 just a tight paragraph.",
144
+ "Mention the most urgent item first. Include deadlines if any.",
145
+ "",
146
+ `Last working on: ${lastWork || "(unknown)"}`,
147
+ `Blockers: ${blockers || "(none)"}`,
148
+ `Next steps: ${nextSteps || "(none)"}`,
149
+ `Active projects (${projects.length}): ${projects.join(", ") || "(none)"}`,
150
+ `Pending commitments: ${commitments.join(", ") || "(none)"}`,
151
+ `Critical observations: ${redItems.join(" | ") || "(none)"}`,
152
+ `Notable observations: ${yellowItems.join(" | ") || "(none)"}`,
153
+ "",
154
+ "Write the briefing now. Be concise."
155
+ ].join("\n");
156
+ try {
157
+ if (process.env.GEMINI_API_KEY) {
158
+ const model = "gemini-2.0-flash";
159
+ const resp = await fetch(
160
+ `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${process.env.GEMINI_API_KEY}`,
161
+ {
162
+ method: "POST",
163
+ headers: { "content-type": "application/json" },
164
+ body: JSON.stringify({
165
+ contents: [{ parts: [{ text: prompt }] }],
166
+ generationConfig: { temperature: 0.3, maxOutputTokens: 300 }
167
+ })
168
+ }
169
+ );
170
+ if (!resp.ok) return null;
171
+ const data = await resp.json();
172
+ return data.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || null;
173
+ }
174
+ } catch {
175
+ return null;
176
+ }
177
+ return null;
178
+ }
116
179
  async function wake(options) {
117
180
  const vaultPath = path.resolve(options.vaultPath);
118
181
  const recovery = await recover(vaultPath, { clearFlag: true });
@@ -125,12 +188,19 @@ async function wake(options) {
125
188
  });
126
189
  const highlights = readRecentObservationHighlights(vaultPath);
127
190
  const observations = formatRecentObservations(highlights);
191
+ const execSummary = options.noSummary ? null : await generateExecutiveSummary(recovery, recap, highlights);
128
192
  const highlightSummaryItems = highlights.map((item) => `${item.priority} ${item.text}`);
129
193
  const wakeSummary = formatSummaryItems(highlightSummaryItems);
130
194
  const baseSummary = buildWakeSummary(recovery, recap);
131
- const summary = wakeSummary ? `${baseSummary} | ${wakeSummary}` : baseSummary;
195
+ const fullBaseSummary = wakeSummary ? `${baseSummary} | ${wakeSummary}` : baseSummary;
196
+ const summary = execSummary || fullBaseSummary;
132
197
  const baseRecapMarkdown = vault.formatRecap(recap, { brief: options.brief ?? true }).trimEnd();
133
- const recapMarkdown = `${baseRecapMarkdown}
198
+ const execSection = execSummary ? `## \u{1F4CB} Executive Summary
199
+
200
+ ${execSummary}
201
+
202
+ ` : "";
203
+ const recapMarkdown = `${execSection}${baseRecapMarkdown}
134
204
 
135
205
  ## Recent Observations
136
206
  ${observations}`;
package/dist/index.js CHANGED
@@ -41,13 +41,13 @@ import {
41
41
  SessionWatcher,
42
42
  observeCommand,
43
43
  registerObserveCommand
44
- } from "./chunk-5VYRT2XZ.js";
44
+ } from "./chunk-GZOBCUVO.js";
45
45
  import {
46
46
  Compressor,
47
47
  Observer,
48
48
  Reflector,
49
49
  parseSessionFile
50
- } from "./chunk-RPMQZZPE.js";
50
+ } from "./chunk-KOF3HYSL.js";
51
51
 
52
52
  // src/index.ts
53
53
  import * as fs from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawvault",
3
- "version": "1.11.0",
3
+ "version": "1.11.2",
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",