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