@wrongstack/core 0.1.9 → 0.1.10
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/dist/agent-bridge-6KPqsFx6.d.ts +33 -0
- package/dist/compactor-B4mQZXf2.d.ts +17 -0
- package/dist/config-BU9f_5yH.d.ts +193 -0
- package/dist/{provider-txgB0Oq9.d.ts → context-BmM2xGUZ.d.ts} +532 -472
- package/dist/coordination/index.d.ts +694 -0
- package/dist/coordination/index.js +1995 -0
- package/dist/coordination/index.js.map +1 -0
- package/dist/defaults/index.d.ts +34 -2309
- package/dist/defaults/index.js +3893 -3803
- package/dist/defaults/index.js.map +1 -1
- package/dist/events-BMNaEFZl.d.ts +218 -0
- package/dist/execution/index.d.ts +260 -0
- package/dist/execution/index.js +1625 -0
- package/dist/execution/index.js.map +1 -0
- package/dist/index.d.ts +47 -10
- package/dist/index.js +6617 -6093
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +10 -0
- package/dist/infrastructure/index.js +575 -0
- package/dist/infrastructure/index.js.map +1 -0
- package/dist/input-reader-E-ffP2ee.d.ts +12 -0
- package/dist/kernel/index.d.ts +15 -4
- package/dist/kernel/index.js.map +1 -1
- package/dist/logger-BH6AE0W9.d.ts +24 -0
- package/dist/logger-BMQgxvdy.d.ts +12 -0
- package/dist/mcp-servers-Dzgg4x1w.d.ts +100 -0
- package/dist/memory-CEXuo7sz.d.ts +16 -0
- package/dist/mode-CV077NjV.d.ts +27 -0
- package/dist/models/index.d.ts +60 -0
- package/dist/models/index.js +621 -0
- package/dist/models/index.js.map +1 -0
- package/dist/models-registry-DqzwpBQy.d.ts +46 -0
- package/dist/models-registry-Y2xbog0E.d.ts +95 -0
- package/dist/multi-agent-fmkRHtof.d.ts +283 -0
- package/dist/observability/index.d.ts +353 -0
- package/dist/observability/index.js +691 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability-BhnVLBLS.d.ts +67 -0
- package/dist/path-resolver-CPRj4bFY.d.ts +10 -0
- package/dist/path-resolver-DBjaoXFq.d.ts +54 -0
- package/dist/plugin-DJk6LL8B.d.ts +434 -0
- package/dist/renderer-rk_1Swwc.d.ts +158 -0
- package/dist/sdd/index.d.ts +206 -0
- package/dist/sdd/index.js +864 -0
- package/dist/sdd/index.js.map +1 -0
- package/dist/secret-scrubber-CicHLN4G.d.ts +31 -0
- package/dist/secret-scrubber-DF88luOe.d.ts +54 -0
- package/dist/secret-vault-DoISxaKO.d.ts +19 -0
- package/dist/security/index.d.ts +30 -0
- package/dist/security/index.js +524 -0
- package/dist/security/index.js.map +1 -0
- package/dist/selector-BbJqiEP4.d.ts +51 -0
- package/dist/session-reader-Drq8RvJu.d.ts +150 -0
- package/dist/skill-DhfSizKv.d.ts +72 -0
- package/dist/storage/index.d.ts +382 -0
- package/dist/storage/index.js +1530 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/{system-prompt-vAB0F54-.d.ts → system-prompt-BC_8ypCG.d.ts} +1 -1
- package/dist/task-graph-BITvWt4t.d.ts +160 -0
- package/dist/tool-executor-CpuJPYm9.d.ts +97 -0
- package/dist/types/index.d.ts +26 -4
- package/dist/types/index.js +1787 -4
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +49 -2
- package/dist/utils/index.js +100 -2
- package/dist/utils/index.js.map +1 -1
- package/package.json +34 -2
- package/dist/mode-Pjt5vMS6.d.ts +0 -815
- package/dist/session-reader-9sOTgmeC.d.ts +0 -1087
|
@@ -0,0 +1,1530 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from 'crypto';
|
|
2
|
+
import * as fsp from 'fs/promises';
|
|
3
|
+
import * as path2 from 'path';
|
|
4
|
+
import 'fs';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
|
|
7
|
+
// src/storage/session-store.ts
|
|
8
|
+
async function atomicWrite(targetPath, content, opts = {}) {
|
|
9
|
+
const dir = path2.dirname(targetPath);
|
|
10
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
11
|
+
const tmp = path2.join(dir, `.${path2.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
|
|
12
|
+
try {
|
|
13
|
+
if (typeof content === "string") {
|
|
14
|
+
await fsp.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
|
|
15
|
+
} else {
|
|
16
|
+
await fsp.writeFile(tmp, content, { flag: "wx" });
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const fh = await fsp.open(tmp, "r+");
|
|
20
|
+
try {
|
|
21
|
+
await fh.sync();
|
|
22
|
+
} finally {
|
|
23
|
+
await fh.close();
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
let mode;
|
|
28
|
+
try {
|
|
29
|
+
const stat3 = await fsp.stat(targetPath);
|
|
30
|
+
mode = stat3.mode & 511;
|
|
31
|
+
} catch {
|
|
32
|
+
mode = opts.mode;
|
|
33
|
+
}
|
|
34
|
+
if (mode !== void 0) {
|
|
35
|
+
await fsp.chmod(tmp, mode);
|
|
36
|
+
}
|
|
37
|
+
await fsp.rename(tmp, targetPath);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
try {
|
|
40
|
+
await fsp.unlink(tmp);
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function ensureDir(dir) {
|
|
47
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/storage/session-store.ts
|
|
51
|
+
var DefaultSessionStore = class {
|
|
52
|
+
dir;
|
|
53
|
+
events;
|
|
54
|
+
constructor(opts) {
|
|
55
|
+
this.dir = opts.dir;
|
|
56
|
+
this.events = opts.events;
|
|
57
|
+
}
|
|
58
|
+
async create(meta) {
|
|
59
|
+
await ensureDir(this.dir);
|
|
60
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
61
|
+
const id = meta.id ?? `${startedAt.replace(/[:.]/g, "-")}-${randomBytes(2).toString("hex")}`;
|
|
62
|
+
const file = path2.join(this.dir, `${id}.jsonl`);
|
|
63
|
+
let handle;
|
|
64
|
+
try {
|
|
65
|
+
handle = await fsp.open(file, "a", 384);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Failed to open session file: ${err instanceof Error ? err.message : String(err)}`,
|
|
69
|
+
{
|
|
70
|
+
cause: err
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
return new FileSessionWriter(id, handle, startedAt, meta, { dir: this.dir, filePath: file });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
await handle.close().catch(() => {
|
|
78
|
+
});
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async resume(id) {
|
|
83
|
+
const data = await this.load(id);
|
|
84
|
+
const file = path2.join(this.dir, `${id}.jsonl`);
|
|
85
|
+
let handle;
|
|
86
|
+
try {
|
|
87
|
+
handle = await fsp.open(file, "a", 384);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Failed to open session "${id}" for append: ${err instanceof Error ? err.message : String(err)}`,
|
|
91
|
+
{ cause: err }
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const writer = new FileSessionWriter(
|
|
95
|
+
id,
|
|
96
|
+
handle,
|
|
97
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
98
|
+
{
|
|
99
|
+
id,
|
|
100
|
+
model: data.metadata.model,
|
|
101
|
+
provider: data.metadata.provider
|
|
102
|
+
},
|
|
103
|
+
{ resumed: true, dir: this.dir, filePath: file }
|
|
104
|
+
);
|
|
105
|
+
return { writer, data };
|
|
106
|
+
}
|
|
107
|
+
async load(id) {
|
|
108
|
+
const file = path2.join(this.dir, `${id}.jsonl`);
|
|
109
|
+
const raw = await fsp.readFile(file, "utf8");
|
|
110
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
111
|
+
const events = [];
|
|
112
|
+
for (const line of lines) {
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(line);
|
|
115
|
+
if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
|
|
116
|
+
events.push(parsed);
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const meta = this.metaFromEvents(id, events);
|
|
122
|
+
const { messages, usage } = this.replay(events, id);
|
|
123
|
+
return { metadata: meta, events, messages, usage };
|
|
124
|
+
}
|
|
125
|
+
async list(limit = 20) {
|
|
126
|
+
try {
|
|
127
|
+
await ensureDir(this.dir);
|
|
128
|
+
const files = await fsp.readdir(this.dir);
|
|
129
|
+
const ids = files.filter((f) => f.endsWith(".jsonl")).map((f) => f.replace(/\.jsonl$/, ""));
|
|
130
|
+
const sessions = await Promise.all(ids.map((id) => this.summaryFor(id).catch(() => null)));
|
|
131
|
+
const out = sessions.filter((s) => s !== null);
|
|
132
|
+
out.sort((a, b) => {
|
|
133
|
+
if (a.startedAt < b.startedAt) return 1;
|
|
134
|
+
if (a.startedAt > b.startedAt) return -1;
|
|
135
|
+
return a.id.localeCompare(b.id);
|
|
136
|
+
});
|
|
137
|
+
return out.slice(0, limit);
|
|
138
|
+
} catch {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async summaryFor(id) {
|
|
143
|
+
const manifest = path2.join(this.dir, `${id}.summary.json`);
|
|
144
|
+
try {
|
|
145
|
+
const raw = await fsp.readFile(manifest, "utf8");
|
|
146
|
+
return JSON.parse(raw);
|
|
147
|
+
} catch {
|
|
148
|
+
const full = path2.join(this.dir, `${id}.jsonl`);
|
|
149
|
+
const stat3 = await fsp.stat(full);
|
|
150
|
+
const summary = await this.summarize(id, stat3.mtime.toISOString());
|
|
151
|
+
await fsp.writeFile(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
|
|
152
|
+
console.warn(
|
|
153
|
+
`[session-store] Failed to write manifest for "${id}":`,
|
|
154
|
+
err instanceof Error ? err.message : String(err)
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
return summary;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async delete(id) {
|
|
161
|
+
await fsp.unlink(path2.join(this.dir, `${id}.jsonl`));
|
|
162
|
+
await fsp.unlink(path2.join(this.dir, `${id}.summary.json`)).catch(() => void 0);
|
|
163
|
+
}
|
|
164
|
+
async summarize(id, mtime) {
|
|
165
|
+
try {
|
|
166
|
+
const data = await this.load(id);
|
|
167
|
+
const firstUser = data.events.find((e) => e.type === "user_input");
|
|
168
|
+
const title = firstUser && firstUser.type === "user_input" ? userInputTitle(firstUser.content) : "(empty session)";
|
|
169
|
+
return {
|
|
170
|
+
id,
|
|
171
|
+
title,
|
|
172
|
+
startedAt: data.metadata.startedAt,
|
|
173
|
+
model: data.metadata.model ?? "unknown",
|
|
174
|
+
provider: data.metadata.provider ?? "unknown",
|
|
175
|
+
tokenTotal: data.usage.input + data.usage.output
|
|
176
|
+
};
|
|
177
|
+
} catch {
|
|
178
|
+
return {
|
|
179
|
+
id,
|
|
180
|
+
title: "(damaged)",
|
|
181
|
+
startedAt: mtime,
|
|
182
|
+
model: "unknown",
|
|
183
|
+
provider: "unknown",
|
|
184
|
+
tokenTotal: 0
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
metaFromEvents(id, events) {
|
|
189
|
+
const start = events.find((e) => e.type === "session_start");
|
|
190
|
+
const end = events.find((e) => e.type === "session_end");
|
|
191
|
+
return {
|
|
192
|
+
id,
|
|
193
|
+
startedAt: start?.ts ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
194
|
+
endedAt: end?.ts,
|
|
195
|
+
model: start?.model,
|
|
196
|
+
provider: start?.provider
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
replay(events, sessionId = "unknown") {
|
|
200
|
+
const messages = [];
|
|
201
|
+
let usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
202
|
+
const openToolUses = /* @__PURE__ */ new Set();
|
|
203
|
+
for (const e of events) {
|
|
204
|
+
if (e.type === "user_input") {
|
|
205
|
+
openToolUses.clear();
|
|
206
|
+
messages.push({ role: "user", content: e.content });
|
|
207
|
+
} else if (e.type === "llm_response") {
|
|
208
|
+
messages.push({ role: "assistant", content: e.content });
|
|
209
|
+
for (const b of e.content) {
|
|
210
|
+
if (b.type === "tool_use") openToolUses.add(b.id);
|
|
211
|
+
}
|
|
212
|
+
usage = {
|
|
213
|
+
input: usage.input + (e.usage.input ?? 0),
|
|
214
|
+
output: usage.output + (e.usage.output ?? 0),
|
|
215
|
+
cacheRead: (usage.cacheRead ?? 0) + (e.usage.cacheRead ?? 0),
|
|
216
|
+
cacheWrite: (usage.cacheWrite ?? 0) + (e.usage.cacheWrite ?? 0)
|
|
217
|
+
};
|
|
218
|
+
} else if (e.type === "tool_result") {
|
|
219
|
+
if (!openToolUses.has(e.id)) {
|
|
220
|
+
this.events?.emit("session.damaged", {
|
|
221
|
+
sessionId,
|
|
222
|
+
detail: `Orphan tool_result "${e.id}" has no matching tool_use`
|
|
223
|
+
});
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
openToolUses.delete(e.id);
|
|
227
|
+
const content = [
|
|
228
|
+
{
|
|
229
|
+
type: "tool_result",
|
|
230
|
+
tool_use_id: e.id,
|
|
231
|
+
content: typeof e.content === "string" ? e.content : JSON.stringify(e.content),
|
|
232
|
+
is_error: e.isError
|
|
233
|
+
}
|
|
234
|
+
];
|
|
235
|
+
const last = messages[messages.length - 1];
|
|
236
|
+
if (last && last.role === "user") {
|
|
237
|
+
if (Array.isArray(last.content)) {
|
|
238
|
+
last.content.push(...content);
|
|
239
|
+
} else if (typeof last.content === "string") {
|
|
240
|
+
last.content = [{ type: "text", text: last.content }, ...content];
|
|
241
|
+
} else {
|
|
242
|
+
messages.push({ role: "user", content });
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
messages.push({ role: "user", content });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (openToolUses.size > 0) {
|
|
250
|
+
this.events?.emit("session.damaged", {
|
|
251
|
+
sessionId,
|
|
252
|
+
detail: `${openToolUses.size} tool_use blocks without matching results \u2014 replay truncated`
|
|
253
|
+
});
|
|
254
|
+
return { messages, usage };
|
|
255
|
+
}
|
|
256
|
+
return { messages, usage };
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
var FileSessionWriter = class {
|
|
260
|
+
constructor(id, handle, startedAt, meta, opts = {}) {
|
|
261
|
+
this.id = id;
|
|
262
|
+
this.handle = handle;
|
|
263
|
+
this.startedAt = startedAt;
|
|
264
|
+
this.meta = meta;
|
|
265
|
+
this.resumed = opts.resumed ?? false;
|
|
266
|
+
this.manifestFile = opts.dir ? path2.join(opts.dir, `${id}.summary.json`) : "";
|
|
267
|
+
this.filePath = opts.filePath ?? "";
|
|
268
|
+
this.summary = {
|
|
269
|
+
id,
|
|
270
|
+
title: "(empty session)",
|
|
271
|
+
startedAt,
|
|
272
|
+
model: meta.model ?? "unknown",
|
|
273
|
+
provider: meta.provider ?? "unknown",
|
|
274
|
+
tokenTotal: 0
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
id;
|
|
278
|
+
handle;
|
|
279
|
+
startedAt;
|
|
280
|
+
meta;
|
|
281
|
+
closed = false;
|
|
282
|
+
manifestFile;
|
|
283
|
+
summary;
|
|
284
|
+
tokenIn = 0;
|
|
285
|
+
tokenOut = 0;
|
|
286
|
+
filePath;
|
|
287
|
+
initDone = false;
|
|
288
|
+
resumed;
|
|
289
|
+
appendFailCount = 0;
|
|
290
|
+
lastAppendWarnAt = 0;
|
|
291
|
+
async writeSessionStart() {
|
|
292
|
+
if (this.initDone || this.closed) return;
|
|
293
|
+
this.initDone = true;
|
|
294
|
+
const record = `${JSON.stringify({
|
|
295
|
+
type: this.resumed ? "session_resumed" : "session_start",
|
|
296
|
+
ts: this.startedAt,
|
|
297
|
+
id: this.id,
|
|
298
|
+
model: this.meta.model ?? "unknown",
|
|
299
|
+
provider: this.meta.provider ?? "unknown"
|
|
300
|
+
})}
|
|
301
|
+
`;
|
|
302
|
+
try {
|
|
303
|
+
if (this.filePath) {
|
|
304
|
+
await fsp.writeFile(this.filePath, record, { flag: "a", mode: 384 });
|
|
305
|
+
}
|
|
306
|
+
} catch {
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async append(event) {
|
|
310
|
+
if (this.closed) return;
|
|
311
|
+
if (!this.initDone) {
|
|
312
|
+
await this.writeSessionStart();
|
|
313
|
+
}
|
|
314
|
+
this.observeForSummary(event);
|
|
315
|
+
try {
|
|
316
|
+
await this.handle.appendFile(`${JSON.stringify(event)}
|
|
317
|
+
`, "utf8");
|
|
318
|
+
} catch (err) {
|
|
319
|
+
this.appendFailCount++;
|
|
320
|
+
const now = Date.now();
|
|
321
|
+
if (now - this.lastAppendWarnAt > 5e3) {
|
|
322
|
+
const suppressed = this.appendFailCount - 1;
|
|
323
|
+
const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
|
|
324
|
+
console.warn(
|
|
325
|
+
"[session] append failed:",
|
|
326
|
+
err instanceof Error ? err.message : String(err),
|
|
327
|
+
tail
|
|
328
|
+
);
|
|
329
|
+
this.lastAppendWarnAt = now;
|
|
330
|
+
this.appendFailCount = 0;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Watch events as they're appended and keep the summary state hot, so
|
|
336
|
+
* `close()` can flush a `<id>.summary.json` manifest without re-reading
|
|
337
|
+
* the JSONL. `list()` reads only manifests, turning a per-session full
|
|
338
|
+
* parse into a single stat+read.
|
|
339
|
+
*/
|
|
340
|
+
observeForSummary(event) {
|
|
341
|
+
if (event.type === "user_input" && this.summary.title === "(empty session)") {
|
|
342
|
+
this.summary = { ...this.summary, title: userInputTitle(event.content) };
|
|
343
|
+
} else if (event.type === "llm_response") {
|
|
344
|
+
this.tokenIn += event.usage.input;
|
|
345
|
+
this.tokenOut += event.usage.output;
|
|
346
|
+
this.summary = { ...this.summary, tokenTotal: this.tokenIn + this.tokenOut };
|
|
347
|
+
} else if (event.type === "session_end") {
|
|
348
|
+
const total = event.usage.input + event.usage.output;
|
|
349
|
+
if (total > 0) this.summary = { ...this.summary, tokenTotal: total };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async close() {
|
|
353
|
+
if (this.closed) return;
|
|
354
|
+
this.closed = true;
|
|
355
|
+
if (this.manifestFile) {
|
|
356
|
+
try {
|
|
357
|
+
await fsp.writeFile(this.manifestFile, JSON.stringify(this.summary), { mode: 384 });
|
|
358
|
+
} catch {
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
await this.handle.close();
|
|
363
|
+
} catch {
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
function userInputTitle(content) {
|
|
368
|
+
if (typeof content === "string") return content.slice(0, 60);
|
|
369
|
+
const text = content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
|
|
370
|
+
return (text || "(non-text input)").slice(0, 60);
|
|
371
|
+
}
|
|
372
|
+
var QueueStore = class {
|
|
373
|
+
file;
|
|
374
|
+
constructor(opts) {
|
|
375
|
+
this.file = path2.join(opts.dir, "queue.json");
|
|
376
|
+
}
|
|
377
|
+
async write(items) {
|
|
378
|
+
if (items.length === 0) {
|
|
379
|
+
await this.clear();
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
await atomicWrite(this.file, JSON.stringify(items), { mode: 384 });
|
|
383
|
+
}
|
|
384
|
+
async read() {
|
|
385
|
+
let raw;
|
|
386
|
+
try {
|
|
387
|
+
raw = await fsp.readFile(this.file, "utf8");
|
|
388
|
+
} catch (err) {
|
|
389
|
+
const code = err.code;
|
|
390
|
+
if (code === "ENOENT") return [];
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
let parsed;
|
|
394
|
+
try {
|
|
395
|
+
parsed = JSON.parse(raw);
|
|
396
|
+
} catch {
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
if (!Array.isArray(parsed)) return [];
|
|
400
|
+
const out = [];
|
|
401
|
+
for (const v of parsed) {
|
|
402
|
+
if (isPersistedQueueItem(v)) out.push(v);
|
|
403
|
+
}
|
|
404
|
+
return out;
|
|
405
|
+
}
|
|
406
|
+
async clear() {
|
|
407
|
+
try {
|
|
408
|
+
await fsp.unlink(this.file);
|
|
409
|
+
} catch (err) {
|
|
410
|
+
const code = err.code;
|
|
411
|
+
if (code === "ENOENT") return;
|
|
412
|
+
console.warn(`QueueStore.clear() failed for ${this.file}: ${err.message}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
function isPersistedQueueItem(v) {
|
|
417
|
+
if (typeof v !== "object" || v === null) return false;
|
|
418
|
+
const o = v;
|
|
419
|
+
return typeof o["displayText"] === "string" && Array.isArray(o["blocks"]);
|
|
420
|
+
}
|
|
421
|
+
var DEFAULT_SPOOL_THRESHOLD = 256 * 1024;
|
|
422
|
+
var PLACEHOLDER_RE = /\[(pasted|image|file) #(\d+)\]/g;
|
|
423
|
+
var DefaultAttachmentStore = class {
|
|
424
|
+
items = /* @__PURE__ */ new Map();
|
|
425
|
+
refs = [];
|
|
426
|
+
nextSeq = { text: 0, image: 0, file: 0 };
|
|
427
|
+
spoolDir;
|
|
428
|
+
spoolThreshold;
|
|
429
|
+
constructor(opts = {}) {
|
|
430
|
+
this.spoolDir = opts.spoolDir;
|
|
431
|
+
this.spoolThreshold = opts.spoolThresholdBytes ?? DEFAULT_SPOOL_THRESHOLD;
|
|
432
|
+
}
|
|
433
|
+
async add(input) {
|
|
434
|
+
const seq = ++this.nextSeq[input.kind];
|
|
435
|
+
const id = `${kindPrefix(input.kind)}-${seq}-${randomBytes(3).toString("hex")}`;
|
|
436
|
+
const bytes = Buffer.byteLength(input.data, input.kind === "image" ? "base64" : "utf8");
|
|
437
|
+
let spooledPath;
|
|
438
|
+
let data = input.data;
|
|
439
|
+
if (this.spoolDir && bytes >= this.spoolThreshold) {
|
|
440
|
+
await fsp.mkdir(this.spoolDir, { recursive: true });
|
|
441
|
+
spooledPath = path2.join(this.spoolDir, `${id}.bin`);
|
|
442
|
+
await fsp.writeFile(spooledPath, input.data, input.kind === "image" ? "base64" : "utf8");
|
|
443
|
+
data = void 0;
|
|
444
|
+
}
|
|
445
|
+
const att = {
|
|
446
|
+
id,
|
|
447
|
+
kind: input.kind,
|
|
448
|
+
meta: input.meta ?? {},
|
|
449
|
+
data,
|
|
450
|
+
path: spooledPath,
|
|
451
|
+
bytes,
|
|
452
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
453
|
+
};
|
|
454
|
+
this.items.set(id, att);
|
|
455
|
+
const ref = { id, kind: input.kind, seq, meta: att.meta };
|
|
456
|
+
this.refs.push(ref);
|
|
457
|
+
return ref;
|
|
458
|
+
}
|
|
459
|
+
async get(id) {
|
|
460
|
+
return this.items.get(id);
|
|
461
|
+
}
|
|
462
|
+
list() {
|
|
463
|
+
return [...this.refs];
|
|
464
|
+
}
|
|
465
|
+
async expand(text) {
|
|
466
|
+
const matches = [...text.matchAll(PLACEHOLDER_RE)];
|
|
467
|
+
if (matches.length === 0) return text ? [{ type: "text", text }] : [];
|
|
468
|
+
const blocks = [];
|
|
469
|
+
let lastIndex = 0;
|
|
470
|
+
for (const m of matches) {
|
|
471
|
+
const idx = m.index ?? 0;
|
|
472
|
+
const before = text.slice(lastIndex, idx);
|
|
473
|
+
if (before) blocks.push({ type: "text", text: before });
|
|
474
|
+
const kind = prefixToKind(m[1]);
|
|
475
|
+
const seq = Number(m[2]);
|
|
476
|
+
const ref = this.refs.find((r) => r.kind === kind && r.seq === seq);
|
|
477
|
+
const att = ref ? this.items.get(ref.id) : void 0;
|
|
478
|
+
if (!att) {
|
|
479
|
+
blocks.push({ type: "text", text: m[0] });
|
|
480
|
+
} else {
|
|
481
|
+
blocks.push(await this.toBlock(att));
|
|
482
|
+
}
|
|
483
|
+
lastIndex = idx + m[0].length;
|
|
484
|
+
}
|
|
485
|
+
const tail = text.slice(lastIndex);
|
|
486
|
+
if (tail) blocks.push({ type: "text", text: tail });
|
|
487
|
+
return mergeAdjacentText(blocks);
|
|
488
|
+
}
|
|
489
|
+
async clear() {
|
|
490
|
+
this.items.clear();
|
|
491
|
+
this.refs.length = 0;
|
|
492
|
+
this.nextSeq = { text: 0, image: 0, file: 0 };
|
|
493
|
+
}
|
|
494
|
+
async toBlock(att) {
|
|
495
|
+
if (att.kind === "image") {
|
|
496
|
+
const data = att.data ?? (att.path ? await fsp.readFile(att.path, { encoding: "base64" }) : "");
|
|
497
|
+
return {
|
|
498
|
+
type: "image",
|
|
499
|
+
source: {
|
|
500
|
+
type: "base64",
|
|
501
|
+
media_type: att.meta.mediaType ?? "image/png",
|
|
502
|
+
data
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
const raw = att.data ?? (att.path ? await fsp.readFile(att.path, "utf8") : "");
|
|
507
|
+
const label = att.meta.filename ? `<file path="${att.meta.filename}">` : "<pasted>";
|
|
508
|
+
const close = att.meta.filename ? "</file>" : "</pasted>";
|
|
509
|
+
return { type: "text", text: `${label}
|
|
510
|
+
${raw}
|
|
511
|
+
${close}` };
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
function kindPrefix(kind) {
|
|
515
|
+
return kind === "text" ? "pasted" : kind;
|
|
516
|
+
}
|
|
517
|
+
function prefixToKind(prefix) {
|
|
518
|
+
if (prefix === "pasted") return "text";
|
|
519
|
+
if (prefix === "image") return "image";
|
|
520
|
+
return "file";
|
|
521
|
+
}
|
|
522
|
+
function mergeAdjacentText(blocks) {
|
|
523
|
+
const out = [];
|
|
524
|
+
for (const b of blocks) {
|
|
525
|
+
const prev = out[out.length - 1];
|
|
526
|
+
if (b.type === "text" && prev && prev.type === "text") {
|
|
527
|
+
prev.text += b.text;
|
|
528
|
+
} else {
|
|
529
|
+
out.push(b);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return out;
|
|
533
|
+
}
|
|
534
|
+
var MAX_BYTES_TOTAL = 32e3;
|
|
535
|
+
var DefaultMemoryStore = class {
|
|
536
|
+
files;
|
|
537
|
+
/**
|
|
538
|
+
* Per-scope serialization queue. `remember` / `forget` / `consolidate` /
|
|
539
|
+
* `clear` are read-modify-write against a single file; without a lock,
|
|
540
|
+
* two concurrent calls on the same scope can read the same baseline and
|
|
541
|
+
* the later write silently drops the earlier entry. We chain each
|
|
542
|
+
* mutation onto the prior promise for the same scope so they run in
|
|
543
|
+
* issue order. Different scopes still proceed in parallel.
|
|
544
|
+
*
|
|
545
|
+
* The chain tracks only the last pending write. If a write fails, its
|
|
546
|
+
* error is caught and swallowed (line 43) so the chain stays alive for
|
|
547
|
+
* subsequent calls. A crash between atomicWrite() and backup copy leaves
|
|
548
|
+
* the file at its new content with no backup — acceptable for an optional
|
|
549
|
+
* backup whose worst case is losing a memory consolidation pass.
|
|
550
|
+
*/
|
|
551
|
+
writeChain = /* @__PURE__ */ new Map();
|
|
552
|
+
constructor(opts) {
|
|
553
|
+
this.files = {
|
|
554
|
+
"project-agents": opts.paths.inProjectAgentsFile,
|
|
555
|
+
"project-memory": opts.paths.projectMemory,
|
|
556
|
+
"user-memory": opts.paths.globalMemory
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
async runSerialized(scope, work) {
|
|
560
|
+
const prior = this.writeChain.get(scope) ?? Promise.resolve();
|
|
561
|
+
const next = prior.catch(() => void 0).then(work);
|
|
562
|
+
this.writeChain.set(scope, next);
|
|
563
|
+
try {
|
|
564
|
+
return await next;
|
|
565
|
+
} finally {
|
|
566
|
+
if (this.writeChain.get(scope) === next) {
|
|
567
|
+
this.writeChain.delete(scope);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
async readAll() {
|
|
572
|
+
const parts = [];
|
|
573
|
+
for (const scope of ["project-agents", "project-memory", "user-memory"]) {
|
|
574
|
+
const body = await this.read(scope);
|
|
575
|
+
if (body.trim()) parts.push(`## ${labelOf(scope)}
|
|
576
|
+
|
|
577
|
+
${body.trim()}`);
|
|
578
|
+
}
|
|
579
|
+
return parts.join("\n\n");
|
|
580
|
+
}
|
|
581
|
+
async read(scope) {
|
|
582
|
+
try {
|
|
583
|
+
return await fsp.readFile(this.files[scope], "utf8");
|
|
584
|
+
} catch {
|
|
585
|
+
return "";
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
async remember(text, scope = "project-memory") {
|
|
589
|
+
return this.runSerialized(scope, async () => {
|
|
590
|
+
const file = this.files[scope];
|
|
591
|
+
await ensureDir(path2.dirname(file));
|
|
592
|
+
let existing = "";
|
|
593
|
+
try {
|
|
594
|
+
existing = await fsp.readFile(file, "utf8");
|
|
595
|
+
} catch {
|
|
596
|
+
}
|
|
597
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
598
|
+
const id = `mem_${Date.now()}_${randomUUID().slice(0, 8)}`;
|
|
599
|
+
const entry = `
|
|
600
|
+
- [${ts}] ${id} ${text.replace(/\n/g, " ")}
|
|
601
|
+
`;
|
|
602
|
+
const next = existing.trim() ? existing.replace(/\n+$/, "") + entry : `# WrongStack Memory
|
|
603
|
+
${entry}`;
|
|
604
|
+
await atomicWrite(file, next);
|
|
605
|
+
const buf = Buffer.byteLength(next, "utf8");
|
|
606
|
+
if (buf > MAX_BYTES_TOTAL) {
|
|
607
|
+
await this.consolidateUnsafe(scope);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
async forget(query, scope = "project-memory") {
|
|
612
|
+
return this.runSerialized(scope, async () => this.forgetUnsafe(query, scope));
|
|
613
|
+
}
|
|
614
|
+
async forgetUnsafe(query, scope) {
|
|
615
|
+
const file = this.files[scope];
|
|
616
|
+
let existing;
|
|
617
|
+
try {
|
|
618
|
+
existing = await fsp.readFile(file, "utf8");
|
|
619
|
+
} catch {
|
|
620
|
+
return 0;
|
|
621
|
+
}
|
|
622
|
+
const needle = query.toLowerCase();
|
|
623
|
+
const idMatcher = /mem_\d+_\w+/;
|
|
624
|
+
let removed = 0;
|
|
625
|
+
const lines = existing.split("\n").filter((line) => {
|
|
626
|
+
const trimmed = line.trim();
|
|
627
|
+
if (!trimmed.startsWith("- ")) return true;
|
|
628
|
+
if (idMatcher.test(query)) {
|
|
629
|
+
const afterBracket = trimmed.indexOf("] ");
|
|
630
|
+
if (afterBracket !== -1) {
|
|
631
|
+
const afterTs = trimmed.slice(afterBracket + 2);
|
|
632
|
+
const entryIdMatch = /^mem_\d+_\w+/.exec(afterTs);
|
|
633
|
+
if (entryIdMatch && entryIdMatch[0] === query) {
|
|
634
|
+
removed++;
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (trimmed.toLowerCase().includes(needle)) {
|
|
640
|
+
removed++;
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
return true;
|
|
644
|
+
});
|
|
645
|
+
if (removed > 0) {
|
|
646
|
+
await atomicWrite(file, lines.join("\n"));
|
|
647
|
+
}
|
|
648
|
+
return removed;
|
|
649
|
+
}
|
|
650
|
+
async consolidate(scope) {
|
|
651
|
+
return this.runSerialized(scope, async () => this.consolidateUnsafe(scope));
|
|
652
|
+
}
|
|
653
|
+
async consolidateUnsafe(scope) {
|
|
654
|
+
const file = this.files[scope];
|
|
655
|
+
let existing;
|
|
656
|
+
try {
|
|
657
|
+
existing = await fsp.readFile(file, "utf8");
|
|
658
|
+
} catch {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const seen = /* @__PURE__ */ new Set();
|
|
662
|
+
const lines = existing.split("\n").filter((line) => {
|
|
663
|
+
const trimmed = line.trim();
|
|
664
|
+
if (!trimmed.startsWith("- ")) return true;
|
|
665
|
+
const norm = trimmed.replace(/\[[^\]]+\]/, "").replace(/\bmem_\d+_\w+\s*/, "").trim().toLowerCase();
|
|
666
|
+
if (seen.has(norm)) return false;
|
|
667
|
+
seen.add(norm);
|
|
668
|
+
return true;
|
|
669
|
+
});
|
|
670
|
+
const next = lines.join("\n");
|
|
671
|
+
const backup = `${file}.bak.${Date.now()}`;
|
|
672
|
+
try {
|
|
673
|
+
await fsp.copyFile(file, backup);
|
|
674
|
+
} catch {
|
|
675
|
+
}
|
|
676
|
+
try {
|
|
677
|
+
await atomicWrite(file, next);
|
|
678
|
+
} catch {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
async clear(scope) {
|
|
683
|
+
if (scope) {
|
|
684
|
+
await this.runSerialized(scope, async () => atomicWrite(this.files[scope], ""));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
await Promise.all(
|
|
688
|
+
["project-agents", "project-memory", "user-memory"].map(
|
|
689
|
+
(s) => this.runSerialized(s, async () => atomicWrite(this.files[s], ""))
|
|
690
|
+
)
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
function labelOf(scope) {
|
|
695
|
+
switch (scope) {
|
|
696
|
+
case "project-agents":
|
|
697
|
+
return "Project AGENTS.md";
|
|
698
|
+
case "project-memory":
|
|
699
|
+
return "Project memory";
|
|
700
|
+
case "user-memory":
|
|
701
|
+
return "User memory";
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// src/storage/config-store.ts
|
|
706
|
+
var DefaultConfigStore = class {
|
|
707
|
+
current;
|
|
708
|
+
watchers = /* @__PURE__ */ new Set();
|
|
709
|
+
constructor(initial) {
|
|
710
|
+
this.current = deepFreeze(structuredClone(initial));
|
|
711
|
+
}
|
|
712
|
+
get() {
|
|
713
|
+
return this.current;
|
|
714
|
+
}
|
|
715
|
+
getSection(key) {
|
|
716
|
+
return this.current[key];
|
|
717
|
+
}
|
|
718
|
+
getExtension(pluginName) {
|
|
719
|
+
const ext = this.current.extensions?.[pluginName];
|
|
720
|
+
return ext ? ext : FROZEN_EMPTY;
|
|
721
|
+
}
|
|
722
|
+
update(partial) {
|
|
723
|
+
const next = deepFreeze(structuredClone({ ...this.current, ...partial }));
|
|
724
|
+
if (next.version !== 1) {
|
|
725
|
+
throw new Error(`ConfigStore.update: version must remain 1, got ${String(next.version)}`);
|
|
726
|
+
}
|
|
727
|
+
const prev = this.current;
|
|
728
|
+
this.current = next;
|
|
729
|
+
for (const w of this.watchers) {
|
|
730
|
+
try {
|
|
731
|
+
w(next, prev);
|
|
732
|
+
} catch (err) {
|
|
733
|
+
console.error("[config-store] watcher threw:", err);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return next;
|
|
737
|
+
}
|
|
738
|
+
watch(cb) {
|
|
739
|
+
this.watchers.add(cb);
|
|
740
|
+
return () => this.watchers.delete(cb);
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
var FROZEN_EMPTY = Object.freeze({});
|
|
744
|
+
function deepFreeze(obj) {
|
|
745
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
746
|
+
if (Object.isFrozen(obj)) return obj;
|
|
747
|
+
for (const key of Object.keys(obj)) {
|
|
748
|
+
const v = obj[key];
|
|
749
|
+
if (v !== null && typeof v === "object" && !Object.isFrozen(v)) {
|
|
750
|
+
deepFreeze(v);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return Object.freeze(obj);
|
|
754
|
+
}
|
|
755
|
+
function decryptConfigSecrets(cfg, vault) {
|
|
756
|
+
return walk(cfg, vault, (v, key) => {
|
|
757
|
+
try {
|
|
758
|
+
return vault.decrypt(v);
|
|
759
|
+
} catch (err) {
|
|
760
|
+
console.warn(
|
|
761
|
+
`[secret-vault] Failed to decrypt "${key}":`,
|
|
762
|
+
err instanceof Error ? err.message : err
|
|
763
|
+
);
|
|
764
|
+
return "";
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
function walk(node, vault, transform) {
|
|
769
|
+
if (node === null || node === void 0) return node;
|
|
770
|
+
if (typeof node !== "object") return node;
|
|
771
|
+
if (Array.isArray(node)) {
|
|
772
|
+
return node.map((item) => walk(item, vault, transform));
|
|
773
|
+
}
|
|
774
|
+
const out = {};
|
|
775
|
+
for (const [k, v] of Object.entries(node)) {
|
|
776
|
+
if (typeof v === "string" && isSecretField(k)) {
|
|
777
|
+
out[k] = transform(v, k);
|
|
778
|
+
} else if (typeof v === "object" && v !== null) {
|
|
779
|
+
out[k] = walk(v, vault, transform);
|
|
780
|
+
} else {
|
|
781
|
+
out[k] = v;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return out;
|
|
785
|
+
}
|
|
786
|
+
var SECRET_KEY_PATTERN = /(?:apikey|api_key|authtoken|auth_token|bearer|secret|password|passwd|pwd|refreshtoken|refresh_token|sessionkey|session_key|access[_-]?token|private[_-]?key)/i;
|
|
787
|
+
var NON_SECRET_OVERRIDES = /* @__PURE__ */ new Set(["publickey", "public_key"]);
|
|
788
|
+
function isSecretField(name) {
|
|
789
|
+
const lc = name.toLowerCase();
|
|
790
|
+
if (NON_SECRET_OVERRIDES.has(lc)) return false;
|
|
791
|
+
return SECRET_KEY_PATTERN.test(lc);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// src/utils/safe-json.ts
|
|
795
|
+
function safeParse(input, maxBytes = 5e6) {
|
|
796
|
+
if (input.length > maxBytes) {
|
|
797
|
+
return { ok: false, error: `Input exceeds limit (${maxBytes} bytes)` };
|
|
798
|
+
}
|
|
799
|
+
try {
|
|
800
|
+
return { ok: true, value: JSON.parse(input) };
|
|
801
|
+
} catch (err) {
|
|
802
|
+
return {
|
|
803
|
+
ok: false,
|
|
804
|
+
error: err instanceof Error ? err.message : String(err)
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/storage/config-loader.ts
|
|
810
|
+
var BEHAVIOR_DEFAULTS = {
|
|
811
|
+
version: 1,
|
|
812
|
+
context: {
|
|
813
|
+
warnThreshold: 0.6,
|
|
814
|
+
softThreshold: 0.75,
|
|
815
|
+
hardThreshold: 0.9,
|
|
816
|
+
autoCompact: true,
|
|
817
|
+
preserveK: 10,
|
|
818
|
+
eliseThreshold: 2e3
|
|
819
|
+
},
|
|
820
|
+
tools: {
|
|
821
|
+
defaultExecutionStrategy: "smart",
|
|
822
|
+
maxIterations: 100,
|
|
823
|
+
iterationTimeoutMs: 3e5,
|
|
824
|
+
sessionTimeoutMs: 18e5,
|
|
825
|
+
perIterationOutputCapBytes: 1e5,
|
|
826
|
+
autoExtendLimit: true
|
|
827
|
+
},
|
|
828
|
+
log: { level: "info" },
|
|
829
|
+
features: {
|
|
830
|
+
mcp: true,
|
|
831
|
+
plugins: true,
|
|
832
|
+
memory: true,
|
|
833
|
+
modelsRegistry: true,
|
|
834
|
+
skills: true
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
var ENV_MAP = {
|
|
838
|
+
WRONGSTACK_PROVIDER: (c, v) => {
|
|
839
|
+
c.provider = v;
|
|
840
|
+
},
|
|
841
|
+
WRONGSTACK_MODEL: (c, v) => {
|
|
842
|
+
c.model = v;
|
|
843
|
+
},
|
|
844
|
+
WRONGSTACK_API_KEY: (c, v) => {
|
|
845
|
+
c.apiKey = v;
|
|
846
|
+
},
|
|
847
|
+
WRONGSTACK_BASE_URL: (c, v) => {
|
|
848
|
+
c.baseUrl = v;
|
|
849
|
+
},
|
|
850
|
+
WRONGSTACK_LOG_LEVEL: (c, v) => {
|
|
851
|
+
if (!c.log) c.log = { level: "info" };
|
|
852
|
+
c.log.level = v;
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
function isPrimitiveArray(a) {
|
|
856
|
+
return a.every((v) => v === null || typeof v !== "object");
|
|
857
|
+
}
|
|
858
|
+
var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
859
|
+
function deepMerge(base, patch) {
|
|
860
|
+
if (typeof base !== "object" || base === null) return patch ?? base;
|
|
861
|
+
if (typeof patch !== "object" || patch === null) return base;
|
|
862
|
+
const out = { ...base };
|
|
863
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
864
|
+
if (FORBIDDEN_PROTO_KEYS.has(k)) continue;
|
|
865
|
+
const existing = out[k];
|
|
866
|
+
if (Array.isArray(v)) {
|
|
867
|
+
if (Array.isArray(existing) && isPrimitiveArray(v) && isPrimitiveArray(existing)) {
|
|
868
|
+
out[k] = [.../* @__PURE__ */ new Set([...existing, ...v])];
|
|
869
|
+
} else {
|
|
870
|
+
out[k] = v;
|
|
871
|
+
if (process.env.WRONGSTACK_DEBUG_CONFIG) {
|
|
872
|
+
console.warn(
|
|
873
|
+
`[config] Non-primitive array for "${k}" replaced (global + local config merge). Global entries: ${existing?.length ?? 0}, local entries: ${v.length}.`
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
} else if (typeof v === "object" && v !== null && typeof existing === "object" && existing !== null) {
|
|
878
|
+
out[k] = deepMerge(existing, v);
|
|
879
|
+
} else if (v !== void 0) {
|
|
880
|
+
out[k] = v;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return out;
|
|
884
|
+
}
|
|
885
|
+
var DefaultConfigLoader = class {
|
|
886
|
+
paths;
|
|
887
|
+
strict;
|
|
888
|
+
vault;
|
|
889
|
+
extraSources;
|
|
890
|
+
constructor(opts) {
|
|
891
|
+
this.paths = opts.paths;
|
|
892
|
+
this.strict = opts.strict ?? false;
|
|
893
|
+
this.vault = opts.vault;
|
|
894
|
+
this.extraSources = opts.sources ?? [];
|
|
895
|
+
}
|
|
896
|
+
async load(opts = {}) {
|
|
897
|
+
let cfg = { ...BEHAVIOR_DEFAULTS };
|
|
898
|
+
const [global, local] = await Promise.all([
|
|
899
|
+
this.readJson(this.paths.globalConfig),
|
|
900
|
+
this.readJson(this.paths.projectLocalConfig)
|
|
901
|
+
]);
|
|
902
|
+
cfg = deepMerge(cfg, global);
|
|
903
|
+
cfg = deepMerge(cfg, local);
|
|
904
|
+
for (const [key, fn] of Object.entries(ENV_MAP)) {
|
|
905
|
+
const v = process.env[key];
|
|
906
|
+
if (v) fn(cfg, v);
|
|
907
|
+
}
|
|
908
|
+
const sorted = [...this.extraSources].sort((a, b) => {
|
|
909
|
+
const pd = (a.priority ?? 50) - (b.priority ?? 50);
|
|
910
|
+
if (pd !== 0) return pd;
|
|
911
|
+
return a.name.localeCompare(b.name);
|
|
912
|
+
});
|
|
913
|
+
for (const src of sorted) {
|
|
914
|
+
try {
|
|
915
|
+
const patch = await src.read();
|
|
916
|
+
if (patch && Object.keys(patch).length > 0) {
|
|
917
|
+
cfg = deepMerge(cfg, patch);
|
|
918
|
+
}
|
|
919
|
+
} catch (err) {
|
|
920
|
+
console.warn(`Config source "${src.name}" failed`, err);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (opts.cliFlags) {
|
|
924
|
+
cfg = deepMerge(cfg, opts.cliFlags);
|
|
925
|
+
}
|
|
926
|
+
if (this.vault) {
|
|
927
|
+
cfg = decryptConfigSecrets(cfg, this.vault);
|
|
928
|
+
}
|
|
929
|
+
if (cfg.providers) {
|
|
930
|
+
for (const pcfg of Object.values(cfg.providers)) {
|
|
931
|
+
if (!pcfg || typeof pcfg !== "object") continue;
|
|
932
|
+
const rawKeys = pcfg.apiKeys;
|
|
933
|
+
if (!Array.isArray(rawKeys) || rawKeys.length === 0) continue;
|
|
934
|
+
const keys = rawKeys.filter(
|
|
935
|
+
(k) => !!k && typeof k === "object" && typeof k.label === "string" && typeof k.apiKey === "string"
|
|
936
|
+
);
|
|
937
|
+
if (keys.length === 0) continue;
|
|
938
|
+
const existing = pcfg.apiKey;
|
|
939
|
+
if (existing && existing.length > 0) continue;
|
|
940
|
+
const activeLabel = pcfg.activeKey;
|
|
941
|
+
const chosen = activeLabel ? keys.find((k) => k.label === activeLabel) ?? keys[0] : keys[0];
|
|
942
|
+
if (chosen?.apiKey) {
|
|
943
|
+
pcfg.apiKey = chosen.apiKey;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
this.validateBehavior(cfg);
|
|
948
|
+
if (this.strict) {
|
|
949
|
+
this.validateIdentity(cfg);
|
|
950
|
+
}
|
|
951
|
+
return Object.freeze(cfg);
|
|
952
|
+
}
|
|
953
|
+
async readJson(file) {
|
|
954
|
+
let raw;
|
|
955
|
+
try {
|
|
956
|
+
raw = await fsp.readFile(file, "utf8");
|
|
957
|
+
} catch (err) {
|
|
958
|
+
if (err.code !== "ENOENT") {
|
|
959
|
+
console.warn(`[config] Failed to read "${file}":`, err);
|
|
960
|
+
}
|
|
961
|
+
return {};
|
|
962
|
+
}
|
|
963
|
+
const parsed = safeParse(raw);
|
|
964
|
+
if (!parsed.ok || !parsed.value) {
|
|
965
|
+
console.warn(
|
|
966
|
+
`[config] Failed to parse "${file}": invalid JSON. Falling back to defaults for this layer.`
|
|
967
|
+
);
|
|
968
|
+
return {};
|
|
969
|
+
}
|
|
970
|
+
return parsed.value;
|
|
971
|
+
}
|
|
972
|
+
validateBehavior(cfg) {
|
|
973
|
+
if (cfg.version === void 0) throw new Error("Config: missing version field");
|
|
974
|
+
if (cfg.version !== 1) throw new Error(`Config: unsupported version ${cfg.version}`);
|
|
975
|
+
const c = cfg.context;
|
|
976
|
+
if (!c) throw new Error("Config: missing context section");
|
|
977
|
+
const fields = ["warnThreshold", "softThreshold", "hardThreshold"];
|
|
978
|
+
for (const f of fields) {
|
|
979
|
+
const v = c[f];
|
|
980
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
981
|
+
throw new Error(`Config: context.${String(f)} must be a finite number (got ${typeof v})`);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
if (c.warnThreshold >= c.softThreshold || c.softThreshold >= c.hardThreshold) {
|
|
985
|
+
throw new Error("Config: context thresholds must satisfy warn < soft < hard");
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
validateIdentity(cfg) {
|
|
989
|
+
if (!cfg.provider) {
|
|
990
|
+
throw new Error(
|
|
991
|
+
"Config: no provider configured. Run `wstack init` or set WRONGSTACK_PROVIDER."
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
if (!cfg.model) {
|
|
995
|
+
throw new Error("Config: no model configured. Run `wstack init` or set WRONGSTACK_MODEL.");
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
// src/storage/config-migration.ts
|
|
1001
|
+
var ConfigMigrationError = class extends Error {
|
|
1002
|
+
fromVersion;
|
|
1003
|
+
targetVersion;
|
|
1004
|
+
missingStep;
|
|
1005
|
+
constructor(opts) {
|
|
1006
|
+
super(opts.message);
|
|
1007
|
+
this.name = "ConfigMigrationError";
|
|
1008
|
+
this.fromVersion = opts.fromVersion;
|
|
1009
|
+
this.targetVersion = opts.targetVersion;
|
|
1010
|
+
this.missingStep = opts.missingStep;
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
function runConfigMigrations(input, targetVersion, migrations) {
|
|
1014
|
+
const initial = typeof input["version"] === "number" ? input["version"] : 1;
|
|
1015
|
+
let current = { ...input };
|
|
1016
|
+
let currentVersion = initial;
|
|
1017
|
+
const applied = [];
|
|
1018
|
+
let shouldPersist = false;
|
|
1019
|
+
let guard = 0;
|
|
1020
|
+
while (currentVersion !== targetVersion) {
|
|
1021
|
+
if (++guard > 100) {
|
|
1022
|
+
throw new ConfigMigrationError({
|
|
1023
|
+
message: `Config migration looped past 100 steps (from v${initial} toward v${targetVersion})`,
|
|
1024
|
+
fromVersion: initial,
|
|
1025
|
+
targetVersion,
|
|
1026
|
+
missingStep: currentVersion
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
const step = migrations.find((m) => m.from === currentVersion);
|
|
1030
|
+
if (!step) {
|
|
1031
|
+
throw new ConfigMigrationError({
|
|
1032
|
+
message: `No migration registered from config v${currentVersion} (target v${targetVersion}). Update the framework or revert the config file.`,
|
|
1033
|
+
fromVersion: initial,
|
|
1034
|
+
targetVersion,
|
|
1035
|
+
missingStep: currentVersion
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
const ctx = { fromVersion: currentVersion, shouldPersist: false };
|
|
1039
|
+
const next = step.migrate(current, ctx);
|
|
1040
|
+
if (typeof next["version"] !== "number" || next["version"] !== step.to) {
|
|
1041
|
+
next["version"] = step.to;
|
|
1042
|
+
}
|
|
1043
|
+
current = next;
|
|
1044
|
+
currentVersion = step.to;
|
|
1045
|
+
applied.push(`v${step.from}\u2192v${step.to}`);
|
|
1046
|
+
shouldPersist = shouldPersist || ctx.shouldPersist || step.from < step.to;
|
|
1047
|
+
}
|
|
1048
|
+
return { config: current, applied, shouldPersist };
|
|
1049
|
+
}
|
|
1050
|
+
var DEFAULT_CONFIG_MIGRATIONS = [];
|
|
1051
|
+
var LOCK_FILE = "active.json";
|
|
1052
|
+
var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
1053
|
+
var RecoveryLock = class {
|
|
1054
|
+
file;
|
|
1055
|
+
pid;
|
|
1056
|
+
hostname;
|
|
1057
|
+
maxAgeMs;
|
|
1058
|
+
sessionStore;
|
|
1059
|
+
probe;
|
|
1060
|
+
constructor(opts) {
|
|
1061
|
+
this.file = path2.join(opts.dir, LOCK_FILE);
|
|
1062
|
+
this.pid = opts.pid ?? process.pid;
|
|
1063
|
+
this.hostname = opts.hostname ?? os.hostname();
|
|
1064
|
+
this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
1065
|
+
this.sessionStore = opts.sessionStore;
|
|
1066
|
+
this.probe = opts.isPidAlive ?? defaultIsPidAlive;
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Examine the lockfile and decide whether it represents an abandoned
|
|
1070
|
+
* session. Returns `null` if the file is missing, points to a live
|
|
1071
|
+
* instance, references a clean-closed session, is too old, or is
|
|
1072
|
+
* malformed. Otherwise returns enough detail to prompt the user.
|
|
1073
|
+
*
|
|
1074
|
+
* Important: this is a read-only check. We never delete an active
|
|
1075
|
+
* lock from here — if another wstack instance is alive, the caller
|
|
1076
|
+
* should bail or run with a fresh session instead.
|
|
1077
|
+
*/
|
|
1078
|
+
async checkAbandoned() {
|
|
1079
|
+
const lock = await this.readLock();
|
|
1080
|
+
if (!lock) return null;
|
|
1081
|
+
const ageMs = Date.now() - new Date(lock.startedAt).getTime();
|
|
1082
|
+
if (Number.isNaN(ageMs) || ageMs < 0) {
|
|
1083
|
+
return null;
|
|
1084
|
+
}
|
|
1085
|
+
if (ageMs > this.maxAgeMs) return null;
|
|
1086
|
+
if (lock.hostname === this.hostname && this.probe(lock.pid)) {
|
|
1087
|
+
return null;
|
|
1088
|
+
}
|
|
1089
|
+
let messageCount = 0;
|
|
1090
|
+
if (this.sessionStore) {
|
|
1091
|
+
try {
|
|
1092
|
+
const data = await this.sessionStore.load(lock.sessionId);
|
|
1093
|
+
const closed = data.events.some((e) => e.type === "session_end");
|
|
1094
|
+
if (closed) return null;
|
|
1095
|
+
messageCount = data.messages.length;
|
|
1096
|
+
} catch {
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return {
|
|
1101
|
+
sessionId: lock.sessionId,
|
|
1102
|
+
pid: lock.pid,
|
|
1103
|
+
startedAt: lock.startedAt,
|
|
1104
|
+
ageMs,
|
|
1105
|
+
messageCount
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Claim the lock for the given session. Overwrites any existing lock
|
|
1110
|
+
* — the caller should have already handled abandonment (via
|
|
1111
|
+
* `checkAbandoned`) before calling this.
|
|
1112
|
+
*/
|
|
1113
|
+
async write(sessionId) {
|
|
1114
|
+
await ensureDir(path2.dirname(this.file));
|
|
1115
|
+
const lock = {
|
|
1116
|
+
v: 1,
|
|
1117
|
+
sessionId,
|
|
1118
|
+
pid: this.pid,
|
|
1119
|
+
hostname: this.hostname,
|
|
1120
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1121
|
+
};
|
|
1122
|
+
const tmp = `${this.file}.tmp`;
|
|
1123
|
+
await fsp.writeFile(tmp, JSON.stringify(lock), { mode: 384 });
|
|
1124
|
+
await fsp.rename(tmp, this.file);
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Release the lock. Idempotent — silently succeeds if the file is
|
|
1128
|
+
* already gone (e.g. someone else cleared it, or the directory was
|
|
1129
|
+
* wiped).
|
|
1130
|
+
*/
|
|
1131
|
+
async clear() {
|
|
1132
|
+
try {
|
|
1133
|
+
await fsp.unlink(this.file);
|
|
1134
|
+
} catch (err) {
|
|
1135
|
+
const code = err.code;
|
|
1136
|
+
if (code === "ENOENT") return;
|
|
1137
|
+
throw err;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
async readLock() {
|
|
1141
|
+
let raw;
|
|
1142
|
+
try {
|
|
1143
|
+
raw = await fsp.readFile(this.file, "utf8");
|
|
1144
|
+
} catch (err) {
|
|
1145
|
+
const code = err.code;
|
|
1146
|
+
if (code === "ENOENT") return null;
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
try {
|
|
1150
|
+
const parsed = JSON.parse(raw);
|
|
1151
|
+
if (!isLockFile(parsed)) return null;
|
|
1152
|
+
return parsed;
|
|
1153
|
+
} catch {
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
function isLockFile(v) {
|
|
1159
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1160
|
+
const o = v;
|
|
1161
|
+
return o["v"] === 1 && typeof o["sessionId"] === "string" && typeof o["pid"] === "number" && typeof o["hostname"] === "string" && typeof o["startedAt"] === "string";
|
|
1162
|
+
}
|
|
1163
|
+
function defaultIsPidAlive(pid) {
|
|
1164
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
1165
|
+
try {
|
|
1166
|
+
process.kill(pid, 0);
|
|
1167
|
+
return true;
|
|
1168
|
+
} catch (err) {
|
|
1169
|
+
const code = err.code;
|
|
1170
|
+
if (code === "EPERM") return true;
|
|
1171
|
+
return false;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// src/storage/session-reader.ts
|
|
1176
|
+
var DefaultSessionReader = class {
|
|
1177
|
+
store;
|
|
1178
|
+
constructor(opts) {
|
|
1179
|
+
this.store = opts.store;
|
|
1180
|
+
}
|
|
1181
|
+
async query(q = {}) {
|
|
1182
|
+
const raw = await this.store.list(q.limit ? Math.max(q.limit * 4, 100) : 1e3);
|
|
1183
|
+
const titleNeedle = q.titleContains?.toLowerCase();
|
|
1184
|
+
const filtered = raw.filter((s) => {
|
|
1185
|
+
if (q.since && s.startedAt < q.since) return false;
|
|
1186
|
+
if (q.until && s.startedAt > q.until) return false;
|
|
1187
|
+
if (q.provider && s.provider !== q.provider) return false;
|
|
1188
|
+
if (q.model && s.model !== q.model) return false;
|
|
1189
|
+
if (q.minTokens !== void 0 && s.tokenTotal < q.minTokens) return false;
|
|
1190
|
+
if (titleNeedle && !s.title.toLowerCase().includes(titleNeedle)) return false;
|
|
1191
|
+
return true;
|
|
1192
|
+
});
|
|
1193
|
+
const out = filtered.map((s) => ({
|
|
1194
|
+
id: s.id,
|
|
1195
|
+
title: s.title,
|
|
1196
|
+
startedAt: s.startedAt,
|
|
1197
|
+
provider: s.provider,
|
|
1198
|
+
model: s.model,
|
|
1199
|
+
tokenTotal: s.tokenTotal
|
|
1200
|
+
}));
|
|
1201
|
+
return q.limit ? out.slice(0, q.limit) : out;
|
|
1202
|
+
}
|
|
1203
|
+
async *replay(sessionId) {
|
|
1204
|
+
const data = await this.store.load(sessionId);
|
|
1205
|
+
for (const e of data.events) yield e;
|
|
1206
|
+
}
|
|
1207
|
+
async search(q, sessionId) {
|
|
1208
|
+
const limit = q.limit ?? 100;
|
|
1209
|
+
const matcher = buildMatcher(q);
|
|
1210
|
+
const allowedTypes = q.types ? new Set(q.types) : null;
|
|
1211
|
+
const ids = sessionId ? [sessionId] : (await this.store.list(1e3)).map((s) => s.id);
|
|
1212
|
+
const hits = [];
|
|
1213
|
+
for (const id of ids) {
|
|
1214
|
+
let data;
|
|
1215
|
+
try {
|
|
1216
|
+
data = await this.store.load(id);
|
|
1217
|
+
} catch {
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
for (let i = 0; i < data.events.length; i++) {
|
|
1221
|
+
const ev = data.events[i];
|
|
1222
|
+
if (allowedTypes && !allowedTypes.has(ev.type)) continue;
|
|
1223
|
+
const text = eventText(ev);
|
|
1224
|
+
if (text === null) continue;
|
|
1225
|
+
const hit = matcher(text);
|
|
1226
|
+
if (!hit) continue;
|
|
1227
|
+
hits.push({
|
|
1228
|
+
sessionId: id,
|
|
1229
|
+
eventIndex: i,
|
|
1230
|
+
ts: ev.ts,
|
|
1231
|
+
type: ev.type,
|
|
1232
|
+
snippet: snippetOf(text, hit.start, hit.end)
|
|
1233
|
+
});
|
|
1234
|
+
if (hits.length >= limit) return hits;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return hits;
|
|
1238
|
+
}
|
|
1239
|
+
async export(sessionId, opts) {
|
|
1240
|
+
const data = await this.store.load(sessionId);
|
|
1241
|
+
const includeTools = opts.includeTools ?? true;
|
|
1242
|
+
const includeDiagnostics = opts.includeDiagnostics ?? true;
|
|
1243
|
+
const filtered = data.events.filter((e) => {
|
|
1244
|
+
if (!includeTools && (e.type === "tool_use" || e.type === "tool_result" || e.type === "tool_call_start" || e.type === "tool_call_end")) {
|
|
1245
|
+
return false;
|
|
1246
|
+
}
|
|
1247
|
+
if (!includeDiagnostics && (e.type === "error" || e.type === "compaction" || e.type === "message_truncated")) {
|
|
1248
|
+
return false;
|
|
1249
|
+
}
|
|
1250
|
+
return true;
|
|
1251
|
+
});
|
|
1252
|
+
if (opts.format === "json") {
|
|
1253
|
+
return JSON.stringify({ metadata: data.metadata, events: filtered }, null, 2);
|
|
1254
|
+
}
|
|
1255
|
+
if (opts.format === "text") {
|
|
1256
|
+
return renderPlainText(data.metadata, filtered);
|
|
1257
|
+
}
|
|
1258
|
+
return renderMarkdown(data.metadata, filtered);
|
|
1259
|
+
}
|
|
1260
|
+
async metadata(sessionId) {
|
|
1261
|
+
const data = await this.store.load(sessionId);
|
|
1262
|
+
return data.metadata;
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
function buildMatcher(q) {
|
|
1266
|
+
const ci = q.caseInsensitive ?? true;
|
|
1267
|
+
if (q.regex) {
|
|
1268
|
+
const flags = ci ? "i" : "";
|
|
1269
|
+
const re = new RegExp(q.query, flags);
|
|
1270
|
+
return (text) => {
|
|
1271
|
+
const m = re.exec(text);
|
|
1272
|
+
return m ? { start: m.index, end: m.index + m[0].length } : null;
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
const needle = ci ? q.query.toLowerCase() : q.query;
|
|
1276
|
+
return (text) => {
|
|
1277
|
+
const hay = ci ? text.toLowerCase() : text;
|
|
1278
|
+
const idx = hay.indexOf(needle);
|
|
1279
|
+
return idx === -1 ? null : { start: idx, end: idx + needle.length };
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
function eventText(e) {
|
|
1283
|
+
switch (e.type) {
|
|
1284
|
+
case "user_input":
|
|
1285
|
+
return contentToString(e.content);
|
|
1286
|
+
case "llm_response":
|
|
1287
|
+
return contentToString(e.content);
|
|
1288
|
+
case "tool_use":
|
|
1289
|
+
return `${e.name} ${JSON.stringify(e.input)}`;
|
|
1290
|
+
case "tool_result":
|
|
1291
|
+
return typeof e.content === "string" ? e.content : JSON.stringify(e.content);
|
|
1292
|
+
case "error":
|
|
1293
|
+
return `${e.phase}: ${e.message}`;
|
|
1294
|
+
case "session_start":
|
|
1295
|
+
case "session_resumed":
|
|
1296
|
+
return `${e.model}/${e.provider}`;
|
|
1297
|
+
case "task_created":
|
|
1298
|
+
case "task_completed":
|
|
1299
|
+
return e.title;
|
|
1300
|
+
case "task_failed":
|
|
1301
|
+
return `${e.title}: ${e.error}`;
|
|
1302
|
+
case "skill_activated":
|
|
1303
|
+
case "skill_deactivated":
|
|
1304
|
+
return e.skillName;
|
|
1305
|
+
default:
|
|
1306
|
+
return null;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
function contentToString(content) {
|
|
1310
|
+
if (typeof content === "string") return content;
|
|
1311
|
+
return content.map((b) => {
|
|
1312
|
+
switch (b.type) {
|
|
1313
|
+
case "text":
|
|
1314
|
+
return b.text;
|
|
1315
|
+
case "tool_use":
|
|
1316
|
+
return `[tool_use:${b.name} ${JSON.stringify(b.input)}]`;
|
|
1317
|
+
case "tool_result":
|
|
1318
|
+
return typeof b.content === "string" ? b.content : JSON.stringify(b.content);
|
|
1319
|
+
default:
|
|
1320
|
+
return "";
|
|
1321
|
+
}
|
|
1322
|
+
}).join("\n");
|
|
1323
|
+
}
|
|
1324
|
+
var SNIPPET_RADIUS = 60;
|
|
1325
|
+
function snippetOf(text, start, end) {
|
|
1326
|
+
const from = Math.max(0, start - SNIPPET_RADIUS);
|
|
1327
|
+
const to = Math.min(text.length, end + SNIPPET_RADIUS);
|
|
1328
|
+
const prefix = from > 0 ? "\u2026" : "";
|
|
1329
|
+
const suffix = to < text.length ? "\u2026" : "";
|
|
1330
|
+
return prefix + text.slice(from, to).replace(/\s+/g, " ").trim() + suffix;
|
|
1331
|
+
}
|
|
1332
|
+
function renderMarkdown(meta, events) {
|
|
1333
|
+
const lines = [];
|
|
1334
|
+
lines.push(`# Session ${meta.id}`);
|
|
1335
|
+
lines.push("");
|
|
1336
|
+
if (meta.model || meta.provider) {
|
|
1337
|
+
lines.push(`- **Model:** ${meta.provider ?? "?"}/${meta.model ?? "?"}`);
|
|
1338
|
+
}
|
|
1339
|
+
lines.push(`- **Started:** ${meta.startedAt}`);
|
|
1340
|
+
if (meta.endedAt) lines.push(`- **Ended:** ${meta.endedAt}`);
|
|
1341
|
+
lines.push("");
|
|
1342
|
+
lines.push("---");
|
|
1343
|
+
lines.push("");
|
|
1344
|
+
for (const e of events) {
|
|
1345
|
+
switch (e.type) {
|
|
1346
|
+
case "user_input": {
|
|
1347
|
+
lines.push(`## User \u2014 ${e.ts}`);
|
|
1348
|
+
lines.push("");
|
|
1349
|
+
lines.push(contentToString(e.content));
|
|
1350
|
+
lines.push("");
|
|
1351
|
+
break;
|
|
1352
|
+
}
|
|
1353
|
+
case "llm_response": {
|
|
1354
|
+
lines.push(`## Assistant \u2014 ${e.ts}`);
|
|
1355
|
+
lines.push("");
|
|
1356
|
+
lines.push(contentToString(e.content));
|
|
1357
|
+
if (e.stopReason && e.stopReason !== "end_turn") {
|
|
1358
|
+
lines.push("");
|
|
1359
|
+
lines.push(`*stop: ${e.stopReason}*`);
|
|
1360
|
+
}
|
|
1361
|
+
lines.push("");
|
|
1362
|
+
break;
|
|
1363
|
+
}
|
|
1364
|
+
case "tool_use": {
|
|
1365
|
+
lines.push(`### Tool call: \`${e.name}\``);
|
|
1366
|
+
lines.push("");
|
|
1367
|
+
lines.push("```json");
|
|
1368
|
+
lines.push(JSON.stringify(e.input, null, 2));
|
|
1369
|
+
lines.push("```");
|
|
1370
|
+
lines.push("");
|
|
1371
|
+
break;
|
|
1372
|
+
}
|
|
1373
|
+
case "tool_result": {
|
|
1374
|
+
const body = typeof e.content === "string" ? e.content : JSON.stringify(e.content, null, 2);
|
|
1375
|
+
lines.push(`### Tool result${e.isError ? " (error)" : ""}`);
|
|
1376
|
+
lines.push("");
|
|
1377
|
+
lines.push("```");
|
|
1378
|
+
lines.push(body);
|
|
1379
|
+
lines.push("```");
|
|
1380
|
+
lines.push("");
|
|
1381
|
+
break;
|
|
1382
|
+
}
|
|
1383
|
+
case "error": {
|
|
1384
|
+
lines.push(`> **Error** (${e.phase}): ${e.message}`);
|
|
1385
|
+
lines.push("");
|
|
1386
|
+
break;
|
|
1387
|
+
}
|
|
1388
|
+
case "compaction": {
|
|
1389
|
+
lines.push(`> **Compaction**: ${e.before} \u2192 ${e.after} tokens`);
|
|
1390
|
+
lines.push("");
|
|
1391
|
+
break;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
return lines.join("\n");
|
|
1396
|
+
}
|
|
1397
|
+
function renderPlainText(meta, events) {
|
|
1398
|
+
const lines = [];
|
|
1399
|
+
lines.push(
|
|
1400
|
+
`Session ${meta.id} \u2014 ${meta.provider ?? "?"}/${meta.model ?? "?"} \u2014 started ${meta.startedAt}`
|
|
1401
|
+
);
|
|
1402
|
+
lines.push("".padEnd(72, "-"));
|
|
1403
|
+
for (const e of events) {
|
|
1404
|
+
switch (e.type) {
|
|
1405
|
+
case "user_input":
|
|
1406
|
+
lines.push(`[${e.ts}] USER`);
|
|
1407
|
+
lines.push(contentToString(e.content));
|
|
1408
|
+
lines.push("");
|
|
1409
|
+
break;
|
|
1410
|
+
case "llm_response":
|
|
1411
|
+
lines.push(`[${e.ts}] ASSISTANT`);
|
|
1412
|
+
lines.push(contentToString(e.content));
|
|
1413
|
+
lines.push("");
|
|
1414
|
+
break;
|
|
1415
|
+
case "tool_use":
|
|
1416
|
+
lines.push(`[${e.ts}] TOOL_USE ${e.name} ${JSON.stringify(e.input)}`);
|
|
1417
|
+
break;
|
|
1418
|
+
case "tool_result":
|
|
1419
|
+
lines.push(
|
|
1420
|
+
`[${e.ts}] TOOL_RESULT${e.isError ? " (error)" : ""} ${typeof e.content === "string" ? e.content : JSON.stringify(e.content)}`
|
|
1421
|
+
);
|
|
1422
|
+
break;
|
|
1423
|
+
case "error":
|
|
1424
|
+
lines.push(`[${e.ts}] ERROR (${e.phase}): ${e.message}`);
|
|
1425
|
+
break;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
return lines.join("\n");
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// src/storage/session-analyzer.ts
|
|
1432
|
+
var SessionAnalyzer = class {
|
|
1433
|
+
analyze(events) {
|
|
1434
|
+
const toolUsageCount = {};
|
|
1435
|
+
const errors = [];
|
|
1436
|
+
const modeChanges = [];
|
|
1437
|
+
const tasksById = /* @__PURE__ */ new Map();
|
|
1438
|
+
let sessionId = "";
|
|
1439
|
+
for (const event of events) {
|
|
1440
|
+
if (event.type === "session_start" || event.type === "session_resumed") {
|
|
1441
|
+
if (!sessionId) sessionId = event.id;
|
|
1442
|
+
}
|
|
1443
|
+
if (event.type === "tool_use") {
|
|
1444
|
+
toolUsageCount[event.name] = (toolUsageCount[event.name] ?? 0) + 1;
|
|
1445
|
+
}
|
|
1446
|
+
if (event.type === "error") {
|
|
1447
|
+
errors.push({ ts: event.ts, phase: event.phase, message: event.message });
|
|
1448
|
+
}
|
|
1449
|
+
if (event.type === "mode_changed") {
|
|
1450
|
+
modeChanges.push({ ts: event.ts, from: event.from, to: event.to });
|
|
1451
|
+
}
|
|
1452
|
+
if (event.type === "task_created") {
|
|
1453
|
+
tasksById.set(event.taskId, {
|
|
1454
|
+
taskId: event.taskId,
|
|
1455
|
+
title: event.title,
|
|
1456
|
+
status: "created",
|
|
1457
|
+
createdAt: event.ts
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
if (event.type === "task_updated") {
|
|
1461
|
+
const t = tasksById.get(event.taskId);
|
|
1462
|
+
if (t) t.status = event.status;
|
|
1463
|
+
}
|
|
1464
|
+
if (event.type === "task_completed") {
|
|
1465
|
+
const t = tasksById.get(event.taskId);
|
|
1466
|
+
if (t) {
|
|
1467
|
+
t.status = "completed";
|
|
1468
|
+
t.completedAt = event.ts;
|
|
1469
|
+
} else {
|
|
1470
|
+
tasksById.set(event.taskId, {
|
|
1471
|
+
taskId: event.taskId,
|
|
1472
|
+
title: event.title,
|
|
1473
|
+
status: "completed",
|
|
1474
|
+
createdAt: event.ts,
|
|
1475
|
+
completedAt: event.ts
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
if (event.type === "task_failed") {
|
|
1480
|
+
const t = tasksById.get(event.taskId);
|
|
1481
|
+
if (t) {
|
|
1482
|
+
t.status = "failed";
|
|
1483
|
+
t.completedAt = event.ts;
|
|
1484
|
+
} else {
|
|
1485
|
+
tasksById.set(event.taskId, {
|
|
1486
|
+
taskId: event.taskId,
|
|
1487
|
+
title: event.title,
|
|
1488
|
+
status: "failed",
|
|
1489
|
+
createdAt: event.ts,
|
|
1490
|
+
completedAt: event.ts
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
return {
|
|
1496
|
+
sessionId,
|
|
1497
|
+
totalDuration: this.calcDuration(events),
|
|
1498
|
+
toolUsageCount,
|
|
1499
|
+
errorCount: errors.length,
|
|
1500
|
+
modeChanges,
|
|
1501
|
+
tasks: Array.from(tasksById.values())
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
query(events, filter) {
|
|
1505
|
+
return events.filter((e) => {
|
|
1506
|
+
if (filter.eventTypes?.length && !filter.eventTypes.includes(e.type)) return false;
|
|
1507
|
+
if (filter.toolNames?.length && e.type === "tool_use") {
|
|
1508
|
+
const toolEvent = e;
|
|
1509
|
+
if (!filter.toolNames.includes(toolEvent.name)) return false;
|
|
1510
|
+
}
|
|
1511
|
+
if (filter.timeRange) {
|
|
1512
|
+
const ts = new Date(e.ts).getTime();
|
|
1513
|
+
const start = new Date(filter.timeRange.start).getTime();
|
|
1514
|
+
const end = new Date(filter.timeRange.end).getTime();
|
|
1515
|
+
if (ts < start || ts > end) return false;
|
|
1516
|
+
}
|
|
1517
|
+
return true;
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
calcDuration(events) {
|
|
1521
|
+
if (events.length < 2) return 0;
|
|
1522
|
+
const first = new Date(events[0].ts).getTime();
|
|
1523
|
+
const last = new Date(events[events.length - 1].ts).getTime();
|
|
1524
|
+
return last - first;
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
|
|
1528
|
+
export { ConfigMigrationError, DEFAULT_CONFIG_MIGRATIONS, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultMemoryStore, DefaultSessionReader, DefaultSessionStore, QueueStore, RecoveryLock, SessionAnalyzer, runConfigMigrations };
|
|
1529
|
+
//# sourceMappingURL=index.js.map
|
|
1530
|
+
//# sourceMappingURL=index.js.map
|