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