clawvault 3.2.1 → 3.4.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 (161) hide show
  1. package/README.md +56 -16
  2. package/bin/clawvault.js +0 -2
  3. package/bin/command-registration.test.js +15 -2
  4. package/bin/help-contract.test.js +16 -0
  5. package/bin/register-core-commands.js +88 -0
  6. package/bin/register-core-commands.test.js +80 -0
  7. package/bin/register-maintenance-commands.js +84 -7
  8. package/bin/register-query-commands.js +45 -28
  9. package/bin/register-query-commands.test.js +15 -0
  10. package/bin/test-helpers/cli-command-fixtures.js +1 -0
  11. package/dist/chunk-2PKBIKDH.js +130 -0
  12. package/dist/{chunk-U67V476Y.js → chunk-2ZDO52B4.js} +18 -1
  13. package/dist/{chunk-ZZA73MFY.js → chunk-33DOSHTA.js} +176 -36
  14. package/dist/chunk-35JCYSRR.js +158 -0
  15. package/dist/{chunk-AZYOKJYC.js → chunk-4PY655YM.js} +13 -1
  16. package/dist/{chunk-2JQ3O2YL.js → chunk-5EFSWZO6.js} +3 -3
  17. package/dist/{chunk-Y3TIJEBP.js → chunk-7SWP5FKU.js} +34 -613
  18. package/dist/{chunk-4VQTUVH7.js → chunk-7YZWHM36.js} +52 -26
  19. package/dist/{chunk-URXDAUVH.js → chunk-AXSJIFOJ.js} +174 -1
  20. package/dist/{chunk-4ITRXIVT.js → chunk-BLQXXX7Q.js} +6 -6
  21. package/dist/chunk-CSHO3PJB.js +684 -0
  22. package/dist/chunk-D5U3Q4N5.js +872 -0
  23. package/dist/chunk-DCF4KMFD.js +158 -0
  24. package/dist/{chunk-S5OJEGFG.js → chunk-DOIUYIXV.js} +2 -2
  25. package/dist/{chunk-YXQCA6B7.js → chunk-DVOUSOR3.js} +112 -7
  26. package/dist/{chunk-YDWHS4LJ.js → chunk-ECGJYWNA.js} +205 -33
  27. package/dist/{chunk-QMHPQYUV.js → chunk-EL6UBSX5.js} +7 -6
  28. package/dist/chunk-FZ5I2NF7.js +352 -0
  29. package/dist/{chunk-WJVWINEM.js → chunk-GFCHWMGD.js} +55 -6
  30. package/dist/{chunk-GNJL4YGR.js → chunk-GJO3CFUN.js} +30 -6
  31. package/dist/chunk-H3JZIB5O.js +322 -0
  32. package/dist/chunk-HEHO7SMV.js +51 -0
  33. package/dist/{chunk-UCQAOZHW.js → chunk-HGDDW24U.js} +3 -3
  34. package/dist/chunk-J3YUXVID.js +907 -0
  35. package/dist/{chunk-Y6VJKXGL.js → chunk-KCYWJDDW.js} +1 -1
  36. package/dist/{chunk-P5EPF6MB.js → chunk-MW5C6ZQA.js} +110 -13
  37. package/dist/chunk-NSXYM6EZ.js +255 -0
  38. package/dist/{chunk-YNIPYN4F.js → chunk-OFOCU2V4.js} +6 -5
  39. package/dist/{chunk-42MXU7A6.js → chunk-P62WHA27.js} +58 -47
  40. package/dist/chunk-PTWPPVC7.js +972 -0
  41. package/dist/{chunk-FAKNOB7Y.js → chunk-QFWERBDP.js} +2 -2
  42. package/dist/chunk-QYQAGBTM.js +2097 -0
  43. package/dist/chunk-RL2L6I6K.js +223 -0
  44. package/dist/{chunk-IIOU45CK.js → chunk-S7N7HI5E.js} +2 -2
  45. package/dist/{chunk-ECRZL5XR.js → chunk-T7E764W3.js} +23 -7
  46. package/dist/{chunk-MNPUYCHQ.js → chunk-TWMI3SNN.js} +6 -5
  47. package/dist/{chunk-2RAZ4ZFE.js → chunk-VBILES4B.js} +1 -1
  48. package/dist/{chunk-PI4WMLMG.js → chunk-VXAGOLDP.js} +1 -1
  49. package/dist/{chunk-SS4B7P7V.js → chunk-YIDV4VV2.js} +1 -1
  50. package/dist/chunk-YTRZNA64.js +37 -0
  51. package/dist/chunk-ZKWPCBYT.js +600 -0
  52. package/dist/cli/index.js +28 -21
  53. package/dist/commands/archive.js +3 -3
  54. package/dist/commands/backlog.js +1 -1
  55. package/dist/commands/benchmark.d.ts +12 -0
  56. package/dist/commands/benchmark.js +12 -0
  57. package/dist/commands/blocked.js +1 -1
  58. package/dist/commands/canvas.js +2 -2
  59. package/dist/commands/checkpoint.js +1 -1
  60. package/dist/commands/compat.js +1 -1
  61. package/dist/commands/context.js +8 -7
  62. package/dist/commands/doctor.d.ts +8 -3
  63. package/dist/commands/doctor.js +8 -22
  64. package/dist/commands/embed.js +6 -5
  65. package/dist/commands/entities.d.ts +8 -1
  66. package/dist/commands/entities.js +46 -3
  67. package/dist/commands/graph.js +4 -4
  68. package/dist/commands/inbox.d.ts +23 -0
  69. package/dist/commands/inbox.js +11 -0
  70. package/dist/commands/inject.d.ts +1 -1
  71. package/dist/commands/inject.js +5 -5
  72. package/dist/commands/kanban.js +1 -1
  73. package/dist/commands/link.js +5 -5
  74. package/dist/commands/maintain.d.ts +32 -0
  75. package/dist/commands/maintain.js +13 -0
  76. package/dist/commands/migrate-observations.js +3 -3
  77. package/dist/commands/observe.js +11 -10
  78. package/dist/commands/project.js +2 -2
  79. package/dist/commands/rebuild-embeddings.js +48 -17
  80. package/dist/commands/rebuild.js +9 -8
  81. package/dist/commands/recall.d.ts +14 -0
  82. package/dist/commands/recall.js +15 -0
  83. package/dist/commands/recover.js +1 -1
  84. package/dist/commands/reflect.js +6 -6
  85. package/dist/commands/repair-session.js +1 -1
  86. package/dist/commands/replay.js +10 -9
  87. package/dist/commands/session-recap.js +1 -1
  88. package/dist/commands/setup.js +4 -3
  89. package/dist/commands/shell-init.js +1 -1
  90. package/dist/commands/sleep.d.ts +1 -1
  91. package/dist/commands/sleep.js +20 -18
  92. package/dist/commands/status.js +40 -26
  93. package/dist/commands/sync-bd.js +3 -3
  94. package/dist/commands/tailscale.js +3 -3
  95. package/dist/commands/task.js +1 -1
  96. package/dist/commands/template.js +1 -1
  97. package/dist/commands/wake.d.ts +1 -1
  98. package/dist/commands/wake.js +10 -9
  99. package/dist/index.d.ts +233 -16
  100. package/dist/index.js +325 -111
  101. package/dist/{inject-DYUrDqQO.d.ts → inject-DEb_jpLi.d.ts} +3 -1
  102. package/dist/lib/auto-linker.js +2 -2
  103. package/dist/lib/canvas-layout.js +1 -1
  104. package/dist/lib/config.js +2 -2
  105. package/dist/lib/entity-index.js +1 -1
  106. package/dist/lib/project-utils.js +2 -2
  107. package/dist/lib/session-repair.js +1 -1
  108. package/dist/lib/session-utils.js +1 -1
  109. package/dist/lib/tailscale.js +1 -1
  110. package/dist/lib/task-utils.js +1 -1
  111. package/dist/lib/template-engine.js +1 -1
  112. package/dist/lib/webdav.js +1 -1
  113. package/dist/onnxruntime_binding-5QEF3SUC.node +0 -0
  114. package/dist/onnxruntime_binding-BKPKNEGC.node +0 -0
  115. package/dist/onnxruntime_binding-FMOXGIUT.node +0 -0
  116. package/dist/onnxruntime_binding-OI2KMXC5.node +0 -0
  117. package/dist/onnxruntime_binding-UX44MLAZ.node +0 -0
  118. package/dist/onnxruntime_binding-Y2W7N7WY.node +0 -0
  119. package/dist/openclaw-plugin--gqA2BZw.d.ts +267 -0
  120. package/dist/openclaw-plugin.d.ts +4 -0
  121. package/dist/openclaw-plugin.js +20 -0
  122. package/dist/transformers.node-A2ZRORSQ.js +46775 -0
  123. package/dist/types-CbL-wIKi.d.ts +36 -0
  124. package/dist/{types-BbWJoC1c.d.ts → types-DslKvCaj.d.ts} +51 -1
  125. package/hooks/clawvault/HOOK.md +25 -8
  126. package/hooks/clawvault/handler.js +215 -78
  127. package/hooks/clawvault/handler.test.js +109 -43
  128. package/hooks/clawvault/integrity.js +112 -0
  129. package/hooks/clawvault/integrity.test.js +32 -0
  130. package/hooks/clawvault/openclaw.plugin.json +133 -15
  131. package/openclaw.plugin.json +161 -194
  132. package/package.json +8 -5
  133. package/bin/register-workgraph-commands.js +0 -451
  134. package/dist/chunk-5PJ4STIC.js +0 -465
  135. package/dist/chunk-ERNE2FZ5.js +0 -189
  136. package/dist/chunk-HR4KN6S2.js +0 -152
  137. package/dist/chunk-IJBFGPCS.js +0 -33
  138. package/dist/chunk-K7PNYS45.js +0 -93
  139. package/dist/chunk-NTOPJI7W.js +0 -207
  140. package/dist/chunk-PG56HX5T.js +0 -154
  141. package/dist/chunk-QPDDIHXE.js +0 -501
  142. package/dist/chunk-WIOLLGAD.js +0 -190
  143. package/dist/chunk-WMGIIABP.js +0 -15
  144. package/dist/ledger-B7g7jhqG.d.ts +0 -44
  145. package/dist/plugin/index.d.ts +0 -352
  146. package/dist/plugin/index.js +0 -4264
  147. package/dist/registry-BR4326o0.d.ts +0 -30
  148. package/dist/store-CA-6sKCJ.d.ts +0 -34
  149. package/dist/thread-B9LhXNU0.d.ts +0 -41
  150. package/dist/workgraph/index.d.ts +0 -5
  151. package/dist/workgraph/index.js +0 -23
  152. package/dist/workgraph/ledger.d.ts +0 -2
  153. package/dist/workgraph/ledger.js +0 -25
  154. package/dist/workgraph/registry.d.ts +0 -2
  155. package/dist/workgraph/registry.js +0 -19
  156. package/dist/workgraph/store.d.ts +0 -2
  157. package/dist/workgraph/store.js +0 -25
  158. package/dist/workgraph/thread.d.ts +0 -2
  159. package/dist/workgraph/thread.js +0 -25
  160. package/dist/workgraph/types.d.ts +0 -54
  161. package/dist/workgraph/types.js +0 -7
@@ -0,0 +1,872 @@
1
+ import {
2
+ requestLlmCompletion,
3
+ resolveLlmProvider
4
+ } from "./chunk-DVOUSOR3.js";
5
+ import {
6
+ readInboxItems
7
+ } from "./chunk-2PKBIKDH.js";
8
+ import {
9
+ buildHeuristicSurveyRecommendations,
10
+ classifyInboxItemHeuristic,
11
+ extractHeuristicInsights,
12
+ normalizeForDedup,
13
+ similarityScore
14
+ } from "./chunk-35JCYSRR.js";
15
+ import {
16
+ resolveVaultPath
17
+ } from "./chunk-GJO3CFUN.js";
18
+
19
+ // src/lib/maintenance/log.ts
20
+ import * as fs from "fs";
21
+ import * as path from "path";
22
+ var MaintenanceLogger = class {
23
+ filePath;
24
+ runId;
25
+ dryRun;
26
+ constructor(vaultPath, runId, dryRun) {
27
+ this.filePath = path.join(path.resolve(vaultPath), ".clawvault", "maintenance-log.jsonl");
28
+ this.runId = runId;
29
+ this.dryRun = dryRun;
30
+ fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
31
+ }
32
+ get path() {
33
+ return this.filePath;
34
+ }
35
+ append(worker, level, message, data) {
36
+ const event = {
37
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
38
+ runId: this.runId,
39
+ worker,
40
+ level,
41
+ message
42
+ };
43
+ if (data && Object.keys(data).length > 0) {
44
+ event.data = data;
45
+ }
46
+ if (this.dryRun) {
47
+ event.data = { ...event.data ?? {}, dryRun: true };
48
+ }
49
+ fs.appendFileSync(this.filePath, `${JSON.stringify(event)}
50
+ `, "utf-8");
51
+ }
52
+ };
53
+
54
+ // src/lib/maintenance/llm.ts
55
+ import * as fs2 from "fs";
56
+ import * as path2 from "path";
57
+ var VALID_PROVIDERS = ["anthropic", "openai", "gemini", "xai", "openclaw"];
58
+ var VAULT_CONFIG_FILE = ".clawvault.json";
59
+ function asProvider(value) {
60
+ if (typeof value !== "string") {
61
+ return null;
62
+ }
63
+ return VALID_PROVIDERS.includes(value) ? value : null;
64
+ }
65
+ function readWorkerLlmOverrides(vaultPath) {
66
+ try {
67
+ const configPath = path2.join(path2.resolve(vaultPath), VAULT_CONFIG_FILE);
68
+ if (!fs2.existsSync(configPath)) {
69
+ return { provider: null, model: null };
70
+ }
71
+ const parsed = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
72
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
73
+ return { provider: null, model: null };
74
+ }
75
+ const observe = parsed.observe;
76
+ if (!observe || typeof observe !== "object" || Array.isArray(observe)) {
77
+ return { provider: null, model: null };
78
+ }
79
+ const record = observe;
80
+ const provider = asProvider(record.provider);
81
+ const model = typeof record.model === "string" && record.model.trim() ? record.model.trim() : null;
82
+ return { provider, model };
83
+ } catch {
84
+ return { provider: null, model: null };
85
+ }
86
+ }
87
+ function createWorkerLlmClient(vaultPath) {
88
+ if (process.env.CLAWVAULT_NO_LLM) {
89
+ return {
90
+ enabled: false,
91
+ provider: null,
92
+ model: null,
93
+ complete: async () => ""
94
+ };
95
+ }
96
+ const { provider: configuredProvider, model: configuredModel } = readWorkerLlmOverrides(vaultPath);
97
+ const resolvedProvider = configuredProvider ?? resolveLlmProvider();
98
+ const enabled = !!resolvedProvider;
99
+ return {
100
+ enabled,
101
+ provider: resolvedProvider,
102
+ model: configuredModel,
103
+ complete: async (systemPrompt, userPrompt, options = {}) => {
104
+ if (!enabled) {
105
+ return "";
106
+ }
107
+ try {
108
+ return await requestLlmCompletion({
109
+ provider: resolvedProvider,
110
+ model: options.model ?? configuredModel ?? void 0,
111
+ tier: options.tier ?? "default",
112
+ systemPrompt,
113
+ prompt: userPrompt,
114
+ temperature: 0.1,
115
+ maxTokens: 1200
116
+ });
117
+ } catch {
118
+ return "";
119
+ }
120
+ }
121
+ };
122
+ }
123
+
124
+ // src/lib/maintenance/state.ts
125
+ import * as fs3 from "fs";
126
+ import * as path3 from "path";
127
+
128
+ // src/lib/maintenance/types.ts
129
+ var MAINTENANCE_WORKERS = ["curator", "janitor", "distiller", "surveyor"];
130
+
131
+ // src/lib/maintenance/state.ts
132
+ var STATE_FILE = "maintenance-state.json";
133
+ function defaultState() {
134
+ return {
135
+ version: 1,
136
+ workers: {
137
+ curator: { processedHashes: [] },
138
+ janitor: { processedHashes: [] },
139
+ distiller: { processedHashes: [] },
140
+ surveyor: { processedHashes: [] }
141
+ }
142
+ };
143
+ }
144
+ function statePath(vaultPath) {
145
+ return path3.join(path3.resolve(vaultPath), ".clawvault", STATE_FILE);
146
+ }
147
+ function readMaintenanceState(vaultPath) {
148
+ const filePath = statePath(vaultPath);
149
+ if (!fs3.existsSync(filePath)) {
150
+ return defaultState();
151
+ }
152
+ try {
153
+ const raw = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
154
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
155
+ return defaultState();
156
+ }
157
+ const record = raw;
158
+ const next = defaultState();
159
+ for (const worker of MAINTENANCE_WORKERS) {
160
+ const candidate = record.workers?.[worker];
161
+ const hashes = Array.isArray(candidate?.processedHashes) ? candidate.processedHashes.filter((value) => typeof value === "string" && value.length > 0) : [];
162
+ next.workers[worker] = {
163
+ processedHashes: hashes,
164
+ updatedAt: typeof candidate?.updatedAt === "string" ? candidate.updatedAt : void 0
165
+ };
166
+ }
167
+ return next;
168
+ } catch {
169
+ return defaultState();
170
+ }
171
+ }
172
+ function writeMaintenanceState(vaultPath, state) {
173
+ const filePath = statePath(vaultPath);
174
+ fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
175
+ fs3.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}
176
+ `, "utf-8");
177
+ }
178
+
179
+ // src/lib/maintenance/curator-worker.ts
180
+ import * as path5 from "path";
181
+ import matter from "gray-matter";
182
+
183
+ // src/lib/maintenance/worker-utils.ts
184
+ import * as fs4 from "fs";
185
+ import * as path4 from "path";
186
+ import { createHash } from "crypto";
187
+ var CURATOR_SYSTEM_PROMPT = [
188
+ "You are Curator, a vault maintenance worker.",
189
+ "Classify each inbox capture into one destination category.",
190
+ "Return strict JSON only with shape:",
191
+ '{"routes":[{"hash":"...","category":"...","reason":"..."}]}'
192
+ ].join(" ");
193
+ var JANITOR_SYSTEM_PROMPT = [
194
+ "You are Janitor, a vault hygiene worker.",
195
+ "Given dedupe/staleness metrics, propose concise cleanup recommendations.",
196
+ "Return plain text bullets."
197
+ ].join(" ");
198
+ var DISTILLER_SYSTEM_PROMPT = [
199
+ "You are Distiller, a memory extraction worker.",
200
+ "Extract facts, decisions, and lessons from long-form captures.",
201
+ "Return strict JSON only with shape:",
202
+ '{"items":[{"hash":"...","facts":["..."],"decisions":["..."],"lessons":["..."]}]}'
203
+ ].join(" ");
204
+ var SURVEYOR_SYSTEM_PROMPT = [
205
+ "You are Surveyor, a graph health worker.",
206
+ "Analyze vault metrics and suggest practical improvements.",
207
+ "Return plain text bullets."
208
+ ].join(" ");
209
+ var CURATOR_ALLOWED_CATEGORIES = /* @__PURE__ */ new Set([
210
+ "rules",
211
+ "preferences",
212
+ "decisions",
213
+ "patterns",
214
+ "people",
215
+ "projects",
216
+ "goals",
217
+ "transcripts",
218
+ "inbox",
219
+ "facts",
220
+ "feelings",
221
+ "lessons",
222
+ "commitments",
223
+ "handoffs",
224
+ "research",
225
+ "agents"
226
+ ]);
227
+ function truncate(text, max) {
228
+ if (text.length <= max) {
229
+ return text;
230
+ }
231
+ return `${text.slice(0, Math.max(0, max - 3))}...`;
232
+ }
233
+ function toRelative(vaultPath, filePath) {
234
+ return path4.relative(path4.resolve(vaultPath), filePath).replace(/\\/g, "/");
235
+ }
236
+ function extractJsonObject(raw) {
237
+ const trimmed = raw.trim();
238
+ if (!trimmed) {
239
+ return null;
240
+ }
241
+ const codeBlockMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
242
+ const candidate = codeBlockMatch ? codeBlockMatch[1].trim() : trimmed;
243
+ const firstBrace = candidate.indexOf("{");
244
+ const lastBrace = candidate.lastIndexOf("}");
245
+ if (firstBrace < 0 || lastBrace <= firstBrace) {
246
+ return null;
247
+ }
248
+ try {
249
+ const parsed = JSON.parse(candidate.slice(firstBrace, lastBrace + 1));
250
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
251
+ return null;
252
+ }
253
+ return parsed;
254
+ } catch {
255
+ return null;
256
+ }
257
+ }
258
+ function writeFileIfChanged(filePath, content, dryRun) {
259
+ const normalized = content.endsWith("\n") ? content : `${content}
260
+ `;
261
+ if (fs4.existsSync(filePath)) {
262
+ const existing = fs4.readFileSync(filePath, "utf-8");
263
+ if (existing === normalized) {
264
+ return false;
265
+ }
266
+ }
267
+ if (!dryRun) {
268
+ fs4.mkdirSync(path4.dirname(filePath), { recursive: true });
269
+ fs4.writeFileSync(filePath, normalized, "utf-8");
270
+ }
271
+ return true;
272
+ }
273
+ function moveToArchive(sourcePath, archiveDir, dryRun) {
274
+ const baseName = path4.basename(sourcePath);
275
+ let destinationPath = path4.join(archiveDir, baseName);
276
+ let counter = 1;
277
+ while (fs4.existsSync(destinationPath)) {
278
+ const ext = path4.extname(baseName);
279
+ const stem = ext ? baseName.slice(0, -ext.length) : baseName;
280
+ destinationPath = path4.join(archiveDir, `${stem}-${counter}${ext}`);
281
+ counter += 1;
282
+ }
283
+ if (!dryRun) {
284
+ fs4.mkdirSync(archiveDir, { recursive: true });
285
+ fs4.renameSync(sourcePath, destinationPath);
286
+ }
287
+ return { moved: true, destinationPath };
288
+ }
289
+ function hashList(values) {
290
+ return createHash("sha256").update(values.join("|")).digest("hex");
291
+ }
292
+ function parseCuratorRoutes(raw) {
293
+ const parsed = extractJsonObject(raw);
294
+ const routesRaw = parsed?.routes;
295
+ if (!Array.isArray(routesRaw)) {
296
+ return /* @__PURE__ */ new Map();
297
+ }
298
+ const routes = /* @__PURE__ */ new Map();
299
+ for (const entry of routesRaw) {
300
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
301
+ continue;
302
+ }
303
+ const record = entry;
304
+ const hash = typeof record.hash === "string" ? record.hash.trim() : "";
305
+ const category = typeof record.category === "string" ? record.category.trim() : "";
306
+ if (!hash || !category || !CURATOR_ALLOWED_CATEGORIES.has(category)) {
307
+ continue;
308
+ }
309
+ routes.set(hash, category);
310
+ }
311
+ return routes;
312
+ }
313
+ function parseDistillerInsights(raw) {
314
+ const parsed = extractJsonObject(raw);
315
+ const itemsRaw = parsed?.items;
316
+ if (!Array.isArray(itemsRaw)) {
317
+ return /* @__PURE__ */ new Map();
318
+ }
319
+ const map = /* @__PURE__ */ new Map();
320
+ for (const entry of itemsRaw) {
321
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
322
+ continue;
323
+ }
324
+ const record = entry;
325
+ const hash = typeof record.hash === "string" ? record.hash.trim() : "";
326
+ if (!hash) {
327
+ continue;
328
+ }
329
+ const facts = Array.isArray(record.facts) ? record.facts.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
330
+ const decisions = Array.isArray(record.decisions) ? record.decisions.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
331
+ const lessons = Array.isArray(record.lessons) ? record.lessons.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
332
+ map.set(hash, { facts, decisions, lessons });
333
+ }
334
+ return map;
335
+ }
336
+ function wordsCount(content) {
337
+ return content.trim().split(/\s+/).filter(Boolean).length;
338
+ }
339
+ function buildCuratorLlmPrompt(items) {
340
+ const payload = items.map((item) => ({
341
+ hash: item.hash,
342
+ title: item.title,
343
+ sample: truncate(item.content, 300)
344
+ }));
345
+ return [
346
+ "Classify each inbox item into a single category.",
347
+ `Allowed categories: ${[...CURATOR_ALLOWED_CATEGORIES].join(", ")}`,
348
+ "Return JSON only.",
349
+ JSON.stringify(payload, null, 2)
350
+ ].join("\n\n");
351
+ }
352
+ function buildDistillerLlmPrompt(items) {
353
+ const payload = items.map((item) => ({
354
+ hash: item.hash,
355
+ title: item.title,
356
+ content: truncate(item.content, 1800)
357
+ }));
358
+ return [
359
+ "Extract facts, decisions, and lessons for each item.",
360
+ "Return concise bullet-style statements and JSON only.",
361
+ JSON.stringify(payload, null, 2)
362
+ ].join("\n\n");
363
+ }
364
+
365
+ // src/lib/maintenance/curator-worker.ts
366
+ function renderCuratedContent(item, now, category) {
367
+ const body = `${item.content.trim()}
368
+
369
+ ---
370
+ Curated from \`${item.relativePath}\` by background curator.
371
+ `;
372
+ return matter.stringify(body, {
373
+ title: item.title,
374
+ date: now.toISOString().split("T")[0],
375
+ source: "inbox",
376
+ inboxHash: item.hash,
377
+ inboxPath: item.relativePath,
378
+ curatedBy: "curator",
379
+ curatedCategory: category
380
+ });
381
+ }
382
+ async function runCuratorWorker(ctx, state, llm, logger) {
383
+ const items = readInboxItems(ctx.vaultPath, { limit: ctx.maxItems });
384
+ const processed = new Set(state.workers.curator.processedHashes);
385
+ const pending = items.filter((item) => !processed.has(item.hash));
386
+ let usedLlm = false;
387
+ let llmRoutes = /* @__PURE__ */ new Map();
388
+ if (pending.length > 0 && llm.enabled) {
389
+ const response = await llm.complete(
390
+ CURATOR_SYSTEM_PROMPT,
391
+ buildCuratorLlmPrompt(pending),
392
+ { tier: "background" }
393
+ );
394
+ if (response.trim()) {
395
+ llmRoutes = parseCuratorRoutes(response);
396
+ usedLlm = llmRoutes.size > 0;
397
+ }
398
+ }
399
+ const actions = [];
400
+ let processedCount = 0;
401
+ for (const item of pending) {
402
+ const category = llmRoutes.get(item.hash) ?? classifyInboxItemHeuristic(item);
403
+ const now = ctx.now();
404
+ const targetPath = path5.join(ctx.vaultPath, category, `inbox-${item.hash.slice(0, 12)}.md`);
405
+ const wrote = writeFileIfChanged(targetPath, renderCuratedContent(item, now, category), ctx.dryRun);
406
+ processed.add(item.hash);
407
+ processedCount += 1;
408
+ const action = `${item.relativePath} -> ${toRelative(ctx.vaultPath, targetPath)}${wrote ? "" : " (unchanged)"}`;
409
+ actions.push(action);
410
+ logger.append("curator", "info", "Curated inbox item", {
411
+ source: item.relativePath,
412
+ category,
413
+ target: toRelative(ctx.vaultPath, targetPath),
414
+ wrote
415
+ });
416
+ }
417
+ state.workers.curator.processedHashes = [...processed];
418
+ state.workers.curator.updatedAt = ctx.now().toISOString();
419
+ return {
420
+ worker: "curator",
421
+ processed: processedCount,
422
+ skipped: items.length - pending.length,
423
+ actions,
424
+ usedLlm,
425
+ degradedMode: !usedLlm
426
+ };
427
+ }
428
+
429
+ // src/lib/maintenance/janitor-worker.ts
430
+ import * as path6 from "path";
431
+ import matter2 from "gray-matter";
432
+ function buildRelatedClusters(items) {
433
+ const clusters = [];
434
+ const used = /* @__PURE__ */ new Set();
435
+ for (let index = 0; index < items.length; index += 1) {
436
+ const seed = items[index];
437
+ if (used.has(seed.hash)) {
438
+ continue;
439
+ }
440
+ const clusterItems = [seed];
441
+ for (let cursor = index + 1; cursor < items.length; cursor += 1) {
442
+ const candidate = items[cursor];
443
+ if (used.has(candidate.hash)) {
444
+ continue;
445
+ }
446
+ const score = similarityScore(seed.content, candidate.content);
447
+ if (score >= 0.55) {
448
+ clusterItems.push(candidate);
449
+ }
450
+ }
451
+ if (clusterItems.length > 1) {
452
+ for (const item of clusterItems) {
453
+ used.add(item.hash);
454
+ }
455
+ const clusterId = hashList(clusterItems.map((item) => item.hash).sort()).slice(0, 12);
456
+ clusters.push({ id: clusterId, items: clusterItems.sort((left, right) => left.relativePath.localeCompare(right.relativePath)) });
457
+ }
458
+ }
459
+ return clusters;
460
+ }
461
+ function renderJanitorMergedCluster(cluster, now) {
462
+ const lines = cluster.items.map((item) => `- **${item.title}** (\`${item.relativePath}\`): ${truncate(item.content, 180)}`);
463
+ return matter2.stringify(
464
+ `## Related captures
465
+ ${lines.join("\n")}
466
+ `,
467
+ {
468
+ title: `Merged inbox cluster ${cluster.id}`,
469
+ date: now.toISOString().split("T")[0],
470
+ type: "inbox-merge",
471
+ sources: cluster.items.map((item) => item.relativePath)
472
+ }
473
+ );
474
+ }
475
+ function renderJanitorReport(params) {
476
+ const recommendations = params.llmRecommendation.trim() ? params.llmRecommendation.trim() : "- Keep capture titles descriptive to improve curator routing quality.";
477
+ return [
478
+ `# Janitor report (${params.now.toISOString().split("T")[0]})`,
479
+ "",
480
+ `- Duplicates archived: ${params.duplicatesMoved}`,
481
+ `- Stale items archived: ${params.staleArchived}`,
482
+ `- Merged related clusters: ${params.mergedClusters}`,
483
+ "",
484
+ "## Recommendations",
485
+ recommendations
486
+ ].join("\n");
487
+ }
488
+ async function runJanitorWorker(ctx, state, llm, logger) {
489
+ const items = readInboxItems(ctx.vaultPath, { limit: ctx.maxItems });
490
+ const byNormalized = /* @__PURE__ */ new Map();
491
+ for (const item of items) {
492
+ const key = normalizeForDedup(item.content);
493
+ if (!key) {
494
+ continue;
495
+ }
496
+ const bucket = byNormalized.get(key) ?? [];
497
+ bucket.push(item);
498
+ byNormalized.set(key, bucket);
499
+ }
500
+ let duplicatesMoved = 0;
501
+ const actions = [];
502
+ for (const [, grouped] of byNormalized) {
503
+ if (grouped.length < 2) {
504
+ continue;
505
+ }
506
+ const sorted = [...grouped].sort((left, right) => left.capturedAt.getTime() - right.capturedAt.getTime());
507
+ for (const duplicate of sorted.slice(1)) {
508
+ const archiveDir = path6.join(ctx.vaultPath, "inbox", "archive", "deduped");
509
+ const moved = moveToArchive(duplicate.path, archiveDir, ctx.dryRun);
510
+ duplicatesMoved += 1;
511
+ actions.push(`Archived duplicate ${duplicate.relativePath} -> ${toRelative(ctx.vaultPath, moved.destinationPath)}`);
512
+ logger.append("janitor", "info", "Archived duplicate inbox capture", {
513
+ source: duplicate.relativePath,
514
+ target: toRelative(ctx.vaultPath, moved.destinationPath)
515
+ });
516
+ }
517
+ }
518
+ const refreshItems = readInboxItems(ctx.vaultPath, { limit: ctx.maxItems });
519
+ let staleArchived = 0;
520
+ const staleCutoffMs = ctx.now().getTime() - 30 * 24 * 60 * 60 * 1e3;
521
+ for (const item of refreshItems) {
522
+ if (item.capturedAt.getTime() >= staleCutoffMs) {
523
+ continue;
524
+ }
525
+ const archiveDir = path6.join(ctx.vaultPath, "inbox", "archive", "stale");
526
+ const moved = moveToArchive(item.path, archiveDir, ctx.dryRun);
527
+ staleArchived += 1;
528
+ actions.push(`Archived stale ${item.relativePath} -> ${toRelative(ctx.vaultPath, moved.destinationPath)}`);
529
+ logger.append("janitor", "info", "Archived stale inbox capture", {
530
+ source: item.relativePath,
531
+ target: toRelative(ctx.vaultPath, moved.destinationPath)
532
+ });
533
+ }
534
+ const postArchiveItems = readInboxItems(ctx.vaultPath, { limit: ctx.maxItems });
535
+ const clusters = buildRelatedClusters(postArchiveItems);
536
+ for (const cluster of clusters) {
537
+ const mergedPath = path6.join(ctx.vaultPath, "inbox", "merged", `merged-${cluster.id}.md`);
538
+ const wrote = writeFileIfChanged(mergedPath, renderJanitorMergedCluster(cluster, ctx.now()), ctx.dryRun);
539
+ actions.push(`${wrote ? "Updated" : "Kept"} merged cluster ${toRelative(ctx.vaultPath, mergedPath)}`);
540
+ logger.append("janitor", "info", "Updated merged inbox cluster", {
541
+ clusterId: cluster.id,
542
+ file: toRelative(ctx.vaultPath, mergedPath),
543
+ items: cluster.items.map((item) => item.relativePath)
544
+ });
545
+ }
546
+ let llmRecommendation = "";
547
+ let usedLlm = false;
548
+ if (llm.enabled) {
549
+ llmRecommendation = await llm.complete(
550
+ JANITOR_SYSTEM_PROMPT,
551
+ [
552
+ "Provide 2-4 concise hygiene recommendations.",
553
+ `duplicatesMoved=${duplicatesMoved}`,
554
+ `staleArchived=${staleArchived}`,
555
+ `mergedClusters=${clusters.length}`
556
+ ].join("\n"),
557
+ { tier: "background" }
558
+ );
559
+ usedLlm = llmRecommendation.trim().length > 0;
560
+ }
561
+ const reportPath = path6.join(ctx.vaultPath, ".clawvault", "maintenance", "janitor-report.md");
562
+ writeFileIfChanged(reportPath, renderJanitorReport({
563
+ now: ctx.now(),
564
+ duplicatesMoved,
565
+ staleArchived,
566
+ mergedClusters: clusters.length,
567
+ llmRecommendation
568
+ }), ctx.dryRun);
569
+ logger.append("janitor", "info", "Updated janitor report", {
570
+ report: toRelative(ctx.vaultPath, reportPath),
571
+ duplicatesMoved,
572
+ staleArchived,
573
+ mergedClusters: clusters.length
574
+ });
575
+ state.workers.janitor.updatedAt = ctx.now().toISOString();
576
+ return {
577
+ worker: "janitor",
578
+ processed: duplicatesMoved + staleArchived + clusters.length,
579
+ skipped: 0,
580
+ actions,
581
+ usedLlm,
582
+ degradedMode: !usedLlm
583
+ };
584
+ }
585
+
586
+ // src/lib/maintenance/distiller-worker.ts
587
+ import * as path7 from "path";
588
+ import matter3 from "gray-matter";
589
+ function renderDistilledContent(item, now, sectionTitle, entries) {
590
+ const body = `## ${sectionTitle}
591
+ ${entries.map((line) => `- ${line}`).join("\n")}
592
+
593
+ ---
594
+ Distilled from \`${item.relativePath}\`.
595
+ `;
596
+ return matter3.stringify(body, {
597
+ title: `${sectionTitle} from ${item.title}`,
598
+ date: now.toISOString().split("T")[0],
599
+ source: "distiller",
600
+ inboxHash: item.hash,
601
+ inboxPath: item.relativePath
602
+ });
603
+ }
604
+ function writeDistilledEntries(ctx, item, category, sectionTitle, entries, logger, actions) {
605
+ if (entries.length === 0) {
606
+ return;
607
+ }
608
+ const filePath = path7.join(ctx.vaultPath, category, `distilled-${item.hash.slice(0, 12)}.md`);
609
+ const wrote = writeFileIfChanged(
610
+ filePath,
611
+ renderDistilledContent(item, ctx.now(), sectionTitle, entries),
612
+ ctx.dryRun
613
+ );
614
+ actions.push(`${wrote ? "Updated" : "Kept"} ${toRelative(ctx.vaultPath, filePath)}`);
615
+ logger.append("distiller", "info", "Updated distilled output", {
616
+ source: item.relativePath,
617
+ category,
618
+ target: toRelative(ctx.vaultPath, filePath),
619
+ entries: entries.length,
620
+ wrote
621
+ });
622
+ }
623
+ async function runDistillerWorker(ctx, state, llm, logger) {
624
+ const items = readInboxItems(ctx.vaultPath, { limit: ctx.maxItems });
625
+ const processed = new Set(state.workers.distiller.processedHashes);
626
+ const pending = items.filter((item) => !processed.has(item.hash));
627
+ const longForm = pending.filter((item) => wordsCount(item.content) >= 80);
628
+ let llmInsights = /* @__PURE__ */ new Map();
629
+ let usedLlm = false;
630
+ if (llm.enabled && longForm.length > 0) {
631
+ const response = await llm.complete(
632
+ DISTILLER_SYSTEM_PROMPT,
633
+ buildDistillerLlmPrompt(longForm),
634
+ { tier: "complex" }
635
+ );
636
+ if (response.trim()) {
637
+ llmInsights = parseDistillerInsights(response);
638
+ usedLlm = llmInsights.size > 0;
639
+ }
640
+ }
641
+ const actions = [];
642
+ let processedCount = 0;
643
+ for (const item of longForm) {
644
+ const insights = llmInsights.get(item.hash) ?? extractHeuristicInsights(item.content);
645
+ writeDistilledEntries(ctx, item, "facts", "Facts", insights.facts, logger, actions);
646
+ writeDistilledEntries(ctx, item, "decisions", "Decisions", insights.decisions, logger, actions);
647
+ writeDistilledEntries(ctx, item, "lessons", "Lessons", insights.lessons, logger, actions);
648
+ processed.add(item.hash);
649
+ processedCount += 1;
650
+ }
651
+ for (const item of pending) {
652
+ if (!processed.has(item.hash)) {
653
+ processed.add(item.hash);
654
+ logger.append("distiller", "info", "Skipped short-form inbox item", {
655
+ source: item.relativePath,
656
+ words: wordsCount(item.content)
657
+ });
658
+ }
659
+ }
660
+ state.workers.distiller.processedHashes = [...processed];
661
+ state.workers.distiller.updatedAt = ctx.now().toISOString();
662
+ return {
663
+ worker: "distiller",
664
+ processed: processedCount,
665
+ skipped: pending.length - processedCount + (items.length - pending.length),
666
+ actions,
667
+ usedLlm,
668
+ degradedMode: !usedLlm
669
+ };
670
+ }
671
+
672
+ // src/lib/maintenance/surveyor-worker.ts
673
+ import * as fs5 from "fs";
674
+ import * as path8 from "path";
675
+ import { globSync } from "glob";
676
+ function collectVaultSurvey(vaultPath) {
677
+ const docs = globSync("**/*.md", {
678
+ cwd: path8.resolve(vaultPath),
679
+ nodir: true,
680
+ absolute: true,
681
+ ignore: [".clawvault/**", "ledger/**", "node_modules/**"]
682
+ });
683
+ const categoryCounts = {};
684
+ let linkedDocs = 0;
685
+ for (const filePath of docs) {
686
+ const relativePath = toRelative(vaultPath, filePath);
687
+ const category = relativePath.split("/")[0] || "root";
688
+ categoryCounts[category] = (categoryCounts[category] ?? 0) + 1;
689
+ const content = fs5.readFileSync(filePath, "utf-8");
690
+ if (/\[\[[^\]]+\]\]/.test(content)) {
691
+ linkedDocs += 1;
692
+ }
693
+ }
694
+ const linkedRatio = docs.length === 0 ? 0 : linkedDocs / docs.length;
695
+ return {
696
+ totalDocs: docs.length,
697
+ linkedDocs,
698
+ linkedRatio,
699
+ categoryCounts
700
+ };
701
+ }
702
+ function renderSurveyorReport(params) {
703
+ const counts = Object.entries(params.survey.categoryCounts).sort((left, right) => right[1] - left[1]).map(([category, count]) => `- ${category}: ${count}`);
704
+ return [
705
+ `# Surveyor report (${params.now.toISOString().split("T")[0]})`,
706
+ "",
707
+ "## Vault health snapshot",
708
+ `- Total markdown docs: ${params.survey.totalDocs}`,
709
+ `- Linked docs: ${params.survey.linkedDocs}`,
710
+ `- Link coverage ratio: ${(params.survey.linkedRatio * 100).toFixed(1)}%`,
711
+ `- Inbox active captures: ${params.inboxCount}`,
712
+ "",
713
+ "## Category distribution",
714
+ ...counts.length > 0 ? counts : ["- No markdown categories found."],
715
+ "",
716
+ "## Recommendations",
717
+ ...params.recommendations.map((entry) => entry.startsWith("- ") ? entry : `- ${entry}`)
718
+ ].join("\n");
719
+ }
720
+ async function runSurveyorWorker(ctx, state, llm, logger) {
721
+ const survey = collectVaultSurvey(ctx.vaultPath);
722
+ const inboxCount = readInboxItems(ctx.vaultPath).length;
723
+ const heuristicRecommendations = buildHeuristicSurveyRecommendations({
724
+ inboxCount,
725
+ linkedRatio: survey.linkedRatio,
726
+ categoryCounts: survey.categoryCounts
727
+ });
728
+ let llmSuggestions = "";
729
+ let usedLlm = false;
730
+ if (llm.enabled) {
731
+ llmSuggestions = await llm.complete(
732
+ SURVEYOR_SYSTEM_PROMPT,
733
+ [
734
+ "Review this vault health summary and suggest 2-5 actionable improvements.",
735
+ JSON.stringify({
736
+ totalDocs: survey.totalDocs,
737
+ linkedDocs: survey.linkedDocs,
738
+ linkedRatio: survey.linkedRatio,
739
+ inboxCount,
740
+ categoryCounts: survey.categoryCounts
741
+ }, null, 2)
742
+ ].join("\n\n"),
743
+ { tier: "complex" }
744
+ );
745
+ usedLlm = llmSuggestions.trim().length > 0;
746
+ }
747
+ const recommendations = [...heuristicRecommendations];
748
+ if (llmSuggestions.trim()) {
749
+ const lines = llmSuggestions.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
750
+ recommendations.push(...lines);
751
+ }
752
+ const reportPath = path8.join(ctx.vaultPath, ".clawvault", "maintenance", "surveyor-report.md");
753
+ writeFileIfChanged(reportPath, renderSurveyorReport({
754
+ now: ctx.now(),
755
+ survey,
756
+ inboxCount,
757
+ recommendations
758
+ }), ctx.dryRun);
759
+ logger.append("surveyor", "info", "Updated surveyor report", {
760
+ report: toRelative(ctx.vaultPath, reportPath),
761
+ totalDocs: survey.totalDocs,
762
+ inboxCount
763
+ });
764
+ const snapshotHash = hashList([
765
+ String(survey.totalDocs),
766
+ String(survey.linkedDocs),
767
+ String(inboxCount),
768
+ JSON.stringify(survey.categoryCounts)
769
+ ]);
770
+ state.workers.surveyor.processedHashes = [snapshotHash];
771
+ state.workers.surveyor.updatedAt = ctx.now().toISOString();
772
+ return {
773
+ worker: "surveyor",
774
+ processed: 1,
775
+ skipped: 0,
776
+ actions: [`Updated ${toRelative(ctx.vaultPath, reportPath)}`],
777
+ usedLlm,
778
+ degradedMode: !usedLlm
779
+ };
780
+ }
781
+
782
+ // src/commands/maintain.ts
783
+ function parseWorker(value) {
784
+ if (!value) {
785
+ return null;
786
+ }
787
+ return MAINTENANCE_WORKERS.includes(value) ? value : null;
788
+ }
789
+ function buildRunId(now) {
790
+ return `maint-${now.toISOString().replace(/[-:.]/g, "").replace("T", "t")}`;
791
+ }
792
+ async function maintainCommand(options = {}) {
793
+ const resolvedWorker = parseWorker(options.worker);
794
+ if (options.worker && !resolvedWorker) {
795
+ throw new Error(
796
+ `Unknown worker "${options.worker}". Expected one of: ${MAINTENANCE_WORKERS.join(", ")}`
797
+ );
798
+ }
799
+ const vaultPath = resolveVaultPath({ explicitPath: options.vaultPath });
800
+ const now = () => /* @__PURE__ */ new Date();
801
+ const started = now();
802
+ const runId = buildRunId(started);
803
+ const dryRun = options.dryRun ?? false;
804
+ const maxItems = typeof options.limit === "number" && Number.isFinite(options.limit) && options.limit > 0 ? Math.floor(options.limit) : void 0;
805
+ const state = readMaintenanceState(vaultPath);
806
+ const logger = new MaintenanceLogger(vaultPath, runId, dryRun);
807
+ const llm = createWorkerLlmClient(vaultPath);
808
+ const workerList = resolvedWorker ? [resolvedWorker] : [...MAINTENANCE_WORKERS];
809
+ const ctx = {
810
+ vaultPath,
811
+ runId,
812
+ now,
813
+ dryRun,
814
+ maxItems
815
+ };
816
+ const workers = [];
817
+ for (const worker of workerList) {
818
+ logger.append(worker, "info", "Worker started");
819
+ let result;
820
+ if (worker === "curator") {
821
+ result = await runCuratorWorker(ctx, state, llm, logger);
822
+ } else if (worker === "janitor") {
823
+ result = await runJanitorWorker(ctx, state, llm, logger);
824
+ } else if (worker === "distiller") {
825
+ result = await runDistillerWorker(ctx, state, llm, logger);
826
+ } else {
827
+ result = await runSurveyorWorker(ctx, state, llm, logger);
828
+ }
829
+ workers.push(result);
830
+ logger.append(worker, "info", "Worker completed", {
831
+ processed: result.processed,
832
+ skipped: result.skipped,
833
+ degradedMode: result.degradedMode
834
+ });
835
+ }
836
+ writeMaintenanceState(vaultPath, state);
837
+ const finishedAt = now().toISOString();
838
+ const summary = {
839
+ runId,
840
+ vaultPath,
841
+ startedAt: started.toISOString(),
842
+ finishedAt,
843
+ logPath: logger.path,
844
+ workers
845
+ };
846
+ if (!options.quiet) {
847
+ console.log(`Maintenance run: ${runId}`);
848
+ for (const worker of workers) {
849
+ const mode = worker.degradedMode ? "heuristic" : "llm";
850
+ console.log(
851
+ `- ${worker.worker}: processed=${worker.processed}, skipped=${worker.skipped}, mode=${mode}`
852
+ );
853
+ }
854
+ console.log(`Maintenance log: ${summary.logPath}`);
855
+ }
856
+ return summary;
857
+ }
858
+ function registerMaintainCommand(program) {
859
+ program.command("maintain").description("Run inbox maintenance workers (curator, janitor, distiller, surveyor)").option("--worker <name>", `Run a single worker: ${MAINTENANCE_WORKERS.join(", ")}`).option("--limit <n>", "Limit inbox items processed per worker", (value) => Number.parseInt(value, 10)).option("--dry-run", "Preview actions without writing files").option("-v, --vault <path>", "Vault path").action(async (rawOptions) => {
860
+ await maintainCommand({
861
+ vaultPath: rawOptions.vault,
862
+ worker: rawOptions.worker,
863
+ dryRun: rawOptions.dryRun,
864
+ limit: rawOptions.limit
865
+ });
866
+ });
867
+ }
868
+
869
+ export {
870
+ maintainCommand,
871
+ registerMaintainCommand
872
+ };