clawvault 2.5.0 → 2.5.2
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 +1 -0
- package/bin/command-registration.test.js +1 -0
- package/bin/command-runtime.js +52 -1
- package/bin/command-runtime.test.js +52 -0
- package/bin/help-contract.test.js +12 -1
- package/bin/register-config-commands.js +2 -2
- package/bin/register-core-commands.js +15 -10
- package/bin/register-kanban-commands.js +5 -5
- package/bin/register-maintenance-commands.js +44 -10
- package/bin/register-project-commands.js +14 -14
- package/bin/register-project-commands.test.js +5 -0
- package/bin/register-query-commands.js +23 -18
- package/bin/register-query-commands.test.js +65 -0
- package/bin/register-session-lifecycle-commands.js +3 -3
- package/bin/register-tailscale-commands.js +1 -1
- package/bin/register-task-commands.js +5 -5
- package/bin/register-template-commands.js +1 -1
- package/bin/register-vault-operations-commands.js +13 -8
- package/dist/{chunk-G3OQJ2NQ.js → chunk-2YDBJS7M.js} +1 -1
- package/dist/chunk-3FP5BJ42.js +88 -0
- package/dist/{chunk-C3PF7WBA.js → chunk-4IV3R2F5.js} +2 -2
- package/dist/{chunk-7OHQFMJK.js → chunk-AY4PGUVL.js} +5 -4
- package/dist/chunk-BHO7WSAY.js +332 -0
- package/dist/{chunk-WIICLBNF.js → chunk-GFJ3LIIB.js} +1 -1
- package/dist/chunk-HRTPQQF2.js +541 -0
- package/dist/chunk-HWUNREDJ.js +33 -0
- package/dist/{chunk-6RQPD7X6.js → chunk-M25QVSJM.js} +4 -3
- package/dist/{chunk-6B3JWM7J.js → chunk-O7XHXF7F.js} +34 -7
- package/dist/chunk-PLZKZW4I.js +406 -0
- package/dist/{chunk-PAYUH64O.js → chunk-QVMXF7FY.js} +11 -1
- package/dist/{chunk-TMZMN7OS.js → chunk-S2IG7VNM.js} +24 -12
- package/dist/{chunk-LMCC5OC7.js → chunk-TPDH3JPP.js} +1 -1
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +31 -0
- package/dist/commands/canvas.js +3 -3
- package/dist/commands/compat.js +1 -1
- package/dist/commands/context.js +4 -4
- package/dist/commands/doctor.js +16 -309
- package/dist/commands/embed.d.ts +17 -0
- package/dist/commands/embed.js +10 -0
- package/dist/commands/migrate-observations.js +2 -2
- package/dist/commands/observe.d.ts +1 -0
- package/dist/commands/observe.js +7 -6
- package/dist/commands/rebuild.js +5 -5
- package/dist/commands/reflect.js +3 -3
- package/dist/commands/replay.js +7 -7
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +2 -2
- package/dist/commands/sleep.d.ts +2 -1
- package/dist/commands/sleep.js +15 -15
- package/dist/commands/status.d.ts +9 -1
- package/dist/commands/status.js +33 -8
- package/dist/commands/wake.d.ts +1 -1
- package/dist/commands/wake.js +6 -6
- package/dist/index.d.ts +82 -5
- package/dist/index.js +127 -105
- package/dist/{types-jjuYN2Xn.d.ts → types-C74wgGL1.d.ts} +2 -0
- package/hooks/clawvault/handler.js +11 -7
- package/hooks/clawvault/handler.test.js +2 -2
- package/package.json +2 -2
- package/dist/chunk-2RK2AG32.js +0 -743
- package/dist/{chunk-FW465EEA.js → chunk-VXEOHTSL.js} +3 -3
- package/dist/{chunk-KCCHROBR.js → chunk-YOSEUUNB.js} +4 -4
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSessionsDir
|
|
3
|
+
} from "./chunk-HRLWZGMA.js";
|
|
4
|
+
import {
|
|
5
|
+
Observer
|
|
6
|
+
} from "./chunk-S2IG7VNM.js";
|
|
7
|
+
|
|
8
|
+
// src/observer/active-session-observer.ts
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
var ONE_KIB = 1024;
|
|
12
|
+
var ONE_MIB = ONE_KIB * ONE_KIB;
|
|
13
|
+
var SMALL_SESSION_THRESHOLD_BYTES = 50 * ONE_KIB;
|
|
14
|
+
var MEDIUM_SESSION_THRESHOLD_BYTES = 150 * ONE_KIB;
|
|
15
|
+
var LARGE_SESSION_THRESHOLD_BYTES = 300 * ONE_KIB;
|
|
16
|
+
var DEFAULT_AGENT_ID = "clawdious";
|
|
17
|
+
var AGENT_ID_RE = /^[a-zA-Z0-9_-]{1,100}$/;
|
|
18
|
+
var SESSION_ID_RE = /^[a-zA-Z0-9._-]{1,200}$/;
|
|
19
|
+
var CURSOR_FILE_NAME = "observe-cursors.json";
|
|
20
|
+
var STALE_CURSOR_THRESHOLD_MS = 12 * 60 * 60 * 1e3;
|
|
21
|
+
function isFiniteNonNegative(value) {
|
|
22
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
23
|
+
}
|
|
24
|
+
function normalizeAgentId(input) {
|
|
25
|
+
const raw = (input ?? process.env.OPENCLAW_AGENT_ID ?? DEFAULT_AGENT_ID).trim();
|
|
26
|
+
if (!AGENT_ID_RE.test(raw)) {
|
|
27
|
+
return DEFAULT_AGENT_ID;
|
|
28
|
+
}
|
|
29
|
+
return raw;
|
|
30
|
+
}
|
|
31
|
+
function resolveSessionsDirectory(agentId, override) {
|
|
32
|
+
if (override?.trim()) {
|
|
33
|
+
return path.resolve(override.trim());
|
|
34
|
+
}
|
|
35
|
+
return getSessionsDir(agentId);
|
|
36
|
+
}
|
|
37
|
+
function getCursorPath(vaultPath) {
|
|
38
|
+
return path.join(vaultPath, ".clawvault", CURSOR_FILE_NAME);
|
|
39
|
+
}
|
|
40
|
+
function getScaledObservationThresholdBytes(fileSizeBytes) {
|
|
41
|
+
if (fileSizeBytes < ONE_MIB) {
|
|
42
|
+
return SMALL_SESSION_THRESHOLD_BYTES;
|
|
43
|
+
}
|
|
44
|
+
if (fileSizeBytes <= 5 * ONE_MIB) {
|
|
45
|
+
return MEDIUM_SESSION_THRESHOLD_BYTES;
|
|
46
|
+
}
|
|
47
|
+
return LARGE_SESSION_THRESHOLD_BYTES;
|
|
48
|
+
}
|
|
49
|
+
function parseCursorStore(raw) {
|
|
50
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
const input = raw;
|
|
54
|
+
const store = {};
|
|
55
|
+
for (const [sessionId, value] of Object.entries(input)) {
|
|
56
|
+
if (!SESSION_ID_RE.test(sessionId)) continue;
|
|
57
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
58
|
+
const entry = value;
|
|
59
|
+
if (!isFiniteNonNegative(entry.lastObservedOffset)) continue;
|
|
60
|
+
if (!isFiniteNonNegative(entry.lastFileSize)) continue;
|
|
61
|
+
if (typeof entry.lastObservedAt !== "string" || !entry.lastObservedAt.trim()) continue;
|
|
62
|
+
if (typeof entry.sessionKey !== "string" || !entry.sessionKey.trim()) continue;
|
|
63
|
+
store[sessionId] = {
|
|
64
|
+
lastObservedOffset: entry.lastObservedOffset,
|
|
65
|
+
lastObservedAt: entry.lastObservedAt,
|
|
66
|
+
sessionKey: entry.sessionKey,
|
|
67
|
+
lastFileSize: entry.lastFileSize
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return store;
|
|
71
|
+
}
|
|
72
|
+
function loadObserveCursorStore(vaultPath) {
|
|
73
|
+
const cursorPath = getCursorPath(vaultPath);
|
|
74
|
+
if (!fs.existsSync(cursorPath)) {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const raw = JSON.parse(fs.readFileSync(cursorPath, "utf-8"));
|
|
79
|
+
return parseCursorStore(raw);
|
|
80
|
+
} catch {
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function saveObserveCursorStore(vaultPath, store) {
|
|
85
|
+
const cursorPath = getCursorPath(vaultPath);
|
|
86
|
+
fs.mkdirSync(path.dirname(cursorPath), { recursive: true });
|
|
87
|
+
fs.writeFileSync(cursorPath, `${JSON.stringify(store, null, 2)}
|
|
88
|
+
`, "utf-8");
|
|
89
|
+
}
|
|
90
|
+
function parseAgentIdFromSessionKey(sessionKey) {
|
|
91
|
+
const match = /^agent:([^:]+):/.exec(sessionKey);
|
|
92
|
+
if (!match?.[1]) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const candidate = match[1].trim();
|
|
96
|
+
if (!AGENT_ID_RE.test(candidate)) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return candidate;
|
|
100
|
+
}
|
|
101
|
+
function resolveSessionFileForCursor(sessionId, cursor, sessionsDirOverride) {
|
|
102
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
103
|
+
if (sessionsDirOverride?.trim()) {
|
|
104
|
+
candidates.add(path.join(path.resolve(sessionsDirOverride.trim()), `${sessionId}.jsonl`));
|
|
105
|
+
}
|
|
106
|
+
const agentId = parseAgentIdFromSessionKey(cursor.sessionKey) ?? DEFAULT_AGENT_ID;
|
|
107
|
+
candidates.add(path.join(getSessionsDir(agentId), `${sessionId}.jsonl`));
|
|
108
|
+
for (const filePath of candidates) {
|
|
109
|
+
try {
|
|
110
|
+
const stat = fs.statSync(filePath);
|
|
111
|
+
if (stat.isFile()) {
|
|
112
|
+
return filePath;
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
function getObserverStaleness(vaultPath, options = {}) {
|
|
121
|
+
const nowDate = options.now ? options.now() : /* @__PURE__ */ new Date();
|
|
122
|
+
const nowMs = nowDate.getTime();
|
|
123
|
+
if (!Number.isFinite(nowMs)) {
|
|
124
|
+
return {
|
|
125
|
+
staleCount: 0,
|
|
126
|
+
oldestMs: 0,
|
|
127
|
+
newestMs: 0
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const cursorStore = loadObserveCursorStore(path.resolve(vaultPath));
|
|
131
|
+
let staleCount = 0;
|
|
132
|
+
let oldestMs = 0;
|
|
133
|
+
let newestMs = Number.POSITIVE_INFINITY;
|
|
134
|
+
for (const [sessionId, cursor] of Object.entries(cursorStore)) {
|
|
135
|
+
const observedAtMs = Date.parse(cursor.lastObservedAt);
|
|
136
|
+
if (!Number.isFinite(observedAtMs)) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const ageMs = Math.max(0, nowMs - observedAtMs);
|
|
140
|
+
if (ageMs <= STALE_CURSOR_THRESHOLD_MS) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const sessionFilePath = resolveSessionFileForCursor(sessionId, cursor, options.sessionsDir);
|
|
144
|
+
if (!sessionFilePath) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
let sessionStat;
|
|
148
|
+
try {
|
|
149
|
+
sessionStat = fs.statSync(sessionFilePath);
|
|
150
|
+
} catch {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (!sessionStat.isFile()) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (cursor.lastFileSize >= sessionStat.size) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
staleCount += 1;
|
|
160
|
+
oldestMs = Math.max(oldestMs, ageMs);
|
|
161
|
+
newestMs = Math.min(newestMs, ageMs);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
staleCount,
|
|
165
|
+
oldestMs: staleCount > 0 ? oldestMs : 0,
|
|
166
|
+
newestMs: staleCount > 0 ? newestMs : 0
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function loadSessionIndex(sessionsDir) {
|
|
170
|
+
const indexPath = path.join(sessionsDir, "sessions.json");
|
|
171
|
+
if (!fs.existsSync(indexPath)) {
|
|
172
|
+
return {};
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const parsed = JSON.parse(fs.readFileSync(indexPath, "utf-8"));
|
|
176
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
177
|
+
return {};
|
|
178
|
+
}
|
|
179
|
+
return parsed;
|
|
180
|
+
} catch {
|
|
181
|
+
return {};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function resolveTranscriptPath(sessionsDir, sessionId) {
|
|
185
|
+
return path.join(sessionsDir, `${sessionId}.jsonl`);
|
|
186
|
+
}
|
|
187
|
+
function discoverSessionDescriptors(sessionsDir, fallbackAgentId) {
|
|
188
|
+
const descriptors = [];
|
|
189
|
+
const seen = /* @__PURE__ */ new Set();
|
|
190
|
+
const index = loadSessionIndex(sessionsDir);
|
|
191
|
+
const indexedEntries = Object.entries(index).sort((left, right) => {
|
|
192
|
+
const leftUpdated = Number(left[1]?.updatedAt ?? 0);
|
|
193
|
+
const rightUpdated = Number(right[1]?.updatedAt ?? 0);
|
|
194
|
+
return rightUpdated - leftUpdated;
|
|
195
|
+
});
|
|
196
|
+
for (const [sessionKey, entry] of indexedEntries) {
|
|
197
|
+
if (!entry || typeof entry !== "object") continue;
|
|
198
|
+
const sessionId = typeof entry.sessionId === "string" ? entry.sessionId.trim() : "";
|
|
199
|
+
if (!SESSION_ID_RE.test(sessionId) || seen.has(sessionId)) continue;
|
|
200
|
+
const filePath = resolveTranscriptPath(sessionsDir, sessionId);
|
|
201
|
+
try {
|
|
202
|
+
const stat = fs.statSync(filePath);
|
|
203
|
+
if (!stat.isFile()) continue;
|
|
204
|
+
seen.add(sessionId);
|
|
205
|
+
descriptors.push({ sessionId, sessionKey, filePath });
|
|
206
|
+
} catch {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const fallbackPrefix = `agent:${fallbackAgentId}:`;
|
|
211
|
+
for (const fileName of fs.readdirSync(sessionsDir)) {
|
|
212
|
+
if (!fileName.endsWith(".jsonl") || fileName.includes(".backup") || fileName.includes(".deleted")) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const sessionId = fileName.slice(0, -".jsonl".length);
|
|
216
|
+
if (!SESSION_ID_RE.test(sessionId) || seen.has(sessionId)) continue;
|
|
217
|
+
const filePath = path.join(sessionsDir, fileName);
|
|
218
|
+
try {
|
|
219
|
+
const stat = fs.statSync(filePath);
|
|
220
|
+
if (!stat.isFile()) continue;
|
|
221
|
+
seen.add(sessionId);
|
|
222
|
+
descriptors.push({
|
|
223
|
+
sessionId,
|
|
224
|
+
sessionKey: `${fallbackPrefix}unknown:${sessionId}`,
|
|
225
|
+
filePath
|
|
226
|
+
});
|
|
227
|
+
} catch {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return descriptors;
|
|
232
|
+
}
|
|
233
|
+
function normalizeWhitespace(value) {
|
|
234
|
+
return value.replace(/\s+/g, " ").trim();
|
|
235
|
+
}
|
|
236
|
+
function extractContentText(value) {
|
|
237
|
+
if (typeof value === "string") {
|
|
238
|
+
return normalizeWhitespace(value);
|
|
239
|
+
}
|
|
240
|
+
if (Array.isArray(value)) {
|
|
241
|
+
const parts = value.map((item) => extractContentText(item)).filter(Boolean);
|
|
242
|
+
return normalizeWhitespace(parts.join(" "));
|
|
243
|
+
}
|
|
244
|
+
if (!value || typeof value !== "object") {
|
|
245
|
+
return "";
|
|
246
|
+
}
|
|
247
|
+
const input = value;
|
|
248
|
+
if (typeof input.text === "string") {
|
|
249
|
+
return normalizeWhitespace(input.text);
|
|
250
|
+
}
|
|
251
|
+
if (typeof input.content === "string") {
|
|
252
|
+
return normalizeWhitespace(input.content);
|
|
253
|
+
}
|
|
254
|
+
return "";
|
|
255
|
+
}
|
|
256
|
+
function normalizeRole(role) {
|
|
257
|
+
if (typeof role !== "string") {
|
|
258
|
+
return "";
|
|
259
|
+
}
|
|
260
|
+
return role.trim().toLowerCase();
|
|
261
|
+
}
|
|
262
|
+
function parseOpenClawJsonLine(line) {
|
|
263
|
+
if (!line.trim()) {
|
|
264
|
+
return "";
|
|
265
|
+
}
|
|
266
|
+
let parsed;
|
|
267
|
+
try {
|
|
268
|
+
parsed = JSON.parse(line);
|
|
269
|
+
} catch {
|
|
270
|
+
return "";
|
|
271
|
+
}
|
|
272
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
273
|
+
return "";
|
|
274
|
+
}
|
|
275
|
+
const entry = parsed;
|
|
276
|
+
if ("role" in entry && "content" in entry) {
|
|
277
|
+
const role = normalizeRole(entry.role);
|
|
278
|
+
const content = extractContentText(entry.content);
|
|
279
|
+
if (!content) return "";
|
|
280
|
+
return role ? `${role}: ${content}` : content;
|
|
281
|
+
}
|
|
282
|
+
if (entry.type === "message" && entry.message && typeof entry.message === "object") {
|
|
283
|
+
const message = entry.message;
|
|
284
|
+
const role = normalizeRole(message.role);
|
|
285
|
+
const content = extractContentText(message.content);
|
|
286
|
+
if (!content) return "";
|
|
287
|
+
return role ? `${role}: ${content}` : content;
|
|
288
|
+
}
|
|
289
|
+
return "";
|
|
290
|
+
}
|
|
291
|
+
function decodeLineBuffer(lineBuffer) {
|
|
292
|
+
if (lineBuffer.length === 0) {
|
|
293
|
+
return "";
|
|
294
|
+
}
|
|
295
|
+
const normalized = lineBuffer[lineBuffer.length - 1] === 13 ? lineBuffer.subarray(0, lineBuffer.length - 1) : lineBuffer;
|
|
296
|
+
return normalized.toString("utf-8").trim();
|
|
297
|
+
}
|
|
298
|
+
async function readIncrementalMessages(filePath, startOffset) {
|
|
299
|
+
const messages = [];
|
|
300
|
+
let nextOffset = startOffset;
|
|
301
|
+
let remainder = Buffer.alloc(0);
|
|
302
|
+
const stream = fs.createReadStream(filePath, {
|
|
303
|
+
start: startOffset
|
|
304
|
+
});
|
|
305
|
+
for await (const chunk of stream) {
|
|
306
|
+
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
307
|
+
const combined = remainder.length > 0 ? Buffer.concat([remainder, chunkBuffer]) : chunkBuffer;
|
|
308
|
+
let lineStart = 0;
|
|
309
|
+
for (let index = 0; index < combined.length; index += 1) {
|
|
310
|
+
if (combined[index] !== 10) continue;
|
|
311
|
+
const lineBuffer = combined.subarray(lineStart, index);
|
|
312
|
+
const line = decodeLineBuffer(lineBuffer);
|
|
313
|
+
const parsed = parseOpenClawJsonLine(line);
|
|
314
|
+
if (parsed) {
|
|
315
|
+
messages.push(parsed);
|
|
316
|
+
}
|
|
317
|
+
nextOffset += index - lineStart + 1;
|
|
318
|
+
lineStart = index + 1;
|
|
319
|
+
}
|
|
320
|
+
remainder = combined.subarray(lineStart);
|
|
321
|
+
}
|
|
322
|
+
if (remainder.length > 0) {
|
|
323
|
+
const trailing = decodeLineBuffer(remainder);
|
|
324
|
+
if (trailing) {
|
|
325
|
+
const parsed = parseOpenClawJsonLine(trailing);
|
|
326
|
+
if (parsed) {
|
|
327
|
+
messages.push(parsed);
|
|
328
|
+
nextOffset += remainder.length;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return { messages, nextOffset };
|
|
333
|
+
}
|
|
334
|
+
function parseSessionSourceLabel(sessionKey) {
|
|
335
|
+
const parts = sessionKey.split(":");
|
|
336
|
+
if (parts.length < 3 || parts[0] !== "agent") {
|
|
337
|
+
return "session";
|
|
338
|
+
}
|
|
339
|
+
const scope = parts.slice(2);
|
|
340
|
+
if (scope[0] === "main") {
|
|
341
|
+
return "main";
|
|
342
|
+
}
|
|
343
|
+
if (scope[0] === "telegram" && scope[1] === "dm") {
|
|
344
|
+
return "telegram-dm";
|
|
345
|
+
}
|
|
346
|
+
if (scope[0] === "telegram" && scope[1] === "group") {
|
|
347
|
+
return "telegram-group";
|
|
348
|
+
}
|
|
349
|
+
if (scope[0] === "discord") {
|
|
350
|
+
return "discord";
|
|
351
|
+
}
|
|
352
|
+
if (scope[0] === "telegram") {
|
|
353
|
+
return "telegram";
|
|
354
|
+
}
|
|
355
|
+
if (scope[0] === "slack") {
|
|
356
|
+
return "slack";
|
|
357
|
+
}
|
|
358
|
+
return scope[0] || "session";
|
|
359
|
+
}
|
|
360
|
+
function createDefaultObserver(vaultPath, options) {
|
|
361
|
+
return new Observer(vaultPath, options);
|
|
362
|
+
}
|
|
363
|
+
function parseRoutingCounts(routingSummary) {
|
|
364
|
+
const counts = {};
|
|
365
|
+
const [, categorySection = ""] = routingSummary.split("\u2192");
|
|
366
|
+
const summaryCategories = categorySection.split("(")[0] ?? "";
|
|
367
|
+
if (!summaryCategories) {
|
|
368
|
+
return counts;
|
|
369
|
+
}
|
|
370
|
+
for (const match of summaryCategories.matchAll(/\b([a-z][a-z0-9-]*):\s*(\d+)\b/gi)) {
|
|
371
|
+
const category = match[1].toLowerCase();
|
|
372
|
+
const count = Number.parseInt(match[2], 10);
|
|
373
|
+
if (!Number.isFinite(count) || count <= 0) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
counts[category] = (counts[category] ?? 0) + count;
|
|
377
|
+
}
|
|
378
|
+
return counts;
|
|
379
|
+
}
|
|
380
|
+
function mergeRoutingCounts(target, incoming) {
|
|
381
|
+
for (const [category, count] of Object.entries(incoming)) {
|
|
382
|
+
target[category] = (target[category] ?? 0) + count;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function stringifyError(error) {
|
|
386
|
+
if (error instanceof Error && error.message) {
|
|
387
|
+
return error.message;
|
|
388
|
+
}
|
|
389
|
+
return String(error);
|
|
390
|
+
}
|
|
391
|
+
function selectCandidates(descriptors, cursors, minNewBytes) {
|
|
392
|
+
const candidates = [];
|
|
393
|
+
for (const descriptor of descriptors) {
|
|
394
|
+
let stat;
|
|
395
|
+
try {
|
|
396
|
+
stat = fs.statSync(descriptor.filePath);
|
|
397
|
+
} catch {
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (!stat.isFile()) continue;
|
|
401
|
+
const fileSize = stat.size;
|
|
402
|
+
const cursor = cursors[descriptor.sessionId];
|
|
403
|
+
const previousOffset = cursor && isFiniteNonNegative(cursor.lastObservedOffset) ? cursor.lastObservedOffset : 0;
|
|
404
|
+
const startOffset = previousOffset <= fileSize ? previousOffset : 0;
|
|
405
|
+
const newBytes = Math.max(0, fileSize - startOffset);
|
|
406
|
+
const thresholdBytes = minNewBytes ?? getScaledObservationThresholdBytes(fileSize);
|
|
407
|
+
if (newBytes < thresholdBytes) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
candidates.push({
|
|
411
|
+
sessionId: descriptor.sessionId,
|
|
412
|
+
sessionKey: descriptor.sessionKey,
|
|
413
|
+
sourceLabel: parseSessionSourceLabel(descriptor.sessionKey),
|
|
414
|
+
filePath: descriptor.filePath,
|
|
415
|
+
fileSize,
|
|
416
|
+
startOffset,
|
|
417
|
+
newBytes,
|
|
418
|
+
thresholdBytes
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
return candidates;
|
|
422
|
+
}
|
|
423
|
+
async function observeActiveSessions(options, dependencies = {}) {
|
|
424
|
+
const vaultPath = path.resolve(options.vaultPath);
|
|
425
|
+
const agentId = normalizeAgentId(options.agentId);
|
|
426
|
+
const sessionsDir = resolveSessionsDirectory(agentId, options.sessionsDir);
|
|
427
|
+
const dryRun = Boolean(options.dryRun);
|
|
428
|
+
if (!fs.existsSync(sessionsDir) || !fs.statSync(sessionsDir).isDirectory()) {
|
|
429
|
+
return {
|
|
430
|
+
agentId,
|
|
431
|
+
sessionsDir,
|
|
432
|
+
checkedSessions: 0,
|
|
433
|
+
candidateSessions: 0,
|
|
434
|
+
observedSessions: 0,
|
|
435
|
+
cursorUpdates: 0,
|
|
436
|
+
dryRun,
|
|
437
|
+
totalNewBytes: 0,
|
|
438
|
+
observedNewBytes: 0,
|
|
439
|
+
routedCounts: {},
|
|
440
|
+
failedSessionCount: 0,
|
|
441
|
+
failedSessions: [],
|
|
442
|
+
candidates: []
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
const now = dependencies.now ?? (() => /* @__PURE__ */ new Date());
|
|
446
|
+
const cursors = loadObserveCursorStore(vaultPath);
|
|
447
|
+
const descriptors = discoverSessionDescriptors(sessionsDir, agentId);
|
|
448
|
+
const candidates = selectCandidates(descriptors, cursors, options.minNewBytes);
|
|
449
|
+
if (dryRun || candidates.length === 0) {
|
|
450
|
+
return {
|
|
451
|
+
agentId,
|
|
452
|
+
sessionsDir,
|
|
453
|
+
checkedSessions: descriptors.length,
|
|
454
|
+
candidateSessions: candidates.length,
|
|
455
|
+
observedSessions: 0,
|
|
456
|
+
cursorUpdates: 0,
|
|
457
|
+
dryRun,
|
|
458
|
+
totalNewBytes: candidates.reduce((sum, candidate) => sum + candidate.newBytes, 0),
|
|
459
|
+
observedNewBytes: 0,
|
|
460
|
+
routedCounts: {},
|
|
461
|
+
failedSessionCount: 0,
|
|
462
|
+
failedSessions: [],
|
|
463
|
+
candidates
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
const observerFactory = dependencies.createObserver ?? createDefaultObserver;
|
|
467
|
+
const observerOptions = {
|
|
468
|
+
tokenThreshold: options.threshold,
|
|
469
|
+
reflectThreshold: options.reflectThreshold,
|
|
470
|
+
model: options.model,
|
|
471
|
+
extractTasks: options.extractTasks
|
|
472
|
+
};
|
|
473
|
+
let observedSessions = 0;
|
|
474
|
+
let cursorUpdates = 0;
|
|
475
|
+
let observedNewBytes = 0;
|
|
476
|
+
const routedCounts = {};
|
|
477
|
+
const failedSessions = [];
|
|
478
|
+
for (const candidate of candidates) {
|
|
479
|
+
try {
|
|
480
|
+
const observer = observerFactory(vaultPath, observerOptions);
|
|
481
|
+
const { messages, nextOffset } = await readIncrementalMessages(candidate.filePath, candidate.startOffset);
|
|
482
|
+
const taggedMessages = messages.map((message) => `[${candidate.sourceLabel}] ${message}`);
|
|
483
|
+
if (taggedMessages.length > 0) {
|
|
484
|
+
await observer.processMessages(taggedMessages, {
|
|
485
|
+
source: "openclaw",
|
|
486
|
+
sessionKey: candidate.sessionKey,
|
|
487
|
+
transcriptId: candidate.sessionId
|
|
488
|
+
});
|
|
489
|
+
const flushResult = await observer.flush();
|
|
490
|
+
mergeRoutingCounts(routedCounts, parseRoutingCounts(flushResult.routingSummary));
|
|
491
|
+
observedSessions += 1;
|
|
492
|
+
observedNewBytes += candidate.newBytes;
|
|
493
|
+
}
|
|
494
|
+
if (nextOffset > candidate.startOffset) {
|
|
495
|
+
cursors[candidate.sessionId] = {
|
|
496
|
+
lastObservedOffset: nextOffset,
|
|
497
|
+
lastObservedAt: now().toISOString(),
|
|
498
|
+
sessionKey: candidate.sessionKey,
|
|
499
|
+
lastFileSize: candidate.fileSize
|
|
500
|
+
};
|
|
501
|
+
cursorUpdates += 1;
|
|
502
|
+
}
|
|
503
|
+
} catch (error) {
|
|
504
|
+
const reason = stringifyError(error);
|
|
505
|
+
failedSessions.push({
|
|
506
|
+
sessionId: candidate.sessionId,
|
|
507
|
+
sessionKey: candidate.sessionKey,
|
|
508
|
+
sourceLabel: candidate.sourceLabel,
|
|
509
|
+
error: reason
|
|
510
|
+
});
|
|
511
|
+
console.error(
|
|
512
|
+
`[observer] failed to observe session ${candidate.sessionKey} (${candidate.sessionId}): ${reason}`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (cursorUpdates > 0) {
|
|
517
|
+
saveObserveCursorStore(vaultPath, cursors);
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
agentId,
|
|
521
|
+
sessionsDir,
|
|
522
|
+
checkedSessions: descriptors.length,
|
|
523
|
+
candidateSessions: candidates.length,
|
|
524
|
+
observedSessions,
|
|
525
|
+
cursorUpdates,
|
|
526
|
+
dryRun,
|
|
527
|
+
totalNewBytes: candidates.reduce((sum, candidate) => sum + candidate.newBytes, 0),
|
|
528
|
+
observedNewBytes,
|
|
529
|
+
routedCounts,
|
|
530
|
+
failedSessionCount: failedSessions.length,
|
|
531
|
+
failedSessions,
|
|
532
|
+
candidates
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export {
|
|
537
|
+
getScaledObservationThresholdBytes,
|
|
538
|
+
getObserverStaleness,
|
|
539
|
+
parseSessionSourceLabel,
|
|
540
|
+
observeActiveSessions
|
|
541
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
registerTailscaleCommands
|
|
3
|
+
} from "./chunk-NZ4ZZNSR.js";
|
|
4
|
+
import {
|
|
5
|
+
registerObserveCommand
|
|
6
|
+
} from "./chunk-PLZKZW4I.js";
|
|
7
|
+
import {
|
|
8
|
+
registerReflectCommand
|
|
9
|
+
} from "./chunk-2YDBJS7M.js";
|
|
10
|
+
import {
|
|
11
|
+
registerContextCommand
|
|
12
|
+
} from "./chunk-GFJ3LIIB.js";
|
|
13
|
+
import {
|
|
14
|
+
registerEmbedCommand
|
|
15
|
+
} from "./chunk-3FP5BJ42.js";
|
|
16
|
+
import {
|
|
17
|
+
registerInjectCommand
|
|
18
|
+
} from "./chunk-GSD4ALSI.js";
|
|
19
|
+
|
|
20
|
+
// src/cli/index.ts
|
|
21
|
+
function registerCliCommands(program) {
|
|
22
|
+
registerContextCommand(program);
|
|
23
|
+
registerInjectCommand(program);
|
|
24
|
+
registerObserveCommand(program);
|
|
25
|
+
registerReflectCommand(program);
|
|
26
|
+
registerEmbedCommand(program);
|
|
27
|
+
registerTailscaleCommands(program);
|
|
28
|
+
return program;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
registerCliCommands
|
|
33
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
hasQmd
|
|
3
|
-
|
|
2
|
+
hasQmd,
|
|
3
|
+
withQmdIndexArgs
|
|
4
|
+
} from "./chunk-O7XHXF7F.js";
|
|
4
5
|
import {
|
|
5
6
|
DEFAULT_CATEGORIES
|
|
6
7
|
} from "./chunk-2CDEETQN.js";
|
|
@@ -337,7 +338,7 @@ async function setupCommand(options = {}) {
|
|
|
337
338
|
if (hasQmd()) {
|
|
338
339
|
const { collection, root } = getQmdConfig(target.vaultPath);
|
|
339
340
|
try {
|
|
340
|
-
execFileSync("qmd", ["collection", "add", root, "--name", collection, "--mask", "**/*.md"], {
|
|
341
|
+
execFileSync("qmd", withQmdIndexArgs(["collection", "add", root, "--name", collection, "--mask", "**/*.md"], options.qmdIndexName), {
|
|
341
342
|
stdio: "ignore"
|
|
342
343
|
});
|
|
343
344
|
console.log(`\u2713 qmd collection ready: ${collection}`);
|
|
@@ -5,6 +5,7 @@ import * as path from "path";
|
|
|
5
5
|
var QMD_INSTALL_URL = "https://github.com/tobi/qmd";
|
|
6
6
|
var QMD_INSTALL_COMMAND = "bun install -g github:tobi/qmd";
|
|
7
7
|
var QMD_NOT_INSTALLED_MESSAGE = `ClawVault requires qmd. Install: ${QMD_INSTALL_COMMAND}`;
|
|
8
|
+
var QMD_INDEX_ENV_VAR = "CLAWVAULT_QMD_INDEX";
|
|
8
9
|
var QmdUnavailableError = class extends Error {
|
|
9
10
|
constructor(message = QMD_NOT_INSTALLED_MESSAGE) {
|
|
10
11
|
super(message);
|
|
@@ -14,6 +15,24 @@ var QmdUnavailableError = class extends Error {
|
|
|
14
15
|
function ensureJsonArgs(args) {
|
|
15
16
|
return args.includes("--json") ? args : [...args, "--json"];
|
|
16
17
|
}
|
|
18
|
+
function resolveQmdIndexName(indexName) {
|
|
19
|
+
const explicit = indexName?.trim();
|
|
20
|
+
if (explicit) {
|
|
21
|
+
return explicit;
|
|
22
|
+
}
|
|
23
|
+
const fromEnv = process.env[QMD_INDEX_ENV_VAR]?.trim();
|
|
24
|
+
return fromEnv || void 0;
|
|
25
|
+
}
|
|
26
|
+
function withQmdIndexArgs(args, indexName) {
|
|
27
|
+
if (args.includes("--index")) {
|
|
28
|
+
return [...args];
|
|
29
|
+
}
|
|
30
|
+
const resolvedIndexName = resolveQmdIndexName(indexName);
|
|
31
|
+
if (!resolvedIndexName) {
|
|
32
|
+
return [...args];
|
|
33
|
+
}
|
|
34
|
+
return ["--index", resolvedIndexName, ...args];
|
|
35
|
+
}
|
|
17
36
|
function tryParseJson(raw) {
|
|
18
37
|
try {
|
|
19
38
|
return JSON.parse(raw);
|
|
@@ -64,9 +83,9 @@ function ensureQmdAvailable() {
|
|
|
64
83
|
throw new QmdUnavailableError();
|
|
65
84
|
}
|
|
66
85
|
}
|
|
67
|
-
function execQmd(args) {
|
|
86
|
+
function execQmd(args, indexName) {
|
|
68
87
|
ensureQmdAvailable();
|
|
69
|
-
const finalArgs = ensureJsonArgs(args);
|
|
88
|
+
const finalArgs = withQmdIndexArgs(ensureJsonArgs(args), indexName);
|
|
70
89
|
try {
|
|
71
90
|
const result = execFileSync("qmd", finalArgs, {
|
|
72
91
|
encoding: "utf-8",
|
|
@@ -94,27 +113,28 @@ function hasQmd() {
|
|
|
94
113
|
const result = spawnSync("qmd", ["--version"], { stdio: "ignore" });
|
|
95
114
|
return !result.error;
|
|
96
115
|
}
|
|
97
|
-
function qmdUpdate(collection) {
|
|
116
|
+
function qmdUpdate(collection, indexName) {
|
|
98
117
|
ensureQmdAvailable();
|
|
99
118
|
const args = ["update"];
|
|
100
119
|
if (collection) {
|
|
101
120
|
args.push("-c", collection);
|
|
102
121
|
}
|
|
103
|
-
execFileSync("qmd", args, { stdio: "inherit" });
|
|
122
|
+
execFileSync("qmd", withQmdIndexArgs(args, indexName), { stdio: "inherit" });
|
|
104
123
|
}
|
|
105
|
-
function qmdEmbed(collection) {
|
|
124
|
+
function qmdEmbed(collection, indexName) {
|
|
106
125
|
ensureQmdAvailable();
|
|
107
126
|
const args = ["embed"];
|
|
108
127
|
if (collection) {
|
|
109
128
|
args.push("-c", collection);
|
|
110
129
|
}
|
|
111
|
-
execFileSync("qmd", args, { stdio: "inherit" });
|
|
130
|
+
execFileSync("qmd", withQmdIndexArgs(args, indexName), { stdio: "inherit" });
|
|
112
131
|
}
|
|
113
132
|
var SearchEngine = class {
|
|
114
133
|
documents = /* @__PURE__ */ new Map();
|
|
115
134
|
collection = "clawvault";
|
|
116
135
|
vaultPath = "";
|
|
117
136
|
collectionRoot = "";
|
|
137
|
+
qmdIndexName;
|
|
118
138
|
/**
|
|
119
139
|
* Set the collection name (usually vault name)
|
|
120
140
|
*/
|
|
@@ -133,6 +153,12 @@ var SearchEngine = class {
|
|
|
133
153
|
setCollectionRoot(root) {
|
|
134
154
|
this.collectionRoot = path.resolve(root);
|
|
135
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Set qmd index name (defaults to qmd global default when omitted)
|
|
158
|
+
*/
|
|
159
|
+
setIndexName(indexName) {
|
|
160
|
+
this.qmdIndexName = indexName;
|
|
161
|
+
}
|
|
136
162
|
/**
|
|
137
163
|
* Add or update a document in the local cache
|
|
138
164
|
* Note: qmd indexing happens via qmd update command
|
|
@@ -189,7 +215,7 @@ var SearchEngine = class {
|
|
|
189
215
|
if (this.collection) {
|
|
190
216
|
args.push("-c", this.collection);
|
|
191
217
|
}
|
|
192
|
-
const qmdResults = execQmd(args);
|
|
218
|
+
const qmdResults = execQmd(args, this.qmdIndexName);
|
|
193
219
|
return this.convertResults(qmdResults, {
|
|
194
220
|
limit,
|
|
195
221
|
minScore,
|
|
@@ -339,6 +365,7 @@ export {
|
|
|
339
365
|
QMD_INSTALL_URL,
|
|
340
366
|
QMD_INSTALL_COMMAND,
|
|
341
367
|
QmdUnavailableError,
|
|
368
|
+
withQmdIndexArgs,
|
|
342
369
|
hasQmd,
|
|
343
370
|
qmdUpdate,
|
|
344
371
|
qmdEmbed,
|