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,3136 @@
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/rebuild.ts
31
+ var rebuild_exports = {};
32
+ __export(rebuild_exports, {
33
+ rebuildCommand: () => rebuildCommand,
34
+ registerRebuildCommand: () => registerRebuildCommand
35
+ });
36
+ module.exports = __toCommonJS(rebuild_exports);
37
+ var fs9 = __toESM(require("fs"), 1);
38
+
39
+ // src/lib/config.ts
40
+ var fs = __toESM(require("fs"), 1);
41
+ var path = __toESM(require("path"), 1);
42
+ function findNearestVaultPath(startPath = process.cwd()) {
43
+ let current = path.resolve(startPath);
44
+ while (true) {
45
+ if (fs.existsSync(path.join(current, ".clawvault.json"))) {
46
+ return current;
47
+ }
48
+ const parent = path.dirname(current);
49
+ if (parent === current) {
50
+ return null;
51
+ }
52
+ current = parent;
53
+ }
54
+ }
55
+ function resolveVaultPath(options = {}) {
56
+ if (options.explicitPath) {
57
+ return path.resolve(options.explicitPath);
58
+ }
59
+ if (process.env.CLAWVAULT_PATH) {
60
+ return path.resolve(process.env.CLAWVAULT_PATH);
61
+ }
62
+ const discovered = findNearestVaultPath(options.cwd ?? process.cwd());
63
+ if (discovered) {
64
+ return discovered;
65
+ }
66
+ throw new Error("No vault path found. Set CLAWVAULT_PATH, use --vault, or run inside a vault.");
67
+ }
68
+
69
+ // src/lib/ledger.ts
70
+ var fs2 = __toESM(require("fs"), 1);
71
+ var path2 = __toESM(require("path"), 1);
72
+ var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
73
+ var YEAR_RE = /^\d{4}$/;
74
+ var MONTH_RE = /^(0[1-9]|1[0-2])$/;
75
+ var DAY_FILE_RE = /^(0[1-9]|[12]\d|3[01])\.md$/;
76
+ var RAW_DAY_FILE_RE = /^(0[1-9]|[12]\d|3[01])\.jsonl$/;
77
+ function normalizeDateKey(date) {
78
+ if (typeof date === "string") {
79
+ if (!DATE_RE.test(date)) {
80
+ throw new Error(`Invalid date key: ${date}`);
81
+ }
82
+ return date;
83
+ }
84
+ return date.toISOString().slice(0, 10);
85
+ }
86
+ function ensureDir(dirPath) {
87
+ fs2.mkdirSync(dirPath, { recursive: true });
88
+ }
89
+ function walkThreeLevelDateTree(rootPath, extension) {
90
+ if (!fs2.existsSync(rootPath)) {
91
+ return [];
92
+ }
93
+ const results = [];
94
+ for (const yearEntry of fs2.readdirSync(rootPath, { withFileTypes: true })) {
95
+ if (!yearEntry.isDirectory() || !YEAR_RE.test(yearEntry.name)) continue;
96
+ const yearDir = path2.join(rootPath, yearEntry.name);
97
+ for (const monthEntry of fs2.readdirSync(yearDir, { withFileTypes: true })) {
98
+ if (!monthEntry.isDirectory() || !MONTH_RE.test(monthEntry.name)) continue;
99
+ const monthDir = path2.join(yearDir, monthEntry.name);
100
+ for (const dayEntry of fs2.readdirSync(monthDir, { withFileTypes: true })) {
101
+ if (!dayEntry.isFile()) continue;
102
+ const matches = extension === ".md" ? DAY_FILE_RE.test(dayEntry.name) : RAW_DAY_FILE_RE.test(dayEntry.name);
103
+ if (!matches) continue;
104
+ const day = dayEntry.name.slice(0, extension.length * -1);
105
+ const date = `${yearEntry.name}-${monthEntry.name}-${day}`;
106
+ if (!DATE_RE.test(date)) continue;
107
+ results.push({
108
+ date,
109
+ absolutePath: path2.join(monthDir, dayEntry.name)
110
+ });
111
+ }
112
+ }
113
+ }
114
+ return results;
115
+ }
116
+ function inDateRange(date, fromDate, toDate) {
117
+ if (fromDate && date < fromDate) {
118
+ return false;
119
+ }
120
+ if (toDate && date > toDate) {
121
+ return false;
122
+ }
123
+ return true;
124
+ }
125
+ function toDateKey(date) {
126
+ return date.toISOString().slice(0, 10);
127
+ }
128
+ function getLedgerRoot(vaultPath) {
129
+ return path2.join(path2.resolve(vaultPath), "ledger");
130
+ }
131
+ function getRawRoot(vaultPath) {
132
+ return path2.join(getLedgerRoot(vaultPath), "raw");
133
+ }
134
+ function getRawSourceDir(vaultPath, source) {
135
+ return path2.join(getRawRoot(vaultPath), source);
136
+ }
137
+ function getObservationsRoot(vaultPath) {
138
+ return path2.join(getLedgerRoot(vaultPath), "observations");
139
+ }
140
+ function getReflectionsRoot(vaultPath) {
141
+ return path2.join(getLedgerRoot(vaultPath), "reflections");
142
+ }
143
+ function getArchiveObservationsRoot(vaultPath) {
144
+ return path2.join(getLedgerRoot(vaultPath), "archive", "observations");
145
+ }
146
+ function getLegacyObservationsRoot(vaultPath) {
147
+ return path2.join(path2.resolve(vaultPath), "observations");
148
+ }
149
+ function getObservationPath(vaultPath, date) {
150
+ const dateKey = normalizeDateKey(date);
151
+ const [year, month, day] = dateKey.split("-");
152
+ return path2.join(getObservationsRoot(vaultPath), year, month, `${day}.md`);
153
+ }
154
+ function getLegacyObservationPath(vaultPath, date) {
155
+ const dateKey = normalizeDateKey(date);
156
+ return path2.join(getLegacyObservationsRoot(vaultPath), `${dateKey}.md`);
157
+ }
158
+ function getRawTranscriptPath(vaultPath, source, date) {
159
+ const dateKey = normalizeDateKey(date);
160
+ const [year, month, day] = dateKey.split("-");
161
+ return path2.join(getRawSourceDir(vaultPath, source), year, month, `${day}.jsonl`);
162
+ }
163
+ function ensureLedgerStructure(vaultPath) {
164
+ const root = getLedgerRoot(vaultPath);
165
+ const rawRoot = getRawRoot(vaultPath);
166
+ ensureDir(root);
167
+ ensureDir(rawRoot);
168
+ for (const source of ["openclaw", "chatgpt", "claude", "opencode"]) {
169
+ ensureDir(path2.join(rawRoot, source));
170
+ }
171
+ ensureDir(getObservationsRoot(vaultPath));
172
+ ensureDir(getReflectionsRoot(vaultPath));
173
+ ensureDir(getArchiveObservationsRoot(vaultPath));
174
+ }
175
+ function listRawTranscriptFiles(vaultPath, options = {}) {
176
+ const rawRoot = getRawRoot(vaultPath);
177
+ if (!fs2.existsSync(rawRoot)) {
178
+ return [];
179
+ }
180
+ const sources = options.source ? [options.source] : fs2.readdirSync(rawRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
181
+ const files = [];
182
+ for (const source of sources) {
183
+ const sourceRoot = path2.join(rawRoot, source);
184
+ const datedFiles = walkThreeLevelDateTree(sourceRoot, ".jsonl");
185
+ for (const entry of datedFiles) {
186
+ if (!inDateRange(entry.date, options.fromDate, options.toDate)) {
187
+ continue;
188
+ }
189
+ files.push({
190
+ source,
191
+ date: entry.date,
192
+ path: entry.absolutePath
193
+ });
194
+ }
195
+ }
196
+ return files.sort(
197
+ (left, right) => left.date === right.date ? left.path.localeCompare(right.path) : left.date.localeCompare(right.date)
198
+ );
199
+ }
200
+ function ensureParentDir(filePath) {
201
+ ensureDir(path2.dirname(filePath));
202
+ }
203
+
204
+ // src/observer/observer.ts
205
+ var fs8 = __toESM(require("fs"), 1);
206
+ var path8 = __toESM(require("path"), 1);
207
+
208
+ // src/lib/observation-format.ts
209
+ var DATE_HEADING_RE = /^##\s+(\d{4}-\d{2}-\d{2})\s*$/;
210
+ 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;
211
+ var EMOJI_LINE_RE = /^(?:-\s*)?(🔴|🟡|🟢)\s+(\d{2}:\d{2})?\s*(.+)$/u;
212
+ var DECISION_RE = /\b(decis(?:ion|ions)?|decid(?:e|ed|ing)|chose|selected|opted|went with|picked)\b/i;
213
+ 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;
214
+ var COMMITMENT_RE = /\b(commit(?:ment|ted)?|promised|deadline|due|scheduled|will deliver|agreed to)\b/i;
215
+ var TODO_RE = /(?:\btodo:\s*|\bwe need to\b|\bdon't forget(?: to)?\b|\bremember to\b|\bmake sure to\b)/i;
216
+ var COMMITMENT_TASK_RE = /\b(?:i'?ll|i will|let me|(?:i'?m\s+)?going to|plan to|should)\b/i;
217
+ var UNRESOLVED_RE = /\b(?:need to figure out|tbd|to be determined)\b/i;
218
+ var DEADLINE_RE = /\b(?:by\s+(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday|tomorrow)|before\s+the\s+\w+|deadline is)\b/i;
219
+ var MILESTONE_RE = /\b(released?|shipped|launched|merged|published|milestone|v\d+\.\d+)\b/i;
220
+ var LESSON_RE = /\b(learn(?:ed|ing|t)|lesson|insight|realized|discovered|never again)\b/i;
221
+ var RELATIONSHIP_RE = /\b(talked to|met with|spoke with|asked|client|partner|teammate|colleague)\b/i;
222
+ var PROJECT_RE = /\b(project|feature|service|repo|api|roadmap|sprint)\b/i;
223
+ function clamp01(value) {
224
+ if (!Number.isFinite(value)) return 0;
225
+ if (value < 0) return 0;
226
+ if (value > 1) return 1;
227
+ return value;
228
+ }
229
+ function scoreFromLegacyPriority(priority) {
230
+ if (priority === "\u{1F534}") return 0.9;
231
+ if (priority === "\u{1F7E1}") return 0.6;
232
+ return 0.2;
233
+ }
234
+ function confidenceFromLegacyPriority(priority) {
235
+ if (priority === "\u{1F534}") return 0.9;
236
+ if (priority === "\u{1F7E1}") return 0.8;
237
+ return 0.7;
238
+ }
239
+ function inferObservationType(content) {
240
+ if (DECISION_RE.test(content)) return "decision";
241
+ if (UNRESOLVED_RE.test(content)) return "commitment-unresolved";
242
+ if (TODO_RE.test(content)) return "todo";
243
+ if (PREFERENCE_RE.test(content)) return "preference";
244
+ if (COMMITMENT_TASK_RE.test(content) || DEADLINE_RE.test(content)) return "task";
245
+ if (COMMITMENT_RE.test(content)) return "commitment";
246
+ if (MILESTONE_RE.test(content)) return "milestone";
247
+ if (LESSON_RE.test(content)) return "lesson";
248
+ if (RELATIONSHIP_RE.test(content)) return "relationship";
249
+ if (PROJECT_RE.test(content)) return "project";
250
+ return "fact";
251
+ }
252
+ function formatScore(value) {
253
+ return clamp01(value).toFixed(2);
254
+ }
255
+ function normalizeObservationContent(content) {
256
+ return content.replace(/^\d{2}:\d{2}\s+/, "").replace(/\s+/g, " ").trim().toLowerCase();
257
+ }
258
+ function parseObservationLine(line, date) {
259
+ const scored = line.match(SCORED_LINE_RE);
260
+ if (scored) {
261
+ return {
262
+ date,
263
+ type: scored[1].toLowerCase(),
264
+ confidence: clamp01(Number.parseFloat(scored[2])),
265
+ importance: clamp01(Number.parseFloat(scored[3])),
266
+ content: scored[4].trim(),
267
+ format: "scored",
268
+ rawLine: line
269
+ };
270
+ }
271
+ const emoji = line.match(EMOJI_LINE_RE);
272
+ if (!emoji) {
273
+ return null;
274
+ }
275
+ const priority = emoji[1];
276
+ const time = emoji[2]?.trim();
277
+ const text = emoji[3].trim();
278
+ const content = time ? `${time} ${text}` : text;
279
+ return {
280
+ date,
281
+ type: inferObservationType(content),
282
+ confidence: confidenceFromLegacyPriority(priority),
283
+ importance: scoreFromLegacyPriority(priority),
284
+ content,
285
+ format: "emoji",
286
+ priority,
287
+ time,
288
+ rawLine: line
289
+ };
290
+ }
291
+ function parseObservationMarkdown(markdown) {
292
+ const parsed = [];
293
+ let currentDate = "";
294
+ for (const line of markdown.split(/\r?\n/)) {
295
+ const heading = line.match(DATE_HEADING_RE);
296
+ if (heading) {
297
+ currentDate = heading[1];
298
+ continue;
299
+ }
300
+ if (!currentDate) {
301
+ continue;
302
+ }
303
+ const record = parseObservationLine(line.trim(), currentDate);
304
+ if (record) {
305
+ parsed.push(record);
306
+ }
307
+ }
308
+ return parsed;
309
+ }
310
+ function renderScoredObservationLine(record) {
311
+ return `- [${record.type}|c=${formatScore(record.confidence)}|i=${formatScore(record.importance)}] ${record.content.trim()}`;
312
+ }
313
+ function renderObservationMarkdown(sections) {
314
+ const chunks = [];
315
+ const dates = [...sections.keys()].sort((left, right) => left.localeCompare(right));
316
+ for (const date of dates) {
317
+ const lines = sections.get(date) ?? [];
318
+ if (lines.length === 0) continue;
319
+ chunks.push(`## ${date}`);
320
+ chunks.push("");
321
+ for (const line of lines) {
322
+ chunks.push(renderScoredObservationLine(line));
323
+ }
324
+ chunks.push("");
325
+ }
326
+ return chunks.join("\n").trim();
327
+ }
328
+
329
+ // src/lib/claude-credentials.ts
330
+ var import_child_process = require("child_process");
331
+ var import_fs = require("fs");
332
+ var import_os = require("os");
333
+ var import_path = require("path");
334
+ var CLAUDE_CODE_SERVICE = "Claude Code-credentials";
335
+ var CLAUDE_CODE_ACCOUNT = "Claude Code";
336
+ var OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
337
+ var TOKEN_REFRESH_URL = "https://console.anthropic.com/v1/oauth/token";
338
+ var EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
339
+ function readClaudeCliCredentials(opts) {
340
+ if (process.platform === "darwin") {
341
+ try {
342
+ const raw = (0, import_child_process.execFileSync)(
343
+ "security",
344
+ ["find-generic-password", "-s", CLAUDE_CODE_SERVICE, "-w"],
345
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
346
+ ).trim();
347
+ const parsed = parseCredentialsJson(raw);
348
+ if (parsed) return parsed;
349
+ } catch {
350
+ }
351
+ }
352
+ const home = opts?.homeDir ?? (0, import_os.homedir)();
353
+ const credFile = (0, import_path.join)(home, ".claude", ".credentials.json");
354
+ if (!(0, import_fs.existsSync)(credFile)) {
355
+ return null;
356
+ }
357
+ try {
358
+ const raw = (0, import_fs.readFileSync)(credFile, "utf8");
359
+ return parseCredentialsJson(raw);
360
+ } catch {
361
+ return null;
362
+ }
363
+ }
364
+ function parseCredentialsJson(raw) {
365
+ try {
366
+ const parsed = JSON.parse(raw);
367
+ const oauth = parsed.claudeAiOauth;
368
+ if (oauth && typeof oauth.accessToken === "string" && typeof oauth.refreshToken === "string" && typeof oauth.expiresAt === "number") {
369
+ return {
370
+ accessToken: oauth.accessToken,
371
+ refreshToken: oauth.refreshToken,
372
+ expiresAt: oauth.expiresAt
373
+ };
374
+ }
375
+ } catch {
376
+ }
377
+ return null;
378
+ }
379
+ async function refreshClaudeOAuthToken(refreshToken, fetchImpl) {
380
+ const f = fetchImpl ?? fetch;
381
+ const response = await f(TOKEN_REFRESH_URL, {
382
+ method: "POST",
383
+ headers: { "content-type": "application/json" },
384
+ body: JSON.stringify({
385
+ grant_type: "refresh_token",
386
+ client_id: OAUTH_CLIENT_ID,
387
+ refresh_token: refreshToken
388
+ })
389
+ });
390
+ if (!response.ok) {
391
+ throw new Error(`OAuth token refresh failed (${response.status})`);
392
+ }
393
+ const data = await response.json();
394
+ return {
395
+ accessToken: data.access_token,
396
+ refreshToken: data.refresh_token,
397
+ expiresAt: Date.now() + data.expires_in * 1e3
398
+ };
399
+ }
400
+ function writeClaudeCliCredentials(cred, opts) {
401
+ const payload = JSON.stringify({ claudeAiOauth: cred });
402
+ if (process.platform === "darwin") {
403
+ try {
404
+ (0, import_child_process.execFileSync)(
405
+ "security",
406
+ ["add-generic-password", "-U", "-s", CLAUDE_CODE_SERVICE, "-a", CLAUDE_CODE_ACCOUNT, "-w", payload],
407
+ { stdio: "ignore" }
408
+ );
409
+ return;
410
+ } catch {
411
+ }
412
+ }
413
+ const home = opts?.homeDir ?? (0, import_os.homedir)();
414
+ const credFile = (0, import_path.join)(home, ".claude", ".credentials.json");
415
+ (0, import_fs.writeFileSync)(credFile, payload, "utf8");
416
+ }
417
+ async function resolveClaudeOAuthToken(opts) {
418
+ const cred = readClaudeCliCredentials(opts);
419
+ if (!cred) {
420
+ return null;
421
+ }
422
+ if (cred.expiresAt < Date.now() + EXPIRY_BUFFER_MS) {
423
+ try {
424
+ const refreshed = await refreshClaudeOAuthToken(cred.refreshToken, opts?.fetchImpl);
425
+ writeClaudeCliCredentials(refreshed, opts);
426
+ return refreshed.accessToken;
427
+ } catch {
428
+ return cred.accessToken;
429
+ }
430
+ }
431
+ return cred.accessToken;
432
+ }
433
+
434
+ // src/observer/compressor.ts
435
+ var OPENAI_BASE_URL = "https://api.openai.com/v1";
436
+ var OLLAMA_BASE_URL = "http://localhost:11434/v1";
437
+ var DEFAULT_PROVIDER_MODELS = {
438
+ anthropic: "claude-haiku-4-5",
439
+ openai: "gpt-4o-mini",
440
+ gemini: "gemini-2.0-flash",
441
+ "openai-compatible": "gpt-4o-mini",
442
+ ollama: "llama3.2"
443
+ };
444
+ 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;
445
+ 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;
446
+ 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;
447
+ var TODO_SIGNAL_RE = /(?:\btodo:\s*|\bwe need to\b|\bdon't forget(?: to)?\b|\bremember to\b|\bmake sure to\b)/i;
448
+ var COMMITMENT_TASK_SIGNAL_RE = /\b(?:i'?ll|i will|let me|(?:i'?m\s+)?going to|plan to|should)\b/i;
449
+ var UNRESOLVED_COMMITMENT_RE = /\b(?:need to figure out|tbd|to be determined)\b/i;
450
+ var DEADLINE_SIGNAL_RE = /\b(?:by\s+(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday|tomorrow)|before\s+the\s+\w+|deadline is)\b/i;
451
+ var Compressor = class {
452
+ provider;
453
+ model;
454
+ baseUrl;
455
+ apiKey;
456
+ now;
457
+ fetchImpl;
458
+ constructor(options = {}) {
459
+ this.provider = options.provider;
460
+ this.model = options.model;
461
+ this.baseUrl = options.baseUrl;
462
+ this.apiKey = options.apiKey;
463
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
464
+ this.fetchImpl = options.fetchImpl ?? fetch;
465
+ }
466
+ async compress(messages, existingObservations) {
467
+ const cleanedMessages = messages.map((message) => message.trim()).filter(Boolean);
468
+ if (cleanedMessages.length === 0) {
469
+ return existingObservations.trim();
470
+ }
471
+ const prompt = this.buildPrompt(cleanedMessages, existingObservations);
472
+ const backend = await this.resolveProvider();
473
+ if (backend) {
474
+ try {
475
+ 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);
476
+ const normalized = this.normalizeLlmOutput(llmOutput);
477
+ if (normalized) {
478
+ return this.mergeObservations(existingObservations, normalized);
479
+ }
480
+ } catch {
481
+ }
482
+ }
483
+ const fallback = this.fallbackCompression(cleanedMessages);
484
+ return this.mergeObservations(existingObservations, fallback);
485
+ }
486
+ async resolveProvider() {
487
+ if (process.env.CLAWVAULT_NO_LLM) return null;
488
+ if (this.provider) {
489
+ const configured = this.resolveConfiguredProvider(this.provider);
490
+ if (configured) {
491
+ return configured;
492
+ }
493
+ return await this.resolveProviderFromEnv(false);
494
+ }
495
+ return await this.resolveProviderFromEnv(true);
496
+ }
497
+ resolveConfiguredProvider(provider) {
498
+ const model = this.resolveModel(provider);
499
+ if (provider === "anthropic") {
500
+ const apiKey2 = this.resolveApiKey(provider);
501
+ if (!apiKey2) {
502
+ return null;
503
+ }
504
+ return {
505
+ provider,
506
+ model,
507
+ apiKey: apiKey2
508
+ };
509
+ }
510
+ if (provider === "gemini") {
511
+ const apiKey2 = this.resolveApiKey(provider);
512
+ if (!apiKey2) {
513
+ return null;
514
+ }
515
+ return {
516
+ provider,
517
+ model,
518
+ apiKey: apiKey2
519
+ };
520
+ }
521
+ if (provider === "openai") {
522
+ const apiKey2 = this.resolveApiKey(provider);
523
+ if (!apiKey2) {
524
+ return null;
525
+ }
526
+ return {
527
+ provider,
528
+ model,
529
+ apiKey: apiKey2,
530
+ baseUrl: this.resolveBaseUrl(provider)
531
+ };
532
+ }
533
+ const apiKey = this.resolveApiKey(provider) ?? void 0;
534
+ return {
535
+ provider,
536
+ model,
537
+ apiKey,
538
+ baseUrl: this.resolveBaseUrl(provider)
539
+ };
540
+ }
541
+ async resolveProviderFromEnv(allowConfiguredModel) {
542
+ const anthropicModel = allowConfiguredModel ? this.resolveModel("anthropic") : DEFAULT_PROVIDER_MODELS.anthropic;
543
+ const oauthEnvToken = this.readEnvValue("ANTHROPIC_OAUTH_TOKEN");
544
+ if (oauthEnvToken) {
545
+ return { provider: "anthropic", model: anthropicModel, apiKey: oauthEnvToken, isOAuth: true };
546
+ }
547
+ const anthropicApiKey = this.readEnvValue("ANTHROPIC_API_KEY");
548
+ if (anthropicApiKey) {
549
+ return { provider: "anthropic", model: anthropicModel, apiKey: anthropicApiKey };
550
+ }
551
+ if (this.readEnvValue("CLAWVAULT_CLAUDE_AUTH")) {
552
+ const oauthToken = await resolveClaudeOAuthToken();
553
+ if (oauthToken) {
554
+ return { provider: "anthropic", model: anthropicModel, apiKey: oauthToken, isOAuth: true };
555
+ }
556
+ }
557
+ const openAiApiKey = this.readEnvValue("OPENAI_API_KEY");
558
+ if (openAiApiKey) {
559
+ return {
560
+ provider: "openai",
561
+ model: allowConfiguredModel ? this.resolveModel("openai") : DEFAULT_PROVIDER_MODELS.openai,
562
+ apiKey: openAiApiKey,
563
+ baseUrl: OPENAI_BASE_URL
564
+ };
565
+ }
566
+ const geminiApiKey = this.readEnvValue("GEMINI_API_KEY");
567
+ if (geminiApiKey) {
568
+ return {
569
+ provider: "gemini",
570
+ model: allowConfiguredModel ? this.resolveModel("gemini") : DEFAULT_PROVIDER_MODELS.gemini,
571
+ apiKey: geminiApiKey
572
+ };
573
+ }
574
+ return null;
575
+ }
576
+ resolveModel(provider) {
577
+ const configuredModel = this.model?.trim();
578
+ if (configuredModel) {
579
+ return configuredModel;
580
+ }
581
+ return DEFAULT_PROVIDER_MODELS[provider];
582
+ }
583
+ resolveApiKey(provider) {
584
+ const configuredApiKey = this.apiKey?.trim();
585
+ if (configuredApiKey) {
586
+ return configuredApiKey;
587
+ }
588
+ if (provider === "anthropic") {
589
+ return this.readEnvValue("ANTHROPIC_API_KEY");
590
+ }
591
+ if (provider === "gemini") {
592
+ return this.readEnvValue("GEMINI_API_KEY");
593
+ }
594
+ return this.readEnvValue("OPENAI_API_KEY");
595
+ }
596
+ resolveBaseUrl(provider) {
597
+ const configuredBaseUrl = this.baseUrl?.trim();
598
+ if (configuredBaseUrl) {
599
+ return configuredBaseUrl.replace(/\/+$/, "");
600
+ }
601
+ if (provider === "ollama") {
602
+ return OLLAMA_BASE_URL;
603
+ }
604
+ return OPENAI_BASE_URL;
605
+ }
606
+ readEnvValue(name) {
607
+ const value = process.env[name]?.trim();
608
+ return value ? value : null;
609
+ }
610
+ buildPrompt(messages, existingObservations) {
611
+ return [
612
+ "You are an observer that compresses raw AI session messages into durable, human-meaningful observations.",
613
+ "",
614
+ "Rules:",
615
+ "- Output markdown only.",
616
+ "- Group observations by date heading: ## YYYY-MM-DD",
617
+ "- Each observation line MUST follow: - [type|c=<0.00-1.00>|i=<0.00-1.00>] <observation>",
618
+ "- Allowed type tags: decision, preference, fact, commitment, task, todo, commitment-unresolved, milestone, lesson, relationship, project",
619
+ "- i >= 0.80 for structural/persistent observations (major decisions, blockers, releases, commitments)",
620
+ "- i 0.40-0.79 for potentially important observations (notable context, preferences, milestones)",
621
+ "- i < 0.40 for contextual/routine observations",
622
+ "- Confidence c reflects extraction certainty, not importance.",
623
+ "- Preserve source tags when present (e.g., [main], [telegram-dm], [discord], [telegram-group]).",
624
+ "",
625
+ "PREFERENCE & PERSONAL CONTEXT EXTRACTION (critical for personalization):",
626
+ "- Emit [preference] for ANY personal detail that reveals tastes, habits, equipment, or context:",
627
+ ' * Explicit: "I prefer X", "I like Y", "I always use Z"',
628
+ ' * Ownership: "my Sony A7R IV", "I have a...", "I use...", "I own..."',
629
+ ' * Habits/routines: "I usually...", "every morning I...", "I tend to..."',
630
+ " * Interests: topics the user is enthusiastic about, hobbies mentioned in passing",
631
+ " * Constraints: dietary restrictions, allergies, phobias, limitations",
632
+ ` * Goals: "I'm trying to...", "I want to learn..."`,
633
+ "- These are HIGH VALUE observations (i >= 0.60) \u2014 they enable personalized responses.",
634
+ "- When in doubt between [preference] and [task], choose [preference] if it describes a lasting trait.",
635
+ "",
636
+ "TASK EXTRACTION (required):",
637
+ `- Emit [todo] for explicit TODO phrasing: "TODO:", "we need to", "don't forget", "remember to", "make sure to".`,
638
+ `- Emit [task] for commitments/action intent: "I'll", "I will", "let me", "going to", "plan to", "should".`,
639
+ '- Emit [commitment-unresolved] for unresolved commitments/questions: "need to figure out", "TBD", "to be determined".',
640
+ '- Deadline language ("by Friday", "before the demo", "deadline is") should increase importance and usually map to [task] unless unresolved.',
641
+ "",
642
+ "QUALITY FILTERS (important):",
643
+ "- DO NOT observe: CLI errors, command failures, tool output parsing issues, retry attempts, debug logs.",
644
+ " These are transient noise, not memories. Only observe errors if they represent a BLOCKER or an unresolved problem.",
645
+ '- DO NOT observe: "acknowledged the conversation", "said okay", routine confirmations.',
646
+ '- MERGE related events into single observations. If 5 images were generated, say "Generated 5 images for X" not 5 separate lines.',
647
+ '- MERGE retry sequences: "Tried X, failed, tried Y, succeeded" \u2192 "Resolved X using Y (after initial failure)"',
648
+ '- Prefer OUTCOMES over PROCESSES: "Deployed v1.2 to Railway" not "Started deploy... build finished... deploy succeeded"',
649
+ "",
650
+ "AGENT ATTRIBUTION:",
651
+ '- If the transcript shows multiple speakers/agents, prefix observations with who did it: "Pedro asked...", "Clawdious deployed...", "Zeca generated..."',
652
+ "- If only one agent is acting, attribution is optional.",
653
+ "",
654
+ "PROJECT MILESTONES (critical \u2014 these are the most valuable observations):",
655
+ "Projects are NOT just code. Milestones include business, strategy, client, and operational events.",
656
+ "- Use milestone/decision/commitment types for strategic events with high importance.",
657
+ "- Use preference/lesson/relationship/project/fact when appropriate.",
658
+ "- Examples:",
659
+ ' "- [decision|c=0.95|i=0.90] 14:00 Pricing decision: $33K one-time + $3K/mo for Artemisa"',
660
+ ' "- [milestone|c=0.93|i=0.88] 14:00 Published clawvault@2.1.0 to npm"',
661
+ ' "- [project|c=0.84|i=0.58] 14:00 Deployed pitch deck to artemisa-pitch-deck.vercel.app"',
662
+ "- Do NOT collapse multiple milestones into one line \u2014 each matters for history.",
663
+ "",
664
+ "COMMITMENT FORMAT (when someone promises/agrees to something):",
665
+ '- Prefer: "- [commitment|c=...|i=...] HH:MM [COMMITMENT] <who> committed to <what> by <when>"',
666
+ "",
667
+ "Keep observations concise and factual. Aim for signal, not completeness.",
668
+ "",
669
+ "Existing observations (may be empty):",
670
+ existingObservations.trim() || "(none)",
671
+ "",
672
+ "Raw messages:",
673
+ ...messages.map((message, index) => `[${index + 1}] ${message}`),
674
+ "",
675
+ "Return only the updated observation markdown."
676
+ ].join("\n");
677
+ }
678
+ buildOpenAICompatibleUrl(baseUrl) {
679
+ const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
680
+ return `${normalizedBaseUrl}/chat/completions`;
681
+ }
682
+ buildOpenAICompatibleHeaders(apiKey) {
683
+ const headers = {
684
+ "content-type": "application/json"
685
+ };
686
+ if (apiKey) {
687
+ headers.authorization = `Bearer ${apiKey}`;
688
+ }
689
+ return headers;
690
+ }
691
+ extractOpenAIContent(content) {
692
+ if (typeof content === "string") {
693
+ return content.trim();
694
+ }
695
+ if (!Array.isArray(content)) {
696
+ return "";
697
+ }
698
+ const parts = content.map((part) => {
699
+ if (typeof part === "string") {
700
+ return part;
701
+ }
702
+ if (!part || typeof part !== "object") {
703
+ return "";
704
+ }
705
+ const candidate = part;
706
+ return typeof candidate.text === "string" ? candidate.text : "";
707
+ }).filter((part) => part.trim().length > 0);
708
+ return parts.join("\n").trim();
709
+ }
710
+ async callAnthropic(prompt, backend) {
711
+ if (!backend.apiKey) {
712
+ return "";
713
+ }
714
+ const isOAuth = backend.isOAuth || backend.apiKey.includes("sk-ant-oat");
715
+ const headers = isOAuth ? {
716
+ "content-type": "application/json",
717
+ "authorization": `Bearer ${backend.apiKey}`,
718
+ "anthropic-version": "2023-06-01",
719
+ "anthropic-beta": "claude-code-20250219,oauth-2025-04-20",
720
+ "x-app": "cli",
721
+ "user-agent": "claude-cli/1.0.0 (external, cli)"
722
+ } : {
723
+ "content-type": "application/json",
724
+ "x-api-key": backend.apiKey,
725
+ "anthropic-version": "2023-06-01"
726
+ };
727
+ const body = isOAuth ? {
728
+ model: backend.model,
729
+ temperature: 0.1,
730
+ max_tokens: 1400,
731
+ system: [{ type: "text", text: "You are Claude Code, Anthropic's official CLI for Claude." }],
732
+ messages: [{ role: "user", content: prompt }]
733
+ } : {
734
+ model: backend.model,
735
+ temperature: 0.1,
736
+ max_tokens: 1400,
737
+ messages: [{ role: "user", content: prompt }]
738
+ };
739
+ const response = await this.fetchImpl("https://api.anthropic.com/v1/messages", {
740
+ method: "POST",
741
+ headers,
742
+ body: JSON.stringify(body)
743
+ });
744
+ if (!response.ok) {
745
+ throw new Error(`Anthropic request failed (${response.status})`);
746
+ }
747
+ const payload = await response.json();
748
+ return payload.content?.filter((part) => part.type === "text" && part.text).map((part) => part.text).join("\n").trim() ?? "";
749
+ }
750
+ async callOpenAI(prompt, backend) {
751
+ return this.callOpenAICompatible(prompt, backend);
752
+ }
753
+ async callOpenAICompatible(prompt, backend) {
754
+ const baseUrl = backend.baseUrl ?? this.resolveBaseUrl(backend.provider);
755
+ const response = await this.fetchImpl(this.buildOpenAICompatibleUrl(baseUrl), {
756
+ method: "POST",
757
+ headers: this.buildOpenAICompatibleHeaders(backend.apiKey),
758
+ body: JSON.stringify({
759
+ model: backend.model,
760
+ temperature: 0.1,
761
+ messages: [
762
+ { role: "system", content: "You transform session logs into concise observations." },
763
+ { role: "user", content: prompt }
764
+ ]
765
+ })
766
+ });
767
+ if (!response.ok) {
768
+ throw new Error(`OpenAI-compatible request failed (${response.status})`);
769
+ }
770
+ const payload = await response.json();
771
+ return this.extractOpenAIContent(payload.choices?.[0]?.message?.content);
772
+ }
773
+ async callGemini(prompt, backend) {
774
+ if (!backend.apiKey) {
775
+ return "";
776
+ }
777
+ const model = encodeURIComponent(backend.model);
778
+ const response = await this.fetchImpl(
779
+ `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${backend.apiKey}`,
780
+ {
781
+ method: "POST",
782
+ headers: { "content-type": "application/json" },
783
+ body: JSON.stringify({
784
+ contents: [{ parts: [{ text: prompt }] }],
785
+ generationConfig: { temperature: 0.1, maxOutputTokens: 1400 }
786
+ })
787
+ }
788
+ );
789
+ if (!response.ok) {
790
+ throw new Error(`Gemini request failed (${response.status})`);
791
+ }
792
+ const payload = await response.json();
793
+ return payload.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? "";
794
+ }
795
+ normalizeLlmOutput(output) {
796
+ if (!output.trim()) {
797
+ return "";
798
+ }
799
+ const cleaned = output.replace(/^```(?:markdown)?\s*/i, "").replace(/\s*```$/, "").trim();
800
+ const lines = cleaned.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
801
+ const hasObservationLine = lines.some((line) => line.startsWith("- [") || /^(?:-\s*)?(🔴|🟡|🟢)\s+/.test(line));
802
+ if (!hasObservationLine) {
803
+ return "";
804
+ }
805
+ const hasDateHeading = lines.some((line) => DATE_HEADING_RE.test(line));
806
+ const result = hasDateHeading ? cleaned : `## ${this.formatDate(this.now())}
807
+
808
+ ${cleaned}`;
809
+ const sanitized = this.sanitizeWikiLinks(result);
810
+ return this.enforceImportanceRules(sanitized);
811
+ }
812
+ /**
813
+ * Fix wiki-link corruption from LLM compression.
814
+ * LLMs often fuse preceding word fragments into wiki-links during rewriting:
815
+ * "reque[[people/pedro]]" → "[[people/pedro]]"
816
+ * "Linke[[agents/zeca]]" → "[[agents/zeca]]"
817
+ * "taske[[people/pedro]]a" → "[[people/pedro]]"
818
+ * Also fixes trailing word fragments fused after closing brackets.
819
+ */
820
+ sanitizeWikiLinks(markdown) {
821
+ let result = markdown.replace(/\w+\[\[/g, " [[");
822
+ result = result.replace(/\]\]\w+/g, "]]");
823
+ result = result.replace(/ {2,}/g, " ");
824
+ return result;
825
+ }
826
+ enforceImportanceRules(markdown) {
827
+ const parsed = parseObservationMarkdown(markdown);
828
+ if (parsed.length === 0) {
829
+ return "";
830
+ }
831
+ const grouped = /* @__PURE__ */ new Map();
832
+ for (const record of parsed) {
833
+ const adjusted = this.enforceImportanceForRecord(record);
834
+ const bucket = grouped.get(record.date) ?? [];
835
+ bucket.push(adjusted);
836
+ grouped.set(record.date, bucket);
837
+ }
838
+ return renderObservationMarkdown(grouped);
839
+ }
840
+ enforceImportanceForRecord(record) {
841
+ let importance = record.importance;
842
+ let confidence = record.confidence;
843
+ let type = record.type;
844
+ const inferredTaskType = this.inferTaskType(record.content);
845
+ if (this.isCriticalContent(record.content)) {
846
+ importance = Math.max(importance, 0.85);
847
+ confidence = Math.max(confidence, 0.85);
848
+ if (type === "fact") {
849
+ type = inferObservationType(record.content);
850
+ }
851
+ } else if (this.isNotableContent(record.content)) {
852
+ importance = Math.max(importance, 0.5);
853
+ confidence = Math.max(confidence, 0.75);
854
+ }
855
+ if (inferredTaskType) {
856
+ type = type === "fact" || type === "commitment" ? inferredTaskType : type;
857
+ importance = Math.max(importance, inferredTaskType === "commitment-unresolved" ? 0.72 : 0.65);
858
+ confidence = Math.max(confidence, 0.8);
859
+ }
860
+ if (type === "decision" || type === "commitment" || type === "milestone") {
861
+ importance = Math.max(importance, 0.6);
862
+ }
863
+ return {
864
+ type,
865
+ confidence: this.clamp01(confidence),
866
+ importance: this.clamp01(importance),
867
+ content: record.content
868
+ };
869
+ }
870
+ fallbackCompression(messages) {
871
+ const sections = /* @__PURE__ */ new Map();
872
+ const seen = /* @__PURE__ */ new Set();
873
+ for (const message of messages) {
874
+ const normalized = this.normalizeText(message);
875
+ if (!normalized) continue;
876
+ const date = this.extractDate(message) ?? this.formatDate(this.now());
877
+ const time = this.extractTime(message) ?? this.formatTime(this.now());
878
+ const line = `${time} ${normalized}`;
879
+ const type = inferObservationType(line);
880
+ const importance = this.inferImportance(line, type);
881
+ const confidence = this.inferConfidence(line, type, importance);
882
+ const dedupeKey = `${date}|${type}|${normalizeObservationContent(line)}`;
883
+ if (seen.has(dedupeKey)) continue;
884
+ seen.add(dedupeKey);
885
+ const bucket = sections.get(date) ?? [];
886
+ bucket.push({ type, confidence, importance, content: line });
887
+ sections.set(date, bucket);
888
+ }
889
+ if (sections.size === 0) {
890
+ const date = this.formatDate(this.now());
891
+ sections.set(date, [{
892
+ type: "fact",
893
+ confidence: 0.7,
894
+ importance: 0.2,
895
+ content: `${this.formatTime(this.now())} Processed session updates.`
896
+ }]);
897
+ }
898
+ return this.renderSections(sections);
899
+ }
900
+ mergeObservations(existing, incoming) {
901
+ const existingRecords = parseObservationMarkdown(existing);
902
+ const incomingRecords = parseObservationMarkdown(incoming);
903
+ if (incomingRecords.length === 0) {
904
+ return existing.trim();
905
+ }
906
+ const merged = /* @__PURE__ */ new Map();
907
+ for (const record of existingRecords) {
908
+ this.mergeRecord(merged, {
909
+ date: record.date,
910
+ type: record.type,
911
+ confidence: record.confidence,
912
+ importance: record.importance,
913
+ content: record.content
914
+ });
915
+ }
916
+ for (const record of incomingRecords) {
917
+ this.mergeRecord(merged, {
918
+ date: record.date,
919
+ type: record.type,
920
+ confidence: record.confidence,
921
+ importance: record.importance,
922
+ content: record.content
923
+ });
924
+ }
925
+ return this.renderSections(merged);
926
+ }
927
+ mergeRecord(sections, input) {
928
+ const bucket = sections.get(input.date) ?? [];
929
+ const key = normalizeObservationContent(input.content);
930
+ const index = bucket.findIndex((line) => normalizeObservationContent(line.content) === key);
931
+ if (index === -1) {
932
+ bucket.push({
933
+ type: input.type,
934
+ confidence: this.clamp01(input.confidence),
935
+ importance: this.clamp01(input.importance),
936
+ content: input.content.trim()
937
+ });
938
+ sections.set(input.date, bucket);
939
+ return;
940
+ }
941
+ const existing = bucket[index];
942
+ bucket[index] = {
943
+ type: input.importance >= existing.importance ? input.type : existing.type,
944
+ confidence: this.clamp01(Math.max(existing.confidence, input.confidence)),
945
+ importance: this.clamp01(Math.max(existing.importance, input.importance)),
946
+ content: existing.content.length >= input.content.length ? existing.content : input.content
947
+ };
948
+ sections.set(input.date, bucket);
949
+ }
950
+ renderSections(sections) {
951
+ return renderObservationMarkdown(sections);
952
+ }
953
+ inferImportance(text, type) {
954
+ const inferredTaskType = this.inferTaskType(text);
955
+ if (this.isCriticalContent(text)) return 0.9;
956
+ if (inferredTaskType === "commitment-unresolved") return 0.72;
957
+ if (inferredTaskType === "task" || inferredTaskType === "todo") return 0.65;
958
+ if (this.isNotableContent(text)) return 0.6;
959
+ if (type === "decision" || type === "commitment" || type === "milestone") return 0.55;
960
+ if (type === "preference" || type === "lesson" || type === "relationship" || type === "project") return 0.45;
961
+ return 0.2;
962
+ }
963
+ inferConfidence(text, type, importance) {
964
+ const inferredTaskType = this.inferTaskType(text);
965
+ let confidence = 0.72;
966
+ if (importance >= 0.8) confidence += 0.12;
967
+ if (type === "decision" || type === "commitment" || type === "milestone") confidence += 0.06;
968
+ if (inferredTaskType) confidence += 0.06;
969
+ if (/\b(?:decided|chose|committed|deadline|released|merged)\b/i.test(text)) {
970
+ confidence += 0.05;
971
+ }
972
+ return this.clamp01(confidence);
973
+ }
974
+ isCriticalContent(text) {
975
+ return CRITICAL_RE.test(text) || DEADLINE_WITH_DATE_RE.test(text);
976
+ }
977
+ isNotableContent(text) {
978
+ return NOTABLE_RE.test(text);
979
+ }
980
+ inferTaskType(text) {
981
+ if (UNRESOLVED_COMMITMENT_RE.test(text)) {
982
+ return "commitment-unresolved";
983
+ }
984
+ if (TODO_SIGNAL_RE.test(text)) {
985
+ return "todo";
986
+ }
987
+ if (COMMITMENT_TASK_SIGNAL_RE.test(text) || DEADLINE_SIGNAL_RE.test(text)) {
988
+ return "task";
989
+ }
990
+ return null;
991
+ }
992
+ normalizeText(text) {
993
+ return text.replace(/\s+/g, " ").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").trim().slice(0, 280);
994
+ }
995
+ extractDate(text) {
996
+ const match = text.match(/\b(\d{4}-\d{2}-\d{2})\b/);
997
+ return match?.[1] ?? null;
998
+ }
999
+ extractTime(text) {
1000
+ const match = text.match(/\b([01]\d|2[0-3]):([0-5]\d)\b/);
1001
+ if (!match) {
1002
+ return null;
1003
+ }
1004
+ return `${match[1]}:${match[2]}`;
1005
+ }
1006
+ formatDate(date) {
1007
+ return date.toISOString().split("T")[0];
1008
+ }
1009
+ formatTime(date) {
1010
+ return date.toISOString().slice(11, 16);
1011
+ }
1012
+ clamp01(value) {
1013
+ if (!Number.isFinite(value)) return 0;
1014
+ if (value < 0) return 0;
1015
+ if (value > 1) return 1;
1016
+ return value;
1017
+ }
1018
+ };
1019
+
1020
+ // src/observer/reflector.ts
1021
+ var DATE_HEADING_RE2 = /^##\s+(\d{4}-\d{2}-\d{2})\s*$/;
1022
+ var OBSERVATION_LINE_RE = /^(🔴|🟡|🟢)\s+(.+)$/u;
1023
+ var Reflector = class {
1024
+ now;
1025
+ constructor(options = {}) {
1026
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
1027
+ }
1028
+ reflect(observations) {
1029
+ const sections = this.parseSections(observations);
1030
+ if (sections.size === 0) {
1031
+ return observations.trim();
1032
+ }
1033
+ const cutoff = this.buildCutoffDate();
1034
+ const dedupeKeys = [];
1035
+ const reflected = /* @__PURE__ */ new Map();
1036
+ const dates = [...sections.keys()].sort((a, b) => b.localeCompare(a));
1037
+ for (const date of dates) {
1038
+ const sectionDate = this.parseDate(date);
1039
+ const olderThanCutoff = sectionDate ? sectionDate.getTime() < cutoff.getTime() : false;
1040
+ const lines = sections.get(date) ?? [];
1041
+ const kept = [];
1042
+ for (const line of lines) {
1043
+ if (line.priority === "\u{1F534}") {
1044
+ kept.push(line);
1045
+ continue;
1046
+ }
1047
+ if (line.priority === "\u{1F7E2}" && olderThanCutoff) {
1048
+ continue;
1049
+ }
1050
+ const key = this.normalizeText(line.content);
1051
+ const isDuplicate = dedupeKeys.some((existing) => this.isSimilar(existing, key));
1052
+ if (isDuplicate) {
1053
+ continue;
1054
+ }
1055
+ dedupeKeys.push(key);
1056
+ kept.push(line);
1057
+ }
1058
+ if (kept.length > 0) {
1059
+ reflected.set(date, kept);
1060
+ }
1061
+ }
1062
+ return this.renderSections(reflected);
1063
+ }
1064
+ buildCutoffDate() {
1065
+ const cutoff = new Date(this.now());
1066
+ cutoff.setHours(0, 0, 0, 0);
1067
+ cutoff.setDate(cutoff.getDate() - 7);
1068
+ return cutoff;
1069
+ }
1070
+ parseDate(date) {
1071
+ const parsed = /* @__PURE__ */ new Date(`${date}T00:00:00.000Z`);
1072
+ if (Number.isNaN(parsed.getTime())) {
1073
+ return null;
1074
+ }
1075
+ return parsed;
1076
+ }
1077
+ parseSections(markdown) {
1078
+ const sections = /* @__PURE__ */ new Map();
1079
+ let currentDate = null;
1080
+ for (const rawLine of markdown.split(/\r?\n/)) {
1081
+ const dateMatch = rawLine.match(DATE_HEADING_RE2);
1082
+ if (dateMatch) {
1083
+ currentDate = dateMatch[1];
1084
+ if (!sections.has(currentDate)) {
1085
+ sections.set(currentDate, []);
1086
+ }
1087
+ continue;
1088
+ }
1089
+ if (!currentDate) continue;
1090
+ const lineMatch = rawLine.match(OBSERVATION_LINE_RE);
1091
+ if (!lineMatch) continue;
1092
+ const bucket = sections.get(currentDate) ?? [];
1093
+ bucket.push({
1094
+ priority: lineMatch[1],
1095
+ content: lineMatch[2].trim()
1096
+ });
1097
+ sections.set(currentDate, bucket);
1098
+ }
1099
+ return sections;
1100
+ }
1101
+ renderSections(sections) {
1102
+ const chunks = [];
1103
+ const dates = [...sections.keys()].sort((a, b) => a.localeCompare(b));
1104
+ for (const date of dates) {
1105
+ const lines = sections.get(date) ?? [];
1106
+ if (lines.length === 0) continue;
1107
+ chunks.push(`## ${date}`);
1108
+ chunks.push("");
1109
+ for (const line of lines) {
1110
+ chunks.push(`${line.priority} ${line.content}`);
1111
+ }
1112
+ chunks.push("");
1113
+ }
1114
+ return chunks.join("\n").trim();
1115
+ }
1116
+ normalizeText(text) {
1117
+ return text.toLowerCase().replace(/\s+/g, " ").replace(/[^\w\s:.-]/g, "").trim();
1118
+ }
1119
+ isSimilar(a, b) {
1120
+ if (a === b) return true;
1121
+ if (a.length >= 24 && (a.includes(b) || b.includes(a))) {
1122
+ return true;
1123
+ }
1124
+ return false;
1125
+ }
1126
+ };
1127
+
1128
+ // src/observer/router.ts
1129
+ var fs7 = __toESM(require("fs"), 1);
1130
+ var path7 = __toESM(require("path"), 1);
1131
+
1132
+ // src/lib/task-utils.ts
1133
+ var fs4 = __toESM(require("fs"), 1);
1134
+ var path4 = __toESM(require("path"), 1);
1135
+ var import_gray_matter2 = __toESM(require("gray-matter"), 1);
1136
+
1137
+ // src/lib/transition-ledger.ts
1138
+ var fs3 = __toESM(require("fs"), 1);
1139
+ var path3 = __toESM(require("path"), 1);
1140
+ var REGRESSION_PAIRS = [
1141
+ ["done", "open"],
1142
+ ["done", "blocked"],
1143
+ ["in-progress", "blocked"]
1144
+ ];
1145
+ function isRegression(from, to) {
1146
+ return REGRESSION_PAIRS.some(([f, t]) => f === from && t === to);
1147
+ }
1148
+ function getLedgerDir(vaultPath) {
1149
+ return path3.join(path3.resolve(vaultPath), "ledger", "transitions");
1150
+ }
1151
+ function getTodayLedgerPath(vaultPath) {
1152
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1153
+ return path3.join(getLedgerDir(vaultPath), `${date}.jsonl`);
1154
+ }
1155
+ var RETRYABLE_APPEND_CODES = /* @__PURE__ */ new Set(["ENOENT", "EAGAIN", "EBUSY"]);
1156
+ var MAX_APPEND_RETRIES = 2;
1157
+ function asErrno(error) {
1158
+ if (!error || typeof error !== "object") {
1159
+ return null;
1160
+ }
1161
+ return error;
1162
+ }
1163
+ function formatLedgerWriteError(filePath, error) {
1164
+ const errno = asErrno(error);
1165
+ const message = error instanceof Error ? error.message : String(error);
1166
+ if (errno?.code === "ENOSPC") {
1167
+ return new Error(`Failed to write transition ledger at ${filePath}: no space left on device.`);
1168
+ }
1169
+ if (errno?.code === "EACCES" || errno?.code === "EPERM") {
1170
+ return new Error(`Failed to write transition ledger at ${filePath}: permission denied.`);
1171
+ }
1172
+ return new Error(`Failed to write transition ledger at ${filePath}: ${message}`);
1173
+ }
1174
+ function appendTransition(vaultPath, event) {
1175
+ const ledgerDir = getLedgerDir(vaultPath);
1176
+ try {
1177
+ fs3.mkdirSync(ledgerDir, { recursive: true });
1178
+ } catch (error) {
1179
+ throw formatLedgerWriteError(ledgerDir, error);
1180
+ }
1181
+ const filePath = getTodayLedgerPath(vaultPath);
1182
+ const payload = JSON.stringify(event) + "\n";
1183
+ for (let attempt = 0; attempt <= MAX_APPEND_RETRIES; attempt += 1) {
1184
+ try {
1185
+ fs3.appendFileSync(filePath, payload);
1186
+ return;
1187
+ } catch (error) {
1188
+ const errno = asErrno(error);
1189
+ const code = errno?.code;
1190
+ if (code === "ENOENT") {
1191
+ try {
1192
+ fs3.mkdirSync(ledgerDir, { recursive: true });
1193
+ } catch (mkdirError) {
1194
+ throw formatLedgerWriteError(filePath, mkdirError);
1195
+ }
1196
+ }
1197
+ if (code && RETRYABLE_APPEND_CODES.has(code) && attempt < MAX_APPEND_RETRIES) {
1198
+ continue;
1199
+ }
1200
+ throw formatLedgerWriteError(filePath, error);
1201
+ }
1202
+ }
1203
+ }
1204
+ function buildTransitionEvent(taskId, fromStatus, toStatus, options = {}) {
1205
+ const agentId = process.env.OPENCLAW_AGENT_ID || "manual";
1206
+ const costTokensRaw = process.env.OPENCLAW_TOKEN_ESTIMATE;
1207
+ const costTokens = costTokensRaw ? parseInt(costTokensRaw, 10) : null;
1208
+ return {
1209
+ task_id: taskId,
1210
+ agent_id: agentId,
1211
+ from_status: fromStatus,
1212
+ to_status: toStatus,
1213
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1214
+ confidence: options.confidence ?? (agentId === "manual" ? 1 : 1),
1215
+ cost_tokens: costTokens !== null && !isNaN(costTokens) ? costTokens : null,
1216
+ reason: options.reason || null
1217
+ };
1218
+ }
1219
+ function readAllTransitions(vaultPath) {
1220
+ const ledgerDir = getLedgerDir(vaultPath);
1221
+ if (!fs3.existsSync(ledgerDir)) return [];
1222
+ let files = [];
1223
+ try {
1224
+ files = fs3.readdirSync(ledgerDir).filter((f) => f.endsWith(".jsonl")).sort();
1225
+ } catch {
1226
+ return [];
1227
+ }
1228
+ const events = [];
1229
+ for (const file of files) {
1230
+ let lines = [];
1231
+ try {
1232
+ lines = fs3.readFileSync(path3.join(ledgerDir, file), "utf-8").split("\n").filter((l) => l.trim());
1233
+ } catch {
1234
+ continue;
1235
+ }
1236
+ for (const line of lines) {
1237
+ try {
1238
+ events.push(JSON.parse(line));
1239
+ } catch {
1240
+ }
1241
+ }
1242
+ }
1243
+ return events;
1244
+ }
1245
+ function countBlockedTransitions(vaultPath, taskId) {
1246
+ const events = readAllTransitions(vaultPath);
1247
+ return events.filter((e) => e.task_id === taskId && e.to_status === "blocked").length;
1248
+ }
1249
+
1250
+ // src/lib/primitive-templates.ts
1251
+ var import_gray_matter = __toESM(require("gray-matter"), 1);
1252
+
1253
+ // src/lib/task-utils.ts
1254
+ function slugify(text) {
1255
+ return text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").trim();
1256
+ }
1257
+ function getTasksDir(vaultPath) {
1258
+ return path4.join(path4.resolve(vaultPath), "tasks");
1259
+ }
1260
+ function getBacklogDir(vaultPath) {
1261
+ return path4.join(path4.resolve(vaultPath), "backlog");
1262
+ }
1263
+ function ensureBacklogDir(vaultPath) {
1264
+ const backlogDir = getBacklogDir(vaultPath);
1265
+ if (!fs4.existsSync(backlogDir)) {
1266
+ fs4.mkdirSync(backlogDir, { recursive: true });
1267
+ }
1268
+ }
1269
+ function getTaskPath(vaultPath, slug) {
1270
+ return path4.join(getTasksDir(vaultPath), `${slug}.md`);
1271
+ }
1272
+ function getBacklogPath(vaultPath, slug) {
1273
+ return path4.join(getBacklogDir(vaultPath), `${slug}.md`);
1274
+ }
1275
+ function extractTitle(content) {
1276
+ const match = content.match(/^#\s+(.+)$/m);
1277
+ return match ? match[1].trim() : "";
1278
+ }
1279
+ function parseDueDate(value) {
1280
+ if (!value) return null;
1281
+ const timestamp = Date.parse(value);
1282
+ if (Number.isNaN(timestamp)) return null;
1283
+ return timestamp;
1284
+ }
1285
+ function startOfToday() {
1286
+ const now = /* @__PURE__ */ new Date();
1287
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
1288
+ }
1289
+ var VALID_TASK_STATUSES = /* @__PURE__ */ new Set([
1290
+ "open",
1291
+ "in-progress",
1292
+ "blocked",
1293
+ "done"
1294
+ ]);
1295
+ function isTaskStatus(value) {
1296
+ return typeof value === "string" && VALID_TASK_STATUSES.has(value);
1297
+ }
1298
+ function persistTaskFrontmatter(task, frontmatter) {
1299
+ fs4.writeFileSync(task.path, import_gray_matter2.default.stringify(task.content, frontmatter));
1300
+ }
1301
+ function resolveStatusTransition(previousStatus, nextStatus) {
1302
+ if (!isTaskStatus(previousStatus) || !isTaskStatus(nextStatus)) {
1303
+ return null;
1304
+ }
1305
+ if (previousStatus === nextStatus) {
1306
+ return null;
1307
+ }
1308
+ return { fromStatus: previousStatus, toStatus: nextStatus };
1309
+ }
1310
+ function logStatusTransition({
1311
+ vaultPath,
1312
+ task,
1313
+ fromStatus,
1314
+ toStatus,
1315
+ frontmatter,
1316
+ options
1317
+ }) {
1318
+ const normalizedReason = typeof options.reason === "string" ? options.reason.trim() : "";
1319
+ const reason = normalizedReason || (isRegression(fromStatus, toStatus) ? `regression: ${fromStatus} -> ${toStatus}` : void 0);
1320
+ const event = buildTransitionEvent(task.slug, fromStatus, toStatus, {
1321
+ confidence: options.confidence,
1322
+ reason
1323
+ });
1324
+ try {
1325
+ appendTransition(vaultPath, event);
1326
+ } catch {
1327
+ return frontmatter;
1328
+ }
1329
+ if (toStatus !== "blocked" || frontmatter.escalation) {
1330
+ return frontmatter;
1331
+ }
1332
+ let blockedCount = 0;
1333
+ try {
1334
+ blockedCount = countBlockedTransitions(vaultPath, task.slug);
1335
+ } catch {
1336
+ return frontmatter;
1337
+ }
1338
+ if (blockedCount < 3) {
1339
+ return frontmatter;
1340
+ }
1341
+ const escalatedFrontmatter = {
1342
+ ...frontmatter,
1343
+ escalation: true
1344
+ };
1345
+ try {
1346
+ persistTaskFrontmatter(task, escalatedFrontmatter);
1347
+ return escalatedFrontmatter;
1348
+ } catch {
1349
+ return frontmatter;
1350
+ }
1351
+ }
1352
+ function readTask(vaultPath, slug) {
1353
+ const taskPath = getTaskPath(vaultPath, slug);
1354
+ if (!fs4.existsSync(taskPath)) {
1355
+ return null;
1356
+ }
1357
+ try {
1358
+ const raw = fs4.readFileSync(taskPath, "utf-8");
1359
+ const { data, content } = (0, import_gray_matter2.default)(raw);
1360
+ const title = extractTitle(content) || slug;
1361
+ return {
1362
+ slug,
1363
+ title,
1364
+ content,
1365
+ frontmatter: data,
1366
+ path: taskPath
1367
+ };
1368
+ } catch {
1369
+ return null;
1370
+ }
1371
+ }
1372
+ function readBacklogItem(vaultPath, slug) {
1373
+ const backlogPath = getBacklogPath(vaultPath, slug);
1374
+ if (!fs4.existsSync(backlogPath)) {
1375
+ return null;
1376
+ }
1377
+ try {
1378
+ const raw = fs4.readFileSync(backlogPath, "utf-8");
1379
+ const { data, content } = (0, import_gray_matter2.default)(raw);
1380
+ const title = extractTitle(content) || slug;
1381
+ return {
1382
+ slug,
1383
+ title,
1384
+ content,
1385
+ frontmatter: data,
1386
+ path: backlogPath
1387
+ };
1388
+ } catch {
1389
+ return null;
1390
+ }
1391
+ }
1392
+ function listTasks(vaultPath, filters) {
1393
+ const tasksDir = getTasksDir(vaultPath);
1394
+ if (!fs4.existsSync(tasksDir)) {
1395
+ return [];
1396
+ }
1397
+ const tasks = [];
1398
+ const entries = fs4.readdirSync(tasksDir, { withFileTypes: true });
1399
+ const today = startOfToday();
1400
+ for (const entry of entries) {
1401
+ if (!entry.isFile() || !entry.name.endsWith(".md")) {
1402
+ continue;
1403
+ }
1404
+ const slug = entry.name.replace(/\.md$/, "");
1405
+ const task = readTask(vaultPath, slug);
1406
+ if (!task) continue;
1407
+ if (filters) {
1408
+ if (filters.status && task.frontmatter.status !== filters.status) continue;
1409
+ if (filters.owner && task.frontmatter.owner !== filters.owner) continue;
1410
+ if (filters.project && task.frontmatter.project !== filters.project) continue;
1411
+ if (filters.priority && task.frontmatter.priority !== filters.priority) continue;
1412
+ if (filters.due && !task.frontmatter.due) continue;
1413
+ if (filters.tag) {
1414
+ const tags = task.frontmatter.tags || [];
1415
+ const hasTag = tags.some((tag) => tag.toLowerCase() === filters.tag?.toLowerCase());
1416
+ if (!hasTag) continue;
1417
+ }
1418
+ if (filters.overdue) {
1419
+ const dueTime = parseDueDate(task.frontmatter.due);
1420
+ if (task.frontmatter.status === "done" || dueTime === null || dueTime >= today) continue;
1421
+ }
1422
+ }
1423
+ tasks.push(task);
1424
+ }
1425
+ const priorityOrder = {
1426
+ critical: 0,
1427
+ high: 1,
1428
+ medium: 2,
1429
+ low: 3
1430
+ };
1431
+ if (filters?.due || filters?.overdue) {
1432
+ return tasks.sort((a, b) => {
1433
+ const aDue = parseDueDate(a.frontmatter.due);
1434
+ const bDue = parseDueDate(b.frontmatter.due);
1435
+ if (aDue !== null && bDue !== null && aDue !== bDue) {
1436
+ return aDue - bDue;
1437
+ }
1438
+ if (aDue !== null && bDue === null) return -1;
1439
+ if (aDue === null && bDue !== null) return 1;
1440
+ return new Date(b.frontmatter.created).getTime() - new Date(a.frontmatter.created).getTime();
1441
+ });
1442
+ }
1443
+ return tasks.sort((a, b) => {
1444
+ const aPriority = priorityOrder[a.frontmatter.priority || "low"];
1445
+ const bPriority = priorityOrder[b.frontmatter.priority || "low"];
1446
+ if (aPriority !== bPriority) {
1447
+ return aPriority - bPriority;
1448
+ }
1449
+ return new Date(b.frontmatter.created).getTime() - new Date(a.frontmatter.created).getTime();
1450
+ });
1451
+ }
1452
+ function listBacklogItems(vaultPath, filters) {
1453
+ const backlogDir = getBacklogDir(vaultPath);
1454
+ if (!fs4.existsSync(backlogDir)) {
1455
+ return [];
1456
+ }
1457
+ const items = [];
1458
+ const entries = fs4.readdirSync(backlogDir, { withFileTypes: true });
1459
+ for (const entry of entries) {
1460
+ if (!entry.isFile() || !entry.name.endsWith(".md")) {
1461
+ continue;
1462
+ }
1463
+ const slug = entry.name.replace(/\.md$/, "");
1464
+ const item = readBacklogItem(vaultPath, slug);
1465
+ if (!item) continue;
1466
+ if (filters) {
1467
+ if (filters.project && item.frontmatter.project !== filters.project) continue;
1468
+ if (filters.source && item.frontmatter.source !== filters.source) continue;
1469
+ }
1470
+ items.push(item);
1471
+ }
1472
+ return items.sort((a, b) => {
1473
+ return new Date(b.frontmatter.created).getTime() - new Date(a.frontmatter.created).getTime();
1474
+ });
1475
+ }
1476
+ function updateTask(vaultPath, slug, updates, options = {}) {
1477
+ const task = readTask(vaultPath, slug);
1478
+ if (!task) {
1479
+ throw new Error(`Task not found: ${slug}`);
1480
+ }
1481
+ if (updates.status !== void 0 && !isTaskStatus(updates.status)) {
1482
+ throw new Error(`Invalid task status: ${String(updates.status)}`);
1483
+ }
1484
+ const previousStatus = task.frontmatter.status;
1485
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1486
+ let newFrontmatter = {
1487
+ ...task.frontmatter,
1488
+ updated: now
1489
+ };
1490
+ if (updates.status !== void 0) {
1491
+ newFrontmatter.status = updates.status;
1492
+ if (updates.status === "done" && !newFrontmatter.completed) {
1493
+ newFrontmatter.completed = now;
1494
+ }
1495
+ if (updates.status !== "done") {
1496
+ delete newFrontmatter.completed;
1497
+ }
1498
+ }
1499
+ if (updates.source !== void 0) {
1500
+ if (updates.source === null || updates.source.trim() === "") {
1501
+ delete newFrontmatter.source;
1502
+ } else {
1503
+ newFrontmatter.source = updates.source;
1504
+ }
1505
+ }
1506
+ if (updates.owner !== void 0) {
1507
+ if (updates.owner === null || updates.owner.trim() === "") {
1508
+ delete newFrontmatter.owner;
1509
+ } else {
1510
+ newFrontmatter.owner = updates.owner;
1511
+ }
1512
+ }
1513
+ if (updates.project !== void 0) {
1514
+ if (updates.project === null || updates.project.trim() === "") {
1515
+ delete newFrontmatter.project;
1516
+ } else {
1517
+ newFrontmatter.project = updates.project;
1518
+ }
1519
+ }
1520
+ if (updates.priority !== void 0) {
1521
+ if (updates.priority === null) {
1522
+ delete newFrontmatter.priority;
1523
+ } else {
1524
+ newFrontmatter.priority = updates.priority;
1525
+ }
1526
+ }
1527
+ if (updates.due !== void 0) {
1528
+ if (updates.due === null || updates.due.trim() === "") {
1529
+ delete newFrontmatter.due;
1530
+ } else {
1531
+ newFrontmatter.due = updates.due;
1532
+ }
1533
+ }
1534
+ if (updates.tags !== void 0) {
1535
+ if (updates.tags === null) {
1536
+ delete newFrontmatter.tags;
1537
+ } else {
1538
+ const normalizedTags = updates.tags.map((tag) => tag.trim()).filter(Boolean);
1539
+ if (normalizedTags.length === 0) {
1540
+ delete newFrontmatter.tags;
1541
+ } else {
1542
+ newFrontmatter.tags = normalizedTags;
1543
+ }
1544
+ }
1545
+ }
1546
+ if (updates.completed !== void 0) {
1547
+ if (updates.completed === null || updates.completed.trim() === "") {
1548
+ delete newFrontmatter.completed;
1549
+ } else {
1550
+ newFrontmatter.completed = updates.completed;
1551
+ }
1552
+ }
1553
+ if (updates.escalation !== void 0) {
1554
+ if (updates.escalation === null) {
1555
+ delete newFrontmatter.escalation;
1556
+ } else {
1557
+ newFrontmatter.escalation = updates.escalation;
1558
+ }
1559
+ }
1560
+ if (updates.confidence !== void 0) {
1561
+ if (updates.confidence === null) {
1562
+ delete newFrontmatter.confidence;
1563
+ } else {
1564
+ newFrontmatter.confidence = updates.confidence;
1565
+ }
1566
+ }
1567
+ if (updates.reason !== void 0) {
1568
+ if (updates.reason === null || updates.reason.trim() === "") {
1569
+ delete newFrontmatter.reason;
1570
+ } else {
1571
+ newFrontmatter.reason = updates.reason;
1572
+ }
1573
+ }
1574
+ if (updates.description !== void 0) {
1575
+ if (updates.description === null || updates.description.trim() === "") {
1576
+ delete newFrontmatter.description;
1577
+ } else {
1578
+ newFrontmatter.description = updates.description;
1579
+ }
1580
+ }
1581
+ if (updates.estimate !== void 0) {
1582
+ if (updates.estimate === null || updates.estimate.trim() === "") {
1583
+ delete newFrontmatter.estimate;
1584
+ } else {
1585
+ newFrontmatter.estimate = updates.estimate;
1586
+ }
1587
+ }
1588
+ if (updates.parent !== void 0) {
1589
+ if (updates.parent === null || updates.parent.trim() === "") {
1590
+ delete newFrontmatter.parent;
1591
+ } else {
1592
+ newFrontmatter.parent = updates.parent;
1593
+ }
1594
+ }
1595
+ if (updates.depends_on !== void 0) {
1596
+ if (updates.depends_on === null) {
1597
+ delete newFrontmatter.depends_on;
1598
+ } else {
1599
+ const normalizedDeps = updates.depends_on.map((dep) => dep.trim()).filter(Boolean);
1600
+ if (normalizedDeps.length === 0) {
1601
+ delete newFrontmatter.depends_on;
1602
+ } else {
1603
+ newFrontmatter.depends_on = normalizedDeps;
1604
+ }
1605
+ }
1606
+ }
1607
+ if (updates.blocked_by !== void 0) {
1608
+ if (updates.blocked_by === null || updates.blocked_by.trim() === "") {
1609
+ delete newFrontmatter.blocked_by;
1610
+ } else {
1611
+ newFrontmatter.blocked_by = updates.blocked_by;
1612
+ }
1613
+ } else if (updates.status !== void 0 && updates.status !== "blocked") {
1614
+ delete newFrontmatter.blocked_by;
1615
+ }
1616
+ persistTaskFrontmatter(task, newFrontmatter);
1617
+ const transition = options.skipTransition ? null : resolveStatusTransition(previousStatus, newFrontmatter.status);
1618
+ if (transition) {
1619
+ const confidence = options.confidence ?? (typeof updates.confidence === "number" ? updates.confidence : void 0);
1620
+ const reason = options.reason ?? updates.reason ?? null;
1621
+ newFrontmatter = logStatusTransition({
1622
+ vaultPath,
1623
+ task,
1624
+ fromStatus: transition.fromStatus,
1625
+ toStatus: transition.toStatus,
1626
+ frontmatter: newFrontmatter,
1627
+ options: {
1628
+ confidence,
1629
+ reason
1630
+ }
1631
+ });
1632
+ }
1633
+ return {
1634
+ ...task,
1635
+ frontmatter: newFrontmatter
1636
+ };
1637
+ }
1638
+ function createBacklogItem(vaultPath, title, options = {}) {
1639
+ ensureBacklogDir(vaultPath);
1640
+ const slug = slugify(title);
1641
+ const backlogPath = getBacklogPath(vaultPath, slug);
1642
+ if (fs4.existsSync(backlogPath)) {
1643
+ throw new Error(`Backlog item already exists: ${slug}`);
1644
+ }
1645
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1646
+ const frontmatter = {
1647
+ created: now
1648
+ };
1649
+ if (options.source) frontmatter.source = options.source;
1650
+ if (options.project) frontmatter.project = options.project;
1651
+ if (options.tags && options.tags.length > 0) frontmatter.tags = options.tags;
1652
+ let content = `# ${title}
1653
+ `;
1654
+ const links = [];
1655
+ if (options.source) links.push(`[[${options.source}]]`);
1656
+ if (options.project) links.push(`[[${options.project}]]`);
1657
+ if (links.length > 0) {
1658
+ content += `
1659
+ ${links.join(" | ")}
1660
+ `;
1661
+ }
1662
+ if (options.content) {
1663
+ content += `
1664
+ ${options.content}
1665
+ `;
1666
+ }
1667
+ const fileContent = import_gray_matter2.default.stringify(content, frontmatter);
1668
+ fs4.writeFileSync(backlogPath, fileContent);
1669
+ return {
1670
+ slug,
1671
+ title,
1672
+ content,
1673
+ frontmatter,
1674
+ path: backlogPath
1675
+ };
1676
+ }
1677
+ function updateBacklogItem(vaultPath, slug, updates) {
1678
+ const backlogItem = readBacklogItem(vaultPath, slug);
1679
+ if (!backlogItem) {
1680
+ throw new Error(`Backlog item not found: ${slug}`);
1681
+ }
1682
+ const newFrontmatter = {
1683
+ ...backlogItem.frontmatter
1684
+ };
1685
+ if (updates.source !== void 0) newFrontmatter.source = updates.source;
1686
+ if (updates.project !== void 0) newFrontmatter.project = updates.project;
1687
+ if (updates.tags !== void 0) newFrontmatter.tags = updates.tags;
1688
+ if (updates.lastSeen !== void 0) newFrontmatter.lastSeen = updates.lastSeen;
1689
+ const fileContent = import_gray_matter2.default.stringify(backlogItem.content, newFrontmatter);
1690
+ fs4.writeFileSync(backlogItem.path, fileContent);
1691
+ return {
1692
+ ...backlogItem,
1693
+ frontmatter: newFrontmatter
1694
+ };
1695
+ }
1696
+
1697
+ // src/lib/project-utils.ts
1698
+ var fs5 = __toESM(require("fs"), 1);
1699
+ var path5 = __toESM(require("path"), 1);
1700
+ var import_gray_matter3 = __toESM(require("gray-matter"), 1);
1701
+ function extractTitle2(content) {
1702
+ const match = content.match(/^#\s+(.+)$/m);
1703
+ return match ? match[1].trim() : "";
1704
+ }
1705
+ function isDateSlug(slug) {
1706
+ return /^\d{4}-\d{2}-\d{2}$/.test(slug);
1707
+ }
1708
+ function normalizeStringArray(value) {
1709
+ return value.map((item) => item.trim()).filter(Boolean);
1710
+ }
1711
+ function getProjectsDir(vaultPath) {
1712
+ return path5.join(path5.resolve(vaultPath), "projects");
1713
+ }
1714
+ function getProjectPath(vaultPath, slug) {
1715
+ return path5.join(getProjectsDir(vaultPath), `${slug}.md`);
1716
+ }
1717
+ function parseSortableTimestamp(value) {
1718
+ if (!value) return 0;
1719
+ const timestamp = Date.parse(value);
1720
+ return Number.isNaN(timestamp) ? 0 : timestamp;
1721
+ }
1722
+ function normalizeProjectStatus(value) {
1723
+ if (value === "active" || value === "paused" || value === "completed" || value === "archived") {
1724
+ return value;
1725
+ }
1726
+ return "active";
1727
+ }
1728
+ function normalizeProjectFrontmatter(frontmatter) {
1729
+ const normalizedCreated = typeof frontmatter.created === "string" && frontmatter.created ? frontmatter.created : (/* @__PURE__ */ new Date(0)).toISOString();
1730
+ const normalizedUpdated = typeof frontmatter.updated === "string" && frontmatter.updated ? frontmatter.updated : normalizedCreated;
1731
+ const normalized = {
1732
+ ...frontmatter,
1733
+ type: "project",
1734
+ status: normalizeProjectStatus(frontmatter.status),
1735
+ created: normalizedCreated,
1736
+ updated: normalizedUpdated
1737
+ };
1738
+ if (normalized.team) {
1739
+ const team = normalizeStringArray(normalized.team);
1740
+ if (team.length === 0) {
1741
+ delete normalized.team;
1742
+ } else {
1743
+ normalized.team = team;
1744
+ }
1745
+ }
1746
+ if (normalized.tags) {
1747
+ const tags = normalizeStringArray(normalized.tags);
1748
+ if (tags.length === 0) {
1749
+ delete normalized.tags;
1750
+ } else {
1751
+ normalized.tags = tags;
1752
+ }
1753
+ }
1754
+ return normalized;
1755
+ }
1756
+ function listProjects(vaultPath, filters) {
1757
+ const projectsDir = getProjectsDir(vaultPath);
1758
+ if (!fs5.existsSync(projectsDir)) {
1759
+ return [];
1760
+ }
1761
+ const projects = [];
1762
+ const entries = fs5.readdirSync(projectsDir, { withFileTypes: true });
1763
+ for (const entry of entries) {
1764
+ if (!entry.isFile() || !entry.name.endsWith(".md")) {
1765
+ continue;
1766
+ }
1767
+ const slug = entry.name.replace(/\.md$/, "");
1768
+ if (isDateSlug(slug)) {
1769
+ continue;
1770
+ }
1771
+ const project = readProject(vaultPath, slug);
1772
+ if (!project) continue;
1773
+ if (filters) {
1774
+ if (filters.status && project.frontmatter.status !== filters.status) continue;
1775
+ if (filters.owner && project.frontmatter.owner !== filters.owner) continue;
1776
+ if (filters.client && project.frontmatter.client !== filters.client) continue;
1777
+ if (filters.tag) {
1778
+ const tags = project.frontmatter.tags || [];
1779
+ const hasTag = tags.some((tag) => tag.toLowerCase() === filters.tag?.toLowerCase());
1780
+ if (!hasTag) continue;
1781
+ }
1782
+ }
1783
+ projects.push(project);
1784
+ }
1785
+ return projects.sort((left, right) => {
1786
+ const rightTime = parseSortableTimestamp(right.frontmatter.updated || right.frontmatter.created);
1787
+ const leftTime = parseSortableTimestamp(left.frontmatter.updated || left.frontmatter.created);
1788
+ return rightTime - leftTime;
1789
+ });
1790
+ }
1791
+ function readProject(vaultPath, slug) {
1792
+ if (!slug || isDateSlug(slug) || slug.includes(path5.sep)) {
1793
+ return null;
1794
+ }
1795
+ const projectPath = getProjectPath(vaultPath, slug);
1796
+ if (!fs5.existsSync(projectPath)) {
1797
+ return null;
1798
+ }
1799
+ try {
1800
+ const raw = fs5.readFileSync(projectPath, "utf-8");
1801
+ const { data, content } = (0, import_gray_matter3.default)(raw);
1802
+ if (data.type !== "project") {
1803
+ return null;
1804
+ }
1805
+ const frontmatter = normalizeProjectFrontmatter(data);
1806
+ const title = extractTitle2(content) || slug;
1807
+ return {
1808
+ slug,
1809
+ title,
1810
+ content,
1811
+ frontmatter
1812
+ };
1813
+ } catch {
1814
+ return null;
1815
+ }
1816
+ }
1817
+
1818
+ // src/lib/config-manager.ts
1819
+ var fs6 = __toESM(require("fs"), 1);
1820
+ var path6 = __toESM(require("path"), 1);
1821
+
1822
+ // src/types.ts
1823
+ var DEFAULT_CATEGORIES = [
1824
+ "rules",
1825
+ "preferences",
1826
+ "decisions",
1827
+ "patterns",
1828
+ "people",
1829
+ "projects",
1830
+ "goals",
1831
+ "transcripts",
1832
+ "inbox",
1833
+ "templates",
1834
+ "lessons",
1835
+ "agents",
1836
+ "commitments",
1837
+ "handoffs",
1838
+ "research",
1839
+ "tasks",
1840
+ "backlog"
1841
+ ];
1842
+
1843
+ // src/lib/config-manager.ts
1844
+ var CONFIG_FILE = ".clawvault.json";
1845
+ var OBSERVE_PROVIDERS = ["anthropic", "openai", "gemini"];
1846
+ var OBSERVER_COMPRESSION_PROVIDERS = [
1847
+ "anthropic",
1848
+ "openai",
1849
+ "gemini",
1850
+ "openai-compatible",
1851
+ "ollama"
1852
+ ];
1853
+ var THEMES = ["neural", "minimal", "none"];
1854
+ var CONTEXT_PROFILES = ["default", "planning", "incident", "handoff", "auto"];
1855
+ var DEFAULT_THEME = "none";
1856
+ var DEFAULT_OBSERVE_MODEL = "gemini-2.0-flash";
1857
+ var DEFAULT_OBSERVE_PROVIDER = "gemini";
1858
+ var DEFAULT_CONTEXT_MAX_RESULTS = 5;
1859
+ var DEFAULT_CONTEXT_PROFILE = "default";
1860
+ var DEFAULT_GRAPH_MAX_HOPS = 2;
1861
+ var DEFAULT_INJECT_MAX_RESULTS = 8;
1862
+ var DEFAULT_INJECT_USE_LLM = true;
1863
+ var DEFAULT_INJECT_SCOPE = ["global"];
1864
+ function configPathFor(vaultPath) {
1865
+ return path6.join(path6.resolve(vaultPath), CONFIG_FILE);
1866
+ }
1867
+ function readConfigDocument(vaultPath) {
1868
+ const configPath = configPathFor(vaultPath);
1869
+ if (!fs6.existsSync(configPath)) {
1870
+ throw new Error(`No ClawVault config found at ${configPath}`);
1871
+ }
1872
+ try {
1873
+ const parsed = JSON.parse(fs6.readFileSync(configPath, "utf-8"));
1874
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1875
+ throw new Error("Config root must be a JSON object.");
1876
+ }
1877
+ return { ...parsed };
1878
+ } catch (error) {
1879
+ if (error instanceof Error) {
1880
+ throw new Error(`Failed to parse ${CONFIG_FILE}: ${error.message}`);
1881
+ }
1882
+ throw new Error(`Failed to parse ${CONFIG_FILE}.`);
1883
+ }
1884
+ }
1885
+ function asStringArray(value) {
1886
+ if (!Array.isArray(value)) {
1887
+ return null;
1888
+ }
1889
+ const output = value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
1890
+ return output.length > 0 ? output : null;
1891
+ }
1892
+ function asPositiveInteger(value) {
1893
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) {
1894
+ return value;
1895
+ }
1896
+ if (typeof value === "string") {
1897
+ const parsed = Number.parseInt(value, 10);
1898
+ if (Number.isInteger(parsed) && parsed > 0) {
1899
+ return parsed;
1900
+ }
1901
+ }
1902
+ return null;
1903
+ }
1904
+ function asBoolean(value) {
1905
+ if (typeof value === "boolean") {
1906
+ return value;
1907
+ }
1908
+ if (typeof value === "string") {
1909
+ const normalized = value.trim().toLowerCase();
1910
+ if (["true", "1", "yes", "on"].includes(normalized)) {
1911
+ return true;
1912
+ }
1913
+ if (["false", "0", "no", "off"].includes(normalized)) {
1914
+ return false;
1915
+ }
1916
+ }
1917
+ return null;
1918
+ }
1919
+ function isObserveProvider(value) {
1920
+ return typeof value === "string" && OBSERVE_PROVIDERS.includes(value);
1921
+ }
1922
+ function isObserverCompressionProvider(value) {
1923
+ return typeof value === "string" && OBSERVER_COMPRESSION_PROVIDERS.includes(value);
1924
+ }
1925
+ function isTheme(value) {
1926
+ return typeof value === "string" && THEMES.includes(value);
1927
+ }
1928
+ function isContextProfile(value) {
1929
+ return typeof value === "string" && CONTEXT_PROFILES.includes(value);
1930
+ }
1931
+ function normalizeRouteTarget(target) {
1932
+ const trimmed = target.trim().replace(/^\/+/, "").replace(/\/+$/, "");
1933
+ if (!trimmed) {
1934
+ throw new Error("Route target cannot be empty.");
1935
+ }
1936
+ const segments = trimmed.split("/").map((segment) => segment.trim()).filter(Boolean);
1937
+ if (segments.length === 0) {
1938
+ throw new Error("Route target cannot be empty.");
1939
+ }
1940
+ if (segments.some((segment) => segment === "." || segment === "..")) {
1941
+ throw new Error(`Route target cannot contain relative path segments: ${target}`);
1942
+ }
1943
+ return segments.join("/");
1944
+ }
1945
+ function normalizeRouteRule(raw) {
1946
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1947
+ return null;
1948
+ }
1949
+ const record = raw;
1950
+ const pattern = typeof record.pattern === "string" ? record.pattern.trim() : "";
1951
+ const target = typeof record.target === "string" ? record.target.trim() : "";
1952
+ const priority = asPositiveInteger(record.priority);
1953
+ if (!pattern || !target || priority === null) {
1954
+ return null;
1955
+ }
1956
+ return {
1957
+ pattern,
1958
+ target: normalizeRouteTarget(target),
1959
+ priority
1960
+ };
1961
+ }
1962
+ function normalizeRoutes(value) {
1963
+ if (!Array.isArray(value)) {
1964
+ return [];
1965
+ }
1966
+ return value.map((entry) => normalizeRouteRule(entry)).filter((entry) => entry !== null).sort((left, right) => right.priority - left.priority || left.pattern.localeCompare(right.pattern));
1967
+ }
1968
+ function parseRegexLiteral(pattern) {
1969
+ const match = pattern.match(/^\/(.+)\/([a-z]*)$/i);
1970
+ if (!match) {
1971
+ return null;
1972
+ }
1973
+ try {
1974
+ return new RegExp(match[1], match[2]);
1975
+ } catch (error) {
1976
+ throw new Error(`Invalid route regex pattern "${pattern}": ${error instanceof Error ? error.message : "parse error"}`);
1977
+ }
1978
+ }
1979
+ function withDefaults(vaultPath, config) {
1980
+ const resolvedPath = path6.resolve(vaultPath);
1981
+ const defaults = {
1982
+ name: path6.basename(resolvedPath),
1983
+ categories: [...DEFAULT_CATEGORIES],
1984
+ theme: DEFAULT_THEME,
1985
+ observe: {
1986
+ model: DEFAULT_OBSERVE_MODEL,
1987
+ provider: DEFAULT_OBSERVE_PROVIDER
1988
+ },
1989
+ observer: {
1990
+ compression: {}
1991
+ },
1992
+ context: {
1993
+ maxResults: DEFAULT_CONTEXT_MAX_RESULTS,
1994
+ defaultProfile: DEFAULT_CONTEXT_PROFILE
1995
+ },
1996
+ graph: {
1997
+ maxHops: DEFAULT_GRAPH_MAX_HOPS
1998
+ },
1999
+ inject: {
2000
+ maxResults: DEFAULT_INJECT_MAX_RESULTS,
2001
+ useLlm: DEFAULT_INJECT_USE_LLM,
2002
+ scope: [...DEFAULT_INJECT_SCOPE]
2003
+ },
2004
+ routes: []
2005
+ };
2006
+ const observeRecord = config.observe && typeof config.observe === "object" && !Array.isArray(config.observe) ? config.observe : {};
2007
+ const contextRecord = config.context && typeof config.context === "object" && !Array.isArray(config.context) ? config.context : {};
2008
+ const observerRecord = config.observer && typeof config.observer === "object" && !Array.isArray(config.observer) ? config.observer : {};
2009
+ const compressionRecord = observerRecord.compression && typeof observerRecord.compression === "object" && !Array.isArray(observerRecord.compression) ? observerRecord.compression : {};
2010
+ const graphRecord = config.graph && typeof config.graph === "object" && !Array.isArray(config.graph) ? config.graph : {};
2011
+ const compressionProvider = isObserverCompressionProvider(compressionRecord.provider) ? compressionRecord.provider : void 0;
2012
+ const compressionModel = typeof compressionRecord.model === "string" && compressionRecord.model.trim() ? compressionRecord.model.trim() : void 0;
2013
+ const compressionBaseUrl = typeof compressionRecord.baseUrl === "string" && compressionRecord.baseUrl.trim() ? compressionRecord.baseUrl.trim() : void 0;
2014
+ const compressionApiKey = typeof compressionRecord.apiKey === "string" && compressionRecord.apiKey.trim() ? compressionRecord.apiKey.trim() : void 0;
2015
+ const normalizedCompression = {};
2016
+ if (compressionProvider) {
2017
+ normalizedCompression.provider = compressionProvider;
2018
+ }
2019
+ if (compressionModel) {
2020
+ normalizedCompression.model = compressionModel;
2021
+ }
2022
+ if (compressionBaseUrl) {
2023
+ normalizedCompression.baseUrl = compressionBaseUrl;
2024
+ }
2025
+ if (compressionApiKey) {
2026
+ normalizedCompression.apiKey = compressionApiKey;
2027
+ }
2028
+ const injectRecord = config.inject && typeof config.inject === "object" && !Array.isArray(config.inject) ? config.inject : {};
2029
+ return {
2030
+ ...config,
2031
+ name: typeof config.name === "string" && config.name.trim() ? config.name.trim() : defaults.name,
2032
+ categories: asStringArray(config.categories) ?? defaults.categories,
2033
+ theme: isTheme(config.theme) ? config.theme : defaults.theme,
2034
+ observe: {
2035
+ ...observeRecord,
2036
+ model: typeof observeRecord.model === "string" && observeRecord.model.trim() ? observeRecord.model.trim() : defaults.observe.model,
2037
+ provider: isObserveProvider(observeRecord.provider) ? observeRecord.provider : defaults.observe.provider
2038
+ },
2039
+ observer: {
2040
+ ...observerRecord,
2041
+ compression: normalizedCompression
2042
+ },
2043
+ context: {
2044
+ ...contextRecord,
2045
+ maxResults: asPositiveInteger(contextRecord.maxResults) ?? defaults.context.maxResults,
2046
+ defaultProfile: isContextProfile(contextRecord.defaultProfile) ? contextRecord.defaultProfile : defaults.context.defaultProfile
2047
+ },
2048
+ graph: {
2049
+ ...graphRecord,
2050
+ maxHops: asPositiveInteger(graphRecord.maxHops) ?? defaults.graph.maxHops
2051
+ },
2052
+ inject: {
2053
+ ...injectRecord,
2054
+ maxResults: asPositiveInteger(injectRecord.maxResults) ?? defaults.inject.maxResults,
2055
+ useLlm: asBoolean(injectRecord.useLlm) ?? defaults.inject.useLlm,
2056
+ scope: asStringArray(injectRecord.scope) ?? (typeof injectRecord.scope === "string" ? injectRecord.scope.split(",").map((entry) => entry.trim()).filter(Boolean) : null) ?? [...defaults.inject.scope]
2057
+ },
2058
+ routes: normalizeRoutes(config.routes)
2059
+ };
2060
+ }
2061
+ function listConfig(vaultPath) {
2062
+ const config = readConfigDocument(vaultPath);
2063
+ return withDefaults(vaultPath, config);
2064
+ }
2065
+ function listRouteRules(vaultPath) {
2066
+ const config = listConfig(vaultPath);
2067
+ return normalizeRoutes(config.routes);
2068
+ }
2069
+ function matchRouteRule(text, routes) {
2070
+ for (const route of routes) {
2071
+ const regex = parseRegexLiteral(route.pattern);
2072
+ if (regex) {
2073
+ if (regex.test(text)) {
2074
+ return route;
2075
+ }
2076
+ continue;
2077
+ }
2078
+ if (text.toLowerCase().includes(route.pattern.toLowerCase())) {
2079
+ return route;
2080
+ }
2081
+ }
2082
+ return null;
2083
+ }
2084
+
2085
+ // src/observer/router.ts
2086
+ var CATEGORY_PATTERNS = [
2087
+ {
2088
+ category: "decisions",
2089
+ patterns: [
2090
+ /\b(decid(?:e|ed|ing|ion)|chose|picked|went with|selected|opted)\b/i,
2091
+ /\b(decision|trade[- ]?off|alternative|rationale)\b/i
2092
+ ]
2093
+ },
2094
+ {
2095
+ category: "lessons",
2096
+ patterns: [
2097
+ /\b(learn(?:ed|ing|t)|lesson|mistake|insight|realized|discovered)\b/i,
2098
+ /\b(note to self|remember|important|don'?t forget|never again)\b/i
2099
+ ]
2100
+ },
2101
+ {
2102
+ category: "people",
2103
+ patterns: [
2104
+ /\b(said|asked|told|mentioned|emailed|called|messaged|met with)\b/i,
2105
+ /\b(client|partner|team|colleague|contact)\b/i,
2106
+ /\b(?:Pedro|Justin|Maria|Sarah|[A-Z][a-z]+ (?:said|asked|told|mentioned))\b/,
2107
+ /\b(?:talked to|met with|spoke with|chatted with|discussed with)\s+[A-Z][a-z]+\b/i,
2108
+ /\b[A-Z][a-z]+\s+(?:from|at)\s+[A-Z]/,
2109
+ /\b[A-Z][a-z]+\s+from\b/
2110
+ ]
2111
+ },
2112
+ {
2113
+ category: "preferences",
2114
+ patterns: [
2115
+ /\b(prefer(?:s|red|ence)?|like(?:s|d)?|want(?:s|ed)?|style|convention)\b/i,
2116
+ /\b(always use|never use|default to)\b/i
2117
+ ]
2118
+ },
2119
+ {
2120
+ category: "commitments",
2121
+ patterns: [
2122
+ /\b(promised|committed|deadline|due|scheduled|will do|agreed to)\b/i,
2123
+ /\b(todo|task|action item|follow[- ]?up)\b/i
2124
+ ]
2125
+ },
2126
+ {
2127
+ category: "projects",
2128
+ patterns: [
2129
+ /\b(deployed|shipped|launched|released|merged|built|created)\b/i,
2130
+ /\b(project|repo|service|api|feature|bug fix)\b/i
2131
+ ]
2132
+ }
2133
+ ];
2134
+ var TYPE_TO_CATEGORY = {
2135
+ decision: "decisions",
2136
+ preference: "preferences",
2137
+ fact: "facts",
2138
+ commitment: "commitments",
2139
+ task: "commitments",
2140
+ todo: "commitments",
2141
+ "commitment-unresolved": "commitments",
2142
+ milestone: "projects",
2143
+ lesson: "lessons",
2144
+ relationship: "people",
2145
+ project: "projects"
2146
+ };
2147
+ var PAST_TENSE_TASK_HINT_RE = /\b(completed|shipped|deployed|fixed|merged|finished|resolved|closed)\b/i;
2148
+ var FUTURE_TASK_HINT_RE = /\b(need to|should|todo|must|plan to)\b/i;
2149
+ var Router = class {
2150
+ vaultPath;
2151
+ extractTasks;
2152
+ now;
2153
+ customRoutes;
2154
+ constructor(vaultPath, options = {}) {
2155
+ this.vaultPath = path7.resolve(vaultPath);
2156
+ this.extractTasks = options.extractTasks ?? true;
2157
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
2158
+ this.customRoutes = this.loadCustomRoutes();
2159
+ }
2160
+ /**
2161
+ * Takes observation markdown and routes items to appropriate vault categories.
2162
+ * Routes only items with importance >= 0.4.
2163
+ * Returns a summary of what was routed where.
2164
+ */
2165
+ route(observationMarkdown, context = {}) {
2166
+ this.customRoutes = this.loadCustomRoutes();
2167
+ const items = this.parseObservations(observationMarkdown);
2168
+ const routed = [];
2169
+ const knownWorkItems = this.extractTasks ? this.loadExistingWorkItems() : [];
2170
+ const knownProjectDefinitions = this.loadKnownProjectDefinitions();
2171
+ let dedupHits = 0;
2172
+ for (const item of items) {
2173
+ if (item.importance < 0.4) continue;
2174
+ if (this.extractTasks && this.isTaskObservation(item.type)) {
2175
+ const taskResult = this.routeTaskObservation(item, context, knownWorkItems);
2176
+ if (taskResult.routedItem) {
2177
+ routed.push(taskResult.routedItem);
2178
+ }
2179
+ if (taskResult.dedupHit) {
2180
+ dedupHits += 1;
2181
+ }
2182
+ continue;
2183
+ }
2184
+ const category = this.categorize(item.type, item.content);
2185
+ if (!category) continue;
2186
+ const routedItem = {
2187
+ category,
2188
+ title: item.title,
2189
+ content: item.content,
2190
+ type: item.type,
2191
+ confidence: item.confidence,
2192
+ importance: item.importance,
2193
+ date: item.date
2194
+ };
2195
+ routed.push(routedItem);
2196
+ this.appendToCategory(category, routedItem, knownProjectDefinitions);
2197
+ }
2198
+ const summary = this.buildSummary(routed, dedupHits);
2199
+ return { routed, summary };
2200
+ }
2201
+ isTaskObservation(type) {
2202
+ return type === "task" || type === "todo" || type === "commitment-unresolved";
2203
+ }
2204
+ routeTaskObservation(item, context, knownWorkItems) {
2205
+ if (this.shouldSkipCompletedTaskCandidate(item.content)) {
2206
+ console.log("[observer] skipped likely-completed task candidate");
2207
+ return { routedItem: null, dedupHit: false };
2208
+ }
2209
+ const title = this.deriveTaskTitle(item.content, item.type);
2210
+ if (!title) {
2211
+ return { routedItem: null, dedupHit: false };
2212
+ }
2213
+ const duplicate = this.findDuplicateWorkItem(title, knownWorkItems);
2214
+ if (duplicate) {
2215
+ if (item.type === "commitment-unresolved" && this.isOpenWorkItem(duplicate)) {
2216
+ this.touchExistingWorkItem(duplicate);
2217
+ }
2218
+ console.log(`[observer] dedup hit for task candidate: "${title}"`);
2219
+ return { routedItem: null, dedupHit: true };
2220
+ }
2221
+ const tags = this.mergeTags(
2222
+ ["open", "observer"],
2223
+ item.type === "task" ? ["task"] : [],
2224
+ item.type === "todo" ? ["todo"] : [],
2225
+ item.type === "commitment-unresolved" ? ["commitment"] : []
2226
+ );
2227
+ const content = this.buildTaskContextContent(item, context);
2228
+ let backlogItem;
2229
+ try {
2230
+ backlogItem = createBacklogItem(this.vaultPath, title, {
2231
+ source: "observer",
2232
+ content,
2233
+ tags
2234
+ });
2235
+ } catch (error) {
2236
+ if (error instanceof Error && /already exists/i.test(error.message)) {
2237
+ console.log(`[observer] dedup hit for task candidate: "${title}"`);
2238
+ return { routedItem: null, dedupHit: true };
2239
+ }
2240
+ throw error;
2241
+ }
2242
+ knownWorkItems.push({
2243
+ kind: "backlog",
2244
+ slug: backlogItem.slug,
2245
+ title: backlogItem.title,
2246
+ status: "open",
2247
+ source: backlogItem.frontmatter.source,
2248
+ tags: backlogItem.frontmatter.tags ?? []
2249
+ });
2250
+ return {
2251
+ dedupHit: false,
2252
+ routedItem: {
2253
+ category: "backlog",
2254
+ title: backlogItem.title,
2255
+ content: item.content,
2256
+ type: item.type,
2257
+ confidence: item.confidence,
2258
+ importance: item.importance,
2259
+ date: item.date
2260
+ }
2261
+ };
2262
+ }
2263
+ loadExistingWorkItems() {
2264
+ const taskItems = listTasks(this.vaultPath).map((task) => ({
2265
+ kind: "task",
2266
+ slug: task.slug,
2267
+ title: task.title,
2268
+ status: task.frontmatter.status,
2269
+ source: task.frontmatter.source,
2270
+ tags: task.frontmatter.tags ?? []
2271
+ }));
2272
+ const backlogItems = listBacklogItems(this.vaultPath).map((item) => ({
2273
+ kind: "backlog",
2274
+ slug: item.slug,
2275
+ title: item.title,
2276
+ status: item.frontmatter.tags?.includes("done") ? "done" : "open",
2277
+ source: item.frontmatter.source,
2278
+ tags: item.frontmatter.tags ?? []
2279
+ }));
2280
+ return [...taskItems, ...backlogItems];
2281
+ }
2282
+ findDuplicateWorkItem(title, knownWorkItems) {
2283
+ const normalizedTitle = this.normalizeTaskTitle(title);
2284
+ if (!normalizedTitle) {
2285
+ return null;
2286
+ }
2287
+ for (const item of knownWorkItems) {
2288
+ const normalizedExisting = this.normalizeTaskTitle(item.title);
2289
+ if (!normalizedExisting) {
2290
+ continue;
2291
+ }
2292
+ if (normalizedExisting === normalizedTitle) {
2293
+ return item;
2294
+ }
2295
+ if (this.jaccardWordSimilarity(normalizedTitle, normalizedExisting) > 0.8) {
2296
+ return item;
2297
+ }
2298
+ }
2299
+ return null;
2300
+ }
2301
+ normalizeTaskTitle(title) {
2302
+ return title.toLowerCase().replace(/[^\w\s]/g, " ").replace(/\s+/g, " ").trim().slice(0, 50);
2303
+ }
2304
+ jaccardWordSimilarity(a, b) {
2305
+ const aWords = new Set(a.split(" ").filter(Boolean));
2306
+ const bWords = new Set(b.split(" ").filter(Boolean));
2307
+ if (aWords.size === 0 || bWords.size === 0) {
2308
+ return 0;
2309
+ }
2310
+ let intersection = 0;
2311
+ for (const word of aWords) {
2312
+ if (bWords.has(word)) {
2313
+ intersection += 1;
2314
+ }
2315
+ }
2316
+ const unionSize = aWords.size + bWords.size - intersection;
2317
+ return unionSize === 0 ? 0 : intersection / unionSize;
2318
+ }
2319
+ deriveTaskTitle(content, type) {
2320
+ let title = content.replace(/^\d{2}:\d{2}\s+/, "").replace(/\[[^\]]+\]\s*/g, "").trim();
2321
+ if (type === "todo") {
2322
+ title = title.replace(
2323
+ /^(?:todo:\s*|we need to\s+|don't forget(?: to)?\s+|remember to\s+|make sure to\s+)/i,
2324
+ ""
2325
+ );
2326
+ } else if (type === "task") {
2327
+ title = title.replace(
2328
+ /^(?:i'?ll\s+|i will\s+|let me\s+|(?:i'?m\s+)?going to\s+|plan to\s+|should\s+)/i,
2329
+ ""
2330
+ );
2331
+ } else if (type === "commitment-unresolved") {
2332
+ title = title.replace(/^(?:need to figure out\s+|tbd[:\s-]*|to be determined[:\s-]*)/i, "");
2333
+ }
2334
+ title = title.replace(/\s+/g, " ").replace(/^[^a-zA-Z0-9]+/, "").replace(/[.?!:;,]+$/, "").trim();
2335
+ return title.slice(0, 120);
2336
+ }
2337
+ shouldSkipCompletedTaskCandidate(content) {
2338
+ if (!PAST_TENSE_TASK_HINT_RE.test(content)) {
2339
+ return false;
2340
+ }
2341
+ return !FUTURE_TASK_HINT_RE.test(content);
2342
+ }
2343
+ buildTaskContextContent(item, context) {
2344
+ const lines = ["Auto-extracted by observer from session transcript."];
2345
+ if (context.sessionKey) {
2346
+ lines.push(`Session: ${context.sessionKey}`);
2347
+ }
2348
+ if (context.transcriptId) {
2349
+ lines.push(`Transcript: ${context.transcriptId}`);
2350
+ }
2351
+ if (context.source) {
2352
+ lines.push(`Source: ${context.source}`);
2353
+ }
2354
+ const approximateTimestamp = this.extractApproximateTimestamp(item.date, item.content, context.timestamp);
2355
+ lines.push(`Approximate timestamp: ${approximateTimestamp}`);
2356
+ lines.push(`Observation type: ${item.type}`);
2357
+ lines.push(`Original observation: ${item.content}`);
2358
+ return lines.join("\n");
2359
+ }
2360
+ extractApproximateTimestamp(date, content, timestamp) {
2361
+ if (timestamp) {
2362
+ return timestamp.toISOString();
2363
+ }
2364
+ const timeMatch = content.match(/\b([01]\d|2[0-3]):([0-5]\d)\b/);
2365
+ if (timeMatch) {
2366
+ return `${date} ${timeMatch[0]}`;
2367
+ }
2368
+ return date;
2369
+ }
2370
+ isOpenWorkItem(item) {
2371
+ if (item.kind === "task") {
2372
+ return item.status !== "done";
2373
+ }
2374
+ return item.status !== "done";
2375
+ }
2376
+ touchExistingWorkItem(item) {
2377
+ if (item.kind === "task") {
2378
+ if (!this.isOpenWorkItem(item)) {
2379
+ return;
2380
+ }
2381
+ updateTask(this.vaultPath, item.slug, {});
2382
+ return;
2383
+ }
2384
+ const nextTags = this.mergeTags(item.tags, ["commitment"]);
2385
+ updateBacklogItem(this.vaultPath, item.slug, {
2386
+ source: item.source ?? "observer",
2387
+ tags: nextTags,
2388
+ lastSeen: this.now().toISOString()
2389
+ });
2390
+ item.tags = nextTags;
2391
+ }
2392
+ mergeTags(...groups) {
2393
+ const merged = /* @__PURE__ */ new Set();
2394
+ for (const group of groups) {
2395
+ for (const tag of group) {
2396
+ const normalized = tag.trim().toLowerCase();
2397
+ if (normalized) {
2398
+ merged.add(normalized);
2399
+ }
2400
+ }
2401
+ }
2402
+ return [...merged];
2403
+ }
2404
+ parseObservations(markdown) {
2405
+ const records = parseObservationMarkdown(markdown);
2406
+ return records.map((record) => ({
2407
+ type: record.type,
2408
+ confidence: record.confidence,
2409
+ importance: record.importance,
2410
+ content: record.content,
2411
+ date: record.date,
2412
+ title: record.content.slice(0, 80).replace(/[^a-zA-Z0-9\s-]/g, "").trim()
2413
+ }));
2414
+ }
2415
+ categorize(type, content) {
2416
+ const typedCategory = TYPE_TO_CATEGORY[type];
2417
+ if (typedCategory) {
2418
+ return typedCategory;
2419
+ }
2420
+ for (const { category, patterns } of CATEGORY_PATTERNS) {
2421
+ if (patterns.some((p) => p.test(content))) {
2422
+ return category;
2423
+ }
2424
+ }
2425
+ return null;
2426
+ }
2427
+ normalizeForDedup(content) {
2428
+ return normalizeObservationContent(
2429
+ content.replace(/\[\[[^\]]*\]\]/g, (match) => match.replace(/\[\[|\]\]/g, ""))
2430
+ );
2431
+ }
2432
+ /**
2433
+ * Extract entity slug from observation content for people/projects routing.
2434
+ * Returns null if no entity can be identified.
2435
+ */
2436
+ extractEntitySlug(content, category) {
2437
+ if (category !== "people" && category !== "projects") return null;
2438
+ if (category === "people") {
2439
+ const patterns = [
2440
+ /(?:talked to|met with|spoke with|chatted with|discussed with|emailed|called|messaged)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)/,
2441
+ /([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\s+(?:said|asked|told|mentioned|from|at)\b/,
2442
+ /\b(?:client|partner|colleague|contact)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)/i
2443
+ ];
2444
+ for (const pattern of patterns) {
2445
+ const match = content.match(pattern);
2446
+ if (match?.[1]) return this.toSlug(match[1]);
2447
+ }
2448
+ }
2449
+ if (category === "projects") {
2450
+ const patterns = [
2451
+ /(?:deployed|shipped|launched|released|built|created|working on)\s+([A-Z][a-zA-Z0-9-]+)/,
2452
+ /"([^"]+)"\s+(?:project|repo|service)/i
2453
+ ];
2454
+ for (const pattern of patterns) {
2455
+ const match = content.match(pattern);
2456
+ if (match?.[1]) return this.toSlug(match[1]);
2457
+ }
2458
+ }
2459
+ return null;
2460
+ }
2461
+ toSlug(name) {
2462
+ return name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
2463
+ }
2464
+ normalizeProjectReference(value) {
2465
+ return value.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").trim();
2466
+ }
2467
+ escapeRegExp(value) {
2468
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2469
+ }
2470
+ extractWikiTargets(content) {
2471
+ const targets = [];
2472
+ for (const match of content.matchAll(/\[\[([^\]]+)\]\]/g)) {
2473
+ const candidate = match[1];
2474
+ if (!candidate) continue;
2475
+ const target = candidate.split("|")[0].split("#")[0].trim();
2476
+ if (target) targets.push(target);
2477
+ }
2478
+ return targets;
2479
+ }
2480
+ loadKnownProjectDefinitions() {
2481
+ try {
2482
+ return listProjects(this.vaultPath).map((project) => ({
2483
+ slug: project.slug,
2484
+ normalizedSlug: this.normalizeProjectReference(project.slug),
2485
+ title: project.title,
2486
+ normalizedTitle: project.title.toLowerCase()
2487
+ }));
2488
+ } catch {
2489
+ return [];
2490
+ }
2491
+ }
2492
+ matchKnownProjectSlug(content, knownProjects) {
2493
+ if (knownProjects.length === 0) {
2494
+ return null;
2495
+ }
2496
+ const normalizedContent = content.toLowerCase();
2497
+ const wikiTargets = this.extractWikiTargets(content).map((target) => this.normalizeProjectReference(target));
2498
+ for (const project of knownProjects) {
2499
+ if (wikiTargets.includes(project.normalizedSlug)) {
2500
+ return project.slug;
2501
+ }
2502
+ if (project.normalizedTitle && normalizedContent.includes(project.normalizedTitle)) {
2503
+ return project.slug;
2504
+ }
2505
+ const slugPattern = new RegExp(`\\b${this.escapeRegExp(project.normalizedSlug)}\\b`, "i");
2506
+ if (slugPattern.test(content)) {
2507
+ return project.slug;
2508
+ }
2509
+ }
2510
+ return null;
2511
+ }
2512
+ loadCustomRoutes() {
2513
+ try {
2514
+ return listRouteRules(this.vaultPath);
2515
+ } catch {
2516
+ return [];
2517
+ }
2518
+ }
2519
+ resolveCustomEntityPath(content, category) {
2520
+ if (category !== "people" && category !== "projects" || this.customRoutes.length === 0) {
2521
+ return null;
2522
+ }
2523
+ const matchedRule = matchRouteRule(content, this.customRoutes);
2524
+ if (!matchedRule) {
2525
+ return null;
2526
+ }
2527
+ const targetParts = matchedRule.target.split("/").map((segment) => segment.trim()).filter(Boolean);
2528
+ if (targetParts.length < 2 || targetParts[0] !== category) {
2529
+ return null;
2530
+ }
2531
+ return targetParts.slice(1).join("/");
2532
+ }
2533
+ /**
2534
+ * Resolve the file path for a routed item.
2535
+ * For people/projects: entity-slug subfolder with date file (e.g., people/pedro/2026-02-12.md)
2536
+ * For other categories: category/date.md
2537
+ */
2538
+ resolveFilePath(category, item, knownProjectDefinitions) {
2539
+ const customEntityPath = this.resolveCustomEntityPath(item.content, category);
2540
+ if (customEntityPath) {
2541
+ const customEntityDir = path7.join(this.vaultPath, category, customEntityPath);
2542
+ fs7.mkdirSync(customEntityDir, { recursive: true });
2543
+ return {
2544
+ filePath: path7.join(customEntityDir, `${item.date}.md`),
2545
+ headerLabel: `${category}/${customEntityPath}`
2546
+ };
2547
+ }
2548
+ if (category === "projects") {
2549
+ const matchedProjectSlug = this.matchKnownProjectSlug(item.content, knownProjectDefinitions);
2550
+ if (matchedProjectSlug) {
2551
+ const projectDir = path7.join(this.vaultPath, category, matchedProjectSlug);
2552
+ fs7.mkdirSync(projectDir, { recursive: true });
2553
+ return {
2554
+ filePath: path7.join(projectDir, `${item.date}.md`),
2555
+ headerLabel: `${category}/${matchedProjectSlug}`
2556
+ };
2557
+ }
2558
+ } else {
2559
+ const entitySlug = this.extractEntitySlug(item.content, category);
2560
+ if (entitySlug) {
2561
+ const entityDir = path7.join(this.vaultPath, category, entitySlug);
2562
+ fs7.mkdirSync(entityDir, { recursive: true });
2563
+ return {
2564
+ filePath: path7.join(entityDir, `${item.date}.md`),
2565
+ headerLabel: `${category}/${entitySlug}`
2566
+ };
2567
+ }
2568
+ }
2569
+ const categoryDir = path7.join(this.vaultPath, category);
2570
+ fs7.mkdirSync(categoryDir, { recursive: true });
2571
+ return {
2572
+ filePath: path7.join(categoryDir, `${item.date}.md`),
2573
+ headerLabel: category
2574
+ };
2575
+ }
2576
+ appendToCategory(category, item, knownProjectDefinitions) {
2577
+ const destination = this.resolveFilePath(category, item, knownProjectDefinitions);
2578
+ const filePath = destination.filePath;
2579
+ fs7.mkdirSync(path7.dirname(filePath), { recursive: true });
2580
+ const existing = fs7.existsSync(filePath) ? fs7.readFileSync(filePath, "utf-8").trim() : "";
2581
+ const normalizedNew = this.normalizeForDedup(item.content);
2582
+ const existingLines = existing.split(/\r?\n/);
2583
+ for (const line of existingLines) {
2584
+ const lineContent = line.replace(/^-\s*/, "").trim();
2585
+ const parsed = parseObservationLine(lineContent, item.date);
2586
+ const candidate = parsed ? parsed.content : lineContent;
2587
+ if (this.normalizeForDedup(candidate) === normalizedNew) return;
2588
+ }
2589
+ for (const line of existingLines) {
2590
+ const lineContent = line.replace(/^-\s*/, "").trim();
2591
+ const parsed = parseObservationLine(lineContent, item.date);
2592
+ const normalizedExisting = this.normalizeForDedup(parsed ? parsed.content : lineContent);
2593
+ if (normalizedExisting.length > 10 && normalizedNew.length > 10) {
2594
+ const shorter = normalizedNew.length < normalizedExisting.length ? normalizedNew : normalizedExisting;
2595
+ const longer = normalizedNew.length >= normalizedExisting.length ? normalizedNew : normalizedExisting;
2596
+ if (longer.includes(shorter) || this.similarity(normalizedNew, normalizedExisting) > 0.8) return;
2597
+ }
2598
+ }
2599
+ const linkedContent = this.addWikiLinks(item.content);
2600
+ const entry = renderScoredObservationLine({
2601
+ type: item.type,
2602
+ confidence: item.confidence,
2603
+ importance: item.importance,
2604
+ content: linkedContent
2605
+ });
2606
+ const headerLabel = destination.headerLabel;
2607
+ const header = existing ? "" : `# ${headerLabel} \u2014 ${item.date}
2608
+ `;
2609
+ const newContent = existing ? `${existing}
2610
+ ${entry}
2611
+ ` : `${header}
2612
+ ${entry}
2613
+ `;
2614
+ fs7.writeFileSync(filePath, newContent, "utf-8");
2615
+ }
2616
+ /**
2617
+ * Auto-link proper nouns and known entities with [[wiki-links]].
2618
+ * Scans for capitalized names, project names, and tool names.
2619
+ * Skips content already inside [[brackets]].
2620
+ */
2621
+ addWikiLinks(content) {
2622
+ if (content.includes("[[")) return content;
2623
+ const namePattern = /\b([A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,})?)\b/g;
2624
+ const skipWords = /* @__PURE__ */ new Set([
2625
+ "The",
2626
+ "This",
2627
+ "That",
2628
+ "These",
2629
+ "Those",
2630
+ "There",
2631
+ "Then",
2632
+ "Than",
2633
+ "When",
2634
+ "Where",
2635
+ "What",
2636
+ "Which",
2637
+ "While",
2638
+ "With",
2639
+ "Would",
2640
+ "Will",
2641
+ "Should",
2642
+ "Could",
2643
+ "About",
2644
+ "After",
2645
+ "Before",
2646
+ "Between",
2647
+ "Because",
2648
+ "Also",
2649
+ "Always",
2650
+ "Already",
2651
+ "Another",
2652
+ "Any",
2653
+ "Each",
2654
+ "Every",
2655
+ "From",
2656
+ "Have",
2657
+ "Has",
2658
+ "Had",
2659
+ "Into",
2660
+ "Just",
2661
+ "Keep",
2662
+ "Like",
2663
+ "Made",
2664
+ "Make",
2665
+ "Many",
2666
+ "More",
2667
+ "Most",
2668
+ "Much",
2669
+ "Must",
2670
+ "Need",
2671
+ "Never",
2672
+ "Next",
2673
+ "None",
2674
+ "Not",
2675
+ "Now",
2676
+ "Only",
2677
+ "Other",
2678
+ "Over",
2679
+ "Same",
2680
+ "Some",
2681
+ "Such",
2682
+ "Sure",
2683
+ "Take",
2684
+ "Them",
2685
+ "They",
2686
+ "Too",
2687
+ "Under",
2688
+ "Until",
2689
+ "Upon",
2690
+ "Very",
2691
+ "Want",
2692
+ "Were",
2693
+ "Work",
2694
+ "Yet",
2695
+ "Decision",
2696
+ "Error",
2697
+ "Deadline",
2698
+ "Friday",
2699
+ "Monday",
2700
+ "Tuesday",
2701
+ "Wednesday",
2702
+ "Thursday",
2703
+ "Saturday",
2704
+ "Sunday",
2705
+ "January",
2706
+ "February",
2707
+ "March",
2708
+ "April",
2709
+ "May",
2710
+ "June",
2711
+ "July",
2712
+ "August",
2713
+ "September",
2714
+ "October",
2715
+ "November",
2716
+ "December",
2717
+ "Today",
2718
+ "Tomorrow",
2719
+ "Yesterday",
2720
+ "Message",
2721
+ "Feature",
2722
+ "Session",
2723
+ "Update",
2724
+ "System",
2725
+ "User",
2726
+ "Processed",
2727
+ "Working",
2728
+ "Built",
2729
+ "Deployed",
2730
+ "Discussed",
2731
+ "Talked",
2732
+ "Mentioned",
2733
+ "Requested",
2734
+ "Asked",
2735
+ "Said"
2736
+ ]);
2737
+ const knownEntities = /* @__PURE__ */ new Set([
2738
+ "PostgreSQL",
2739
+ "MongoDB",
2740
+ "Railway",
2741
+ "Vercel",
2742
+ "React",
2743
+ "Vue",
2744
+ "Svelte",
2745
+ "Express",
2746
+ "NestJS",
2747
+ "Prisma",
2748
+ "Docker",
2749
+ "Kubernetes",
2750
+ "Redis",
2751
+ "GraphQL",
2752
+ "Stripe",
2753
+ "ClawVault",
2754
+ "OpenClaw",
2755
+ "GitHub",
2756
+ "Obsidian"
2757
+ ]);
2758
+ return content.replace(namePattern, (match) => {
2759
+ if (skipWords.has(match)) return match;
2760
+ if (knownEntities.has(match)) return `[[${match}]]`;
2761
+ if (/^[A-Z][a-z]+$/.test(match) && match.length >= 3) {
2762
+ return `[[${match}]]`;
2763
+ }
2764
+ if (/^[A-Z][a-z]+ [A-Z][a-z]+$/.test(match)) {
2765
+ return `[[${match}]]`;
2766
+ }
2767
+ return match;
2768
+ });
2769
+ }
2770
+ /**
2771
+ * Jaccard similarity on word bigrams — cheap approximation.
2772
+ */
2773
+ similarity(a, b) {
2774
+ const bigrams = (s) => {
2775
+ const words = s.split(" ");
2776
+ const bg = /* @__PURE__ */ new Set();
2777
+ for (let i = 0; i < words.length - 1; i++) bg.add(`${words[i]} ${words[i + 1]}`);
2778
+ return bg;
2779
+ };
2780
+ const setA = bigrams(a);
2781
+ const setB = bigrams(b);
2782
+ if (setA.size === 0 || setB.size === 0) return 0;
2783
+ let intersection = 0;
2784
+ for (const bg of setA) if (setB.has(bg)) intersection++;
2785
+ return intersection / (setA.size + setB.size - intersection);
2786
+ }
2787
+ buildSummary(routed, dedupHits) {
2788
+ if (routed.length === 0) {
2789
+ if (dedupHits > 0) {
2790
+ return `No items routed to vault categories (dedup hits: ${dedupHits}).`;
2791
+ }
2792
+ return "No items routed to vault categories.";
2793
+ }
2794
+ const byCat = /* @__PURE__ */ new Map();
2795
+ for (const item of routed) {
2796
+ byCat.set(item.category, (byCat.get(item.category) ?? 0) + 1);
2797
+ }
2798
+ const parts = [...byCat.entries()].map(([cat, count]) => `${cat}: ${count}`);
2799
+ const suffix = dedupHits > 0 ? ` (dedup hits: ${dedupHits})` : "";
2800
+ return `Routed ${routed.length} observations \u2192 ${parts.join(", ")}${suffix}`;
2801
+ }
2802
+ };
2803
+
2804
+ // src/observer/observer.ts
2805
+ var COMPRESSION_PROVIDERS = /* @__PURE__ */ new Set([
2806
+ "anthropic",
2807
+ "openai",
2808
+ "gemini",
2809
+ "openai-compatible",
2810
+ "ollama"
2811
+ ]);
2812
+ function asRecord(value) {
2813
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2814
+ return null;
2815
+ }
2816
+ return value;
2817
+ }
2818
+ function asNonEmptyString(value) {
2819
+ if (typeof value !== "string") {
2820
+ return void 0;
2821
+ }
2822
+ const trimmed = value.trim();
2823
+ return trimmed ? trimmed : void 0;
2824
+ }
2825
+ function asCompressionProvider(value) {
2826
+ if (typeof value !== "string") {
2827
+ return void 0;
2828
+ }
2829
+ const normalized = value.trim();
2830
+ return COMPRESSION_PROVIDERS.has(normalized) ? normalized : void 0;
2831
+ }
2832
+ function readCompressionConfig(vaultPath) {
2833
+ try {
2834
+ const config = listConfig(vaultPath);
2835
+ const root = asRecord(config);
2836
+ const observer = asRecord(root?.observer);
2837
+ const compression = asRecord(observer?.compression);
2838
+ if (!compression) {
2839
+ return {};
2840
+ }
2841
+ return {
2842
+ provider: asCompressionProvider(compression.provider),
2843
+ model: asNonEmptyString(compression.model),
2844
+ baseUrl: asNonEmptyString(compression.baseUrl),
2845
+ apiKey: asNonEmptyString(compression.apiKey)
2846
+ };
2847
+ } catch {
2848
+ return {};
2849
+ }
2850
+ }
2851
+ var Observer = class {
2852
+ vaultPath;
2853
+ tokenThreshold;
2854
+ // Kept for backwards API compatibility with callers that still pass this.
2855
+ // Reflection now runs explicitly via clawvault reflect.
2856
+ reflectThreshold;
2857
+ compressor;
2858
+ reflector;
2859
+ now;
2860
+ rawCapture;
2861
+ router;
2862
+ pendingMessages = [];
2863
+ pendingRouteContext = {};
2864
+ observationsCache = "";
2865
+ lastRoutingSummary = "";
2866
+ constructor(vaultPath, options = {}) {
2867
+ this.vaultPath = path8.resolve(vaultPath);
2868
+ this.tokenThreshold = options.tokenThreshold ?? 3e4;
2869
+ this.reflectThreshold = options.reflectThreshold ?? 4e4;
2870
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
2871
+ const compressionConfig = readCompressionConfig(this.vaultPath);
2872
+ this.compressor = options.compressor ?? new Compressor({
2873
+ provider: options.compressionProvider ?? compressionConfig.provider,
2874
+ model: options.model ?? compressionConfig.model,
2875
+ baseUrl: options.compressionBaseUrl ?? compressionConfig.baseUrl,
2876
+ apiKey: options.compressionApiKey ?? compressionConfig.apiKey,
2877
+ now: this.now
2878
+ });
2879
+ this.reflector = options.reflector ?? new Reflector({ now: this.now });
2880
+ this.rawCapture = options.rawCapture ?? true;
2881
+ this.router = new Router(vaultPath, {
2882
+ extractTasks: options.extractTasks,
2883
+ now: this.now
2884
+ });
2885
+ ensureLedgerStructure(this.vaultPath);
2886
+ this.observationsCache = this.readTodayObservations();
2887
+ }
2888
+ async processMessages(messages, options = {}) {
2889
+ const incoming = messages.map((message) => message.trim()).filter(Boolean);
2890
+ if (incoming.length === 0) {
2891
+ return;
2892
+ }
2893
+ if (this.rawCapture) {
2894
+ this.persistRawMessages(incoming, options);
2895
+ }
2896
+ this.pendingMessages.push(...incoming);
2897
+ this.pendingRouteContext = this.mergeRouteContext(this.pendingRouteContext, options);
2898
+ const buffered = this.pendingMessages.join("\n");
2899
+ if (this.estimateTokens(buffered) < this.tokenThreshold) {
2900
+ return;
2901
+ }
2902
+ const today = this.now();
2903
+ const todayPath = getObservationPath(this.vaultPath, today);
2904
+ const existingRaw = this.readObservationForDate(today);
2905
+ const existing = this.deduplicateObservationMarkdown(existingRaw);
2906
+ if (existingRaw.trim() !== existing) {
2907
+ this.writeObservationFile(todayPath, existing);
2908
+ }
2909
+ const compressedRaw = (await this.compressor.compress(this.pendingMessages, existing)).trim();
2910
+ const routeContext = this.pendingRouteContext;
2911
+ this.pendingMessages = [];
2912
+ this.pendingRouteContext = {};
2913
+ const compressed = this.deduplicateObservationMarkdown(compressedRaw);
2914
+ if (!compressed) {
2915
+ return;
2916
+ }
2917
+ this.writeObservationFile(todayPath, compressed);
2918
+ this.observationsCache = compressed;
2919
+ const { summary } = this.router.route(compressed, routeContext);
2920
+ if (summary) {
2921
+ this.lastRoutingSummary = summary;
2922
+ }
2923
+ }
2924
+ /**
2925
+ * Force-flush pending messages regardless of threshold.
2926
+ * Call this on session end to capture everything.
2927
+ */
2928
+ async flush() {
2929
+ if (this.pendingMessages.length === 0) {
2930
+ return { observations: this.observationsCache, routingSummary: this.lastRoutingSummary };
2931
+ }
2932
+ const today = this.now();
2933
+ const todayPath = getObservationPath(this.vaultPath, today);
2934
+ const existingRaw = this.readObservationForDate(today);
2935
+ const existing = this.deduplicateObservationMarkdown(existingRaw);
2936
+ if (existingRaw.trim() !== existing) {
2937
+ this.writeObservationFile(todayPath, existing);
2938
+ }
2939
+ const compressedRaw = (await this.compressor.compress(this.pendingMessages, existing)).trim();
2940
+ const routeContext = this.pendingRouteContext;
2941
+ this.pendingMessages = [];
2942
+ this.pendingRouteContext = {};
2943
+ const compressed = this.deduplicateObservationMarkdown(compressedRaw);
2944
+ if (compressed) {
2945
+ this.writeObservationFile(todayPath, compressed);
2946
+ this.observationsCache = compressed;
2947
+ const { summary } = this.router.route(compressed, routeContext);
2948
+ this.lastRoutingSummary = summary;
2949
+ }
2950
+ return { observations: this.observationsCache, routingSummary: this.lastRoutingSummary };
2951
+ }
2952
+ getObservations() {
2953
+ this.observationsCache = this.readTodayObservations();
2954
+ return this.observationsCache;
2955
+ }
2956
+ estimateTokens(input) {
2957
+ return Math.ceil(input.length / 4);
2958
+ }
2959
+ readTodayObservations() {
2960
+ return this.readObservationForDate(this.now());
2961
+ }
2962
+ readObservationForDate(date) {
2963
+ const ledgerPath = getObservationPath(this.vaultPath, date);
2964
+ const ledgerValue = this.readObservationFile(ledgerPath);
2965
+ if (ledgerValue) {
2966
+ return ledgerValue;
2967
+ }
2968
+ return this.readObservationFile(getLegacyObservationPath(this.vaultPath, toDateKey(date)));
2969
+ }
2970
+ readObservationFile(filePath) {
2971
+ if (!fs8.existsSync(filePath)) {
2972
+ return "";
2973
+ }
2974
+ return fs8.readFileSync(filePath, "utf-8").trim();
2975
+ }
2976
+ writeObservationFile(filePath, content) {
2977
+ ensureParentDir(filePath);
2978
+ fs8.writeFileSync(filePath, `${content.trim()}
2979
+ `, "utf-8");
2980
+ }
2981
+ deduplicateObservationMarkdown(markdown) {
2982
+ const parsed = parseObservationMarkdown(markdown);
2983
+ if (parsed.length === 0) {
2984
+ return markdown.trim();
2985
+ }
2986
+ const grouped = /* @__PURE__ */ new Map();
2987
+ for (const record of parsed) {
2988
+ const bucket = grouped.get(record.date) ?? [];
2989
+ const normalized = normalizeObservationContent(record.content);
2990
+ const existingIndex = bucket.findIndex(
2991
+ (line) => normalizeObservationContent(line.content) === normalized
2992
+ );
2993
+ if (existingIndex === -1) {
2994
+ bucket.push({
2995
+ type: record.type,
2996
+ confidence: record.confidence,
2997
+ importance: record.importance,
2998
+ content: record.content
2999
+ });
3000
+ } else {
3001
+ const existing = bucket[existingIndex];
3002
+ bucket[existingIndex] = {
3003
+ type: record.importance >= existing.importance ? record.type : existing.type,
3004
+ confidence: Math.max(existing.confidence, record.confidence),
3005
+ importance: Math.max(existing.importance, record.importance),
3006
+ content: existing.content.length >= record.content.length ? existing.content : record.content
3007
+ };
3008
+ }
3009
+ grouped.set(record.date, bucket);
3010
+ }
3011
+ return renderObservationMarkdown(grouped);
3012
+ }
3013
+ persistRawMessages(messages, options) {
3014
+ const source = this.sanitizeSource(options.source ?? "openclaw");
3015
+ const messageTimestamp = options.timestamp ?? this.now();
3016
+ const rawPath = getRawTranscriptPath(this.vaultPath, source, messageTimestamp);
3017
+ ensureParentDir(rawPath);
3018
+ const records = messages.map((message) => JSON.stringify({
3019
+ recordedAt: this.now().toISOString(),
3020
+ timestamp: messageTimestamp.toISOString(),
3021
+ source,
3022
+ sessionKey: options.sessionKey ?? null,
3023
+ transcriptId: options.transcriptId ?? null,
3024
+ message
3025
+ }));
3026
+ fs8.appendFileSync(rawPath, `${records.join("\n")}
3027
+ `, "utf-8");
3028
+ }
3029
+ sanitizeSource(source) {
3030
+ const normalized = source.trim().toLowerCase();
3031
+ if (/^[a-z0-9_-]{1,64}$/.test(normalized)) {
3032
+ return normalized;
3033
+ }
3034
+ return "openclaw";
3035
+ }
3036
+ mergeRouteContext(existing, incoming) {
3037
+ const merged = { ...existing };
3038
+ if (incoming.source) merged.source = incoming.source;
3039
+ if (incoming.sessionKey) merged.sessionKey = incoming.sessionKey;
3040
+ if (incoming.transcriptId) merged.transcriptId = incoming.transcriptId;
3041
+ if (incoming.timestamp) merged.timestamp = incoming.timestamp;
3042
+ return merged;
3043
+ }
3044
+ };
3045
+
3046
+ // src/commands/rebuild.ts
3047
+ var DATE_RE2 = /^\d{4}-\d{2}-\d{2}$/;
3048
+ function parseDateFlag(raw, label) {
3049
+ if (!raw) return void 0;
3050
+ const trimmed = raw.trim();
3051
+ if (!DATE_RE2.test(trimmed)) {
3052
+ throw new Error(`Invalid ${label} date. Expected YYYY-MM-DD: ${raw}`);
3053
+ }
3054
+ return trimmed;
3055
+ }
3056
+ function loadRawMessages(rawFilePath) {
3057
+ const lines = fs9.readFileSync(rawFilePath, "utf-8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
3058
+ const messages = [];
3059
+ for (const line of lines) {
3060
+ try {
3061
+ const parsed = JSON.parse(line);
3062
+ if (typeof parsed.message === "string" && parsed.message.trim()) {
3063
+ messages.push(parsed.message.trim());
3064
+ }
3065
+ } catch {
3066
+ messages.push(line);
3067
+ }
3068
+ }
3069
+ return messages;
3070
+ }
3071
+ async function rebuildCommand(options) {
3072
+ const vaultPath = resolveVaultPath({ explicitPath: options.vaultPath });
3073
+ const fromDate = parseDateFlag(options.from, "from");
3074
+ const toDate = parseDateFlag(options.to, "to");
3075
+ if (fromDate && toDate && fromDate > toDate) {
3076
+ throw new Error(`Invalid range: --from ${fromDate} is after --to ${toDate}.`);
3077
+ }
3078
+ const rawFiles = listRawTranscriptFiles(vaultPath, {
3079
+ fromDate,
3080
+ toDate
3081
+ });
3082
+ if (rawFiles.length === 0) {
3083
+ console.log("No raw transcripts found for rebuild range.");
3084
+ return;
3085
+ }
3086
+ const filesByDate = /* @__PURE__ */ new Map();
3087
+ for (const file of rawFiles) {
3088
+ const bucket = filesByDate.get(file.date) ?? [];
3089
+ bucket.push({ source: file.source, path: file.path });
3090
+ filesByDate.set(file.date, bucket);
3091
+ }
3092
+ const dates = [...filesByDate.keys()].sort((left, right) => left.localeCompare(right));
3093
+ let rebuiltDates = 0;
3094
+ let processedFiles = 0;
3095
+ for (const date of dates) {
3096
+ const ledgerObservationPath = getObservationPath(vaultPath, date);
3097
+ const legacyObservationPath = getLegacyObservationPath(vaultPath, date);
3098
+ fs9.rmSync(ledgerObservationPath, { force: true });
3099
+ fs9.rmSync(legacyObservationPath, { force: true });
3100
+ const fixedNow = () => /* @__PURE__ */ new Date(`${date}T12:00:00.000Z`);
3101
+ const observer = new Observer(vaultPath, {
3102
+ tokenThreshold: 1,
3103
+ reflectThreshold: Number.MAX_SAFE_INTEGER,
3104
+ now: fixedNow,
3105
+ rawCapture: false
3106
+ });
3107
+ for (const file of filesByDate.get(date) ?? []) {
3108
+ const messages = loadRawMessages(file.path);
3109
+ if (messages.length === 0) {
3110
+ continue;
3111
+ }
3112
+ await observer.processMessages(messages, {
3113
+ source: file.source,
3114
+ timestamp: fixedNow()
3115
+ });
3116
+ processedFiles += 1;
3117
+ }
3118
+ await observer.flush();
3119
+ rebuiltDates += 1;
3120
+ }
3121
+ console.log(`Rebuild complete: ${rebuiltDates} day(s), ${processedFiles} raw file(s) replayed.`);
3122
+ }
3123
+ function registerRebuildCommand(program) {
3124
+ program.command("rebuild").description("Rebuild compiled observations from raw ledger transcripts").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)").option("-v, --vault <path>", "Vault path").action(async (rawOptions) => {
3125
+ await rebuildCommand({
3126
+ vaultPath: rawOptions.vault,
3127
+ from: rawOptions.from,
3128
+ to: rawOptions.to
3129
+ });
3130
+ });
3131
+ }
3132
+ // Annotate the CommonJS export names for ESM import in node:
3133
+ 0 && (module.exports = {
3134
+ rebuildCommand,
3135
+ registerRebuildCommand
3136
+ });