clawvault 2.6.0 → 3.0.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 (232) hide show
  1. package/bin/command-registration.test.js +1 -3
  2. package/bin/register-core-commands.js +10 -23
  3. package/bin/register-maintenance-commands.js +3 -20
  4. package/bin/register-query-commands.js +23 -0
  5. package/bin/register-task-commands.js +1 -18
  6. package/bin/register-task-commands.test.js +0 -16
  7. package/bin/register-vault-operations-commands.js +1 -29
  8. package/dist/{chunk-QVMXF7FY.js → chunk-3D6BCTP6.js} +39 -1
  9. package/dist/{chunk-R2MIW5G7.js → chunk-3DHXQHYG.js} +1 -1
  10. package/dist/{chunk-Q2J5YTUF.js → chunk-3NSBOUT3.js} +73 -36
  11. package/dist/chunk-3RG5ZIWI.js +10 -0
  12. package/dist/{chunk-AZYOKJYC.js → chunk-62YTUT6J.js} +2 -2
  13. package/dist/chunk-6U6MK36V.js +205 -0
  14. package/dist/{chunk-4QYGFWRM.js → chunk-7R7O6STJ.js} +4 -4
  15. package/dist/{chunk-VXEOHTSL.js → chunk-C7OK5WKP.js} +4 -4
  16. package/dist/chunk-CMB7UL7C.js +327 -0
  17. package/dist/chunk-DEFFDRVP.js +938 -0
  18. package/dist/{chunk-K3CDT7IH.js → chunk-E7MFQB6D.js} +61 -20
  19. package/dist/{chunk-ME37YNW3.js → chunk-F2JEUD4J.js} +6 -4
  20. package/dist/chunk-GAJV4IGR.js +82 -0
  21. package/dist/chunk-GQSLDZTS.js +560 -0
  22. package/dist/{chunk-4OXMU5S2.js → chunk-GUKMRGM7.js} +1 -1
  23. package/dist/{chunk-YOSEUUNB.js → chunk-H34S76MB.js} +6 -6
  24. package/dist/{chunk-4TE4JMLA.js → chunk-JY6FYXIT.js} +10 -5
  25. package/dist/chunk-K234IDRJ.js +1073 -0
  26. package/dist/{chunk-IEVLHNLU.js → chunk-LNJA2UGL.js} +86 -9
  27. package/dist/{chunk-MFAWT5O5.js → chunk-LYHGEHXG.js} +1 -0
  28. package/dist/chunk-MFM6K7PU.js +374 -0
  29. package/dist/{chunk-QWQ3TIKS.js → chunk-N2AXRYLC.js} +1 -1
  30. package/dist/chunk-PAH27GSN.js +108 -0
  31. package/dist/{chunk-OIWVQYQF.js → chunk-QBLMXKF2.js} +1 -1
  32. package/dist/{chunk-FHFUXL6G.js → chunk-QK3UCXWL.js} +2 -2
  33. package/dist/{chunk-2YDBJS7M.js → chunk-SJSFRIYS.js} +1 -1
  34. package/dist/{chunk-GSD4ALSI.js → chunk-U55BGUAU.js} +2 -2
  35. package/dist/{chunk-PBEE567J.js → chunk-VGLOTGAS.js} +1 -1
  36. package/dist/{chunk-F55HGNU4.js → chunk-WAZ3NLWL.js} +47 -0
  37. package/dist/{chunk-KL4NAOMO.js → chunk-WGRQ6HDV.js} +1 -1
  38. package/dist/{chunk-UEOUADMO.js → chunk-YKTA5JOJ.js} +13 -10
  39. package/dist/{chunk-XAVB4GB4.js → chunk-ZVVFWOLW.js} +4 -4
  40. package/dist/cli/index.cjs +10033 -0
  41. package/dist/cli/index.d.cts +5 -0
  42. package/dist/cli/index.js +20 -18
  43. package/dist/commands/archive.cjs +287 -0
  44. package/dist/commands/archive.d.cts +11 -0
  45. package/dist/commands/archive.js +1 -0
  46. package/dist/commands/backlog.cjs +721 -0
  47. package/dist/commands/backlog.d.cts +53 -0
  48. package/dist/commands/backlog.js +3 -2
  49. package/dist/commands/blocked.cjs +204 -0
  50. package/dist/commands/blocked.d.cts +26 -0
  51. package/dist/commands/blocked.js +3 -2
  52. package/dist/commands/checkpoint.cjs +244 -0
  53. package/dist/commands/checkpoint.d.cts +41 -0
  54. package/dist/commands/checkpoint.js +2 -1
  55. package/dist/commands/compat.cjs +369 -0
  56. package/dist/commands/compat.d.cts +28 -0
  57. package/dist/commands/compat.js +2 -1
  58. package/dist/commands/context.cjs +2989 -0
  59. package/dist/commands/context.d.cts +2 -0
  60. package/dist/commands/context.js +5 -4
  61. package/dist/commands/doctor.cjs +3062 -0
  62. package/dist/commands/doctor.d.cts +21 -0
  63. package/dist/commands/doctor.d.ts +6 -1
  64. package/dist/commands/doctor.js +13 -11
  65. package/dist/commands/embed.cjs +232 -0
  66. package/dist/commands/embed.d.cts +17 -0
  67. package/dist/commands/embed.js +5 -2
  68. package/dist/commands/entities.cjs +141 -0
  69. package/dist/commands/entities.d.cts +7 -0
  70. package/dist/commands/entities.js +1 -0
  71. package/dist/commands/graph.cjs +501 -0
  72. package/dist/commands/graph.d.cts +21 -0
  73. package/dist/commands/graph.js +1 -0
  74. package/dist/commands/inject.cjs +1636 -0
  75. package/dist/commands/inject.d.cts +2 -0
  76. package/dist/commands/inject.d.ts +1 -1
  77. package/dist/commands/inject.js +4 -2
  78. package/dist/commands/kanban.cjs +884 -0
  79. package/dist/commands/kanban.d.cts +63 -0
  80. package/dist/commands/kanban.js +4 -3
  81. package/dist/commands/link.cjs +965 -0
  82. package/dist/commands/link.d.cts +11 -0
  83. package/dist/commands/link.js +1 -0
  84. package/dist/commands/migrate-observations.cjs +362 -0
  85. package/dist/commands/migrate-observations.d.cts +19 -0
  86. package/dist/commands/migrate-observations.js +3 -2
  87. package/dist/commands/observe.cjs +4099 -0
  88. package/dist/commands/observe.d.cts +23 -0
  89. package/dist/commands/observe.d.ts +1 -0
  90. package/dist/commands/observe.js +11 -9
  91. package/dist/commands/project.cjs +1341 -0
  92. package/dist/commands/project.d.cts +85 -0
  93. package/dist/commands/project.js +5 -4
  94. package/dist/commands/rebuild.cjs +3136 -0
  95. package/dist/commands/rebuild.d.cts +11 -0
  96. package/dist/commands/rebuild.js +10 -8
  97. package/dist/commands/recover.cjs +361 -0
  98. package/dist/commands/recover.d.cts +38 -0
  99. package/dist/commands/recover.js +3 -2
  100. package/dist/commands/reflect.cjs +1008 -0
  101. package/dist/commands/reflect.d.cts +11 -0
  102. package/dist/commands/reflect.js +6 -4
  103. package/dist/commands/repair-session.cjs +457 -0
  104. package/dist/commands/repair-session.d.cts +38 -0
  105. package/dist/commands/repair-session.js +1 -0
  106. package/dist/commands/replay.cjs +4103 -0
  107. package/dist/commands/replay.d.cts +16 -0
  108. package/dist/commands/replay.js +12 -10
  109. package/dist/commands/session-recap.cjs +353 -0
  110. package/dist/commands/session-recap.d.cts +27 -0
  111. package/dist/commands/session-recap.js +1 -0
  112. package/dist/commands/setup.cjs +1345 -0
  113. package/dist/commands/setup.d.cts +100 -0
  114. package/dist/commands/setup.d.ts +90 -2
  115. package/dist/commands/setup.js +21 -2
  116. package/dist/commands/shell-init.cjs +75 -0
  117. package/dist/commands/shell-init.d.cts +7 -0
  118. package/dist/commands/shell-init.js +2 -0
  119. package/dist/commands/sleep.cjs +6028 -0
  120. package/dist/commands/sleep.d.cts +36 -0
  121. package/dist/commands/sleep.d.ts +1 -1
  122. package/dist/commands/sleep.js +17 -15
  123. package/dist/commands/status.cjs +2736 -0
  124. package/dist/commands/status.d.cts +52 -0
  125. package/dist/commands/status.js +12 -10
  126. package/dist/commands/tailscale.cjs +1532 -0
  127. package/dist/commands/tailscale.d.cts +52 -0
  128. package/dist/commands/tailscale.js +1 -0
  129. package/dist/commands/task.cjs +1236 -0
  130. package/dist/commands/task.d.cts +97 -0
  131. package/dist/commands/task.js +4 -3
  132. package/dist/commands/template.cjs +457 -0
  133. package/dist/commands/template.d.cts +36 -0
  134. package/dist/commands/template.js +2 -1
  135. package/dist/commands/wake.cjs +2626 -0
  136. package/dist/commands/wake.d.cts +22 -0
  137. package/dist/commands/wake.d.ts +1 -1
  138. package/dist/commands/wake.js +12 -11
  139. package/dist/context-BUGaWpyL.d.cts +46 -0
  140. package/dist/index.cjs +14526 -0
  141. package/dist/index.d.cts +858 -0
  142. package/dist/index.d.ts +192 -7
  143. package/dist/index.js +101 -75
  144. package/dist/{inject-x65KXWPk.d.ts → inject-Bzi5E-By.d.cts} +1 -1
  145. package/dist/inject-Bzi5E-By.d.ts +137 -0
  146. package/dist/lib/auto-linker.cjs +176 -0
  147. package/dist/lib/auto-linker.d.cts +26 -0
  148. package/dist/lib/auto-linker.js +1 -0
  149. package/dist/lib/canvas-layout.cjs +136 -0
  150. package/dist/lib/canvas-layout.d.cts +31 -0
  151. package/dist/lib/canvas-layout.d.ts +16 -100
  152. package/dist/lib/canvas-layout.js +78 -20
  153. package/dist/lib/config.cjs +78 -0
  154. package/dist/lib/config.d.cts +11 -0
  155. package/dist/lib/config.js +1 -0
  156. package/dist/lib/entity-index.cjs +84 -0
  157. package/dist/lib/entity-index.d.cts +26 -0
  158. package/dist/lib/entity-index.js +1 -0
  159. package/dist/lib/project-utils.cjs +864 -0
  160. package/dist/lib/project-utils.d.cts +97 -0
  161. package/dist/lib/project-utils.js +4 -3
  162. package/dist/lib/session-repair.cjs +239 -0
  163. package/dist/lib/session-repair.d.cts +110 -0
  164. package/dist/lib/session-repair.js +1 -0
  165. package/dist/lib/session-utils.cjs +209 -0
  166. package/dist/lib/session-utils.d.cts +63 -0
  167. package/dist/lib/session-utils.js +1 -0
  168. package/dist/lib/tailscale.cjs +1183 -0
  169. package/dist/lib/tailscale.d.cts +225 -0
  170. package/dist/lib/tailscale.js +1 -0
  171. package/dist/lib/task-utils.cjs +1137 -0
  172. package/dist/lib/task-utils.d.cts +208 -0
  173. package/dist/lib/task-utils.js +3 -2
  174. package/dist/lib/template-engine.cjs +47 -0
  175. package/dist/lib/template-engine.d.cts +11 -0
  176. package/dist/lib/template-engine.js +1 -0
  177. package/dist/lib/webdav.cjs +568 -0
  178. package/dist/lib/webdav.d.cts +109 -0
  179. package/dist/lib/webdav.js +1 -0
  180. package/dist/plugin/index.cjs +1907 -0
  181. package/dist/plugin/index.d.cts +36 -0
  182. package/dist/plugin/index.d.ts +36 -0
  183. package/dist/plugin/index.js +572 -0
  184. package/dist/plugin/inject.cjs +356 -0
  185. package/dist/plugin/inject.d.cts +54 -0
  186. package/dist/plugin/inject.d.ts +54 -0
  187. package/dist/plugin/inject.js +17 -0
  188. package/dist/plugin/observe.cjs +631 -0
  189. package/dist/plugin/observe.d.cts +39 -0
  190. package/dist/plugin/observe.d.ts +39 -0
  191. package/dist/plugin/observe.js +18 -0
  192. package/dist/plugin/templates.cjs +593 -0
  193. package/dist/plugin/templates.d.cts +52 -0
  194. package/dist/plugin/templates.d.ts +52 -0
  195. package/dist/plugin/templates.js +25 -0
  196. package/dist/plugin/types.cjs +18 -0
  197. package/dist/plugin/types.d.cts +209 -0
  198. package/dist/plugin/types.d.ts +209 -0
  199. package/dist/plugin/types.js +0 -0
  200. package/dist/plugin/vault.cjs +927 -0
  201. package/dist/plugin/vault.d.cts +68 -0
  202. package/dist/plugin/vault.d.ts +68 -0
  203. package/dist/plugin/vault.js +22 -0
  204. package/dist/{types-C74wgGL1.d.ts → types-Y2_Um2Ls.d.cts} +44 -1
  205. package/dist/types-Y2_Um2Ls.d.ts +205 -0
  206. package/hooks/clawvault/handler.js +70 -7
  207. package/hooks/clawvault/handler.test.js +91 -0
  208. package/openclaw.plugin.json +56 -0
  209. package/package.json +17 -7
  210. package/templates/memory-event.md +67 -0
  211. package/templates/party.md +63 -0
  212. package/templates/primitive-registry.yaml +551 -0
  213. package/templates/run.md +68 -0
  214. package/templates/trigger.md +68 -0
  215. package/templates/workspace.md +50 -0
  216. package/dashboard/lib/graph-diff.js +0 -104
  217. package/dashboard/lib/graph-diff.test.js +0 -75
  218. package/dashboard/lib/vault-parser.js +0 -556
  219. package/dashboard/lib/vault-parser.test.js +0 -254
  220. package/dashboard/public/app.js +0 -796
  221. package/dashboard/public/index.html +0 -52
  222. package/dashboard/public/styles.css +0 -221
  223. package/dashboard/server.js +0 -374
  224. package/dist/chunk-HA5M6KJB.js +0 -33
  225. package/dist/chunk-MAKNAHAW.js +0 -375
  226. package/dist/chunk-MDIH26GC.js +0 -183
  227. package/dist/chunk-MGDEINGP.js +0 -99
  228. package/dist/chunk-RVYA52PY.js +0 -363
  229. package/dist/commands/canvas.d.ts +0 -15
  230. package/dist/commands/canvas.js +0 -199
  231. package/dist/commands/sync-bd.d.ts +0 -10
  232. package/dist/commands/sync-bd.js +0 -9
@@ -0,0 +1,4099 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/commands/observe.ts
31
+ var observe_exports = {};
32
+ __export(observe_exports, {
33
+ observeCommand: () => observeCommand,
34
+ registerObserveCommand: () => registerObserveCommand
35
+ });
36
+ module.exports = __toCommonJS(observe_exports);
37
+ var fs12 = __toESM(require("fs"), 1);
38
+ var path13 = __toESM(require("path"), 1);
39
+ var import_child_process2 = require("child_process");
40
+
41
+ // src/observer/observer.ts
42
+ var fs7 = __toESM(require("fs"), 1);
43
+ var path7 = __toESM(require("path"), 1);
44
+
45
+ // src/lib/observation-format.ts
46
+ var DATE_HEADING_RE = /^##\s+(\d{4}-\d{2}-\d{2})\s*$/;
47
+ var SCORED_LINE_RE = /^(?:-\s*)?\[(decision|preference|fact|commitment|task|todo|commitment-unresolved|milestone|lesson|relationship|project)\|c=(0(?:\.\d+)?|1(?:\.0+)?)\|i=(0(?:\.\d+)?|1(?:\.0+)?)\]\s+(.+)$/i;
48
+ var EMOJI_LINE_RE = /^(?:-\s*)?(🔴|🟡|🟢)\s+(\d{2}:\d{2})?\s*(.+)$/u;
49
+ var DECISION_RE = /\b(decis(?:ion|ions)?|decid(?:e|ed|ing)|chose|selected|opted|went with|picked)\b/i;
50
+ var PREFERENCE_RE = /\b(prefer(?:ence|s|red)?|likes?|dislikes?|default to|always use|never use|enjoys?|loves?|favou?rite|fan of|interested in|go-to|tend(?:s)? to use|passionate about|hobby|hobbies|(?:I|my|our)\s+(?:own|have|use|got|bought|drive|wear|eat|drink|cook|play|watch|read|listen)|(?:I(?:'m| am))\s+(?:a |an |into |allergic|vegetarian|vegan|gluten|lactose|trying to|learning)|usually|every (?:morning|evening|night|day|week)|routine)\b/i;
51
+ var COMMITMENT_RE = /\b(commit(?:ment|ted)?|promised|deadline|due|scheduled|will deliver|agreed to)\b/i;
52
+ var TODO_RE = /(?:\btodo:\s*|\bwe need to\b|\bdon't forget(?: to)?\b|\bremember to\b|\bmake sure to\b)/i;
53
+ var COMMITMENT_TASK_RE = /\b(?:i'?ll|i will|let me|(?:i'?m\s+)?going to|plan to|should)\b/i;
54
+ var UNRESOLVED_RE = /\b(?:need to figure out|tbd|to be determined)\b/i;
55
+ var DEADLINE_RE = /\b(?:by\s+(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday|tomorrow)|before\s+the\s+\w+|deadline is)\b/i;
56
+ var MILESTONE_RE = /\b(released?|shipped|launched|merged|published|milestone|v\d+\.\d+)\b/i;
57
+ var LESSON_RE = /\b(learn(?:ed|ing|t)|lesson|insight|realized|discovered|never again)\b/i;
58
+ var RELATIONSHIP_RE = /\b(talked to|met with|spoke with|asked|client|partner|teammate|colleague)\b/i;
59
+ var PROJECT_RE = /\b(project|feature|service|repo|api|roadmap|sprint)\b/i;
60
+ function clamp01(value) {
61
+ if (!Number.isFinite(value)) return 0;
62
+ if (value < 0) return 0;
63
+ if (value > 1) return 1;
64
+ return value;
65
+ }
66
+ function scoreFromLegacyPriority(priority) {
67
+ if (priority === "\u{1F534}") return 0.9;
68
+ if (priority === "\u{1F7E1}") return 0.6;
69
+ return 0.2;
70
+ }
71
+ function confidenceFromLegacyPriority(priority) {
72
+ if (priority === "\u{1F534}") return 0.9;
73
+ if (priority === "\u{1F7E1}") return 0.8;
74
+ return 0.7;
75
+ }
76
+ function inferObservationType(content) {
77
+ if (DECISION_RE.test(content)) return "decision";
78
+ if (UNRESOLVED_RE.test(content)) return "commitment-unresolved";
79
+ if (TODO_RE.test(content)) return "todo";
80
+ if (PREFERENCE_RE.test(content)) return "preference";
81
+ if (COMMITMENT_TASK_RE.test(content) || DEADLINE_RE.test(content)) return "task";
82
+ if (COMMITMENT_RE.test(content)) return "commitment";
83
+ if (MILESTONE_RE.test(content)) return "milestone";
84
+ if (LESSON_RE.test(content)) return "lesson";
85
+ if (RELATIONSHIP_RE.test(content)) return "relationship";
86
+ if (PROJECT_RE.test(content)) return "project";
87
+ return "fact";
88
+ }
89
+ function formatScore(value) {
90
+ return clamp01(value).toFixed(2);
91
+ }
92
+ function normalizeObservationContent(content) {
93
+ return content.replace(/^\d{2}:\d{2}\s+/, "").replace(/\s+/g, " ").trim().toLowerCase();
94
+ }
95
+ function parseObservationLine(line, date) {
96
+ const scored = line.match(SCORED_LINE_RE);
97
+ if (scored) {
98
+ return {
99
+ date,
100
+ type: scored[1].toLowerCase(),
101
+ confidence: clamp01(Number.parseFloat(scored[2])),
102
+ importance: clamp01(Number.parseFloat(scored[3])),
103
+ content: scored[4].trim(),
104
+ format: "scored",
105
+ rawLine: line
106
+ };
107
+ }
108
+ const emoji = line.match(EMOJI_LINE_RE);
109
+ if (!emoji) {
110
+ return null;
111
+ }
112
+ const priority = emoji[1];
113
+ const time = emoji[2]?.trim();
114
+ const text = emoji[3].trim();
115
+ const content = time ? `${time} ${text}` : text;
116
+ return {
117
+ date,
118
+ type: inferObservationType(content),
119
+ confidence: confidenceFromLegacyPriority(priority),
120
+ importance: scoreFromLegacyPriority(priority),
121
+ content,
122
+ format: "emoji",
123
+ priority,
124
+ time,
125
+ rawLine: line
126
+ };
127
+ }
128
+ function parseObservationMarkdown(markdown) {
129
+ const parsed = [];
130
+ let currentDate = "";
131
+ for (const line of markdown.split(/\r?\n/)) {
132
+ const heading = line.match(DATE_HEADING_RE);
133
+ if (heading) {
134
+ currentDate = heading[1];
135
+ continue;
136
+ }
137
+ if (!currentDate) {
138
+ continue;
139
+ }
140
+ const record = parseObservationLine(line.trim(), currentDate);
141
+ if (record) {
142
+ parsed.push(record);
143
+ }
144
+ }
145
+ return parsed;
146
+ }
147
+ function renderScoredObservationLine(record) {
148
+ return `- [${record.type}|c=${formatScore(record.confidence)}|i=${formatScore(record.importance)}] ${record.content.trim()}`;
149
+ }
150
+ function renderObservationMarkdown(sections) {
151
+ const chunks = [];
152
+ const dates = [...sections.keys()].sort((left, right) => left.localeCompare(right));
153
+ for (const date of dates) {
154
+ const lines = sections.get(date) ?? [];
155
+ if (lines.length === 0) continue;
156
+ chunks.push(`## ${date}`);
157
+ chunks.push("");
158
+ for (const line of lines) {
159
+ chunks.push(renderScoredObservationLine(line));
160
+ }
161
+ chunks.push("");
162
+ }
163
+ return chunks.join("\n").trim();
164
+ }
165
+
166
+ // src/lib/claude-credentials.ts
167
+ var import_child_process = require("child_process");
168
+ var import_fs = require("fs");
169
+ var import_os = require("os");
170
+ var import_path = require("path");
171
+ var CLAUDE_CODE_SERVICE = "Claude Code-credentials";
172
+ var CLAUDE_CODE_ACCOUNT = "Claude Code";
173
+ var OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
174
+ var TOKEN_REFRESH_URL = "https://console.anthropic.com/v1/oauth/token";
175
+ var EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
176
+ function readClaudeCliCredentials(opts) {
177
+ if (process.platform === "darwin") {
178
+ try {
179
+ const raw = (0, import_child_process.execFileSync)(
180
+ "security",
181
+ ["find-generic-password", "-s", CLAUDE_CODE_SERVICE, "-w"],
182
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
183
+ ).trim();
184
+ const parsed = parseCredentialsJson(raw);
185
+ if (parsed) return parsed;
186
+ } catch {
187
+ }
188
+ }
189
+ const home = opts?.homeDir ?? (0, import_os.homedir)();
190
+ const credFile = (0, import_path.join)(home, ".claude", ".credentials.json");
191
+ if (!(0, import_fs.existsSync)(credFile)) {
192
+ return null;
193
+ }
194
+ try {
195
+ const raw = (0, import_fs.readFileSync)(credFile, "utf8");
196
+ return parseCredentialsJson(raw);
197
+ } catch {
198
+ return null;
199
+ }
200
+ }
201
+ function parseCredentialsJson(raw) {
202
+ try {
203
+ const parsed = JSON.parse(raw);
204
+ const oauth = parsed.claudeAiOauth;
205
+ if (oauth && typeof oauth.accessToken === "string" && typeof oauth.refreshToken === "string" && typeof oauth.expiresAt === "number") {
206
+ return {
207
+ accessToken: oauth.accessToken,
208
+ refreshToken: oauth.refreshToken,
209
+ expiresAt: oauth.expiresAt
210
+ };
211
+ }
212
+ } catch {
213
+ }
214
+ return null;
215
+ }
216
+ async function refreshClaudeOAuthToken(refreshToken, fetchImpl) {
217
+ const f = fetchImpl ?? fetch;
218
+ const response = await f(TOKEN_REFRESH_URL, {
219
+ method: "POST",
220
+ headers: { "content-type": "application/json" },
221
+ body: JSON.stringify({
222
+ grant_type: "refresh_token",
223
+ client_id: OAUTH_CLIENT_ID,
224
+ refresh_token: refreshToken
225
+ })
226
+ });
227
+ if (!response.ok) {
228
+ throw new Error(`OAuth token refresh failed (${response.status})`);
229
+ }
230
+ const data = await response.json();
231
+ return {
232
+ accessToken: data.access_token,
233
+ refreshToken: data.refresh_token,
234
+ expiresAt: Date.now() + data.expires_in * 1e3
235
+ };
236
+ }
237
+ function writeClaudeCliCredentials(cred, opts) {
238
+ const payload = JSON.stringify({ claudeAiOauth: cred });
239
+ if (process.platform === "darwin") {
240
+ try {
241
+ (0, import_child_process.execFileSync)(
242
+ "security",
243
+ ["add-generic-password", "-U", "-s", CLAUDE_CODE_SERVICE, "-a", CLAUDE_CODE_ACCOUNT, "-w", payload],
244
+ { stdio: "ignore" }
245
+ );
246
+ return;
247
+ } catch {
248
+ }
249
+ }
250
+ const home = opts?.homeDir ?? (0, import_os.homedir)();
251
+ const credFile = (0, import_path.join)(home, ".claude", ".credentials.json");
252
+ (0, import_fs.writeFileSync)(credFile, payload, "utf8");
253
+ }
254
+ async function resolveClaudeOAuthToken(opts) {
255
+ const cred = readClaudeCliCredentials(opts);
256
+ if (!cred) {
257
+ return null;
258
+ }
259
+ if (cred.expiresAt < Date.now() + EXPIRY_BUFFER_MS) {
260
+ try {
261
+ const refreshed = await refreshClaudeOAuthToken(cred.refreshToken, opts?.fetchImpl);
262
+ writeClaudeCliCredentials(refreshed, opts);
263
+ return refreshed.accessToken;
264
+ } catch {
265
+ return cred.accessToken;
266
+ }
267
+ }
268
+ return cred.accessToken;
269
+ }
270
+
271
+ // src/observer/compressor.ts
272
+ var OPENAI_BASE_URL = "https://api.openai.com/v1";
273
+ var OLLAMA_BASE_URL = "http://localhost:11434/v1";
274
+ var DEFAULT_PROVIDER_MODELS = {
275
+ anthropic: "claude-haiku-4-5",
276
+ openai: "gpt-4o-mini",
277
+ gemini: "gemini-2.0-flash",
278
+ "openai-compatible": "gpt-4o-mini",
279
+ ollama: "llama3.2"
280
+ };
281
+ var CRITICAL_RE = /(?:\b(?:decision|decided|chose|chosen|selected|picked|opted|switched to)\s*:?|\bdecid(?:e|ed|ing|ion)\b|\berror\b|\bfail(?:ed|ure|ing)?\b|\bblock(?:ed|er)?\b|\bbreaking(?:\s+change)?s?\b|\bcritical\b|\b\w+\s+chosen\s+(?:for|over|as)\b|\bpublish(?:ed)?\b.*@?\d+\.\d+|\bmerge[d]?\s+(?:PR|pull\s+request)\b|\bshipped\b|\breleased?\b.*v?\d+\.\d+|\bsigned\b.*\b(?:contract|agreement|deal)\b|\bpricing\b.*\$|\bdemo\b.*\b(?:completed?|done|finished)\b|\bmeeting\b.*\b(?:completed?|done|finished)\b|\bstrategy\b.*\b(?:pivot|change|shift)\b)/i;
282
+ var DEADLINE_WITH_DATE_RE = /(?:(?:\bdeadline\b|\bdue(?:\s+date)?\b|\bcutoff\b).*(?:\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}(?:\/\d{2,4})?|(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\s+\d{1,2})|(?:\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}(?:\/\d{2,4})?|(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\s+\d{1,2}).*(?:\bdeadline\b|\bdue(?:\s+date)?\b|\bcutoff\b))/i;
283
+ var NOTABLE_RE = /\b(prefer(?:ence|s)?|likes?|dislikes?|context|pattern|architecture|approach|trade[- ]?off|milestone|stakeholder|teammate|collaborat(?:e|ed|ion)|discussion|notable|deadline|due|timeline|deploy(?:ed|ment)?|built|configured|launched|proposal|pitch|onboard(?:ed|ing)?|migrat(?:e|ed|ion)|domain|DNS|infra(?:structure)?)\b/i;
284
+ var TODO_SIGNAL_RE = /(?:\btodo:\s*|\bwe need to\b|\bdon't forget(?: to)?\b|\bremember to\b|\bmake sure to\b)/i;
285
+ var COMMITMENT_TASK_SIGNAL_RE = /\b(?:i'?ll|i will|let me|(?:i'?m\s+)?going to|plan to|should)\b/i;
286
+ var UNRESOLVED_COMMITMENT_RE = /\b(?:need to figure out|tbd|to be determined)\b/i;
287
+ var DEADLINE_SIGNAL_RE = /\b(?:by\s+(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday|tomorrow)|before\s+the\s+\w+|deadline is)\b/i;
288
+ var Compressor = class {
289
+ provider;
290
+ model;
291
+ baseUrl;
292
+ apiKey;
293
+ now;
294
+ fetchImpl;
295
+ constructor(options = {}) {
296
+ this.provider = options.provider;
297
+ this.model = options.model;
298
+ this.baseUrl = options.baseUrl;
299
+ this.apiKey = options.apiKey;
300
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
301
+ this.fetchImpl = options.fetchImpl ?? fetch;
302
+ }
303
+ async compress(messages, existingObservations) {
304
+ const cleanedMessages = messages.map((message) => message.trim()).filter(Boolean);
305
+ if (cleanedMessages.length === 0) {
306
+ return existingObservations.trim();
307
+ }
308
+ const prompt = this.buildPrompt(cleanedMessages, existingObservations);
309
+ const backend = await this.resolveProvider();
310
+ if (backend) {
311
+ try {
312
+ const llmOutput = backend.provider === "anthropic" ? await this.callAnthropic(prompt, backend) : backend.provider === "gemini" ? await this.callGemini(prompt, backend) : backend.provider === "openai" ? await this.callOpenAI(prompt, backend) : await this.callOpenAICompatible(prompt, backend);
313
+ const normalized = this.normalizeLlmOutput(llmOutput);
314
+ if (normalized) {
315
+ return this.mergeObservations(existingObservations, normalized);
316
+ }
317
+ } catch {
318
+ }
319
+ }
320
+ const fallback = this.fallbackCompression(cleanedMessages);
321
+ return this.mergeObservations(existingObservations, fallback);
322
+ }
323
+ async resolveProvider() {
324
+ if (process.env.CLAWVAULT_NO_LLM) return null;
325
+ if (this.provider) {
326
+ const configured = this.resolveConfiguredProvider(this.provider);
327
+ if (configured) {
328
+ return configured;
329
+ }
330
+ return await this.resolveProviderFromEnv(false);
331
+ }
332
+ return await this.resolveProviderFromEnv(true);
333
+ }
334
+ resolveConfiguredProvider(provider) {
335
+ const model = this.resolveModel(provider);
336
+ if (provider === "anthropic") {
337
+ const apiKey2 = this.resolveApiKey(provider);
338
+ if (!apiKey2) {
339
+ return null;
340
+ }
341
+ return {
342
+ provider,
343
+ model,
344
+ apiKey: apiKey2
345
+ };
346
+ }
347
+ if (provider === "gemini") {
348
+ const apiKey2 = this.resolveApiKey(provider);
349
+ if (!apiKey2) {
350
+ return null;
351
+ }
352
+ return {
353
+ provider,
354
+ model,
355
+ apiKey: apiKey2
356
+ };
357
+ }
358
+ if (provider === "openai") {
359
+ const apiKey2 = this.resolveApiKey(provider);
360
+ if (!apiKey2) {
361
+ return null;
362
+ }
363
+ return {
364
+ provider,
365
+ model,
366
+ apiKey: apiKey2,
367
+ baseUrl: this.resolveBaseUrl(provider)
368
+ };
369
+ }
370
+ const apiKey = this.resolveApiKey(provider) ?? void 0;
371
+ return {
372
+ provider,
373
+ model,
374
+ apiKey,
375
+ baseUrl: this.resolveBaseUrl(provider)
376
+ };
377
+ }
378
+ async resolveProviderFromEnv(allowConfiguredModel) {
379
+ const anthropicModel = allowConfiguredModel ? this.resolveModel("anthropic") : DEFAULT_PROVIDER_MODELS.anthropic;
380
+ const oauthEnvToken = this.readEnvValue("ANTHROPIC_OAUTH_TOKEN");
381
+ if (oauthEnvToken) {
382
+ return { provider: "anthropic", model: anthropicModel, apiKey: oauthEnvToken, isOAuth: true };
383
+ }
384
+ const anthropicApiKey = this.readEnvValue("ANTHROPIC_API_KEY");
385
+ if (anthropicApiKey) {
386
+ return { provider: "anthropic", model: anthropicModel, apiKey: anthropicApiKey };
387
+ }
388
+ if (this.readEnvValue("CLAWVAULT_CLAUDE_AUTH")) {
389
+ const oauthToken = await resolveClaudeOAuthToken();
390
+ if (oauthToken) {
391
+ return { provider: "anthropic", model: anthropicModel, apiKey: oauthToken, isOAuth: true };
392
+ }
393
+ }
394
+ const openAiApiKey = this.readEnvValue("OPENAI_API_KEY");
395
+ if (openAiApiKey) {
396
+ return {
397
+ provider: "openai",
398
+ model: allowConfiguredModel ? this.resolveModel("openai") : DEFAULT_PROVIDER_MODELS.openai,
399
+ apiKey: openAiApiKey,
400
+ baseUrl: OPENAI_BASE_URL
401
+ };
402
+ }
403
+ const geminiApiKey = this.readEnvValue("GEMINI_API_KEY");
404
+ if (geminiApiKey) {
405
+ return {
406
+ provider: "gemini",
407
+ model: allowConfiguredModel ? this.resolveModel("gemini") : DEFAULT_PROVIDER_MODELS.gemini,
408
+ apiKey: geminiApiKey
409
+ };
410
+ }
411
+ return null;
412
+ }
413
+ resolveModel(provider) {
414
+ const configuredModel = this.model?.trim();
415
+ if (configuredModel) {
416
+ return configuredModel;
417
+ }
418
+ return DEFAULT_PROVIDER_MODELS[provider];
419
+ }
420
+ resolveApiKey(provider) {
421
+ const configuredApiKey = this.apiKey?.trim();
422
+ if (configuredApiKey) {
423
+ return configuredApiKey;
424
+ }
425
+ if (provider === "anthropic") {
426
+ return this.readEnvValue("ANTHROPIC_API_KEY");
427
+ }
428
+ if (provider === "gemini") {
429
+ return this.readEnvValue("GEMINI_API_KEY");
430
+ }
431
+ return this.readEnvValue("OPENAI_API_KEY");
432
+ }
433
+ resolveBaseUrl(provider) {
434
+ const configuredBaseUrl = this.baseUrl?.trim();
435
+ if (configuredBaseUrl) {
436
+ return configuredBaseUrl.replace(/\/+$/, "");
437
+ }
438
+ if (provider === "ollama") {
439
+ return OLLAMA_BASE_URL;
440
+ }
441
+ return OPENAI_BASE_URL;
442
+ }
443
+ readEnvValue(name) {
444
+ const value = process.env[name]?.trim();
445
+ return value ? value : null;
446
+ }
447
+ buildPrompt(messages, existingObservations) {
448
+ return [
449
+ "You are an observer that compresses raw AI session messages into durable, human-meaningful observations.",
450
+ "",
451
+ "Rules:",
452
+ "- Output markdown only.",
453
+ "- Group observations by date heading: ## YYYY-MM-DD",
454
+ "- Each observation line MUST follow: - [type|c=<0.00-1.00>|i=<0.00-1.00>] <observation>",
455
+ "- Allowed type tags: decision, preference, fact, commitment, task, todo, commitment-unresolved, milestone, lesson, relationship, project",
456
+ "- i >= 0.80 for structural/persistent observations (major decisions, blockers, releases, commitments)",
457
+ "- i 0.40-0.79 for potentially important observations (notable context, preferences, milestones)",
458
+ "- i < 0.40 for contextual/routine observations",
459
+ "- Confidence c reflects extraction certainty, not importance.",
460
+ "- Preserve source tags when present (e.g., [main], [telegram-dm], [discord], [telegram-group]).",
461
+ "",
462
+ "PREFERENCE & PERSONAL CONTEXT EXTRACTION (critical for personalization):",
463
+ "- Emit [preference] for ANY personal detail that reveals tastes, habits, equipment, or context:",
464
+ ' * Explicit: "I prefer X", "I like Y", "I always use Z"',
465
+ ' * Ownership: "my Sony A7R IV", "I have a...", "I use...", "I own..."',
466
+ ' * Habits/routines: "I usually...", "every morning I...", "I tend to..."',
467
+ " * Interests: topics the user is enthusiastic about, hobbies mentioned in passing",
468
+ " * Constraints: dietary restrictions, allergies, phobias, limitations",
469
+ ` * Goals: "I'm trying to...", "I want to learn..."`,
470
+ "- These are HIGH VALUE observations (i >= 0.60) \u2014 they enable personalized responses.",
471
+ "- When in doubt between [preference] and [task], choose [preference] if it describes a lasting trait.",
472
+ "",
473
+ "TASK EXTRACTION (required):",
474
+ `- Emit [todo] for explicit TODO phrasing: "TODO:", "we need to", "don't forget", "remember to", "make sure to".`,
475
+ `- Emit [task] for commitments/action intent: "I'll", "I will", "let me", "going to", "plan to", "should".`,
476
+ '- Emit [commitment-unresolved] for unresolved commitments/questions: "need to figure out", "TBD", "to be determined".',
477
+ '- Deadline language ("by Friday", "before the demo", "deadline is") should increase importance and usually map to [task] unless unresolved.',
478
+ "",
479
+ "QUALITY FILTERS (important):",
480
+ "- DO NOT observe: CLI errors, command failures, tool output parsing issues, retry attempts, debug logs.",
481
+ " These are transient noise, not memories. Only observe errors if they represent a BLOCKER or an unresolved problem.",
482
+ '- DO NOT observe: "acknowledged the conversation", "said okay", routine confirmations.',
483
+ '- MERGE related events into single observations. If 5 images were generated, say "Generated 5 images for X" not 5 separate lines.',
484
+ '- MERGE retry sequences: "Tried X, failed, tried Y, succeeded" \u2192 "Resolved X using Y (after initial failure)"',
485
+ '- Prefer OUTCOMES over PROCESSES: "Deployed v1.2 to Railway" not "Started deploy... build finished... deploy succeeded"',
486
+ "",
487
+ "AGENT ATTRIBUTION:",
488
+ '- If the transcript shows multiple speakers/agents, prefix observations with who did it: "Pedro asked...", "Clawdious deployed...", "Zeca generated..."',
489
+ "- If only one agent is acting, attribution is optional.",
490
+ "",
491
+ "PROJECT MILESTONES (critical \u2014 these are the most valuable observations):",
492
+ "Projects are NOT just code. Milestones include business, strategy, client, and operational events.",
493
+ "- Use milestone/decision/commitment types for strategic events with high importance.",
494
+ "- Use preference/lesson/relationship/project/fact when appropriate.",
495
+ "- Examples:",
496
+ ' "- [decision|c=0.95|i=0.90] 14:00 Pricing decision: $33K one-time + $3K/mo for Artemisa"',
497
+ ' "- [milestone|c=0.93|i=0.88] 14:00 Published clawvault@2.1.0 to npm"',
498
+ ' "- [project|c=0.84|i=0.58] 14:00 Deployed pitch deck to artemisa-pitch-deck.vercel.app"',
499
+ "- Do NOT collapse multiple milestones into one line \u2014 each matters for history.",
500
+ "",
501
+ "COMMITMENT FORMAT (when someone promises/agrees to something):",
502
+ '- Prefer: "- [commitment|c=...|i=...] HH:MM [COMMITMENT] <who> committed to <what> by <when>"',
503
+ "",
504
+ "Keep observations concise and factual. Aim for signal, not completeness.",
505
+ "",
506
+ "Existing observations (may be empty):",
507
+ existingObservations.trim() || "(none)",
508
+ "",
509
+ "Raw messages:",
510
+ ...messages.map((message, index) => `[${index + 1}] ${message}`),
511
+ "",
512
+ "Return only the updated observation markdown."
513
+ ].join("\n");
514
+ }
515
+ buildOpenAICompatibleUrl(baseUrl) {
516
+ const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
517
+ return `${normalizedBaseUrl}/chat/completions`;
518
+ }
519
+ buildOpenAICompatibleHeaders(apiKey) {
520
+ const headers = {
521
+ "content-type": "application/json"
522
+ };
523
+ if (apiKey) {
524
+ headers.authorization = `Bearer ${apiKey}`;
525
+ }
526
+ return headers;
527
+ }
528
+ extractOpenAIContent(content) {
529
+ if (typeof content === "string") {
530
+ return content.trim();
531
+ }
532
+ if (!Array.isArray(content)) {
533
+ return "";
534
+ }
535
+ const parts = content.map((part) => {
536
+ if (typeof part === "string") {
537
+ return part;
538
+ }
539
+ if (!part || typeof part !== "object") {
540
+ return "";
541
+ }
542
+ const candidate = part;
543
+ return typeof candidate.text === "string" ? candidate.text : "";
544
+ }).filter((part) => part.trim().length > 0);
545
+ return parts.join("\n").trim();
546
+ }
547
+ async callAnthropic(prompt, backend) {
548
+ if (!backend.apiKey) {
549
+ return "";
550
+ }
551
+ const isOAuth = backend.isOAuth || backend.apiKey.includes("sk-ant-oat");
552
+ const headers = isOAuth ? {
553
+ "content-type": "application/json",
554
+ "authorization": `Bearer ${backend.apiKey}`,
555
+ "anthropic-version": "2023-06-01",
556
+ "anthropic-beta": "claude-code-20250219,oauth-2025-04-20",
557
+ "x-app": "cli",
558
+ "user-agent": "claude-cli/1.0.0 (external, cli)"
559
+ } : {
560
+ "content-type": "application/json",
561
+ "x-api-key": backend.apiKey,
562
+ "anthropic-version": "2023-06-01"
563
+ };
564
+ const body = isOAuth ? {
565
+ model: backend.model,
566
+ temperature: 0.1,
567
+ max_tokens: 1400,
568
+ system: [{ type: "text", text: "You are Claude Code, Anthropic's official CLI for Claude." }],
569
+ messages: [{ role: "user", content: prompt }]
570
+ } : {
571
+ model: backend.model,
572
+ temperature: 0.1,
573
+ max_tokens: 1400,
574
+ messages: [{ role: "user", content: prompt }]
575
+ };
576
+ const response = await this.fetchImpl("https://api.anthropic.com/v1/messages", {
577
+ method: "POST",
578
+ headers,
579
+ body: JSON.stringify(body)
580
+ });
581
+ if (!response.ok) {
582
+ throw new Error(`Anthropic request failed (${response.status})`);
583
+ }
584
+ const payload = await response.json();
585
+ return payload.content?.filter((part) => part.type === "text" && part.text).map((part) => part.text).join("\n").trim() ?? "";
586
+ }
587
+ async callOpenAI(prompt, backend) {
588
+ return this.callOpenAICompatible(prompt, backend);
589
+ }
590
+ async callOpenAICompatible(prompt, backend) {
591
+ const baseUrl = backend.baseUrl ?? this.resolveBaseUrl(backend.provider);
592
+ const response = await this.fetchImpl(this.buildOpenAICompatibleUrl(baseUrl), {
593
+ method: "POST",
594
+ headers: this.buildOpenAICompatibleHeaders(backend.apiKey),
595
+ body: JSON.stringify({
596
+ model: backend.model,
597
+ temperature: 0.1,
598
+ messages: [
599
+ { role: "system", content: "You transform session logs into concise observations." },
600
+ { role: "user", content: prompt }
601
+ ]
602
+ })
603
+ });
604
+ if (!response.ok) {
605
+ throw new Error(`OpenAI-compatible request failed (${response.status})`);
606
+ }
607
+ const payload = await response.json();
608
+ return this.extractOpenAIContent(payload.choices?.[0]?.message?.content);
609
+ }
610
+ async callGemini(prompt, backend) {
611
+ if (!backend.apiKey) {
612
+ return "";
613
+ }
614
+ const model = encodeURIComponent(backend.model);
615
+ const response = await this.fetchImpl(
616
+ `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${backend.apiKey}`,
617
+ {
618
+ method: "POST",
619
+ headers: { "content-type": "application/json" },
620
+ body: JSON.stringify({
621
+ contents: [{ parts: [{ text: prompt }] }],
622
+ generationConfig: { temperature: 0.1, maxOutputTokens: 1400 }
623
+ })
624
+ }
625
+ );
626
+ if (!response.ok) {
627
+ throw new Error(`Gemini request failed (${response.status})`);
628
+ }
629
+ const payload = await response.json();
630
+ return payload.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? "";
631
+ }
632
+ normalizeLlmOutput(output) {
633
+ if (!output.trim()) {
634
+ return "";
635
+ }
636
+ const cleaned = output.replace(/^```(?:markdown)?\s*/i, "").replace(/\s*```$/, "").trim();
637
+ const lines = cleaned.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
638
+ const hasObservationLine = lines.some((line) => line.startsWith("- [") || /^(?:-\s*)?(🔴|🟡|🟢)\s+/.test(line));
639
+ if (!hasObservationLine) {
640
+ return "";
641
+ }
642
+ const hasDateHeading = lines.some((line) => DATE_HEADING_RE.test(line));
643
+ const result = hasDateHeading ? cleaned : `## ${this.formatDate(this.now())}
644
+
645
+ ${cleaned}`;
646
+ const sanitized = this.sanitizeWikiLinks(result);
647
+ return this.enforceImportanceRules(sanitized);
648
+ }
649
+ /**
650
+ * Fix wiki-link corruption from LLM compression.
651
+ * LLMs often fuse preceding word fragments into wiki-links during rewriting:
652
+ * "reque[[people/pedro]]" → "[[people/pedro]]"
653
+ * "Linke[[agents/zeca]]" → "[[agents/zeca]]"
654
+ * "taske[[people/pedro]]a" → "[[people/pedro]]"
655
+ * Also fixes trailing word fragments fused after closing brackets.
656
+ */
657
+ sanitizeWikiLinks(markdown) {
658
+ let result = markdown.replace(/\w+\[\[/g, " [[");
659
+ result = result.replace(/\]\]\w+/g, "]]");
660
+ result = result.replace(/ {2,}/g, " ");
661
+ return result;
662
+ }
663
+ enforceImportanceRules(markdown) {
664
+ const parsed = parseObservationMarkdown(markdown);
665
+ if (parsed.length === 0) {
666
+ return "";
667
+ }
668
+ const grouped = /* @__PURE__ */ new Map();
669
+ for (const record of parsed) {
670
+ const adjusted = this.enforceImportanceForRecord(record);
671
+ const bucket = grouped.get(record.date) ?? [];
672
+ bucket.push(adjusted);
673
+ grouped.set(record.date, bucket);
674
+ }
675
+ return renderObservationMarkdown(grouped);
676
+ }
677
+ enforceImportanceForRecord(record) {
678
+ let importance = record.importance;
679
+ let confidence = record.confidence;
680
+ let type = record.type;
681
+ const inferredTaskType = this.inferTaskType(record.content);
682
+ if (this.isCriticalContent(record.content)) {
683
+ importance = Math.max(importance, 0.85);
684
+ confidence = Math.max(confidence, 0.85);
685
+ if (type === "fact") {
686
+ type = inferObservationType(record.content);
687
+ }
688
+ } else if (this.isNotableContent(record.content)) {
689
+ importance = Math.max(importance, 0.5);
690
+ confidence = Math.max(confidence, 0.75);
691
+ }
692
+ if (inferredTaskType) {
693
+ type = type === "fact" || type === "commitment" ? inferredTaskType : type;
694
+ importance = Math.max(importance, inferredTaskType === "commitment-unresolved" ? 0.72 : 0.65);
695
+ confidence = Math.max(confidence, 0.8);
696
+ }
697
+ if (type === "decision" || type === "commitment" || type === "milestone") {
698
+ importance = Math.max(importance, 0.6);
699
+ }
700
+ return {
701
+ type,
702
+ confidence: this.clamp01(confidence),
703
+ importance: this.clamp01(importance),
704
+ content: record.content
705
+ };
706
+ }
707
+ fallbackCompression(messages) {
708
+ const sections = /* @__PURE__ */ new Map();
709
+ const seen = /* @__PURE__ */ new Set();
710
+ for (const message of messages) {
711
+ const normalized = this.normalizeText(message);
712
+ if (!normalized) continue;
713
+ const date = this.extractDate(message) ?? this.formatDate(this.now());
714
+ const time = this.extractTime(message) ?? this.formatTime(this.now());
715
+ const line = `${time} ${normalized}`;
716
+ const type = inferObservationType(line);
717
+ const importance = this.inferImportance(line, type);
718
+ const confidence = this.inferConfidence(line, type, importance);
719
+ const dedupeKey = `${date}|${type}|${normalizeObservationContent(line)}`;
720
+ if (seen.has(dedupeKey)) continue;
721
+ seen.add(dedupeKey);
722
+ const bucket = sections.get(date) ?? [];
723
+ bucket.push({ type, confidence, importance, content: line });
724
+ sections.set(date, bucket);
725
+ }
726
+ if (sections.size === 0) {
727
+ const date = this.formatDate(this.now());
728
+ sections.set(date, [{
729
+ type: "fact",
730
+ confidence: 0.7,
731
+ importance: 0.2,
732
+ content: `${this.formatTime(this.now())} Processed session updates.`
733
+ }]);
734
+ }
735
+ return this.renderSections(sections);
736
+ }
737
+ mergeObservations(existing, incoming) {
738
+ const existingRecords = parseObservationMarkdown(existing);
739
+ const incomingRecords = parseObservationMarkdown(incoming);
740
+ if (incomingRecords.length === 0) {
741
+ return existing.trim();
742
+ }
743
+ const merged = /* @__PURE__ */ new Map();
744
+ for (const record of existingRecords) {
745
+ this.mergeRecord(merged, {
746
+ date: record.date,
747
+ type: record.type,
748
+ confidence: record.confidence,
749
+ importance: record.importance,
750
+ content: record.content
751
+ });
752
+ }
753
+ for (const record of incomingRecords) {
754
+ this.mergeRecord(merged, {
755
+ date: record.date,
756
+ type: record.type,
757
+ confidence: record.confidence,
758
+ importance: record.importance,
759
+ content: record.content
760
+ });
761
+ }
762
+ return this.renderSections(merged);
763
+ }
764
+ mergeRecord(sections, input) {
765
+ const bucket = sections.get(input.date) ?? [];
766
+ const key = normalizeObservationContent(input.content);
767
+ const index = bucket.findIndex((line) => normalizeObservationContent(line.content) === key);
768
+ if (index === -1) {
769
+ bucket.push({
770
+ type: input.type,
771
+ confidence: this.clamp01(input.confidence),
772
+ importance: this.clamp01(input.importance),
773
+ content: input.content.trim()
774
+ });
775
+ sections.set(input.date, bucket);
776
+ return;
777
+ }
778
+ const existing = bucket[index];
779
+ bucket[index] = {
780
+ type: input.importance >= existing.importance ? input.type : existing.type,
781
+ confidence: this.clamp01(Math.max(existing.confidence, input.confidence)),
782
+ importance: this.clamp01(Math.max(existing.importance, input.importance)),
783
+ content: existing.content.length >= input.content.length ? existing.content : input.content
784
+ };
785
+ sections.set(input.date, bucket);
786
+ }
787
+ renderSections(sections) {
788
+ return renderObservationMarkdown(sections);
789
+ }
790
+ inferImportance(text, type) {
791
+ const inferredTaskType = this.inferTaskType(text);
792
+ if (this.isCriticalContent(text)) return 0.9;
793
+ if (inferredTaskType === "commitment-unresolved") return 0.72;
794
+ if (inferredTaskType === "task" || inferredTaskType === "todo") return 0.65;
795
+ if (this.isNotableContent(text)) return 0.6;
796
+ if (type === "decision" || type === "commitment" || type === "milestone") return 0.55;
797
+ if (type === "preference" || type === "lesson" || type === "relationship" || type === "project") return 0.45;
798
+ return 0.2;
799
+ }
800
+ inferConfidence(text, type, importance) {
801
+ const inferredTaskType = this.inferTaskType(text);
802
+ let confidence = 0.72;
803
+ if (importance >= 0.8) confidence += 0.12;
804
+ if (type === "decision" || type === "commitment" || type === "milestone") confidence += 0.06;
805
+ if (inferredTaskType) confidence += 0.06;
806
+ if (/\b(?:decided|chose|committed|deadline|released|merged)\b/i.test(text)) {
807
+ confidence += 0.05;
808
+ }
809
+ return this.clamp01(confidence);
810
+ }
811
+ isCriticalContent(text) {
812
+ return CRITICAL_RE.test(text) || DEADLINE_WITH_DATE_RE.test(text);
813
+ }
814
+ isNotableContent(text) {
815
+ return NOTABLE_RE.test(text);
816
+ }
817
+ inferTaskType(text) {
818
+ if (UNRESOLVED_COMMITMENT_RE.test(text)) {
819
+ return "commitment-unresolved";
820
+ }
821
+ if (TODO_SIGNAL_RE.test(text)) {
822
+ return "todo";
823
+ }
824
+ if (COMMITMENT_TASK_SIGNAL_RE.test(text) || DEADLINE_SIGNAL_RE.test(text)) {
825
+ return "task";
826
+ }
827
+ return null;
828
+ }
829
+ normalizeText(text) {
830
+ return text.replace(/\s+/g, " ").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").trim().slice(0, 280);
831
+ }
832
+ extractDate(text) {
833
+ const match = text.match(/\b(\d{4}-\d{2}-\d{2})\b/);
834
+ return match?.[1] ?? null;
835
+ }
836
+ extractTime(text) {
837
+ const match = text.match(/\b([01]\d|2[0-3]):([0-5]\d)\b/);
838
+ if (!match) {
839
+ return null;
840
+ }
841
+ return `${match[1]}:${match[2]}`;
842
+ }
843
+ formatDate(date) {
844
+ return date.toISOString().split("T")[0];
845
+ }
846
+ formatTime(date) {
847
+ return date.toISOString().slice(11, 16);
848
+ }
849
+ clamp01(value) {
850
+ if (!Number.isFinite(value)) return 0;
851
+ if (value < 0) return 0;
852
+ if (value > 1) return 1;
853
+ return value;
854
+ }
855
+ };
856
+
857
+ // src/observer/reflector.ts
858
+ var DATE_HEADING_RE2 = /^##\s+(\d{4}-\d{2}-\d{2})\s*$/;
859
+ var OBSERVATION_LINE_RE = /^(🔴|🟡|🟢)\s+(.+)$/u;
860
+ var Reflector = class {
861
+ now;
862
+ constructor(options = {}) {
863
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
864
+ }
865
+ reflect(observations) {
866
+ const sections = this.parseSections(observations);
867
+ if (sections.size === 0) {
868
+ return observations.trim();
869
+ }
870
+ const cutoff = this.buildCutoffDate();
871
+ const dedupeKeys = [];
872
+ const reflected = /* @__PURE__ */ new Map();
873
+ const dates = [...sections.keys()].sort((a, b) => b.localeCompare(a));
874
+ for (const date of dates) {
875
+ const sectionDate = this.parseDate(date);
876
+ const olderThanCutoff = sectionDate ? sectionDate.getTime() < cutoff.getTime() : false;
877
+ const lines = sections.get(date) ?? [];
878
+ const kept = [];
879
+ for (const line of lines) {
880
+ if (line.priority === "\u{1F534}") {
881
+ kept.push(line);
882
+ continue;
883
+ }
884
+ if (line.priority === "\u{1F7E2}" && olderThanCutoff) {
885
+ continue;
886
+ }
887
+ const key = this.normalizeText(line.content);
888
+ const isDuplicate = dedupeKeys.some((existing) => this.isSimilar(existing, key));
889
+ if (isDuplicate) {
890
+ continue;
891
+ }
892
+ dedupeKeys.push(key);
893
+ kept.push(line);
894
+ }
895
+ if (kept.length > 0) {
896
+ reflected.set(date, kept);
897
+ }
898
+ }
899
+ return this.renderSections(reflected);
900
+ }
901
+ buildCutoffDate() {
902
+ const cutoff = new Date(this.now());
903
+ cutoff.setHours(0, 0, 0, 0);
904
+ cutoff.setDate(cutoff.getDate() - 7);
905
+ return cutoff;
906
+ }
907
+ parseDate(date) {
908
+ const parsed = /* @__PURE__ */ new Date(`${date}T00:00:00.000Z`);
909
+ if (Number.isNaN(parsed.getTime())) {
910
+ return null;
911
+ }
912
+ return parsed;
913
+ }
914
+ parseSections(markdown) {
915
+ const sections = /* @__PURE__ */ new Map();
916
+ let currentDate = null;
917
+ for (const rawLine of markdown.split(/\r?\n/)) {
918
+ const dateMatch = rawLine.match(DATE_HEADING_RE2);
919
+ if (dateMatch) {
920
+ currentDate = dateMatch[1];
921
+ if (!sections.has(currentDate)) {
922
+ sections.set(currentDate, []);
923
+ }
924
+ continue;
925
+ }
926
+ if (!currentDate) continue;
927
+ const lineMatch = rawLine.match(OBSERVATION_LINE_RE);
928
+ if (!lineMatch) continue;
929
+ const bucket = sections.get(currentDate) ?? [];
930
+ bucket.push({
931
+ priority: lineMatch[1],
932
+ content: lineMatch[2].trim()
933
+ });
934
+ sections.set(currentDate, bucket);
935
+ }
936
+ return sections;
937
+ }
938
+ renderSections(sections) {
939
+ const chunks = [];
940
+ const dates = [...sections.keys()].sort((a, b) => a.localeCompare(b));
941
+ for (const date of dates) {
942
+ const lines = sections.get(date) ?? [];
943
+ if (lines.length === 0) continue;
944
+ chunks.push(`## ${date}`);
945
+ chunks.push("");
946
+ for (const line of lines) {
947
+ chunks.push(`${line.priority} ${line.content}`);
948
+ }
949
+ chunks.push("");
950
+ }
951
+ return chunks.join("\n").trim();
952
+ }
953
+ normalizeText(text) {
954
+ return text.toLowerCase().replace(/\s+/g, " ").replace(/[^\w\s:.-]/g, "").trim();
955
+ }
956
+ isSimilar(a, b) {
957
+ if (a === b) return true;
958
+ if (a.length >= 24 && (a.includes(b) || b.includes(a))) {
959
+ return true;
960
+ }
961
+ return false;
962
+ }
963
+ };
964
+
965
+ // src/observer/router.ts
966
+ var fs5 = __toESM(require("fs"), 1);
967
+ var path5 = __toESM(require("path"), 1);
968
+
969
+ // src/lib/task-utils.ts
970
+ var fs2 = __toESM(require("fs"), 1);
971
+ var path2 = __toESM(require("path"), 1);
972
+ var import_gray_matter2 = __toESM(require("gray-matter"), 1);
973
+
974
+ // src/lib/transition-ledger.ts
975
+ var fs = __toESM(require("fs"), 1);
976
+ var path = __toESM(require("path"), 1);
977
+ var REGRESSION_PAIRS = [
978
+ ["done", "open"],
979
+ ["done", "blocked"],
980
+ ["in-progress", "blocked"]
981
+ ];
982
+ function isRegression(from, to) {
983
+ return REGRESSION_PAIRS.some(([f, t]) => f === from && t === to);
984
+ }
985
+ function getLedgerDir(vaultPath) {
986
+ return path.join(path.resolve(vaultPath), "ledger", "transitions");
987
+ }
988
+ function getTodayLedgerPath(vaultPath) {
989
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
990
+ return path.join(getLedgerDir(vaultPath), `${date}.jsonl`);
991
+ }
992
+ var RETRYABLE_APPEND_CODES = /* @__PURE__ */ new Set(["ENOENT", "EAGAIN", "EBUSY"]);
993
+ var MAX_APPEND_RETRIES = 2;
994
+ function asErrno(error) {
995
+ if (!error || typeof error !== "object") {
996
+ return null;
997
+ }
998
+ return error;
999
+ }
1000
+ function formatLedgerWriteError(filePath, error) {
1001
+ const errno = asErrno(error);
1002
+ const message = error instanceof Error ? error.message : String(error);
1003
+ if (errno?.code === "ENOSPC") {
1004
+ return new Error(`Failed to write transition ledger at ${filePath}: no space left on device.`);
1005
+ }
1006
+ if (errno?.code === "EACCES" || errno?.code === "EPERM") {
1007
+ return new Error(`Failed to write transition ledger at ${filePath}: permission denied.`);
1008
+ }
1009
+ return new Error(`Failed to write transition ledger at ${filePath}: ${message}`);
1010
+ }
1011
+ function appendTransition(vaultPath, event) {
1012
+ const ledgerDir = getLedgerDir(vaultPath);
1013
+ try {
1014
+ fs.mkdirSync(ledgerDir, { recursive: true });
1015
+ } catch (error) {
1016
+ throw formatLedgerWriteError(ledgerDir, error);
1017
+ }
1018
+ const filePath = getTodayLedgerPath(vaultPath);
1019
+ const payload = JSON.stringify(event) + "\n";
1020
+ for (let attempt = 0; attempt <= MAX_APPEND_RETRIES; attempt += 1) {
1021
+ try {
1022
+ fs.appendFileSync(filePath, payload);
1023
+ return;
1024
+ } catch (error) {
1025
+ const errno = asErrno(error);
1026
+ const code = errno?.code;
1027
+ if (code === "ENOENT") {
1028
+ try {
1029
+ fs.mkdirSync(ledgerDir, { recursive: true });
1030
+ } catch (mkdirError) {
1031
+ throw formatLedgerWriteError(filePath, mkdirError);
1032
+ }
1033
+ }
1034
+ if (code && RETRYABLE_APPEND_CODES.has(code) && attempt < MAX_APPEND_RETRIES) {
1035
+ continue;
1036
+ }
1037
+ throw formatLedgerWriteError(filePath, error);
1038
+ }
1039
+ }
1040
+ }
1041
+ function buildTransitionEvent(taskId, fromStatus, toStatus, options = {}) {
1042
+ const agentId = process.env.OPENCLAW_AGENT_ID || "manual";
1043
+ const costTokensRaw = process.env.OPENCLAW_TOKEN_ESTIMATE;
1044
+ const costTokens = costTokensRaw ? parseInt(costTokensRaw, 10) : null;
1045
+ return {
1046
+ task_id: taskId,
1047
+ agent_id: agentId,
1048
+ from_status: fromStatus,
1049
+ to_status: toStatus,
1050
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1051
+ confidence: options.confidence ?? (agentId === "manual" ? 1 : 1),
1052
+ cost_tokens: costTokens !== null && !isNaN(costTokens) ? costTokens : null,
1053
+ reason: options.reason || null
1054
+ };
1055
+ }
1056
+ function readAllTransitions(vaultPath) {
1057
+ const ledgerDir = getLedgerDir(vaultPath);
1058
+ if (!fs.existsSync(ledgerDir)) return [];
1059
+ let files = [];
1060
+ try {
1061
+ files = fs.readdirSync(ledgerDir).filter((f) => f.endsWith(".jsonl")).sort();
1062
+ } catch {
1063
+ return [];
1064
+ }
1065
+ const events = [];
1066
+ for (const file of files) {
1067
+ let lines = [];
1068
+ try {
1069
+ lines = fs.readFileSync(path.join(ledgerDir, file), "utf-8").split("\n").filter((l) => l.trim());
1070
+ } catch {
1071
+ continue;
1072
+ }
1073
+ for (const line of lines) {
1074
+ try {
1075
+ events.push(JSON.parse(line));
1076
+ } catch {
1077
+ }
1078
+ }
1079
+ }
1080
+ return events;
1081
+ }
1082
+ function countBlockedTransitions(vaultPath, taskId) {
1083
+ const events = readAllTransitions(vaultPath);
1084
+ return events.filter((e) => e.task_id === taskId && e.to_status === "blocked").length;
1085
+ }
1086
+
1087
+ // src/lib/primitive-templates.ts
1088
+ var import_gray_matter = __toESM(require("gray-matter"), 1);
1089
+
1090
+ // src/lib/task-utils.ts
1091
+ function slugify(text) {
1092
+ return text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").trim();
1093
+ }
1094
+ function getTasksDir(vaultPath) {
1095
+ return path2.join(path2.resolve(vaultPath), "tasks");
1096
+ }
1097
+ function getBacklogDir(vaultPath) {
1098
+ return path2.join(path2.resolve(vaultPath), "backlog");
1099
+ }
1100
+ function ensureBacklogDir(vaultPath) {
1101
+ const backlogDir = getBacklogDir(vaultPath);
1102
+ if (!fs2.existsSync(backlogDir)) {
1103
+ fs2.mkdirSync(backlogDir, { recursive: true });
1104
+ }
1105
+ }
1106
+ function getTaskPath(vaultPath, slug) {
1107
+ return path2.join(getTasksDir(vaultPath), `${slug}.md`);
1108
+ }
1109
+ function getBacklogPath(vaultPath, slug) {
1110
+ return path2.join(getBacklogDir(vaultPath), `${slug}.md`);
1111
+ }
1112
+ function extractTitle(content) {
1113
+ const match = content.match(/^#\s+(.+)$/m);
1114
+ return match ? match[1].trim() : "";
1115
+ }
1116
+ function parseDueDate(value) {
1117
+ if (!value) return null;
1118
+ const timestamp = Date.parse(value);
1119
+ if (Number.isNaN(timestamp)) return null;
1120
+ return timestamp;
1121
+ }
1122
+ function startOfToday() {
1123
+ const now = /* @__PURE__ */ new Date();
1124
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
1125
+ }
1126
+ var VALID_TASK_STATUSES = /* @__PURE__ */ new Set([
1127
+ "open",
1128
+ "in-progress",
1129
+ "blocked",
1130
+ "done"
1131
+ ]);
1132
+ function isTaskStatus(value) {
1133
+ return typeof value === "string" && VALID_TASK_STATUSES.has(value);
1134
+ }
1135
+ function persistTaskFrontmatter(task, frontmatter) {
1136
+ fs2.writeFileSync(task.path, import_gray_matter2.default.stringify(task.content, frontmatter));
1137
+ }
1138
+ function resolveStatusTransition(previousStatus, nextStatus) {
1139
+ if (!isTaskStatus(previousStatus) || !isTaskStatus(nextStatus)) {
1140
+ return null;
1141
+ }
1142
+ if (previousStatus === nextStatus) {
1143
+ return null;
1144
+ }
1145
+ return { fromStatus: previousStatus, toStatus: nextStatus };
1146
+ }
1147
+ function logStatusTransition({
1148
+ vaultPath,
1149
+ task,
1150
+ fromStatus,
1151
+ toStatus,
1152
+ frontmatter,
1153
+ options
1154
+ }) {
1155
+ const normalizedReason = typeof options.reason === "string" ? options.reason.trim() : "";
1156
+ const reason = normalizedReason || (isRegression(fromStatus, toStatus) ? `regression: ${fromStatus} -> ${toStatus}` : void 0);
1157
+ const event = buildTransitionEvent(task.slug, fromStatus, toStatus, {
1158
+ confidence: options.confidence,
1159
+ reason
1160
+ });
1161
+ try {
1162
+ appendTransition(vaultPath, event);
1163
+ } catch {
1164
+ return frontmatter;
1165
+ }
1166
+ if (toStatus !== "blocked" || frontmatter.escalation) {
1167
+ return frontmatter;
1168
+ }
1169
+ let blockedCount = 0;
1170
+ try {
1171
+ blockedCount = countBlockedTransitions(vaultPath, task.slug);
1172
+ } catch {
1173
+ return frontmatter;
1174
+ }
1175
+ if (blockedCount < 3) {
1176
+ return frontmatter;
1177
+ }
1178
+ const escalatedFrontmatter = {
1179
+ ...frontmatter,
1180
+ escalation: true
1181
+ };
1182
+ try {
1183
+ persistTaskFrontmatter(task, escalatedFrontmatter);
1184
+ return escalatedFrontmatter;
1185
+ } catch {
1186
+ return frontmatter;
1187
+ }
1188
+ }
1189
+ function readTask(vaultPath, slug) {
1190
+ const taskPath = getTaskPath(vaultPath, slug);
1191
+ if (!fs2.existsSync(taskPath)) {
1192
+ return null;
1193
+ }
1194
+ try {
1195
+ const raw = fs2.readFileSync(taskPath, "utf-8");
1196
+ const { data, content } = (0, import_gray_matter2.default)(raw);
1197
+ const title = extractTitle(content) || slug;
1198
+ return {
1199
+ slug,
1200
+ title,
1201
+ content,
1202
+ frontmatter: data,
1203
+ path: taskPath
1204
+ };
1205
+ } catch {
1206
+ return null;
1207
+ }
1208
+ }
1209
+ function readBacklogItem(vaultPath, slug) {
1210
+ const backlogPath = getBacklogPath(vaultPath, slug);
1211
+ if (!fs2.existsSync(backlogPath)) {
1212
+ return null;
1213
+ }
1214
+ try {
1215
+ const raw = fs2.readFileSync(backlogPath, "utf-8");
1216
+ const { data, content } = (0, import_gray_matter2.default)(raw);
1217
+ const title = extractTitle(content) || slug;
1218
+ return {
1219
+ slug,
1220
+ title,
1221
+ content,
1222
+ frontmatter: data,
1223
+ path: backlogPath
1224
+ };
1225
+ } catch {
1226
+ return null;
1227
+ }
1228
+ }
1229
+ function listTasks(vaultPath, filters) {
1230
+ const tasksDir = getTasksDir(vaultPath);
1231
+ if (!fs2.existsSync(tasksDir)) {
1232
+ return [];
1233
+ }
1234
+ const tasks = [];
1235
+ const entries = fs2.readdirSync(tasksDir, { withFileTypes: true });
1236
+ const today = startOfToday();
1237
+ for (const entry of entries) {
1238
+ if (!entry.isFile() || !entry.name.endsWith(".md")) {
1239
+ continue;
1240
+ }
1241
+ const slug = entry.name.replace(/\.md$/, "");
1242
+ const task = readTask(vaultPath, slug);
1243
+ if (!task) continue;
1244
+ if (filters) {
1245
+ if (filters.status && task.frontmatter.status !== filters.status) continue;
1246
+ if (filters.owner && task.frontmatter.owner !== filters.owner) continue;
1247
+ if (filters.project && task.frontmatter.project !== filters.project) continue;
1248
+ if (filters.priority && task.frontmatter.priority !== filters.priority) continue;
1249
+ if (filters.due && !task.frontmatter.due) continue;
1250
+ if (filters.tag) {
1251
+ const tags = task.frontmatter.tags || [];
1252
+ const hasTag = tags.some((tag) => tag.toLowerCase() === filters.tag?.toLowerCase());
1253
+ if (!hasTag) continue;
1254
+ }
1255
+ if (filters.overdue) {
1256
+ const dueTime = parseDueDate(task.frontmatter.due);
1257
+ if (task.frontmatter.status === "done" || dueTime === null || dueTime >= today) continue;
1258
+ }
1259
+ }
1260
+ tasks.push(task);
1261
+ }
1262
+ const priorityOrder = {
1263
+ critical: 0,
1264
+ high: 1,
1265
+ medium: 2,
1266
+ low: 3
1267
+ };
1268
+ if (filters?.due || filters?.overdue) {
1269
+ return tasks.sort((a, b) => {
1270
+ const aDue = parseDueDate(a.frontmatter.due);
1271
+ const bDue = parseDueDate(b.frontmatter.due);
1272
+ if (aDue !== null && bDue !== null && aDue !== bDue) {
1273
+ return aDue - bDue;
1274
+ }
1275
+ if (aDue !== null && bDue === null) return -1;
1276
+ if (aDue === null && bDue !== null) return 1;
1277
+ return new Date(b.frontmatter.created).getTime() - new Date(a.frontmatter.created).getTime();
1278
+ });
1279
+ }
1280
+ return tasks.sort((a, b) => {
1281
+ const aPriority = priorityOrder[a.frontmatter.priority || "low"];
1282
+ const bPriority = priorityOrder[b.frontmatter.priority || "low"];
1283
+ if (aPriority !== bPriority) {
1284
+ return aPriority - bPriority;
1285
+ }
1286
+ return new Date(b.frontmatter.created).getTime() - new Date(a.frontmatter.created).getTime();
1287
+ });
1288
+ }
1289
+ function listBacklogItems(vaultPath, filters) {
1290
+ const backlogDir = getBacklogDir(vaultPath);
1291
+ if (!fs2.existsSync(backlogDir)) {
1292
+ return [];
1293
+ }
1294
+ const items = [];
1295
+ const entries = fs2.readdirSync(backlogDir, { withFileTypes: true });
1296
+ for (const entry of entries) {
1297
+ if (!entry.isFile() || !entry.name.endsWith(".md")) {
1298
+ continue;
1299
+ }
1300
+ const slug = entry.name.replace(/\.md$/, "");
1301
+ const item = readBacklogItem(vaultPath, slug);
1302
+ if (!item) continue;
1303
+ if (filters) {
1304
+ if (filters.project && item.frontmatter.project !== filters.project) continue;
1305
+ if (filters.source && item.frontmatter.source !== filters.source) continue;
1306
+ }
1307
+ items.push(item);
1308
+ }
1309
+ return items.sort((a, b) => {
1310
+ return new Date(b.frontmatter.created).getTime() - new Date(a.frontmatter.created).getTime();
1311
+ });
1312
+ }
1313
+ function updateTask(vaultPath, slug, updates, options = {}) {
1314
+ const task = readTask(vaultPath, slug);
1315
+ if (!task) {
1316
+ throw new Error(`Task not found: ${slug}`);
1317
+ }
1318
+ if (updates.status !== void 0 && !isTaskStatus(updates.status)) {
1319
+ throw new Error(`Invalid task status: ${String(updates.status)}`);
1320
+ }
1321
+ const previousStatus = task.frontmatter.status;
1322
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1323
+ let newFrontmatter = {
1324
+ ...task.frontmatter,
1325
+ updated: now
1326
+ };
1327
+ if (updates.status !== void 0) {
1328
+ newFrontmatter.status = updates.status;
1329
+ if (updates.status === "done" && !newFrontmatter.completed) {
1330
+ newFrontmatter.completed = now;
1331
+ }
1332
+ if (updates.status !== "done") {
1333
+ delete newFrontmatter.completed;
1334
+ }
1335
+ }
1336
+ if (updates.source !== void 0) {
1337
+ if (updates.source === null || updates.source.trim() === "") {
1338
+ delete newFrontmatter.source;
1339
+ } else {
1340
+ newFrontmatter.source = updates.source;
1341
+ }
1342
+ }
1343
+ if (updates.owner !== void 0) {
1344
+ if (updates.owner === null || updates.owner.trim() === "") {
1345
+ delete newFrontmatter.owner;
1346
+ } else {
1347
+ newFrontmatter.owner = updates.owner;
1348
+ }
1349
+ }
1350
+ if (updates.project !== void 0) {
1351
+ if (updates.project === null || updates.project.trim() === "") {
1352
+ delete newFrontmatter.project;
1353
+ } else {
1354
+ newFrontmatter.project = updates.project;
1355
+ }
1356
+ }
1357
+ if (updates.priority !== void 0) {
1358
+ if (updates.priority === null) {
1359
+ delete newFrontmatter.priority;
1360
+ } else {
1361
+ newFrontmatter.priority = updates.priority;
1362
+ }
1363
+ }
1364
+ if (updates.due !== void 0) {
1365
+ if (updates.due === null || updates.due.trim() === "") {
1366
+ delete newFrontmatter.due;
1367
+ } else {
1368
+ newFrontmatter.due = updates.due;
1369
+ }
1370
+ }
1371
+ if (updates.tags !== void 0) {
1372
+ if (updates.tags === null) {
1373
+ delete newFrontmatter.tags;
1374
+ } else {
1375
+ const normalizedTags = updates.tags.map((tag) => tag.trim()).filter(Boolean);
1376
+ if (normalizedTags.length === 0) {
1377
+ delete newFrontmatter.tags;
1378
+ } else {
1379
+ newFrontmatter.tags = normalizedTags;
1380
+ }
1381
+ }
1382
+ }
1383
+ if (updates.completed !== void 0) {
1384
+ if (updates.completed === null || updates.completed.trim() === "") {
1385
+ delete newFrontmatter.completed;
1386
+ } else {
1387
+ newFrontmatter.completed = updates.completed;
1388
+ }
1389
+ }
1390
+ if (updates.escalation !== void 0) {
1391
+ if (updates.escalation === null) {
1392
+ delete newFrontmatter.escalation;
1393
+ } else {
1394
+ newFrontmatter.escalation = updates.escalation;
1395
+ }
1396
+ }
1397
+ if (updates.confidence !== void 0) {
1398
+ if (updates.confidence === null) {
1399
+ delete newFrontmatter.confidence;
1400
+ } else {
1401
+ newFrontmatter.confidence = updates.confidence;
1402
+ }
1403
+ }
1404
+ if (updates.reason !== void 0) {
1405
+ if (updates.reason === null || updates.reason.trim() === "") {
1406
+ delete newFrontmatter.reason;
1407
+ } else {
1408
+ newFrontmatter.reason = updates.reason;
1409
+ }
1410
+ }
1411
+ if (updates.description !== void 0) {
1412
+ if (updates.description === null || updates.description.trim() === "") {
1413
+ delete newFrontmatter.description;
1414
+ } else {
1415
+ newFrontmatter.description = updates.description;
1416
+ }
1417
+ }
1418
+ if (updates.estimate !== void 0) {
1419
+ if (updates.estimate === null || updates.estimate.trim() === "") {
1420
+ delete newFrontmatter.estimate;
1421
+ } else {
1422
+ newFrontmatter.estimate = updates.estimate;
1423
+ }
1424
+ }
1425
+ if (updates.parent !== void 0) {
1426
+ if (updates.parent === null || updates.parent.trim() === "") {
1427
+ delete newFrontmatter.parent;
1428
+ } else {
1429
+ newFrontmatter.parent = updates.parent;
1430
+ }
1431
+ }
1432
+ if (updates.depends_on !== void 0) {
1433
+ if (updates.depends_on === null) {
1434
+ delete newFrontmatter.depends_on;
1435
+ } else {
1436
+ const normalizedDeps = updates.depends_on.map((dep) => dep.trim()).filter(Boolean);
1437
+ if (normalizedDeps.length === 0) {
1438
+ delete newFrontmatter.depends_on;
1439
+ } else {
1440
+ newFrontmatter.depends_on = normalizedDeps;
1441
+ }
1442
+ }
1443
+ }
1444
+ if (updates.blocked_by !== void 0) {
1445
+ if (updates.blocked_by === null || updates.blocked_by.trim() === "") {
1446
+ delete newFrontmatter.blocked_by;
1447
+ } else {
1448
+ newFrontmatter.blocked_by = updates.blocked_by;
1449
+ }
1450
+ } else if (updates.status !== void 0 && updates.status !== "blocked") {
1451
+ delete newFrontmatter.blocked_by;
1452
+ }
1453
+ persistTaskFrontmatter(task, newFrontmatter);
1454
+ const transition = options.skipTransition ? null : resolveStatusTransition(previousStatus, newFrontmatter.status);
1455
+ if (transition) {
1456
+ const confidence = options.confidence ?? (typeof updates.confidence === "number" ? updates.confidence : void 0);
1457
+ const reason = options.reason ?? updates.reason ?? null;
1458
+ newFrontmatter = logStatusTransition({
1459
+ vaultPath,
1460
+ task,
1461
+ fromStatus: transition.fromStatus,
1462
+ toStatus: transition.toStatus,
1463
+ frontmatter: newFrontmatter,
1464
+ options: {
1465
+ confidence,
1466
+ reason
1467
+ }
1468
+ });
1469
+ }
1470
+ return {
1471
+ ...task,
1472
+ frontmatter: newFrontmatter
1473
+ };
1474
+ }
1475
+ function createBacklogItem(vaultPath, title, options = {}) {
1476
+ ensureBacklogDir(vaultPath);
1477
+ const slug = slugify(title);
1478
+ const backlogPath = getBacklogPath(vaultPath, slug);
1479
+ if (fs2.existsSync(backlogPath)) {
1480
+ throw new Error(`Backlog item already exists: ${slug}`);
1481
+ }
1482
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1483
+ const frontmatter = {
1484
+ created: now
1485
+ };
1486
+ if (options.source) frontmatter.source = options.source;
1487
+ if (options.project) frontmatter.project = options.project;
1488
+ if (options.tags && options.tags.length > 0) frontmatter.tags = options.tags;
1489
+ let content = `# ${title}
1490
+ `;
1491
+ const links = [];
1492
+ if (options.source) links.push(`[[${options.source}]]`);
1493
+ if (options.project) links.push(`[[${options.project}]]`);
1494
+ if (links.length > 0) {
1495
+ content += `
1496
+ ${links.join(" | ")}
1497
+ `;
1498
+ }
1499
+ if (options.content) {
1500
+ content += `
1501
+ ${options.content}
1502
+ `;
1503
+ }
1504
+ const fileContent = import_gray_matter2.default.stringify(content, frontmatter);
1505
+ fs2.writeFileSync(backlogPath, fileContent);
1506
+ return {
1507
+ slug,
1508
+ title,
1509
+ content,
1510
+ frontmatter,
1511
+ path: backlogPath
1512
+ };
1513
+ }
1514
+ function updateBacklogItem(vaultPath, slug, updates) {
1515
+ const backlogItem = readBacklogItem(vaultPath, slug);
1516
+ if (!backlogItem) {
1517
+ throw new Error(`Backlog item not found: ${slug}`);
1518
+ }
1519
+ const newFrontmatter = {
1520
+ ...backlogItem.frontmatter
1521
+ };
1522
+ if (updates.source !== void 0) newFrontmatter.source = updates.source;
1523
+ if (updates.project !== void 0) newFrontmatter.project = updates.project;
1524
+ if (updates.tags !== void 0) newFrontmatter.tags = updates.tags;
1525
+ if (updates.lastSeen !== void 0) newFrontmatter.lastSeen = updates.lastSeen;
1526
+ const fileContent = import_gray_matter2.default.stringify(backlogItem.content, newFrontmatter);
1527
+ fs2.writeFileSync(backlogItem.path, fileContent);
1528
+ return {
1529
+ ...backlogItem,
1530
+ frontmatter: newFrontmatter
1531
+ };
1532
+ }
1533
+
1534
+ // src/lib/project-utils.ts
1535
+ var fs3 = __toESM(require("fs"), 1);
1536
+ var path3 = __toESM(require("path"), 1);
1537
+ var import_gray_matter3 = __toESM(require("gray-matter"), 1);
1538
+ function extractTitle2(content) {
1539
+ const match = content.match(/^#\s+(.+)$/m);
1540
+ return match ? match[1].trim() : "";
1541
+ }
1542
+ function isDateSlug(slug) {
1543
+ return /^\d{4}-\d{2}-\d{2}$/.test(slug);
1544
+ }
1545
+ function normalizeStringArray(value) {
1546
+ return value.map((item) => item.trim()).filter(Boolean);
1547
+ }
1548
+ function getProjectsDir(vaultPath) {
1549
+ return path3.join(path3.resolve(vaultPath), "projects");
1550
+ }
1551
+ function getProjectPath(vaultPath, slug) {
1552
+ return path3.join(getProjectsDir(vaultPath), `${slug}.md`);
1553
+ }
1554
+ function parseSortableTimestamp(value) {
1555
+ if (!value) return 0;
1556
+ const timestamp = Date.parse(value);
1557
+ return Number.isNaN(timestamp) ? 0 : timestamp;
1558
+ }
1559
+ function normalizeProjectStatus(value) {
1560
+ if (value === "active" || value === "paused" || value === "completed" || value === "archived") {
1561
+ return value;
1562
+ }
1563
+ return "active";
1564
+ }
1565
+ function normalizeProjectFrontmatter(frontmatter) {
1566
+ const normalizedCreated = typeof frontmatter.created === "string" && frontmatter.created ? frontmatter.created : (/* @__PURE__ */ new Date(0)).toISOString();
1567
+ const normalizedUpdated = typeof frontmatter.updated === "string" && frontmatter.updated ? frontmatter.updated : normalizedCreated;
1568
+ const normalized = {
1569
+ ...frontmatter,
1570
+ type: "project",
1571
+ status: normalizeProjectStatus(frontmatter.status),
1572
+ created: normalizedCreated,
1573
+ updated: normalizedUpdated
1574
+ };
1575
+ if (normalized.team) {
1576
+ const team = normalizeStringArray(normalized.team);
1577
+ if (team.length === 0) {
1578
+ delete normalized.team;
1579
+ } else {
1580
+ normalized.team = team;
1581
+ }
1582
+ }
1583
+ if (normalized.tags) {
1584
+ const tags = normalizeStringArray(normalized.tags);
1585
+ if (tags.length === 0) {
1586
+ delete normalized.tags;
1587
+ } else {
1588
+ normalized.tags = tags;
1589
+ }
1590
+ }
1591
+ return normalized;
1592
+ }
1593
+ function listProjects(vaultPath, filters) {
1594
+ const projectsDir = getProjectsDir(vaultPath);
1595
+ if (!fs3.existsSync(projectsDir)) {
1596
+ return [];
1597
+ }
1598
+ const projects = [];
1599
+ const entries = fs3.readdirSync(projectsDir, { withFileTypes: true });
1600
+ for (const entry of entries) {
1601
+ if (!entry.isFile() || !entry.name.endsWith(".md")) {
1602
+ continue;
1603
+ }
1604
+ const slug = entry.name.replace(/\.md$/, "");
1605
+ if (isDateSlug(slug)) {
1606
+ continue;
1607
+ }
1608
+ const project = readProject(vaultPath, slug);
1609
+ if (!project) continue;
1610
+ if (filters) {
1611
+ if (filters.status && project.frontmatter.status !== filters.status) continue;
1612
+ if (filters.owner && project.frontmatter.owner !== filters.owner) continue;
1613
+ if (filters.client && project.frontmatter.client !== filters.client) continue;
1614
+ if (filters.tag) {
1615
+ const tags = project.frontmatter.tags || [];
1616
+ const hasTag = tags.some((tag) => tag.toLowerCase() === filters.tag?.toLowerCase());
1617
+ if (!hasTag) continue;
1618
+ }
1619
+ }
1620
+ projects.push(project);
1621
+ }
1622
+ return projects.sort((left, right) => {
1623
+ const rightTime = parseSortableTimestamp(right.frontmatter.updated || right.frontmatter.created);
1624
+ const leftTime = parseSortableTimestamp(left.frontmatter.updated || left.frontmatter.created);
1625
+ return rightTime - leftTime;
1626
+ });
1627
+ }
1628
+ function readProject(vaultPath, slug) {
1629
+ if (!slug || isDateSlug(slug) || slug.includes(path3.sep)) {
1630
+ return null;
1631
+ }
1632
+ const projectPath = getProjectPath(vaultPath, slug);
1633
+ if (!fs3.existsSync(projectPath)) {
1634
+ return null;
1635
+ }
1636
+ try {
1637
+ const raw = fs3.readFileSync(projectPath, "utf-8");
1638
+ const { data, content } = (0, import_gray_matter3.default)(raw);
1639
+ if (data.type !== "project") {
1640
+ return null;
1641
+ }
1642
+ const frontmatter = normalizeProjectFrontmatter(data);
1643
+ const title = extractTitle2(content) || slug;
1644
+ return {
1645
+ slug,
1646
+ title,
1647
+ content,
1648
+ frontmatter
1649
+ };
1650
+ } catch {
1651
+ return null;
1652
+ }
1653
+ }
1654
+
1655
+ // src/lib/config-manager.ts
1656
+ var fs4 = __toESM(require("fs"), 1);
1657
+ var path4 = __toESM(require("path"), 1);
1658
+
1659
+ // src/types.ts
1660
+ var DEFAULT_CATEGORIES = [
1661
+ "rules",
1662
+ "preferences",
1663
+ "decisions",
1664
+ "patterns",
1665
+ "people",
1666
+ "projects",
1667
+ "goals",
1668
+ "transcripts",
1669
+ "inbox",
1670
+ "templates",
1671
+ "lessons",
1672
+ "agents",
1673
+ "commitments",
1674
+ "handoffs",
1675
+ "research",
1676
+ "tasks",
1677
+ "backlog"
1678
+ ];
1679
+
1680
+ // src/lib/config-manager.ts
1681
+ var CONFIG_FILE = ".clawvault.json";
1682
+ var OBSERVE_PROVIDERS = ["anthropic", "openai", "gemini"];
1683
+ var OBSERVER_COMPRESSION_PROVIDERS = [
1684
+ "anthropic",
1685
+ "openai",
1686
+ "gemini",
1687
+ "openai-compatible",
1688
+ "ollama"
1689
+ ];
1690
+ var THEMES = ["neural", "minimal", "none"];
1691
+ var CONTEXT_PROFILES = ["default", "planning", "incident", "handoff", "auto"];
1692
+ var DEFAULT_THEME = "none";
1693
+ var DEFAULT_OBSERVE_MODEL = "gemini-2.0-flash";
1694
+ var DEFAULT_OBSERVE_PROVIDER = "gemini";
1695
+ var DEFAULT_CONTEXT_MAX_RESULTS = 5;
1696
+ var DEFAULT_CONTEXT_PROFILE = "default";
1697
+ var DEFAULT_GRAPH_MAX_HOPS = 2;
1698
+ var DEFAULT_INJECT_MAX_RESULTS = 8;
1699
+ var DEFAULT_INJECT_USE_LLM = true;
1700
+ var DEFAULT_INJECT_SCOPE = ["global"];
1701
+ function configPathFor(vaultPath) {
1702
+ return path4.join(path4.resolve(vaultPath), CONFIG_FILE);
1703
+ }
1704
+ function readConfigDocument(vaultPath) {
1705
+ const configPath = configPathFor(vaultPath);
1706
+ if (!fs4.existsSync(configPath)) {
1707
+ throw new Error(`No ClawVault config found at ${configPath}`);
1708
+ }
1709
+ try {
1710
+ const parsed = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
1711
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1712
+ throw new Error("Config root must be a JSON object.");
1713
+ }
1714
+ return { ...parsed };
1715
+ } catch (error) {
1716
+ if (error instanceof Error) {
1717
+ throw new Error(`Failed to parse ${CONFIG_FILE}: ${error.message}`);
1718
+ }
1719
+ throw new Error(`Failed to parse ${CONFIG_FILE}.`);
1720
+ }
1721
+ }
1722
+ function asStringArray(value) {
1723
+ if (!Array.isArray(value)) {
1724
+ return null;
1725
+ }
1726
+ const output = value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
1727
+ return output.length > 0 ? output : null;
1728
+ }
1729
+ function asPositiveInteger(value) {
1730
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) {
1731
+ return value;
1732
+ }
1733
+ if (typeof value === "string") {
1734
+ const parsed = Number.parseInt(value, 10);
1735
+ if (Number.isInteger(parsed) && parsed > 0) {
1736
+ return parsed;
1737
+ }
1738
+ }
1739
+ return null;
1740
+ }
1741
+ function asBoolean(value) {
1742
+ if (typeof value === "boolean") {
1743
+ return value;
1744
+ }
1745
+ if (typeof value === "string") {
1746
+ const normalized = value.trim().toLowerCase();
1747
+ if (["true", "1", "yes", "on"].includes(normalized)) {
1748
+ return true;
1749
+ }
1750
+ if (["false", "0", "no", "off"].includes(normalized)) {
1751
+ return false;
1752
+ }
1753
+ }
1754
+ return null;
1755
+ }
1756
+ function isObserveProvider(value) {
1757
+ return typeof value === "string" && OBSERVE_PROVIDERS.includes(value);
1758
+ }
1759
+ function isObserverCompressionProvider(value) {
1760
+ return typeof value === "string" && OBSERVER_COMPRESSION_PROVIDERS.includes(value);
1761
+ }
1762
+ function isTheme(value) {
1763
+ return typeof value === "string" && THEMES.includes(value);
1764
+ }
1765
+ function isContextProfile(value) {
1766
+ return typeof value === "string" && CONTEXT_PROFILES.includes(value);
1767
+ }
1768
+ function normalizeRouteTarget(target) {
1769
+ const trimmed = target.trim().replace(/^\/+/, "").replace(/\/+$/, "");
1770
+ if (!trimmed) {
1771
+ throw new Error("Route target cannot be empty.");
1772
+ }
1773
+ const segments = trimmed.split("/").map((segment) => segment.trim()).filter(Boolean);
1774
+ if (segments.length === 0) {
1775
+ throw new Error("Route target cannot be empty.");
1776
+ }
1777
+ if (segments.some((segment) => segment === "." || segment === "..")) {
1778
+ throw new Error(`Route target cannot contain relative path segments: ${target}`);
1779
+ }
1780
+ return segments.join("/");
1781
+ }
1782
+ function normalizeRouteRule(raw) {
1783
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1784
+ return null;
1785
+ }
1786
+ const record = raw;
1787
+ const pattern = typeof record.pattern === "string" ? record.pattern.trim() : "";
1788
+ const target = typeof record.target === "string" ? record.target.trim() : "";
1789
+ const priority = asPositiveInteger(record.priority);
1790
+ if (!pattern || !target || priority === null) {
1791
+ return null;
1792
+ }
1793
+ return {
1794
+ pattern,
1795
+ target: normalizeRouteTarget(target),
1796
+ priority
1797
+ };
1798
+ }
1799
+ function normalizeRoutes(value) {
1800
+ if (!Array.isArray(value)) {
1801
+ return [];
1802
+ }
1803
+ return value.map((entry) => normalizeRouteRule(entry)).filter((entry) => entry !== null).sort((left, right) => right.priority - left.priority || left.pattern.localeCompare(right.pattern));
1804
+ }
1805
+ function parseRegexLiteral(pattern) {
1806
+ const match = pattern.match(/^\/(.+)\/([a-z]*)$/i);
1807
+ if (!match) {
1808
+ return null;
1809
+ }
1810
+ try {
1811
+ return new RegExp(match[1], match[2]);
1812
+ } catch (error) {
1813
+ throw new Error(`Invalid route regex pattern "${pattern}": ${error instanceof Error ? error.message : "parse error"}`);
1814
+ }
1815
+ }
1816
+ function withDefaults(vaultPath, config) {
1817
+ const resolvedPath = path4.resolve(vaultPath);
1818
+ const defaults = {
1819
+ name: path4.basename(resolvedPath),
1820
+ categories: [...DEFAULT_CATEGORIES],
1821
+ theme: DEFAULT_THEME,
1822
+ observe: {
1823
+ model: DEFAULT_OBSERVE_MODEL,
1824
+ provider: DEFAULT_OBSERVE_PROVIDER
1825
+ },
1826
+ observer: {
1827
+ compression: {}
1828
+ },
1829
+ context: {
1830
+ maxResults: DEFAULT_CONTEXT_MAX_RESULTS,
1831
+ defaultProfile: DEFAULT_CONTEXT_PROFILE
1832
+ },
1833
+ graph: {
1834
+ maxHops: DEFAULT_GRAPH_MAX_HOPS
1835
+ },
1836
+ inject: {
1837
+ maxResults: DEFAULT_INJECT_MAX_RESULTS,
1838
+ useLlm: DEFAULT_INJECT_USE_LLM,
1839
+ scope: [...DEFAULT_INJECT_SCOPE]
1840
+ },
1841
+ routes: []
1842
+ };
1843
+ const observeRecord = config.observe && typeof config.observe === "object" && !Array.isArray(config.observe) ? config.observe : {};
1844
+ const contextRecord = config.context && typeof config.context === "object" && !Array.isArray(config.context) ? config.context : {};
1845
+ const observerRecord = config.observer && typeof config.observer === "object" && !Array.isArray(config.observer) ? config.observer : {};
1846
+ const compressionRecord = observerRecord.compression && typeof observerRecord.compression === "object" && !Array.isArray(observerRecord.compression) ? observerRecord.compression : {};
1847
+ const graphRecord = config.graph && typeof config.graph === "object" && !Array.isArray(config.graph) ? config.graph : {};
1848
+ const compressionProvider = isObserverCompressionProvider(compressionRecord.provider) ? compressionRecord.provider : void 0;
1849
+ const compressionModel = typeof compressionRecord.model === "string" && compressionRecord.model.trim() ? compressionRecord.model.trim() : void 0;
1850
+ const compressionBaseUrl = typeof compressionRecord.baseUrl === "string" && compressionRecord.baseUrl.trim() ? compressionRecord.baseUrl.trim() : void 0;
1851
+ const compressionApiKey = typeof compressionRecord.apiKey === "string" && compressionRecord.apiKey.trim() ? compressionRecord.apiKey.trim() : void 0;
1852
+ const normalizedCompression = {};
1853
+ if (compressionProvider) {
1854
+ normalizedCompression.provider = compressionProvider;
1855
+ }
1856
+ if (compressionModel) {
1857
+ normalizedCompression.model = compressionModel;
1858
+ }
1859
+ if (compressionBaseUrl) {
1860
+ normalizedCompression.baseUrl = compressionBaseUrl;
1861
+ }
1862
+ if (compressionApiKey) {
1863
+ normalizedCompression.apiKey = compressionApiKey;
1864
+ }
1865
+ const injectRecord = config.inject && typeof config.inject === "object" && !Array.isArray(config.inject) ? config.inject : {};
1866
+ return {
1867
+ ...config,
1868
+ name: typeof config.name === "string" && config.name.trim() ? config.name.trim() : defaults.name,
1869
+ categories: asStringArray(config.categories) ?? defaults.categories,
1870
+ theme: isTheme(config.theme) ? config.theme : defaults.theme,
1871
+ observe: {
1872
+ ...observeRecord,
1873
+ model: typeof observeRecord.model === "string" && observeRecord.model.trim() ? observeRecord.model.trim() : defaults.observe.model,
1874
+ provider: isObserveProvider(observeRecord.provider) ? observeRecord.provider : defaults.observe.provider
1875
+ },
1876
+ observer: {
1877
+ ...observerRecord,
1878
+ compression: normalizedCompression
1879
+ },
1880
+ context: {
1881
+ ...contextRecord,
1882
+ maxResults: asPositiveInteger(contextRecord.maxResults) ?? defaults.context.maxResults,
1883
+ defaultProfile: isContextProfile(contextRecord.defaultProfile) ? contextRecord.defaultProfile : defaults.context.defaultProfile
1884
+ },
1885
+ graph: {
1886
+ ...graphRecord,
1887
+ maxHops: asPositiveInteger(graphRecord.maxHops) ?? defaults.graph.maxHops
1888
+ },
1889
+ inject: {
1890
+ ...injectRecord,
1891
+ maxResults: asPositiveInteger(injectRecord.maxResults) ?? defaults.inject.maxResults,
1892
+ useLlm: asBoolean(injectRecord.useLlm) ?? defaults.inject.useLlm,
1893
+ scope: asStringArray(injectRecord.scope) ?? (typeof injectRecord.scope === "string" ? injectRecord.scope.split(",").map((entry) => entry.trim()).filter(Boolean) : null) ?? [...defaults.inject.scope]
1894
+ },
1895
+ routes: normalizeRoutes(config.routes)
1896
+ };
1897
+ }
1898
+ function listConfig(vaultPath) {
1899
+ const config = readConfigDocument(vaultPath);
1900
+ return withDefaults(vaultPath, config);
1901
+ }
1902
+ function listRouteRules(vaultPath) {
1903
+ const config = listConfig(vaultPath);
1904
+ return normalizeRoutes(config.routes);
1905
+ }
1906
+ function matchRouteRule(text, routes) {
1907
+ for (const route of routes) {
1908
+ const regex = parseRegexLiteral(route.pattern);
1909
+ if (regex) {
1910
+ if (regex.test(text)) {
1911
+ return route;
1912
+ }
1913
+ continue;
1914
+ }
1915
+ if (text.toLowerCase().includes(route.pattern.toLowerCase())) {
1916
+ return route;
1917
+ }
1918
+ }
1919
+ return null;
1920
+ }
1921
+
1922
+ // src/observer/router.ts
1923
+ var CATEGORY_PATTERNS = [
1924
+ {
1925
+ category: "decisions",
1926
+ patterns: [
1927
+ /\b(decid(?:e|ed|ing|ion)|chose|picked|went with|selected|opted)\b/i,
1928
+ /\b(decision|trade[- ]?off|alternative|rationale)\b/i
1929
+ ]
1930
+ },
1931
+ {
1932
+ category: "lessons",
1933
+ patterns: [
1934
+ /\b(learn(?:ed|ing|t)|lesson|mistake|insight|realized|discovered)\b/i,
1935
+ /\b(note to self|remember|important|don'?t forget|never again)\b/i
1936
+ ]
1937
+ },
1938
+ {
1939
+ category: "people",
1940
+ patterns: [
1941
+ /\b(said|asked|told|mentioned|emailed|called|messaged|met with)\b/i,
1942
+ /\b(client|partner|team|colleague|contact)\b/i,
1943
+ /\b(?:Pedro|Justin|Maria|Sarah|[A-Z][a-z]+ (?:said|asked|told|mentioned))\b/,
1944
+ /\b(?:talked to|met with|spoke with|chatted with|discussed with)\s+[A-Z][a-z]+\b/i,
1945
+ /\b[A-Z][a-z]+\s+(?:from|at)\s+[A-Z]/,
1946
+ /\b[A-Z][a-z]+\s+from\b/
1947
+ ]
1948
+ },
1949
+ {
1950
+ category: "preferences",
1951
+ patterns: [
1952
+ /\b(prefer(?:s|red|ence)?|like(?:s|d)?|want(?:s|ed)?|style|convention)\b/i,
1953
+ /\b(always use|never use|default to)\b/i
1954
+ ]
1955
+ },
1956
+ {
1957
+ category: "commitments",
1958
+ patterns: [
1959
+ /\b(promised|committed|deadline|due|scheduled|will do|agreed to)\b/i,
1960
+ /\b(todo|task|action item|follow[- ]?up)\b/i
1961
+ ]
1962
+ },
1963
+ {
1964
+ category: "projects",
1965
+ patterns: [
1966
+ /\b(deployed|shipped|launched|released|merged|built|created)\b/i,
1967
+ /\b(project|repo|service|api|feature|bug fix)\b/i
1968
+ ]
1969
+ }
1970
+ ];
1971
+ var TYPE_TO_CATEGORY = {
1972
+ decision: "decisions",
1973
+ preference: "preferences",
1974
+ fact: "facts",
1975
+ commitment: "commitments",
1976
+ task: "commitments",
1977
+ todo: "commitments",
1978
+ "commitment-unresolved": "commitments",
1979
+ milestone: "projects",
1980
+ lesson: "lessons",
1981
+ relationship: "people",
1982
+ project: "projects"
1983
+ };
1984
+ var PAST_TENSE_TASK_HINT_RE = /\b(completed|shipped|deployed|fixed|merged|finished|resolved|closed)\b/i;
1985
+ var FUTURE_TASK_HINT_RE = /\b(need to|should|todo|must|plan to)\b/i;
1986
+ var Router = class {
1987
+ vaultPath;
1988
+ extractTasks;
1989
+ now;
1990
+ customRoutes;
1991
+ constructor(vaultPath, options = {}) {
1992
+ this.vaultPath = path5.resolve(vaultPath);
1993
+ this.extractTasks = options.extractTasks ?? true;
1994
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
1995
+ this.customRoutes = this.loadCustomRoutes();
1996
+ }
1997
+ /**
1998
+ * Takes observation markdown and routes items to appropriate vault categories.
1999
+ * Routes only items with importance >= 0.4.
2000
+ * Returns a summary of what was routed where.
2001
+ */
2002
+ route(observationMarkdown, context = {}) {
2003
+ this.customRoutes = this.loadCustomRoutes();
2004
+ const items = this.parseObservations(observationMarkdown);
2005
+ const routed = [];
2006
+ const knownWorkItems = this.extractTasks ? this.loadExistingWorkItems() : [];
2007
+ const knownProjectDefinitions = this.loadKnownProjectDefinitions();
2008
+ let dedupHits = 0;
2009
+ for (const item of items) {
2010
+ if (item.importance < 0.4) continue;
2011
+ if (this.extractTasks && this.isTaskObservation(item.type)) {
2012
+ const taskResult = this.routeTaskObservation(item, context, knownWorkItems);
2013
+ if (taskResult.routedItem) {
2014
+ routed.push(taskResult.routedItem);
2015
+ }
2016
+ if (taskResult.dedupHit) {
2017
+ dedupHits += 1;
2018
+ }
2019
+ continue;
2020
+ }
2021
+ const category = this.categorize(item.type, item.content);
2022
+ if (!category) continue;
2023
+ const routedItem = {
2024
+ category,
2025
+ title: item.title,
2026
+ content: item.content,
2027
+ type: item.type,
2028
+ confidence: item.confidence,
2029
+ importance: item.importance,
2030
+ date: item.date
2031
+ };
2032
+ routed.push(routedItem);
2033
+ this.appendToCategory(category, routedItem, knownProjectDefinitions);
2034
+ }
2035
+ const summary = this.buildSummary(routed, dedupHits);
2036
+ return { routed, summary };
2037
+ }
2038
+ isTaskObservation(type) {
2039
+ return type === "task" || type === "todo" || type === "commitment-unresolved";
2040
+ }
2041
+ routeTaskObservation(item, context, knownWorkItems) {
2042
+ if (this.shouldSkipCompletedTaskCandidate(item.content)) {
2043
+ console.log("[observer] skipped likely-completed task candidate");
2044
+ return { routedItem: null, dedupHit: false };
2045
+ }
2046
+ const title = this.deriveTaskTitle(item.content, item.type);
2047
+ if (!title) {
2048
+ return { routedItem: null, dedupHit: false };
2049
+ }
2050
+ const duplicate = this.findDuplicateWorkItem(title, knownWorkItems);
2051
+ if (duplicate) {
2052
+ if (item.type === "commitment-unresolved" && this.isOpenWorkItem(duplicate)) {
2053
+ this.touchExistingWorkItem(duplicate);
2054
+ }
2055
+ console.log(`[observer] dedup hit for task candidate: "${title}"`);
2056
+ return { routedItem: null, dedupHit: true };
2057
+ }
2058
+ const tags = this.mergeTags(
2059
+ ["open", "observer"],
2060
+ item.type === "task" ? ["task"] : [],
2061
+ item.type === "todo" ? ["todo"] : [],
2062
+ item.type === "commitment-unresolved" ? ["commitment"] : []
2063
+ );
2064
+ const content = this.buildTaskContextContent(item, context);
2065
+ let backlogItem;
2066
+ try {
2067
+ backlogItem = createBacklogItem(this.vaultPath, title, {
2068
+ source: "observer",
2069
+ content,
2070
+ tags
2071
+ });
2072
+ } catch (error) {
2073
+ if (error instanceof Error && /already exists/i.test(error.message)) {
2074
+ console.log(`[observer] dedup hit for task candidate: "${title}"`);
2075
+ return { routedItem: null, dedupHit: true };
2076
+ }
2077
+ throw error;
2078
+ }
2079
+ knownWorkItems.push({
2080
+ kind: "backlog",
2081
+ slug: backlogItem.slug,
2082
+ title: backlogItem.title,
2083
+ status: "open",
2084
+ source: backlogItem.frontmatter.source,
2085
+ tags: backlogItem.frontmatter.tags ?? []
2086
+ });
2087
+ return {
2088
+ dedupHit: false,
2089
+ routedItem: {
2090
+ category: "backlog",
2091
+ title: backlogItem.title,
2092
+ content: item.content,
2093
+ type: item.type,
2094
+ confidence: item.confidence,
2095
+ importance: item.importance,
2096
+ date: item.date
2097
+ }
2098
+ };
2099
+ }
2100
+ loadExistingWorkItems() {
2101
+ const taskItems = listTasks(this.vaultPath).map((task) => ({
2102
+ kind: "task",
2103
+ slug: task.slug,
2104
+ title: task.title,
2105
+ status: task.frontmatter.status,
2106
+ source: task.frontmatter.source,
2107
+ tags: task.frontmatter.tags ?? []
2108
+ }));
2109
+ const backlogItems = listBacklogItems(this.vaultPath).map((item) => ({
2110
+ kind: "backlog",
2111
+ slug: item.slug,
2112
+ title: item.title,
2113
+ status: item.frontmatter.tags?.includes("done") ? "done" : "open",
2114
+ source: item.frontmatter.source,
2115
+ tags: item.frontmatter.tags ?? []
2116
+ }));
2117
+ return [...taskItems, ...backlogItems];
2118
+ }
2119
+ findDuplicateWorkItem(title, knownWorkItems) {
2120
+ const normalizedTitle = this.normalizeTaskTitle(title);
2121
+ if (!normalizedTitle) {
2122
+ return null;
2123
+ }
2124
+ for (const item of knownWorkItems) {
2125
+ const normalizedExisting = this.normalizeTaskTitle(item.title);
2126
+ if (!normalizedExisting) {
2127
+ continue;
2128
+ }
2129
+ if (normalizedExisting === normalizedTitle) {
2130
+ return item;
2131
+ }
2132
+ if (this.jaccardWordSimilarity(normalizedTitle, normalizedExisting) > 0.8) {
2133
+ return item;
2134
+ }
2135
+ }
2136
+ return null;
2137
+ }
2138
+ normalizeTaskTitle(title) {
2139
+ return title.toLowerCase().replace(/[^\w\s]/g, " ").replace(/\s+/g, " ").trim().slice(0, 50);
2140
+ }
2141
+ jaccardWordSimilarity(a, b) {
2142
+ const aWords = new Set(a.split(" ").filter(Boolean));
2143
+ const bWords = new Set(b.split(" ").filter(Boolean));
2144
+ if (aWords.size === 0 || bWords.size === 0) {
2145
+ return 0;
2146
+ }
2147
+ let intersection = 0;
2148
+ for (const word of aWords) {
2149
+ if (bWords.has(word)) {
2150
+ intersection += 1;
2151
+ }
2152
+ }
2153
+ const unionSize = aWords.size + bWords.size - intersection;
2154
+ return unionSize === 0 ? 0 : intersection / unionSize;
2155
+ }
2156
+ deriveTaskTitle(content, type) {
2157
+ let title = content.replace(/^\d{2}:\d{2}\s+/, "").replace(/\[[^\]]+\]\s*/g, "").trim();
2158
+ if (type === "todo") {
2159
+ title = title.replace(
2160
+ /^(?:todo:\s*|we need to\s+|don't forget(?: to)?\s+|remember to\s+|make sure to\s+)/i,
2161
+ ""
2162
+ );
2163
+ } else if (type === "task") {
2164
+ title = title.replace(
2165
+ /^(?:i'?ll\s+|i will\s+|let me\s+|(?:i'?m\s+)?going to\s+|plan to\s+|should\s+)/i,
2166
+ ""
2167
+ );
2168
+ } else if (type === "commitment-unresolved") {
2169
+ title = title.replace(/^(?:need to figure out\s+|tbd[:\s-]*|to be determined[:\s-]*)/i, "");
2170
+ }
2171
+ title = title.replace(/\s+/g, " ").replace(/^[^a-zA-Z0-9]+/, "").replace(/[.?!:;,]+$/, "").trim();
2172
+ return title.slice(0, 120);
2173
+ }
2174
+ shouldSkipCompletedTaskCandidate(content) {
2175
+ if (!PAST_TENSE_TASK_HINT_RE.test(content)) {
2176
+ return false;
2177
+ }
2178
+ return !FUTURE_TASK_HINT_RE.test(content);
2179
+ }
2180
+ buildTaskContextContent(item, context) {
2181
+ const lines = ["Auto-extracted by observer from session transcript."];
2182
+ if (context.sessionKey) {
2183
+ lines.push(`Session: ${context.sessionKey}`);
2184
+ }
2185
+ if (context.transcriptId) {
2186
+ lines.push(`Transcript: ${context.transcriptId}`);
2187
+ }
2188
+ if (context.source) {
2189
+ lines.push(`Source: ${context.source}`);
2190
+ }
2191
+ const approximateTimestamp = this.extractApproximateTimestamp(item.date, item.content, context.timestamp);
2192
+ lines.push(`Approximate timestamp: ${approximateTimestamp}`);
2193
+ lines.push(`Observation type: ${item.type}`);
2194
+ lines.push(`Original observation: ${item.content}`);
2195
+ return lines.join("\n");
2196
+ }
2197
+ extractApproximateTimestamp(date, content, timestamp) {
2198
+ if (timestamp) {
2199
+ return timestamp.toISOString();
2200
+ }
2201
+ const timeMatch = content.match(/\b([01]\d|2[0-3]):([0-5]\d)\b/);
2202
+ if (timeMatch) {
2203
+ return `${date} ${timeMatch[0]}`;
2204
+ }
2205
+ return date;
2206
+ }
2207
+ isOpenWorkItem(item) {
2208
+ if (item.kind === "task") {
2209
+ return item.status !== "done";
2210
+ }
2211
+ return item.status !== "done";
2212
+ }
2213
+ touchExistingWorkItem(item) {
2214
+ if (item.kind === "task") {
2215
+ if (!this.isOpenWorkItem(item)) {
2216
+ return;
2217
+ }
2218
+ updateTask(this.vaultPath, item.slug, {});
2219
+ return;
2220
+ }
2221
+ const nextTags = this.mergeTags(item.tags, ["commitment"]);
2222
+ updateBacklogItem(this.vaultPath, item.slug, {
2223
+ source: item.source ?? "observer",
2224
+ tags: nextTags,
2225
+ lastSeen: this.now().toISOString()
2226
+ });
2227
+ item.tags = nextTags;
2228
+ }
2229
+ mergeTags(...groups) {
2230
+ const merged = /* @__PURE__ */ new Set();
2231
+ for (const group of groups) {
2232
+ for (const tag of group) {
2233
+ const normalized = tag.trim().toLowerCase();
2234
+ if (normalized) {
2235
+ merged.add(normalized);
2236
+ }
2237
+ }
2238
+ }
2239
+ return [...merged];
2240
+ }
2241
+ parseObservations(markdown) {
2242
+ const records = parseObservationMarkdown(markdown);
2243
+ return records.map((record) => ({
2244
+ type: record.type,
2245
+ confidence: record.confidence,
2246
+ importance: record.importance,
2247
+ content: record.content,
2248
+ date: record.date,
2249
+ title: record.content.slice(0, 80).replace(/[^a-zA-Z0-9\s-]/g, "").trim()
2250
+ }));
2251
+ }
2252
+ categorize(type, content) {
2253
+ const typedCategory = TYPE_TO_CATEGORY[type];
2254
+ if (typedCategory) {
2255
+ return typedCategory;
2256
+ }
2257
+ for (const { category, patterns } of CATEGORY_PATTERNS) {
2258
+ if (patterns.some((p) => p.test(content))) {
2259
+ return category;
2260
+ }
2261
+ }
2262
+ return null;
2263
+ }
2264
+ normalizeForDedup(content) {
2265
+ return normalizeObservationContent(
2266
+ content.replace(/\[\[[^\]]*\]\]/g, (match) => match.replace(/\[\[|\]\]/g, ""))
2267
+ );
2268
+ }
2269
+ /**
2270
+ * Extract entity slug from observation content for people/projects routing.
2271
+ * Returns null if no entity can be identified.
2272
+ */
2273
+ extractEntitySlug(content, category) {
2274
+ if (category !== "people" && category !== "projects") return null;
2275
+ if (category === "people") {
2276
+ const patterns = [
2277
+ /(?:talked to|met with|spoke with|chatted with|discussed with|emailed|called|messaged)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)/,
2278
+ /([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\s+(?:said|asked|told|mentioned|from|at)\b/,
2279
+ /\b(?:client|partner|colleague|contact)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)/i
2280
+ ];
2281
+ for (const pattern of patterns) {
2282
+ const match = content.match(pattern);
2283
+ if (match?.[1]) return this.toSlug(match[1]);
2284
+ }
2285
+ }
2286
+ if (category === "projects") {
2287
+ const patterns = [
2288
+ /(?:deployed|shipped|launched|released|built|created|working on)\s+([A-Z][a-zA-Z0-9-]+)/,
2289
+ /"([^"]+)"\s+(?:project|repo|service)/i
2290
+ ];
2291
+ for (const pattern of patterns) {
2292
+ const match = content.match(pattern);
2293
+ if (match?.[1]) return this.toSlug(match[1]);
2294
+ }
2295
+ }
2296
+ return null;
2297
+ }
2298
+ toSlug(name) {
2299
+ return name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
2300
+ }
2301
+ normalizeProjectReference(value) {
2302
+ return value.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").trim();
2303
+ }
2304
+ escapeRegExp(value) {
2305
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2306
+ }
2307
+ extractWikiTargets(content) {
2308
+ const targets = [];
2309
+ for (const match of content.matchAll(/\[\[([^\]]+)\]\]/g)) {
2310
+ const candidate = match[1];
2311
+ if (!candidate) continue;
2312
+ const target = candidate.split("|")[0].split("#")[0].trim();
2313
+ if (target) targets.push(target);
2314
+ }
2315
+ return targets;
2316
+ }
2317
+ loadKnownProjectDefinitions() {
2318
+ try {
2319
+ return listProjects(this.vaultPath).map((project) => ({
2320
+ slug: project.slug,
2321
+ normalizedSlug: this.normalizeProjectReference(project.slug),
2322
+ title: project.title,
2323
+ normalizedTitle: project.title.toLowerCase()
2324
+ }));
2325
+ } catch {
2326
+ return [];
2327
+ }
2328
+ }
2329
+ matchKnownProjectSlug(content, knownProjects) {
2330
+ if (knownProjects.length === 0) {
2331
+ return null;
2332
+ }
2333
+ const normalizedContent = content.toLowerCase();
2334
+ const wikiTargets = this.extractWikiTargets(content).map((target) => this.normalizeProjectReference(target));
2335
+ for (const project of knownProjects) {
2336
+ if (wikiTargets.includes(project.normalizedSlug)) {
2337
+ return project.slug;
2338
+ }
2339
+ if (project.normalizedTitle && normalizedContent.includes(project.normalizedTitle)) {
2340
+ return project.slug;
2341
+ }
2342
+ const slugPattern = new RegExp(`\\b${this.escapeRegExp(project.normalizedSlug)}\\b`, "i");
2343
+ if (slugPattern.test(content)) {
2344
+ return project.slug;
2345
+ }
2346
+ }
2347
+ return null;
2348
+ }
2349
+ loadCustomRoutes() {
2350
+ try {
2351
+ return listRouteRules(this.vaultPath);
2352
+ } catch {
2353
+ return [];
2354
+ }
2355
+ }
2356
+ resolveCustomEntityPath(content, category) {
2357
+ if (category !== "people" && category !== "projects" || this.customRoutes.length === 0) {
2358
+ return null;
2359
+ }
2360
+ const matchedRule = matchRouteRule(content, this.customRoutes);
2361
+ if (!matchedRule) {
2362
+ return null;
2363
+ }
2364
+ const targetParts = matchedRule.target.split("/").map((segment) => segment.trim()).filter(Boolean);
2365
+ if (targetParts.length < 2 || targetParts[0] !== category) {
2366
+ return null;
2367
+ }
2368
+ return targetParts.slice(1).join("/");
2369
+ }
2370
+ /**
2371
+ * Resolve the file path for a routed item.
2372
+ * For people/projects: entity-slug subfolder with date file (e.g., people/pedro/2026-02-12.md)
2373
+ * For other categories: category/date.md
2374
+ */
2375
+ resolveFilePath(category, item, knownProjectDefinitions) {
2376
+ const customEntityPath = this.resolveCustomEntityPath(item.content, category);
2377
+ if (customEntityPath) {
2378
+ const customEntityDir = path5.join(this.vaultPath, category, customEntityPath);
2379
+ fs5.mkdirSync(customEntityDir, { recursive: true });
2380
+ return {
2381
+ filePath: path5.join(customEntityDir, `${item.date}.md`),
2382
+ headerLabel: `${category}/${customEntityPath}`
2383
+ };
2384
+ }
2385
+ if (category === "projects") {
2386
+ const matchedProjectSlug = this.matchKnownProjectSlug(item.content, knownProjectDefinitions);
2387
+ if (matchedProjectSlug) {
2388
+ const projectDir = path5.join(this.vaultPath, category, matchedProjectSlug);
2389
+ fs5.mkdirSync(projectDir, { recursive: true });
2390
+ return {
2391
+ filePath: path5.join(projectDir, `${item.date}.md`),
2392
+ headerLabel: `${category}/${matchedProjectSlug}`
2393
+ };
2394
+ }
2395
+ } else {
2396
+ const entitySlug = this.extractEntitySlug(item.content, category);
2397
+ if (entitySlug) {
2398
+ const entityDir = path5.join(this.vaultPath, category, entitySlug);
2399
+ fs5.mkdirSync(entityDir, { recursive: true });
2400
+ return {
2401
+ filePath: path5.join(entityDir, `${item.date}.md`),
2402
+ headerLabel: `${category}/${entitySlug}`
2403
+ };
2404
+ }
2405
+ }
2406
+ const categoryDir = path5.join(this.vaultPath, category);
2407
+ fs5.mkdirSync(categoryDir, { recursive: true });
2408
+ return {
2409
+ filePath: path5.join(categoryDir, `${item.date}.md`),
2410
+ headerLabel: category
2411
+ };
2412
+ }
2413
+ appendToCategory(category, item, knownProjectDefinitions) {
2414
+ const destination = this.resolveFilePath(category, item, knownProjectDefinitions);
2415
+ const filePath = destination.filePath;
2416
+ fs5.mkdirSync(path5.dirname(filePath), { recursive: true });
2417
+ const existing = fs5.existsSync(filePath) ? fs5.readFileSync(filePath, "utf-8").trim() : "";
2418
+ const normalizedNew = this.normalizeForDedup(item.content);
2419
+ const existingLines = existing.split(/\r?\n/);
2420
+ for (const line of existingLines) {
2421
+ const lineContent = line.replace(/^-\s*/, "").trim();
2422
+ const parsed = parseObservationLine(lineContent, item.date);
2423
+ const candidate = parsed ? parsed.content : lineContent;
2424
+ if (this.normalizeForDedup(candidate) === normalizedNew) return;
2425
+ }
2426
+ for (const line of existingLines) {
2427
+ const lineContent = line.replace(/^-\s*/, "").trim();
2428
+ const parsed = parseObservationLine(lineContent, item.date);
2429
+ const normalizedExisting = this.normalizeForDedup(parsed ? parsed.content : lineContent);
2430
+ if (normalizedExisting.length > 10 && normalizedNew.length > 10) {
2431
+ const shorter = normalizedNew.length < normalizedExisting.length ? normalizedNew : normalizedExisting;
2432
+ const longer = normalizedNew.length >= normalizedExisting.length ? normalizedNew : normalizedExisting;
2433
+ if (longer.includes(shorter) || this.similarity(normalizedNew, normalizedExisting) > 0.8) return;
2434
+ }
2435
+ }
2436
+ const linkedContent = this.addWikiLinks(item.content);
2437
+ const entry = renderScoredObservationLine({
2438
+ type: item.type,
2439
+ confidence: item.confidence,
2440
+ importance: item.importance,
2441
+ content: linkedContent
2442
+ });
2443
+ const headerLabel = destination.headerLabel;
2444
+ const header = existing ? "" : `# ${headerLabel} \u2014 ${item.date}
2445
+ `;
2446
+ const newContent = existing ? `${existing}
2447
+ ${entry}
2448
+ ` : `${header}
2449
+ ${entry}
2450
+ `;
2451
+ fs5.writeFileSync(filePath, newContent, "utf-8");
2452
+ }
2453
+ /**
2454
+ * Auto-link proper nouns and known entities with [[wiki-links]].
2455
+ * Scans for capitalized names, project names, and tool names.
2456
+ * Skips content already inside [[brackets]].
2457
+ */
2458
+ addWikiLinks(content) {
2459
+ if (content.includes("[[")) return content;
2460
+ const namePattern = /\b([A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,})?)\b/g;
2461
+ const skipWords = /* @__PURE__ */ new Set([
2462
+ "The",
2463
+ "This",
2464
+ "That",
2465
+ "These",
2466
+ "Those",
2467
+ "There",
2468
+ "Then",
2469
+ "Than",
2470
+ "When",
2471
+ "Where",
2472
+ "What",
2473
+ "Which",
2474
+ "While",
2475
+ "With",
2476
+ "Would",
2477
+ "Will",
2478
+ "Should",
2479
+ "Could",
2480
+ "About",
2481
+ "After",
2482
+ "Before",
2483
+ "Between",
2484
+ "Because",
2485
+ "Also",
2486
+ "Always",
2487
+ "Already",
2488
+ "Another",
2489
+ "Any",
2490
+ "Each",
2491
+ "Every",
2492
+ "From",
2493
+ "Have",
2494
+ "Has",
2495
+ "Had",
2496
+ "Into",
2497
+ "Just",
2498
+ "Keep",
2499
+ "Like",
2500
+ "Made",
2501
+ "Make",
2502
+ "Many",
2503
+ "More",
2504
+ "Most",
2505
+ "Much",
2506
+ "Must",
2507
+ "Need",
2508
+ "Never",
2509
+ "Next",
2510
+ "None",
2511
+ "Not",
2512
+ "Now",
2513
+ "Only",
2514
+ "Other",
2515
+ "Over",
2516
+ "Same",
2517
+ "Some",
2518
+ "Such",
2519
+ "Sure",
2520
+ "Take",
2521
+ "Them",
2522
+ "They",
2523
+ "Too",
2524
+ "Under",
2525
+ "Until",
2526
+ "Upon",
2527
+ "Very",
2528
+ "Want",
2529
+ "Were",
2530
+ "Work",
2531
+ "Yet",
2532
+ "Decision",
2533
+ "Error",
2534
+ "Deadline",
2535
+ "Friday",
2536
+ "Monday",
2537
+ "Tuesday",
2538
+ "Wednesday",
2539
+ "Thursday",
2540
+ "Saturday",
2541
+ "Sunday",
2542
+ "January",
2543
+ "February",
2544
+ "March",
2545
+ "April",
2546
+ "May",
2547
+ "June",
2548
+ "July",
2549
+ "August",
2550
+ "September",
2551
+ "October",
2552
+ "November",
2553
+ "December",
2554
+ "Today",
2555
+ "Tomorrow",
2556
+ "Yesterday",
2557
+ "Message",
2558
+ "Feature",
2559
+ "Session",
2560
+ "Update",
2561
+ "System",
2562
+ "User",
2563
+ "Processed",
2564
+ "Working",
2565
+ "Built",
2566
+ "Deployed",
2567
+ "Discussed",
2568
+ "Talked",
2569
+ "Mentioned",
2570
+ "Requested",
2571
+ "Asked",
2572
+ "Said"
2573
+ ]);
2574
+ const knownEntities = /* @__PURE__ */ new Set([
2575
+ "PostgreSQL",
2576
+ "MongoDB",
2577
+ "Railway",
2578
+ "Vercel",
2579
+ "React",
2580
+ "Vue",
2581
+ "Svelte",
2582
+ "Express",
2583
+ "NestJS",
2584
+ "Prisma",
2585
+ "Docker",
2586
+ "Kubernetes",
2587
+ "Redis",
2588
+ "GraphQL",
2589
+ "Stripe",
2590
+ "ClawVault",
2591
+ "OpenClaw",
2592
+ "GitHub",
2593
+ "Obsidian"
2594
+ ]);
2595
+ return content.replace(namePattern, (match) => {
2596
+ if (skipWords.has(match)) return match;
2597
+ if (knownEntities.has(match)) return `[[${match}]]`;
2598
+ if (/^[A-Z][a-z]+$/.test(match) && match.length >= 3) {
2599
+ return `[[${match}]]`;
2600
+ }
2601
+ if (/^[A-Z][a-z]+ [A-Z][a-z]+$/.test(match)) {
2602
+ return `[[${match}]]`;
2603
+ }
2604
+ return match;
2605
+ });
2606
+ }
2607
+ /**
2608
+ * Jaccard similarity on word bigrams — cheap approximation.
2609
+ */
2610
+ similarity(a, b) {
2611
+ const bigrams = (s) => {
2612
+ const words = s.split(" ");
2613
+ const bg = /* @__PURE__ */ new Set();
2614
+ for (let i = 0; i < words.length - 1; i++) bg.add(`${words[i]} ${words[i + 1]}`);
2615
+ return bg;
2616
+ };
2617
+ const setA = bigrams(a);
2618
+ const setB = bigrams(b);
2619
+ if (setA.size === 0 || setB.size === 0) return 0;
2620
+ let intersection = 0;
2621
+ for (const bg of setA) if (setB.has(bg)) intersection++;
2622
+ return intersection / (setA.size + setB.size - intersection);
2623
+ }
2624
+ buildSummary(routed, dedupHits) {
2625
+ if (routed.length === 0) {
2626
+ if (dedupHits > 0) {
2627
+ return `No items routed to vault categories (dedup hits: ${dedupHits}).`;
2628
+ }
2629
+ return "No items routed to vault categories.";
2630
+ }
2631
+ const byCat = /* @__PURE__ */ new Map();
2632
+ for (const item of routed) {
2633
+ byCat.set(item.category, (byCat.get(item.category) ?? 0) + 1);
2634
+ }
2635
+ const parts = [...byCat.entries()].map(([cat, count]) => `${cat}: ${count}`);
2636
+ const suffix = dedupHits > 0 ? ` (dedup hits: ${dedupHits})` : "";
2637
+ return `Routed ${routed.length} observations \u2192 ${parts.join(", ")}${suffix}`;
2638
+ }
2639
+ };
2640
+
2641
+ // src/lib/ledger.ts
2642
+ var fs6 = __toESM(require("fs"), 1);
2643
+ var path6 = __toESM(require("path"), 1);
2644
+ var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
2645
+ function normalizeDateKey(date) {
2646
+ if (typeof date === "string") {
2647
+ if (!DATE_RE.test(date)) {
2648
+ throw new Error(`Invalid date key: ${date}`);
2649
+ }
2650
+ return date;
2651
+ }
2652
+ return date.toISOString().slice(0, 10);
2653
+ }
2654
+ function ensureDir(dirPath) {
2655
+ fs6.mkdirSync(dirPath, { recursive: true });
2656
+ }
2657
+ function toDateKey(date) {
2658
+ return date.toISOString().slice(0, 10);
2659
+ }
2660
+ function getLedgerRoot(vaultPath) {
2661
+ return path6.join(path6.resolve(vaultPath), "ledger");
2662
+ }
2663
+ function getRawRoot(vaultPath) {
2664
+ return path6.join(getLedgerRoot(vaultPath), "raw");
2665
+ }
2666
+ function getRawSourceDir(vaultPath, source) {
2667
+ return path6.join(getRawRoot(vaultPath), source);
2668
+ }
2669
+ function getObservationsRoot(vaultPath) {
2670
+ return path6.join(getLedgerRoot(vaultPath), "observations");
2671
+ }
2672
+ function getReflectionsRoot(vaultPath) {
2673
+ return path6.join(getLedgerRoot(vaultPath), "reflections");
2674
+ }
2675
+ function getArchiveObservationsRoot(vaultPath) {
2676
+ return path6.join(getLedgerRoot(vaultPath), "archive", "observations");
2677
+ }
2678
+ function getLegacyObservationsRoot(vaultPath) {
2679
+ return path6.join(path6.resolve(vaultPath), "observations");
2680
+ }
2681
+ function getObservationPath(vaultPath, date) {
2682
+ const dateKey = normalizeDateKey(date);
2683
+ const [year, month, day] = dateKey.split("-");
2684
+ return path6.join(getObservationsRoot(vaultPath), year, month, `${day}.md`);
2685
+ }
2686
+ function getLegacyObservationPath(vaultPath, date) {
2687
+ const dateKey = normalizeDateKey(date);
2688
+ return path6.join(getLegacyObservationsRoot(vaultPath), `${dateKey}.md`);
2689
+ }
2690
+ function getRawTranscriptPath(vaultPath, source, date) {
2691
+ const dateKey = normalizeDateKey(date);
2692
+ const [year, month, day] = dateKey.split("-");
2693
+ return path6.join(getRawSourceDir(vaultPath, source), year, month, `${day}.jsonl`);
2694
+ }
2695
+ function ensureLedgerStructure(vaultPath) {
2696
+ const root = getLedgerRoot(vaultPath);
2697
+ const rawRoot = getRawRoot(vaultPath);
2698
+ ensureDir(root);
2699
+ ensureDir(rawRoot);
2700
+ for (const source of ["openclaw", "chatgpt", "claude", "opencode"]) {
2701
+ ensureDir(path6.join(rawRoot, source));
2702
+ }
2703
+ ensureDir(getObservationsRoot(vaultPath));
2704
+ ensureDir(getReflectionsRoot(vaultPath));
2705
+ ensureDir(getArchiveObservationsRoot(vaultPath));
2706
+ }
2707
+ function ensureParentDir(filePath) {
2708
+ ensureDir(path6.dirname(filePath));
2709
+ }
2710
+
2711
+ // src/observer/observer.ts
2712
+ var COMPRESSION_PROVIDERS = /* @__PURE__ */ new Set([
2713
+ "anthropic",
2714
+ "openai",
2715
+ "gemini",
2716
+ "openai-compatible",
2717
+ "ollama"
2718
+ ]);
2719
+ function asRecord(value) {
2720
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2721
+ return null;
2722
+ }
2723
+ return value;
2724
+ }
2725
+ function asNonEmptyString(value) {
2726
+ if (typeof value !== "string") {
2727
+ return void 0;
2728
+ }
2729
+ const trimmed = value.trim();
2730
+ return trimmed ? trimmed : void 0;
2731
+ }
2732
+ function asCompressionProvider(value) {
2733
+ if (typeof value !== "string") {
2734
+ return void 0;
2735
+ }
2736
+ const normalized = value.trim();
2737
+ return COMPRESSION_PROVIDERS.has(normalized) ? normalized : void 0;
2738
+ }
2739
+ function readCompressionConfig(vaultPath) {
2740
+ try {
2741
+ const config = listConfig(vaultPath);
2742
+ const root = asRecord(config);
2743
+ const observer = asRecord(root?.observer);
2744
+ const compression = asRecord(observer?.compression);
2745
+ if (!compression) {
2746
+ return {};
2747
+ }
2748
+ return {
2749
+ provider: asCompressionProvider(compression.provider),
2750
+ model: asNonEmptyString(compression.model),
2751
+ baseUrl: asNonEmptyString(compression.baseUrl),
2752
+ apiKey: asNonEmptyString(compression.apiKey)
2753
+ };
2754
+ } catch {
2755
+ return {};
2756
+ }
2757
+ }
2758
+ var Observer = class {
2759
+ vaultPath;
2760
+ tokenThreshold;
2761
+ // Kept for backwards API compatibility with callers that still pass this.
2762
+ // Reflection now runs explicitly via clawvault reflect.
2763
+ reflectThreshold;
2764
+ compressor;
2765
+ reflector;
2766
+ now;
2767
+ rawCapture;
2768
+ router;
2769
+ pendingMessages = [];
2770
+ pendingRouteContext = {};
2771
+ observationsCache = "";
2772
+ lastRoutingSummary = "";
2773
+ constructor(vaultPath, options = {}) {
2774
+ this.vaultPath = path7.resolve(vaultPath);
2775
+ this.tokenThreshold = options.tokenThreshold ?? 3e4;
2776
+ this.reflectThreshold = options.reflectThreshold ?? 4e4;
2777
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
2778
+ const compressionConfig = readCompressionConfig(this.vaultPath);
2779
+ this.compressor = options.compressor ?? new Compressor({
2780
+ provider: options.compressionProvider ?? compressionConfig.provider,
2781
+ model: options.model ?? compressionConfig.model,
2782
+ baseUrl: options.compressionBaseUrl ?? compressionConfig.baseUrl,
2783
+ apiKey: options.compressionApiKey ?? compressionConfig.apiKey,
2784
+ now: this.now
2785
+ });
2786
+ this.reflector = options.reflector ?? new Reflector({ now: this.now });
2787
+ this.rawCapture = options.rawCapture ?? true;
2788
+ this.router = new Router(vaultPath, {
2789
+ extractTasks: options.extractTasks,
2790
+ now: this.now
2791
+ });
2792
+ ensureLedgerStructure(this.vaultPath);
2793
+ this.observationsCache = this.readTodayObservations();
2794
+ }
2795
+ async processMessages(messages, options = {}) {
2796
+ const incoming = messages.map((message) => message.trim()).filter(Boolean);
2797
+ if (incoming.length === 0) {
2798
+ return;
2799
+ }
2800
+ if (this.rawCapture) {
2801
+ this.persistRawMessages(incoming, options);
2802
+ }
2803
+ this.pendingMessages.push(...incoming);
2804
+ this.pendingRouteContext = this.mergeRouteContext(this.pendingRouteContext, options);
2805
+ const buffered = this.pendingMessages.join("\n");
2806
+ if (this.estimateTokens(buffered) < this.tokenThreshold) {
2807
+ return;
2808
+ }
2809
+ const today = this.now();
2810
+ const todayPath = getObservationPath(this.vaultPath, today);
2811
+ const existingRaw = this.readObservationForDate(today);
2812
+ const existing = this.deduplicateObservationMarkdown(existingRaw);
2813
+ if (existingRaw.trim() !== existing) {
2814
+ this.writeObservationFile(todayPath, existing);
2815
+ }
2816
+ const compressedRaw = (await this.compressor.compress(this.pendingMessages, existing)).trim();
2817
+ const routeContext = this.pendingRouteContext;
2818
+ this.pendingMessages = [];
2819
+ this.pendingRouteContext = {};
2820
+ const compressed = this.deduplicateObservationMarkdown(compressedRaw);
2821
+ if (!compressed) {
2822
+ return;
2823
+ }
2824
+ this.writeObservationFile(todayPath, compressed);
2825
+ this.observationsCache = compressed;
2826
+ const { summary } = this.router.route(compressed, routeContext);
2827
+ if (summary) {
2828
+ this.lastRoutingSummary = summary;
2829
+ }
2830
+ }
2831
+ /**
2832
+ * Force-flush pending messages regardless of threshold.
2833
+ * Call this on session end to capture everything.
2834
+ */
2835
+ async flush() {
2836
+ if (this.pendingMessages.length === 0) {
2837
+ return { observations: this.observationsCache, routingSummary: this.lastRoutingSummary };
2838
+ }
2839
+ const today = this.now();
2840
+ const todayPath = getObservationPath(this.vaultPath, today);
2841
+ const existingRaw = this.readObservationForDate(today);
2842
+ const existing = this.deduplicateObservationMarkdown(existingRaw);
2843
+ if (existingRaw.trim() !== existing) {
2844
+ this.writeObservationFile(todayPath, existing);
2845
+ }
2846
+ const compressedRaw = (await this.compressor.compress(this.pendingMessages, existing)).trim();
2847
+ const routeContext = this.pendingRouteContext;
2848
+ this.pendingMessages = [];
2849
+ this.pendingRouteContext = {};
2850
+ const compressed = this.deduplicateObservationMarkdown(compressedRaw);
2851
+ if (compressed) {
2852
+ this.writeObservationFile(todayPath, compressed);
2853
+ this.observationsCache = compressed;
2854
+ const { summary } = this.router.route(compressed, routeContext);
2855
+ this.lastRoutingSummary = summary;
2856
+ }
2857
+ return { observations: this.observationsCache, routingSummary: this.lastRoutingSummary };
2858
+ }
2859
+ getObservations() {
2860
+ this.observationsCache = this.readTodayObservations();
2861
+ return this.observationsCache;
2862
+ }
2863
+ estimateTokens(input) {
2864
+ return Math.ceil(input.length / 4);
2865
+ }
2866
+ readTodayObservations() {
2867
+ return this.readObservationForDate(this.now());
2868
+ }
2869
+ readObservationForDate(date) {
2870
+ const ledgerPath = getObservationPath(this.vaultPath, date);
2871
+ const ledgerValue = this.readObservationFile(ledgerPath);
2872
+ if (ledgerValue) {
2873
+ return ledgerValue;
2874
+ }
2875
+ return this.readObservationFile(getLegacyObservationPath(this.vaultPath, toDateKey(date)));
2876
+ }
2877
+ readObservationFile(filePath) {
2878
+ if (!fs7.existsSync(filePath)) {
2879
+ return "";
2880
+ }
2881
+ return fs7.readFileSync(filePath, "utf-8").trim();
2882
+ }
2883
+ writeObservationFile(filePath, content) {
2884
+ ensureParentDir(filePath);
2885
+ fs7.writeFileSync(filePath, `${content.trim()}
2886
+ `, "utf-8");
2887
+ }
2888
+ deduplicateObservationMarkdown(markdown) {
2889
+ const parsed = parseObservationMarkdown(markdown);
2890
+ if (parsed.length === 0) {
2891
+ return markdown.trim();
2892
+ }
2893
+ const grouped = /* @__PURE__ */ new Map();
2894
+ for (const record of parsed) {
2895
+ const bucket = grouped.get(record.date) ?? [];
2896
+ const normalized = normalizeObservationContent(record.content);
2897
+ const existingIndex = bucket.findIndex(
2898
+ (line) => normalizeObservationContent(line.content) === normalized
2899
+ );
2900
+ if (existingIndex === -1) {
2901
+ bucket.push({
2902
+ type: record.type,
2903
+ confidence: record.confidence,
2904
+ importance: record.importance,
2905
+ content: record.content
2906
+ });
2907
+ } else {
2908
+ const existing = bucket[existingIndex];
2909
+ bucket[existingIndex] = {
2910
+ type: record.importance >= existing.importance ? record.type : existing.type,
2911
+ confidence: Math.max(existing.confidence, record.confidence),
2912
+ importance: Math.max(existing.importance, record.importance),
2913
+ content: existing.content.length >= record.content.length ? existing.content : record.content
2914
+ };
2915
+ }
2916
+ grouped.set(record.date, bucket);
2917
+ }
2918
+ return renderObservationMarkdown(grouped);
2919
+ }
2920
+ persistRawMessages(messages, options) {
2921
+ const source = this.sanitizeSource(options.source ?? "openclaw");
2922
+ const messageTimestamp = options.timestamp ?? this.now();
2923
+ const rawPath = getRawTranscriptPath(this.vaultPath, source, messageTimestamp);
2924
+ ensureParentDir(rawPath);
2925
+ const records = messages.map((message) => JSON.stringify({
2926
+ recordedAt: this.now().toISOString(),
2927
+ timestamp: messageTimestamp.toISOString(),
2928
+ source,
2929
+ sessionKey: options.sessionKey ?? null,
2930
+ transcriptId: options.transcriptId ?? null,
2931
+ message
2932
+ }));
2933
+ fs7.appendFileSync(rawPath, `${records.join("\n")}
2934
+ `, "utf-8");
2935
+ }
2936
+ sanitizeSource(source) {
2937
+ const normalized = source.trim().toLowerCase();
2938
+ if (/^[a-z0-9_-]{1,64}$/.test(normalized)) {
2939
+ return normalized;
2940
+ }
2941
+ return "openclaw";
2942
+ }
2943
+ mergeRouteContext(existing, incoming) {
2944
+ const merged = { ...existing };
2945
+ if (incoming.source) merged.source = incoming.source;
2946
+ if (incoming.sessionKey) merged.sessionKey = incoming.sessionKey;
2947
+ if (incoming.transcriptId) merged.transcriptId = incoming.transcriptId;
2948
+ if (incoming.timestamp) merged.timestamp = incoming.timestamp;
2949
+ return merged;
2950
+ }
2951
+ };
2952
+
2953
+ // src/observer/session-parser.ts
2954
+ var fs8 = __toESM(require("fs"), 1);
2955
+ var path8 = __toESM(require("path"), 1);
2956
+ var JSONL_SAMPLE_LIMIT = 20;
2957
+ var MARKDOWN_SIGNAL_RE = /^(#{1,6}\s|[-*+]\s|>\s)/;
2958
+ var MARKDOWN_INLINE_RE = /(\[[^\]]+\]\([^)]+\)|[*_`~])/;
2959
+ function normalizeText(value) {
2960
+ return value.replace(/\s+/g, " ").trim();
2961
+ }
2962
+ function extractText(value) {
2963
+ if (typeof value === "string") {
2964
+ return normalizeText(value);
2965
+ }
2966
+ if (Array.isArray(value)) {
2967
+ const parts = [];
2968
+ for (const part of value) {
2969
+ const extracted = extractText(part);
2970
+ if (extracted) {
2971
+ parts.push(extracted);
2972
+ }
2973
+ }
2974
+ return normalizeText(parts.join(" "));
2975
+ }
2976
+ if (!value || typeof value !== "object") {
2977
+ return "";
2978
+ }
2979
+ const record = value;
2980
+ if (typeof record.text === "string") {
2981
+ return normalizeText(record.text);
2982
+ }
2983
+ if (typeof record.content === "string") {
2984
+ return normalizeText(record.content);
2985
+ }
2986
+ return "";
2987
+ }
2988
+ function normalizeRole(role) {
2989
+ if (typeof role !== "string") {
2990
+ return "";
2991
+ }
2992
+ const normalized = role.trim().toLowerCase();
2993
+ if (!normalized) {
2994
+ return "";
2995
+ }
2996
+ return normalized;
2997
+ }
2998
+ function isLikelyJsonMessage(value) {
2999
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3000
+ return false;
3001
+ }
3002
+ const record = value;
3003
+ if ("role" in record && "content" in record) {
3004
+ return true;
3005
+ }
3006
+ if (record.type === "message" && record.message && typeof record.message === "object") {
3007
+ return true;
3008
+ }
3009
+ return false;
3010
+ }
3011
+ function parseJsonLine(line) {
3012
+ let parsed;
3013
+ try {
3014
+ parsed = JSON.parse(line);
3015
+ } catch {
3016
+ return "";
3017
+ }
3018
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3019
+ return "";
3020
+ }
3021
+ const entry = parsed;
3022
+ if ("role" in entry && "content" in entry) {
3023
+ const role = normalizeRole(entry.role);
3024
+ const content = extractText(entry.content);
3025
+ if (!content) return "";
3026
+ return role ? `${role}: ${content}` : content;
3027
+ }
3028
+ if (entry.type === "message" && entry.message && typeof entry.message === "object") {
3029
+ const message = entry.message;
3030
+ const role = normalizeRole(message.role);
3031
+ const content = extractText(message.content);
3032
+ if (!content) return "";
3033
+ return role ? `${role}: ${content}` : content;
3034
+ }
3035
+ return "";
3036
+ }
3037
+ function parseJsonLines(raw) {
3038
+ const messages = [];
3039
+ for (const line of raw.split(/\r?\n/)) {
3040
+ const trimmed = line.trim();
3041
+ if (!trimmed) continue;
3042
+ const parsed = parseJsonLine(trimmed);
3043
+ if (parsed) {
3044
+ messages.push(parsed);
3045
+ }
3046
+ }
3047
+ return messages;
3048
+ }
3049
+ function stripMarkdownSyntax(text) {
3050
+ return normalizeText(
3051
+ text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[*_`~]/g, "").replace(/<[^>]+>/g, "")
3052
+ );
3053
+ }
3054
+ function normalizeMarkdownLine(line) {
3055
+ return stripMarkdownSyntax(
3056
+ line.replace(/^>\s*/, "").replace(/^[-*+]\s+/, "").replace(/^#{1,6}\s+/, "")
3057
+ );
3058
+ }
3059
+ function parseMarkdown(raw) {
3060
+ const withoutCodeBlocks = raw.replace(/```[\s\S]*?```/g, " ");
3061
+ const blocks = withoutCodeBlocks.split(/\r?\n\s*\r?\n/).map((block) => block.trim()).filter(Boolean);
3062
+ const messages = [];
3063
+ for (const block of blocks) {
3064
+ const lines = block.split(/\r?\n/).map((line) => normalizeMarkdownLine(line)).filter(Boolean);
3065
+ if (lines.length === 0) {
3066
+ continue;
3067
+ }
3068
+ const joined = stripMarkdownSyntax(lines.join(" "));
3069
+ if (!joined) continue;
3070
+ const roleMatch = /^(user|assistant|system|tool)\s*:?\s*(.+)$/i.exec(joined);
3071
+ if (roleMatch) {
3072
+ const role = normalizeRole(roleMatch[1]);
3073
+ const content = normalizeText(roleMatch[2]);
3074
+ if (content) {
3075
+ messages.push(`${role}: ${content}`);
3076
+ }
3077
+ continue;
3078
+ }
3079
+ messages.push(joined);
3080
+ }
3081
+ return messages;
3082
+ }
3083
+ function parsePlainText(raw) {
3084
+ return raw.split(/\r?\n/).map((line) => normalizeText(line)).filter(Boolean);
3085
+ }
3086
+ function detectSessionFormat(raw, filePath) {
3087
+ const nonEmptyLines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
3088
+ if (nonEmptyLines.length === 0) {
3089
+ return "plain";
3090
+ }
3091
+ const sample = nonEmptyLines.slice(0, JSONL_SAMPLE_LIMIT);
3092
+ const jsonHits = sample.filter((line) => {
3093
+ try {
3094
+ const parsed = JSON.parse(line);
3095
+ return isLikelyJsonMessage(parsed);
3096
+ } catch {
3097
+ return false;
3098
+ }
3099
+ }).length;
3100
+ if (jsonHits >= Math.max(1, Math.ceil(sample.length * 0.6))) {
3101
+ return "jsonl";
3102
+ }
3103
+ const ext = path8.extname(filePath).toLowerCase();
3104
+ if (ext === ".md" || ext === ".markdown") {
3105
+ return "markdown";
3106
+ }
3107
+ const markdownSignals = sample.filter((line) => MARKDOWN_SIGNAL_RE.test(line) || MARKDOWN_INLINE_RE.test(line)).length;
3108
+ if (markdownSignals >= Math.max(2, Math.ceil(sample.length * 0.4))) {
3109
+ return "markdown";
3110
+ }
3111
+ return "plain";
3112
+ }
3113
+ function parseSessionFile(filePath) {
3114
+ const resolved = path8.resolve(filePath);
3115
+ const raw = fs8.readFileSync(resolved, "utf-8");
3116
+ const format = detectSessionFormat(raw, resolved);
3117
+ if (format === "jsonl") {
3118
+ const parsed = parseJsonLines(raw);
3119
+ if (parsed.length > 0) {
3120
+ return parsed;
3121
+ }
3122
+ }
3123
+ if (format === "markdown") {
3124
+ const parsed = parseMarkdown(raw);
3125
+ if (parsed.length > 0) {
3126
+ return parsed;
3127
+ }
3128
+ }
3129
+ return parsePlainText(raw);
3130
+ }
3131
+
3132
+ // src/observer/watcher.ts
3133
+ var fs9 = __toESM(require("fs"), 1);
3134
+ var path9 = __toESM(require("path"), 1);
3135
+ var import_chokidar = __toESM(require("chokidar"), 1);
3136
+ var DEFAULT_FLUSH_THRESHOLD_CHARS = 500;
3137
+ var SessionWatcher = class {
3138
+ watchPath;
3139
+ observer;
3140
+ ignoreInitial;
3141
+ debounceMs;
3142
+ flushThresholdChars;
3143
+ watcher = null;
3144
+ fileOffsets = /* @__PURE__ */ new Map();
3145
+ pendingPaths = /* @__PURE__ */ new Set();
3146
+ debounceTimer = null;
3147
+ processingQueue = Promise.resolve();
3148
+ bufferedChars = 0;
3149
+ constructor(watchPath, observer, options = {}) {
3150
+ this.watchPath = path9.resolve(watchPath);
3151
+ this.observer = observer;
3152
+ this.ignoreInitial = options.ignoreInitial ?? false;
3153
+ this.debounceMs = options.debounceMs ?? 500;
3154
+ this.flushThresholdChars = Math.max(1, options.flushThresholdChars ?? DEFAULT_FLUSH_THRESHOLD_CHARS);
3155
+ }
3156
+ async start() {
3157
+ if (!fs9.existsSync(this.watchPath)) {
3158
+ throw new Error(`Watch path does not exist: ${this.watchPath}`);
3159
+ }
3160
+ this.watcher = import_chokidar.default.watch(this.watchPath, {
3161
+ persistent: true,
3162
+ ignoreInitial: this.ignoreInitial,
3163
+ awaitWriteFinish: {
3164
+ stabilityThreshold: 120,
3165
+ pollInterval: 30
3166
+ }
3167
+ });
3168
+ const enqueue = (changedPath) => {
3169
+ this.pendingPaths.add(path9.resolve(changedPath));
3170
+ this.scheduleDrain();
3171
+ };
3172
+ this.watcher.on("add", enqueue);
3173
+ this.watcher.on("change", enqueue);
3174
+ this.watcher.on("unlink", (deletedPath) => {
3175
+ const resolved = path9.resolve(deletedPath);
3176
+ this.fileOffsets.delete(resolved);
3177
+ this.pendingPaths.delete(resolved);
3178
+ });
3179
+ await new Promise((resolve14, reject) => {
3180
+ this.watcher?.once("ready", () => resolve14());
3181
+ this.watcher?.once("error", (error) => reject(error));
3182
+ });
3183
+ if (this.ignoreInitial) {
3184
+ this.primeInitialOffsets();
3185
+ }
3186
+ }
3187
+ async stop() {
3188
+ if (this.debounceTimer) {
3189
+ clearTimeout(this.debounceTimer);
3190
+ this.debounceTimer = null;
3191
+ this.drainPendingPaths();
3192
+ }
3193
+ await this.processingQueue.catch(() => void 0);
3194
+ if (this.bufferedChars > 0) {
3195
+ await this.observer.flush();
3196
+ this.bufferedChars = 0;
3197
+ }
3198
+ this.pendingPaths.clear();
3199
+ await this.watcher?.close();
3200
+ this.watcher = null;
3201
+ }
3202
+ scheduleDrain() {
3203
+ if (this.debounceTimer) {
3204
+ clearTimeout(this.debounceTimer);
3205
+ }
3206
+ this.debounceTimer = setTimeout(() => {
3207
+ this.debounceTimer = null;
3208
+ this.drainPendingPaths();
3209
+ }, this.debounceMs);
3210
+ }
3211
+ drainPendingPaths() {
3212
+ const nextPaths = [...this.pendingPaths];
3213
+ this.pendingPaths.clear();
3214
+ for (const changedPath of nextPaths) {
3215
+ this.processingQueue = this.processingQueue.then(() => this.consumeFile(changedPath)).catch(() => void 0);
3216
+ }
3217
+ }
3218
+ async consumeFile(filePath) {
3219
+ const resolved = path9.resolve(filePath);
3220
+ if (!fs9.existsSync(resolved)) {
3221
+ return;
3222
+ }
3223
+ const stats = fs9.statSync(resolved);
3224
+ if (!stats.isFile()) {
3225
+ return;
3226
+ }
3227
+ const previousOffset = this.fileOffsets.get(resolved) ?? 0;
3228
+ const startOffset = stats.size < previousOffset ? 0 : previousOffset;
3229
+ if (stats.size <= startOffset) {
3230
+ this.fileOffsets.set(resolved, stats.size);
3231
+ return;
3232
+ }
3233
+ const bytesToRead = stats.size - startOffset;
3234
+ const buffer = Buffer.alloc(bytesToRead);
3235
+ const fd = fs9.openSync(resolved, "r");
3236
+ try {
3237
+ fs9.readSync(fd, buffer, 0, bytesToRead, startOffset);
3238
+ } finally {
3239
+ fs9.closeSync(fd);
3240
+ }
3241
+ this.fileOffsets.set(resolved, stats.size);
3242
+ const chunk = buffer.toString("utf-8");
3243
+ const messages = chunk.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
3244
+ if (messages.length === 0) {
3245
+ return;
3246
+ }
3247
+ await this.observer.processMessages(messages);
3248
+ this.bufferedChars += chunk.length;
3249
+ if (this.bufferedChars >= this.flushThresholdChars) {
3250
+ await this.observer.flush();
3251
+ this.bufferedChars = 0;
3252
+ }
3253
+ }
3254
+ primeInitialOffsets() {
3255
+ for (const filePath of this.collectFiles(this.watchPath)) {
3256
+ try {
3257
+ const stats = fs9.statSync(filePath);
3258
+ if (stats.isFile()) {
3259
+ this.fileOffsets.set(filePath, stats.size);
3260
+ }
3261
+ } catch {
3262
+ }
3263
+ }
3264
+ }
3265
+ collectFiles(targetPath) {
3266
+ if (!fs9.existsSync(targetPath)) {
3267
+ return [];
3268
+ }
3269
+ const resolved = path9.resolve(targetPath);
3270
+ const stats = fs9.statSync(resolved);
3271
+ if (stats.isFile()) {
3272
+ return [resolved];
3273
+ }
3274
+ if (!stats.isDirectory()) {
3275
+ return [];
3276
+ }
3277
+ const collected = [];
3278
+ for (const entry of fs9.readdirSync(resolved, { withFileTypes: true })) {
3279
+ const childPath = path9.join(resolved, entry.name);
3280
+ if (entry.isDirectory()) {
3281
+ collected.push(...this.collectFiles(childPath));
3282
+ } else if (entry.isFile()) {
3283
+ collected.push(path9.resolve(childPath));
3284
+ }
3285
+ }
3286
+ return collected;
3287
+ }
3288
+ };
3289
+
3290
+ // src/observer/active-session-observer.ts
3291
+ var fs10 = __toESM(require("fs"), 1);
3292
+ var path11 = __toESM(require("path"), 1);
3293
+
3294
+ // src/lib/session-utils.ts
3295
+ var path10 = __toESM(require("path"), 1);
3296
+ var os = __toESM(require("os"), 1);
3297
+ function validateEnvPath(envValue) {
3298
+ if (!envValue) return null;
3299
+ const trimmed = envValue.trim();
3300
+ if (!trimmed) return null;
3301
+ const resolved = path10.resolve(trimmed);
3302
+ if (!path10.isAbsolute(resolved)) return null;
3303
+ return resolved;
3304
+ }
3305
+ function getOpenClawDir() {
3306
+ const customHome = validateEnvPath(process.env.OPENCLAW_HOME);
3307
+ if (customHome) {
3308
+ return customHome;
3309
+ }
3310
+ return path10.join(os.homedir(), ".openclaw");
3311
+ }
3312
+ function getOpenClawAgentsDir() {
3313
+ const stateDir = validateEnvPath(process.env.OPENCLAW_STATE_DIR);
3314
+ if (stateDir) {
3315
+ return path10.join(stateDir, "agents");
3316
+ }
3317
+ return path10.join(getOpenClawDir(), "agents");
3318
+ }
3319
+ function getSessionsDir(agentId) {
3320
+ return path10.join(getOpenClawAgentsDir(), agentId, "sessions");
3321
+ }
3322
+
3323
+ // src/observer/active-session-observer.ts
3324
+ var ONE_KIB = 1024;
3325
+ var ONE_MIB = ONE_KIB * ONE_KIB;
3326
+ var SMALL_SESSION_THRESHOLD_BYTES = 50 * ONE_KIB;
3327
+ var MEDIUM_SESSION_THRESHOLD_BYTES = 150 * ONE_KIB;
3328
+ var LARGE_SESSION_THRESHOLD_BYTES = 300 * ONE_KIB;
3329
+ var DEFAULT_AGENT_ID = "main";
3330
+ var AGENT_ID_RE = /^[a-zA-Z0-9_-]{1,100}$/;
3331
+ var SESSION_ID_RE = /^[a-zA-Z0-9._-]{1,200}$/;
3332
+ var CURSOR_FILE_NAME = "observe-cursors.json";
3333
+ var STALE_CURSOR_THRESHOLD_MS = 12 * 60 * 60 * 1e3;
3334
+ function formatBytes(bytes) {
3335
+ if (bytes >= ONE_MIB) return `${(bytes / ONE_MIB).toFixed(1)}MB`;
3336
+ if (bytes >= ONE_KIB) return `${Math.round(bytes / ONE_KIB)}KB`;
3337
+ return `${bytes}B`;
3338
+ }
3339
+ function isFiniteNonNegative(value) {
3340
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
3341
+ }
3342
+ function normalizeAgentId(input) {
3343
+ const raw = (input ?? process.env.OPENCLAW_AGENT_ID ?? DEFAULT_AGENT_ID).trim();
3344
+ if (!AGENT_ID_RE.test(raw)) {
3345
+ return DEFAULT_AGENT_ID;
3346
+ }
3347
+ return raw;
3348
+ }
3349
+ function resolveSessionsDirectory(agentId, override) {
3350
+ if (override?.trim()) {
3351
+ return path11.resolve(override.trim());
3352
+ }
3353
+ return getSessionsDir(agentId);
3354
+ }
3355
+ function getCursorPath(vaultPath) {
3356
+ return path11.join(vaultPath, ".clawvault", CURSOR_FILE_NAME);
3357
+ }
3358
+ function getScaledObservationThresholdBytes(fileSizeBytes) {
3359
+ if (fileSizeBytes < ONE_MIB) {
3360
+ return SMALL_SESSION_THRESHOLD_BYTES;
3361
+ }
3362
+ if (fileSizeBytes <= 5 * ONE_MIB) {
3363
+ return MEDIUM_SESSION_THRESHOLD_BYTES;
3364
+ }
3365
+ return LARGE_SESSION_THRESHOLD_BYTES;
3366
+ }
3367
+ function parseCursorStore(raw) {
3368
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
3369
+ return {};
3370
+ }
3371
+ const input = raw;
3372
+ const store = {};
3373
+ for (const [sessionId, value] of Object.entries(input)) {
3374
+ if (!SESSION_ID_RE.test(sessionId)) continue;
3375
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
3376
+ const entry = value;
3377
+ if (!isFiniteNonNegative(entry.lastObservedOffset)) continue;
3378
+ if (!isFiniteNonNegative(entry.lastFileSize)) continue;
3379
+ if (typeof entry.lastObservedAt !== "string" || !entry.lastObservedAt.trim()) continue;
3380
+ if (typeof entry.sessionKey !== "string" || !entry.sessionKey.trim()) continue;
3381
+ store[sessionId] = {
3382
+ lastObservedOffset: entry.lastObservedOffset,
3383
+ lastObservedAt: entry.lastObservedAt,
3384
+ sessionKey: entry.sessionKey,
3385
+ lastFileSize: entry.lastFileSize
3386
+ };
3387
+ }
3388
+ return store;
3389
+ }
3390
+ function loadObserveCursorStore(vaultPath) {
3391
+ const cursorPath = getCursorPath(vaultPath);
3392
+ if (!fs10.existsSync(cursorPath)) {
3393
+ return {};
3394
+ }
3395
+ try {
3396
+ const raw = JSON.parse(fs10.readFileSync(cursorPath, "utf-8"));
3397
+ return parseCursorStore(raw);
3398
+ } catch {
3399
+ return {};
3400
+ }
3401
+ }
3402
+ function saveObserveCursorStore(vaultPath, store) {
3403
+ const cursorPath = getCursorPath(vaultPath);
3404
+ fs10.mkdirSync(path11.dirname(cursorPath), { recursive: true });
3405
+ fs10.writeFileSync(cursorPath, `${JSON.stringify(store, null, 2)}
3406
+ `, "utf-8");
3407
+ }
3408
+ function loadSessionIndex(sessionsDir) {
3409
+ const indexPath = path11.join(sessionsDir, "sessions.json");
3410
+ if (!fs10.existsSync(indexPath)) {
3411
+ return {};
3412
+ }
3413
+ try {
3414
+ const parsed = JSON.parse(fs10.readFileSync(indexPath, "utf-8"));
3415
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3416
+ return {};
3417
+ }
3418
+ return parsed;
3419
+ } catch {
3420
+ return {};
3421
+ }
3422
+ }
3423
+ function findMostRecentResetFile(sessionsDir, sessionId) {
3424
+ const prefix = `${sessionId}.jsonl.reset.`;
3425
+ try {
3426
+ const files = fs10.readdirSync(sessionsDir).filter((f) => f.startsWith(prefix)).sort().reverse();
3427
+ return files.length > 0 ? path11.join(sessionsDir, files[0]) : null;
3428
+ } catch {
3429
+ return null;
3430
+ }
3431
+ }
3432
+ function resolveTranscriptPath(sessionsDir, sessionId) {
3433
+ const mainPath = path11.join(sessionsDir, `${sessionId}.jsonl`);
3434
+ try {
3435
+ const stat = fs10.statSync(mainPath);
3436
+ if (stat.size < 100) {
3437
+ const resetFile = findMostRecentResetFile(sessionsDir, sessionId);
3438
+ if (resetFile) {
3439
+ return resetFile;
3440
+ }
3441
+ }
3442
+ } catch {
3443
+ const resetFile = findMostRecentResetFile(sessionsDir, sessionId);
3444
+ if (resetFile) {
3445
+ return resetFile;
3446
+ }
3447
+ }
3448
+ return mainPath;
3449
+ }
3450
+ function discoverSessionDescriptors(sessionsDir, fallbackAgentId) {
3451
+ const descriptors = [];
3452
+ const seen = /* @__PURE__ */ new Set();
3453
+ const index = loadSessionIndex(sessionsDir);
3454
+ const indexedEntries = Object.entries(index).sort((left, right) => {
3455
+ const leftUpdated = Number(left[1]?.updatedAt ?? 0);
3456
+ const rightUpdated = Number(right[1]?.updatedAt ?? 0);
3457
+ return rightUpdated - leftUpdated;
3458
+ });
3459
+ for (const [sessionKey, entry] of indexedEntries) {
3460
+ if (!entry || typeof entry !== "object") continue;
3461
+ const sessionId = typeof entry.sessionId === "string" ? entry.sessionId.trim() : "";
3462
+ if (!SESSION_ID_RE.test(sessionId) || seen.has(sessionId)) continue;
3463
+ const filePath = resolveTranscriptPath(sessionsDir, sessionId);
3464
+ try {
3465
+ const stat = fs10.statSync(filePath);
3466
+ if (!stat.isFile()) continue;
3467
+ seen.add(sessionId);
3468
+ descriptors.push({ sessionId, sessionKey, filePath });
3469
+ } catch {
3470
+ continue;
3471
+ }
3472
+ }
3473
+ const fallbackPrefix = `agent:${fallbackAgentId}:`;
3474
+ for (const fileName of fs10.readdirSync(sessionsDir)) {
3475
+ if (!fileName.endsWith(".jsonl") || fileName.includes(".backup") || fileName.includes(".deleted")) {
3476
+ continue;
3477
+ }
3478
+ const sessionId = fileName.slice(0, -".jsonl".length);
3479
+ if (!SESSION_ID_RE.test(sessionId) || seen.has(sessionId)) continue;
3480
+ const filePath = path11.join(sessionsDir, fileName);
3481
+ try {
3482
+ const stat = fs10.statSync(filePath);
3483
+ if (!stat.isFile()) continue;
3484
+ seen.add(sessionId);
3485
+ descriptors.push({
3486
+ sessionId,
3487
+ sessionKey: `${fallbackPrefix}unknown:${sessionId}`,
3488
+ filePath
3489
+ });
3490
+ } catch {
3491
+ continue;
3492
+ }
3493
+ }
3494
+ return descriptors;
3495
+ }
3496
+ function normalizeWhitespace(value) {
3497
+ return value.replace(/\s+/g, " ").trim();
3498
+ }
3499
+ function extractContentText(value) {
3500
+ if (typeof value === "string") {
3501
+ return normalizeWhitespace(value);
3502
+ }
3503
+ if (Array.isArray(value)) {
3504
+ const parts = value.map((item) => extractContentText(item)).filter(Boolean);
3505
+ return normalizeWhitespace(parts.join(" "));
3506
+ }
3507
+ if (!value || typeof value !== "object") {
3508
+ return "";
3509
+ }
3510
+ const input = value;
3511
+ if (typeof input.text === "string") {
3512
+ return normalizeWhitespace(input.text);
3513
+ }
3514
+ if (typeof input.content === "string") {
3515
+ return normalizeWhitespace(input.content);
3516
+ }
3517
+ return "";
3518
+ }
3519
+ function normalizeRole2(role) {
3520
+ if (typeof role !== "string") {
3521
+ return "";
3522
+ }
3523
+ return role.trim().toLowerCase();
3524
+ }
3525
+ function summarizeContentArray(content) {
3526
+ const parts = [];
3527
+ for (const block of content) {
3528
+ if (!block || typeof block !== "object") continue;
3529
+ const b = block;
3530
+ if (b.type === "text" && typeof b.text === "string") {
3531
+ const text = normalizeWhitespace(b.text);
3532
+ if (text) parts.push(text);
3533
+ } else if (b.type === "toolCall") {
3534
+ const toolName = typeof b.name === "string" ? b.name : "tool";
3535
+ parts.push(`[tool: ${toolName}]`);
3536
+ }
3537
+ }
3538
+ return parts.join(" ");
3539
+ }
3540
+ function parseOpenClawJsonLine(line) {
3541
+ if (!line.trim()) {
3542
+ return "";
3543
+ }
3544
+ let parsed;
3545
+ try {
3546
+ parsed = JSON.parse(line);
3547
+ } catch {
3548
+ return "";
3549
+ }
3550
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3551
+ return "";
3552
+ }
3553
+ const entry = parsed;
3554
+ if ("role" in entry && "content" in entry) {
3555
+ const role = normalizeRole2(entry.role);
3556
+ if (role === "system") return "";
3557
+ if (role === "toolresult") {
3558
+ const output = extractContentText(entry.content);
3559
+ const preview = output.slice(0, 80) || "ok";
3560
+ return `[tool_result \u2192 ${preview}]`;
3561
+ }
3562
+ const raw = entry.content;
3563
+ let content;
3564
+ if (Array.isArray(raw)) {
3565
+ content = summarizeContentArray(raw);
3566
+ } else {
3567
+ content = extractContentText(raw);
3568
+ }
3569
+ if (!content) return "";
3570
+ return role ? `${role}: ${content}` : content;
3571
+ }
3572
+ if (entry.type === "message" && entry.message && typeof entry.message === "object") {
3573
+ const message = entry.message;
3574
+ const role = normalizeRole2(message.role);
3575
+ if (role === "system") return "";
3576
+ if (role === "toolresult") {
3577
+ const output = extractContentText(message.content);
3578
+ const preview = output.slice(0, 80) || "ok";
3579
+ return `[tool_result \u2192 ${preview}]`;
3580
+ }
3581
+ const raw = message.content;
3582
+ let content;
3583
+ if (Array.isArray(raw)) {
3584
+ content = summarizeContentArray(raw);
3585
+ } else {
3586
+ content = extractContentText(raw);
3587
+ }
3588
+ if (!content) return "";
3589
+ return role ? `${role}: ${content}` : content;
3590
+ }
3591
+ return "";
3592
+ }
3593
+ function decodeLineBuffer(lineBuffer) {
3594
+ if (lineBuffer.length === 0) {
3595
+ return "";
3596
+ }
3597
+ const normalized = lineBuffer[lineBuffer.length - 1] === 13 ? lineBuffer.subarray(0, lineBuffer.length - 1) : lineBuffer;
3598
+ return normalized.toString("utf-8").trim();
3599
+ }
3600
+ async function readIncrementalMessages(filePath, startOffset) {
3601
+ const messages = [];
3602
+ let nextOffset = startOffset;
3603
+ let remainder = Buffer.alloc(0);
3604
+ const stream = fs10.createReadStream(filePath, {
3605
+ start: startOffset
3606
+ });
3607
+ for await (const chunk of stream) {
3608
+ const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
3609
+ const combined = remainder.length > 0 ? Buffer.concat([remainder, chunkBuffer]) : chunkBuffer;
3610
+ let lineStart = 0;
3611
+ for (let index = 0; index < combined.length; index += 1) {
3612
+ if (combined[index] !== 10) continue;
3613
+ const lineBuffer = combined.subarray(lineStart, index);
3614
+ const line = decodeLineBuffer(lineBuffer);
3615
+ const parsed = parseOpenClawJsonLine(line);
3616
+ if (parsed) {
3617
+ messages.push(parsed);
3618
+ }
3619
+ nextOffset += index - lineStart + 1;
3620
+ lineStart = index + 1;
3621
+ }
3622
+ remainder = combined.subarray(lineStart);
3623
+ }
3624
+ if (remainder.length > 0) {
3625
+ const trailing = decodeLineBuffer(remainder);
3626
+ if (trailing) {
3627
+ const parsed = parseOpenClawJsonLine(trailing);
3628
+ if (parsed) {
3629
+ messages.push(parsed);
3630
+ nextOffset += remainder.length;
3631
+ }
3632
+ }
3633
+ }
3634
+ return { messages, nextOffset };
3635
+ }
3636
+ function parseSessionSourceLabel(sessionKey) {
3637
+ const parts = sessionKey.split(":");
3638
+ if (parts.length < 3 || parts[0] !== "agent") {
3639
+ return "session";
3640
+ }
3641
+ const scope = parts.slice(2);
3642
+ if (scope[0] === "main") {
3643
+ return "main";
3644
+ }
3645
+ if (scope[0] === "telegram" && scope[1] === "dm") {
3646
+ return "telegram-dm";
3647
+ }
3648
+ if (scope[0] === "telegram" && scope[1] === "group") {
3649
+ return "telegram-group";
3650
+ }
3651
+ if (scope[0] === "discord") {
3652
+ return "discord";
3653
+ }
3654
+ if (scope[0] === "telegram") {
3655
+ return "telegram";
3656
+ }
3657
+ if (scope[0] === "slack") {
3658
+ return "slack";
3659
+ }
3660
+ return scope[0] || "session";
3661
+ }
3662
+ function createDefaultObserver(vaultPath, options) {
3663
+ return new Observer(vaultPath, options);
3664
+ }
3665
+ function parseRoutingCounts(routingSummary) {
3666
+ const counts = {};
3667
+ const [, categorySection = ""] = routingSummary.split("\u2192");
3668
+ const summaryCategories = categorySection.split("(")[0] ?? "";
3669
+ if (!summaryCategories) {
3670
+ return counts;
3671
+ }
3672
+ for (const match of summaryCategories.matchAll(/\b([a-z][a-z0-9-]*):\s*(\d+)\b/gi)) {
3673
+ const category = match[1].toLowerCase();
3674
+ const count = Number.parseInt(match[2], 10);
3675
+ if (!Number.isFinite(count) || count <= 0) {
3676
+ continue;
3677
+ }
3678
+ counts[category] = (counts[category] ?? 0) + count;
3679
+ }
3680
+ return counts;
3681
+ }
3682
+ function mergeRoutingCounts(target, incoming) {
3683
+ for (const [category, count] of Object.entries(incoming)) {
3684
+ target[category] = (target[category] ?? 0) + count;
3685
+ }
3686
+ }
3687
+ function stringifyError(error) {
3688
+ if (error instanceof Error && error.message) {
3689
+ return error.message;
3690
+ }
3691
+ return String(error);
3692
+ }
3693
+ function selectCandidates(descriptors, cursors, minNewBytes) {
3694
+ const candidates = [];
3695
+ for (const descriptor of descriptors) {
3696
+ let stat;
3697
+ try {
3698
+ stat = fs10.statSync(descriptor.filePath);
3699
+ } catch {
3700
+ continue;
3701
+ }
3702
+ if (!stat.isFile()) continue;
3703
+ const fileSize = stat.size;
3704
+ const cursor = cursors[descriptor.sessionId];
3705
+ const previousOffset = cursor && isFiniteNonNegative(cursor.lastObservedOffset) ? cursor.lastObservedOffset : 0;
3706
+ const startOffset = previousOffset <= fileSize ? previousOffset : 0;
3707
+ const newBytes = Math.max(0, fileSize - startOffset);
3708
+ const thresholdBytes = minNewBytes ?? getScaledObservationThresholdBytes(fileSize);
3709
+ if (newBytes < thresholdBytes) {
3710
+ continue;
3711
+ }
3712
+ candidates.push({
3713
+ sessionId: descriptor.sessionId,
3714
+ sessionKey: descriptor.sessionKey,
3715
+ sourceLabel: parseSessionSourceLabel(descriptor.sessionKey),
3716
+ filePath: descriptor.filePath,
3717
+ fileSize,
3718
+ startOffset,
3719
+ newBytes,
3720
+ thresholdBytes
3721
+ });
3722
+ }
3723
+ return candidates;
3724
+ }
3725
+ async function observeActiveSessions(options, dependencies = {}) {
3726
+ const vaultPath = path11.resolve(options.vaultPath);
3727
+ const agentId = normalizeAgentId(options.agentId);
3728
+ const sessionsDir = resolveSessionsDirectory(agentId, options.sessionsDir);
3729
+ const dryRun = Boolean(options.dryRun);
3730
+ if (!fs10.existsSync(sessionsDir) || !fs10.statSync(sessionsDir).isDirectory()) {
3731
+ return {
3732
+ agentId,
3733
+ sessionsDir,
3734
+ checkedSessions: 0,
3735
+ candidateSessions: 0,
3736
+ observedSessions: 0,
3737
+ cursorUpdates: 0,
3738
+ dryRun,
3739
+ totalNewBytes: 0,
3740
+ observedNewBytes: 0,
3741
+ routedCounts: {},
3742
+ failedSessionCount: 0,
3743
+ failedSessions: [],
3744
+ candidates: []
3745
+ };
3746
+ }
3747
+ const now = dependencies.now ?? (() => /* @__PURE__ */ new Date());
3748
+ const cursors = loadObserveCursorStore(vaultPath);
3749
+ const descriptors = discoverSessionDescriptors(sessionsDir, agentId);
3750
+ const allCandidates = selectCandidates(descriptors, cursors, options.minNewBytes);
3751
+ allCandidates.sort((a, b) => b.newBytes - a.newBytes);
3752
+ const candidates = options.maxSessions != null && options.maxSessions > 0 ? allCandidates.slice(0, options.maxSessions) : allCandidates;
3753
+ if (dryRun || candidates.length === 0) {
3754
+ return {
3755
+ agentId,
3756
+ sessionsDir,
3757
+ checkedSessions: descriptors.length,
3758
+ candidateSessions: candidates.length,
3759
+ observedSessions: 0,
3760
+ cursorUpdates: 0,
3761
+ dryRun,
3762
+ totalNewBytes: candidates.reduce((sum, candidate) => sum + candidate.newBytes, 0),
3763
+ observedNewBytes: 0,
3764
+ routedCounts: {},
3765
+ failedSessionCount: 0,
3766
+ failedSessions: [],
3767
+ candidates
3768
+ };
3769
+ }
3770
+ const observerFactory = dependencies.createObserver ?? createDefaultObserver;
3771
+ const observerOptions = {
3772
+ tokenThreshold: options.threshold,
3773
+ reflectThreshold: options.reflectThreshold,
3774
+ model: options.model,
3775
+ extractTasks: options.extractTasks
3776
+ };
3777
+ let observedSessions = 0;
3778
+ let cursorUpdates = 0;
3779
+ let observedNewBytes = 0;
3780
+ const routedCounts = {};
3781
+ const failedSessions = [];
3782
+ for (let i = 0; i < candidates.length; i += 1) {
3783
+ const candidate = candidates[i];
3784
+ console.log(`[observer] processing session ${i + 1}/${candidates.length}: ${candidate.sessionKey} (${formatBytes(candidate.newBytes)} new)`);
3785
+ try {
3786
+ const observer = observerFactory(vaultPath, observerOptions);
3787
+ const { messages, nextOffset } = await readIncrementalMessages(candidate.filePath, candidate.startOffset);
3788
+ const taggedMessages = messages.map((message) => `[${candidate.sourceLabel}] ${message}`);
3789
+ if (taggedMessages.length > 0) {
3790
+ await observer.processMessages(taggedMessages, {
3791
+ source: "openclaw",
3792
+ sessionKey: candidate.sessionKey,
3793
+ transcriptId: candidate.sessionId
3794
+ });
3795
+ const flushResult = await observer.flush();
3796
+ mergeRoutingCounts(routedCounts, parseRoutingCounts(flushResult.routingSummary));
3797
+ observedSessions += 1;
3798
+ observedNewBytes += candidate.newBytes;
3799
+ }
3800
+ if (nextOffset > candidate.startOffset) {
3801
+ cursors[candidate.sessionId] = {
3802
+ lastObservedOffset: nextOffset,
3803
+ lastObservedAt: now().toISOString(),
3804
+ sessionKey: candidate.sessionKey,
3805
+ lastFileSize: candidate.fileSize
3806
+ };
3807
+ cursorUpdates += 1;
3808
+ }
3809
+ } catch (error) {
3810
+ const reason = stringifyError(error);
3811
+ failedSessions.push({
3812
+ sessionId: candidate.sessionId,
3813
+ sessionKey: candidate.sessionKey,
3814
+ sourceLabel: candidate.sourceLabel,
3815
+ error: reason
3816
+ });
3817
+ console.error(
3818
+ `[observer] failed to observe session ${candidate.sessionKey} (${candidate.sessionId}): ${reason}`
3819
+ );
3820
+ }
3821
+ }
3822
+ if (cursorUpdates > 0) {
3823
+ saveObserveCursorStore(vaultPath, cursors);
3824
+ }
3825
+ return {
3826
+ agentId,
3827
+ sessionsDir,
3828
+ checkedSessions: descriptors.length,
3829
+ candidateSessions: candidates.length,
3830
+ observedSessions,
3831
+ cursorUpdates,
3832
+ dryRun,
3833
+ totalNewBytes: candidates.reduce((sum, candidate) => sum + candidate.newBytes, 0),
3834
+ observedNewBytes,
3835
+ routedCounts,
3836
+ failedSessionCount: failedSessions.length,
3837
+ failedSessions,
3838
+ candidates
3839
+ };
3840
+ }
3841
+
3842
+ // src/lib/config.ts
3843
+ var fs11 = __toESM(require("fs"), 1);
3844
+ var path12 = __toESM(require("path"), 1);
3845
+ function findNearestVaultPath(startPath = process.cwd()) {
3846
+ let current = path12.resolve(startPath);
3847
+ while (true) {
3848
+ if (fs11.existsSync(path12.join(current, ".clawvault.json"))) {
3849
+ return current;
3850
+ }
3851
+ const parent = path12.dirname(current);
3852
+ if (parent === current) {
3853
+ return null;
3854
+ }
3855
+ current = parent;
3856
+ }
3857
+ }
3858
+ function resolveVaultPath(options = {}) {
3859
+ if (options.explicitPath) {
3860
+ return path12.resolve(options.explicitPath);
3861
+ }
3862
+ if (process.env.CLAWVAULT_PATH) {
3863
+ return path12.resolve(process.env.CLAWVAULT_PATH);
3864
+ }
3865
+ const discovered = findNearestVaultPath(options.cwd ?? process.cwd());
3866
+ if (discovered) {
3867
+ return discovered;
3868
+ }
3869
+ throw new Error("No vault path found. Set CLAWVAULT_PATH, use --vault, or run inside a vault.");
3870
+ }
3871
+
3872
+ // src/commands/observe.ts
3873
+ var ONE_KIB2 = 1024;
3874
+ var ONE_MIB2 = ONE_KIB2 * ONE_KIB2;
3875
+ function parsePositiveInteger(raw, optionName) {
3876
+ const parsed = Number.parseInt(raw, 10);
3877
+ if (!Number.isFinite(parsed) || parsed <= 0) {
3878
+ throw new Error(`Invalid ${optionName}: ${raw}`);
3879
+ }
3880
+ return parsed;
3881
+ }
3882
+ function buildDaemonArgs(options) {
3883
+ const cliPath = process.argv[1];
3884
+ if (!cliPath) {
3885
+ throw new Error("Unable to resolve CLI script path for daemon mode.");
3886
+ }
3887
+ const args = [cliPath, "observe"];
3888
+ if (options.watch) {
3889
+ args.push("--watch", options.watch);
3890
+ }
3891
+ if (options.threshold) {
3892
+ args.push("--threshold", String(options.threshold));
3893
+ }
3894
+ if (options.reflectThreshold) {
3895
+ args.push("--reflect-threshold", String(options.reflectThreshold));
3896
+ }
3897
+ if (options.model) {
3898
+ args.push("--model", options.model);
3899
+ }
3900
+ if (options.extractTasks === false) {
3901
+ args.push("--no-extract-tasks");
3902
+ }
3903
+ if (options.vaultPath) {
3904
+ args.push("--vault", options.vaultPath);
3905
+ }
3906
+ return args;
3907
+ }
3908
+ function formatByteSummary(bytes) {
3909
+ const normalized = Number.isFinite(bytes) ? Math.max(0, bytes) : 0;
3910
+ if (normalized === 0) {
3911
+ return "0KB";
3912
+ }
3913
+ if (normalized >= ONE_MIB2) {
3914
+ return `${(normalized / ONE_MIB2).toFixed(1)}MB`;
3915
+ }
3916
+ return `${Math.max(1, Math.round(normalized / ONE_KIB2))}KB`;
3917
+ }
3918
+ function formatCronSummary(result) {
3919
+ const decisionCount = result.routedCounts.decisions ?? 0;
3920
+ return `observed ${result.observedSessions} sessions, ${formatByteSummary(result.observedNewBytes)} new content, ${decisionCount} decision${decisionCount === 1 ? "" : "s"} extracted`;
3921
+ }
3922
+ async function runOneShotCompression(observer, sourceFile, vaultPath) {
3923
+ const resolved = path13.resolve(sourceFile);
3924
+ if (!fs12.existsSync(resolved) || !fs12.statSync(resolved).isFile()) {
3925
+ throw new Error(`Conversation file not found: ${resolved}`);
3926
+ }
3927
+ const messages = parseSessionFile(resolved);
3928
+ const transcriptStat = fs12.statSync(resolved);
3929
+ await observer.processMessages(messages, {
3930
+ source: "openclaw",
3931
+ transcriptId: path13.basename(resolved),
3932
+ timestamp: transcriptStat.mtime
3933
+ });
3934
+ const { observations, routingSummary } = await observer.flush();
3935
+ const outputPath = getObservationPath(vaultPath, /* @__PURE__ */ new Date());
3936
+ console.log(`Observations updated: ${outputPath}`);
3937
+ if (routingSummary) {
3938
+ console.log(routingSummary);
3939
+ }
3940
+ }
3941
+ async function watchSessions(observer, watchPath) {
3942
+ const watcher = new SessionWatcher(watchPath, observer);
3943
+ await watcher.start();
3944
+ console.log(`Watching session updates: ${watchPath}`);
3945
+ await new Promise((resolve14) => {
3946
+ const shutdown = async () => {
3947
+ process.off("SIGINT", onSigInt);
3948
+ process.off("SIGTERM", onSigTerm);
3949
+ await watcher.stop();
3950
+ resolve14();
3951
+ };
3952
+ const onSigInt = () => {
3953
+ void shutdown();
3954
+ };
3955
+ const onSigTerm = () => {
3956
+ void shutdown();
3957
+ };
3958
+ process.once("SIGINT", onSigInt);
3959
+ process.once("SIGTERM", onSigTerm);
3960
+ });
3961
+ }
3962
+ async function observeCommand(options) {
3963
+ if (options.cron && (options.active || options.watch || options.compress || options.daemon)) {
3964
+ throw new Error("--cron cannot be combined with --active, --watch, --compress, or --daemon.");
3965
+ }
3966
+ if (options.cron && options.dryRun) {
3967
+ throw new Error("--cron cannot be combined with --dry-run.");
3968
+ }
3969
+ if (options.active && (options.watch || options.compress || options.daemon)) {
3970
+ throw new Error("--active cannot be combined with --watch, --compress, or --daemon.");
3971
+ }
3972
+ if (options.compress && options.daemon) {
3973
+ throw new Error("--compress cannot be combined with --daemon.");
3974
+ }
3975
+ const vaultPath = resolveVaultPath({ explicitPath: options.vaultPath });
3976
+ if (options.active || options.cron) {
3977
+ const result = await observeActiveSessions({
3978
+ vaultPath,
3979
+ agentId: options.agent,
3980
+ minNewBytes: options.minNew,
3981
+ sessionsDir: options.sessionsDir,
3982
+ dryRun: options.dryRun,
3983
+ threshold: options.threshold,
3984
+ reflectThreshold: options.reflectThreshold,
3985
+ model: options.model,
3986
+ extractTasks: options.extractTasks,
3987
+ maxSessions: options.maxSessions
3988
+ });
3989
+ const failedSessionCount = result.failedSessionCount ?? 0;
3990
+ if (options.cron) {
3991
+ if (failedSessionCount > 0) {
3992
+ const firstFailure = result.failedSessions[0];
3993
+ if (firstFailure) {
3994
+ throw new Error(
3995
+ `observer failed for ${failedSessionCount} session(s); first error: ${firstFailure.sessionKey} - ${firstFailure.error}`
3996
+ );
3997
+ }
3998
+ throw new Error(`observer failed for ${failedSessionCount} session(s).`);
3999
+ }
4000
+ if (result.candidateSessions === 0) {
4001
+ console.log("nothing new");
4002
+ return;
4003
+ }
4004
+ console.log(formatCronSummary({
4005
+ observedSessions: result.observedSessions,
4006
+ observedNewBytes: result.observedNewBytes ?? result.totalNewBytes,
4007
+ routedCounts: result.routedCounts ?? {}
4008
+ }));
4009
+ return;
4010
+ }
4011
+ if (result.candidateSessions === 0) {
4012
+ console.log(`No active sessions crossed threshold (${result.checkedSessions} checked).`);
4013
+ return;
4014
+ }
4015
+ if (result.dryRun) {
4016
+ console.log(
4017
+ `Dry run: ${result.candidateSessions} session(s) would be observed (${result.totalNewBytes} new bytes).`
4018
+ );
4019
+ for (const candidate of result.candidates) {
4020
+ console.log(
4021
+ `- ${candidate.sessionKey} [${candidate.sourceLabel}] \u0394${candidate.newBytes}B (threshold ${candidate.thresholdBytes}B)`
4022
+ );
4023
+ }
4024
+ return;
4025
+ }
4026
+ console.log(
4027
+ `Active observation complete: ${result.observedSessions}/${result.candidateSessions} session(s) observed.${failedSessionCount > 0 ? ` ${failedSessionCount} failed.` : ""}`
4028
+ );
4029
+ if (failedSessionCount > 0) {
4030
+ for (const failure of result.failedSessions) {
4031
+ console.error(
4032
+ `[observer] session failed ${failure.sessionKey} (${failure.sessionId}): ${failure.error}`
4033
+ );
4034
+ }
4035
+ }
4036
+ return;
4037
+ }
4038
+ const observer = new Observer(vaultPath, {
4039
+ tokenThreshold: options.threshold,
4040
+ reflectThreshold: options.reflectThreshold,
4041
+ model: options.model,
4042
+ extractTasks: options.extractTasks
4043
+ });
4044
+ if (options.compress) {
4045
+ await runOneShotCompression(observer, options.compress, vaultPath);
4046
+ return;
4047
+ }
4048
+ let watchPath = options.watch ? path13.resolve(options.watch) : "";
4049
+ if (!watchPath && options.daemon) {
4050
+ watchPath = path13.join(vaultPath, "sessions");
4051
+ }
4052
+ if (!watchPath) {
4053
+ throw new Error("Either --watch or --compress must be provided.");
4054
+ }
4055
+ if (!fs12.existsSync(watchPath)) {
4056
+ if (options.daemon && !options.watch) {
4057
+ fs12.mkdirSync(watchPath, { recursive: true });
4058
+ } else {
4059
+ throw new Error(`Watch path does not exist: ${watchPath}`);
4060
+ }
4061
+ }
4062
+ if (options.daemon) {
4063
+ const daemonArgs = buildDaemonArgs({ ...options, watch: watchPath, vaultPath });
4064
+ const child = (0, import_child_process2.spawn)(process.execPath, daemonArgs, {
4065
+ detached: true,
4066
+ stdio: "ignore"
4067
+ });
4068
+ child.unref();
4069
+ console.log(`Observer daemon started (pid: ${child.pid})`);
4070
+ return;
4071
+ }
4072
+ await watchSessions(observer, watchPath);
4073
+ }
4074
+ function registerObserveCommand(program) {
4075
+ program.command("observe").description("Observe session files and build observational memory").option("--watch <path>", "Watch session file or directory").option("--active", "Observe active OpenClaw sessions incrementally").option("--cron", "Run one-shot active observation for cron hooks").option("--agent <id>", "OpenClaw agent ID (default: OPENCLAW_AGENT_ID or clawdious)").option("--min-new <bytes>", "Override minimum new-content threshold in bytes").option("--sessions-dir <path>", "Override OpenClaw sessions directory").option("--dry-run", "Show active observation candidates without compressing").option("--max-sessions <n>", "Limit number of sessions to observe (recommended: 10)").option("--threshold <n>", "Compression token threshold", "30000").option("--reflect-threshold <n>", "Reflection token threshold", "40000").option("--model <model>", "LLM model override").option("--extract-tasks", "Extract task-like observations into backlog", true).option("--no-extract-tasks", "Disable task extraction from observations").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) => {
4076
+ await observeCommand({
4077
+ watch: rawOptions.watch,
4078
+ active: rawOptions.active,
4079
+ cron: rawOptions.cron,
4080
+ agent: rawOptions.agent,
4081
+ minNew: rawOptions.minNew ? parsePositiveInteger(rawOptions.minNew, "min-new") : void 0,
4082
+ sessionsDir: rawOptions.sessionsDir,
4083
+ dryRun: rawOptions.dryRun,
4084
+ maxSessions: rawOptions.maxSessions ? parsePositiveInteger(rawOptions.maxSessions, "max-sessions") : void 0,
4085
+ threshold: parsePositiveInteger(rawOptions.threshold, "threshold"),
4086
+ reflectThreshold: parsePositiveInteger(rawOptions.reflectThreshold, "reflect-threshold"),
4087
+ model: rawOptions.model,
4088
+ extractTasks: rawOptions.extractTasks,
4089
+ compress: rawOptions.compress,
4090
+ daemon: rawOptions.daemon,
4091
+ vaultPath: rawOptions.vault
4092
+ });
4093
+ });
4094
+ }
4095
+ // Annotate the CommonJS export names for ESM import in node:
4096
+ 0 && (module.exports = {
4097
+ observeCommand,
4098
+ registerObserveCommand
4099
+ });