@netnodeag/kraftwerk 0.4.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +192 -6
- package/dist/cli/doctor.js +77 -2
- package/dist/cli/init.js +65 -15
- package/dist/cli/knowledge.d.ts +2 -0
- package/dist/cli/knowledge.js +213 -0
- package/dist/cli/kraftwerk.js +12 -10
- package/dist/cli/runs.js +13 -6
- package/dist/cli/ui.js +3 -4
- package/dist/config.d.ts +9 -0
- package/dist/config.js +5 -2
- package/dist/gates.d.ts +7 -0
- package/dist/gates.js +27 -0
- package/dist/harnesses/claude.js +0 -1
- package/dist/inspector/chat/acp.d.ts +2 -2
- package/dist/inspector/chat/acp.js +44 -3
- package/dist/inspector/chat/backend.d.ts +14 -0
- package/dist/inspector/chat/pi.d.ts +2 -0
- package/dist/inspector/chat/pi.js +106 -0
- package/dist/inspector/chat/sessions.d.ts +33 -0
- package/dist/inspector/chat/sessions.js +407 -0
- package/dist/inspector/chat/store.d.ts +8 -0
- package/dist/inspector/chat/store.js +69 -0
- package/dist/inspector/chat/types.d.ts +7 -0
- package/dist/inspector/context.d.ts +7 -2
- package/dist/inspector/context.js +11 -3
- package/dist/inspector/knowledge.d.ts +24 -0
- package/dist/inspector/knowledge.js +41 -0
- package/dist/inspector/routines.d.ts +56 -0
- package/dist/inspector/routines.js +266 -0
- package/dist/inspector/runner.d.ts +10 -0
- package/dist/inspector/runner.js +6 -8
- package/dist/inspector/runs.d.ts +2 -0
- package/dist/inspector/runs.js +17 -12
- package/dist/inspector/server.d.ts +2 -0
- package/dist/inspector/server.js +311 -1
- package/dist/inspector/skills.d.ts +26 -0
- package/dist/inspector/skills.js +65 -0
- package/dist/inspector/team.d.ts +57 -0
- package/dist/inspector/team.js +121 -0
- package/dist/okf.d.ts +118 -0
- package/dist/okf.js +398 -0
- package/dist/runner/docker.d.ts +3 -1
- package/dist/runner/docker.js +5 -5
- package/dist/workflow.d.ts +7 -0
- package/dist/workflow.js +19 -0
- package/dist/yaml.js +43 -10
- package/inspector/dist/assets/index-C8AKqc_i.css +1 -0
- package/inspector/dist/assets/index-G3eFm1X4.js +13 -0
- package/inspector/dist/assets/roboto-latin-400-normal-BqEyEoaF.woff2 +0 -0
- package/inspector/dist/assets/roboto-latin-400-normal-D27F5sTu.woff +0 -0
- package/inspector/dist/assets/roboto-latin-500-normal-7RbcRiD8.woff2 +0 -0
- package/inspector/dist/assets/roboto-latin-500-normal-fsTFPL3E.woff +0 -0
- package/inspector/dist/assets/roboto-latin-700-normal-BZpUvMxY.woff2 +0 -0
- package/inspector/dist/assets/roboto-latin-700-normal-YuyVweIx.woff +0 -0
- package/inspector/dist/assets/roboto-mono-latin-400-normal-DBZPkcnn.woff +0 -0
- package/inspector/dist/assets/roboto-mono-latin-400-normal-GekRknry.woff2 +0 -0
- package/inspector/dist/assets/roboto-mono-latin-500-normal-D6j6AMzd.woff +0 -0
- package/inspector/dist/assets/roboto-mono-latin-500-normal-pxIOXZmA.woff2 +0 -0
- package/inspector/dist/assets/roboto-mono-latin-700-normal-CHYrj1DZ.woff +0 -0
- package/inspector/dist/assets/roboto-mono-latin-700-normal-foxDnrFi.woff2 +0 -0
- package/inspector/dist/index.html +2 -2
- package/package.json +1 -1
- package/schema/workflow.schema.json +166 -31
- package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-7sUh57Bg.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-CNHXzs6v.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bth3BMcD.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bxkt5Cjx.woff2 +0 -0
- package/inspector/dist/assets/index-BCSWkslF.css +0 -1
- package/inspector/dist/assets/index-D6WuziFQ.js +0 -9
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parse, stringify } from "yaml";
|
|
4
|
+
import { getOutputDir } from "./context.js";
|
|
5
|
+
import { getMember, listMembers, safeMemberSlug, teamRoot } from "./team.js";
|
|
6
|
+
import { createChat, postMessage } from "./chat/sessions.js";
|
|
7
|
+
/* ---------- cron ---------- */
|
|
8
|
+
const ALIASES = {
|
|
9
|
+
"@hourly": "0 * * * *",
|
|
10
|
+
"@daily": "0 0 * * *",
|
|
11
|
+
"@weekly": "0 0 * * 0",
|
|
12
|
+
"@monthly": "0 0 1 * *",
|
|
13
|
+
};
|
|
14
|
+
function parseField(spec, min, max, label) {
|
|
15
|
+
const out = new Set();
|
|
16
|
+
for (const part of spec.split(",")) {
|
|
17
|
+
const m = /^(\*|\d+(?:-\d+)?)(?:\/(\d+))?$/.exec(part);
|
|
18
|
+
if (!m)
|
|
19
|
+
throw new Error(`invalid ${label} field "${part}"`);
|
|
20
|
+
const step = m[2] ? Number(m[2]) : 1;
|
|
21
|
+
if (step < 1)
|
|
22
|
+
throw new Error(`invalid step in ${label} field`);
|
|
23
|
+
let lo = min;
|
|
24
|
+
let hi = max;
|
|
25
|
+
if (m[1] !== "*") {
|
|
26
|
+
const [a, b] = m[1].split("-").map(Number);
|
|
27
|
+
lo = a;
|
|
28
|
+
hi = b ?? (m[2] ? max : a); // "a/n" means a..max stepped; bare "a" is exact
|
|
29
|
+
}
|
|
30
|
+
if (lo < min || hi > max || lo > hi)
|
|
31
|
+
throw new Error(`${label} field out of range (${min}-${max})`);
|
|
32
|
+
for (let v = lo; v <= hi; v += step)
|
|
33
|
+
out.add(v);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
export function parseCron(expr) {
|
|
38
|
+
const fields = (ALIASES[expr.trim()] ?? expr.trim()).split(/\s+/);
|
|
39
|
+
if (fields.length !== 5) {
|
|
40
|
+
throw new Error("schedule must be 5 cron fields (min hour dom month dow) or an @alias");
|
|
41
|
+
}
|
|
42
|
+
const spec = {
|
|
43
|
+
minute: parseField(fields[0], 0, 59, "minute"),
|
|
44
|
+
hour: parseField(fields[1], 0, 23, "hour"),
|
|
45
|
+
dom: parseField(fields[2], 1, 31, "day-of-month"),
|
|
46
|
+
month: parseField(fields[3], 1, 12, "month"),
|
|
47
|
+
dow: new Set(
|
|
48
|
+
// 7 is an alias for Sunday (0)
|
|
49
|
+
[...parseField(fields[4], 0, 7, "day-of-week")].map((v) => (v === 7 ? 0 : v))),
|
|
50
|
+
domStar: fields[2] === "*",
|
|
51
|
+
dowStar: fields[4] === "*",
|
|
52
|
+
};
|
|
53
|
+
return spec;
|
|
54
|
+
}
|
|
55
|
+
function cronMatches(spec, d) {
|
|
56
|
+
if (!spec.minute.has(d.getMinutes()))
|
|
57
|
+
return false;
|
|
58
|
+
if (!spec.hour.has(d.getHours()))
|
|
59
|
+
return false;
|
|
60
|
+
if (!spec.month.has(d.getMonth() + 1))
|
|
61
|
+
return false;
|
|
62
|
+
const domOk = spec.dom.has(d.getDate());
|
|
63
|
+
const dowOk = spec.dow.has(d.getDay());
|
|
64
|
+
// Standard cron rule: if both day fields are restricted, either may match.
|
|
65
|
+
if (!spec.domStar && !spec.dowStar)
|
|
66
|
+
return domOk || dowOk;
|
|
67
|
+
return domOk && dowOk;
|
|
68
|
+
}
|
|
69
|
+
/** Next matching instant after `from`, scanning up to 60 days; undefined if none. */
|
|
70
|
+
export function nextRun(schedule, from = new Date()) {
|
|
71
|
+
let spec;
|
|
72
|
+
try {
|
|
73
|
+
spec = parseCron(schedule);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const d = new Date(from);
|
|
79
|
+
d.setSeconds(0, 0);
|
|
80
|
+
for (let i = 0; i < 60 * 24 * 60; i++) {
|
|
81
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
82
|
+
if (cronMatches(spec, d))
|
|
83
|
+
return d.toISOString();
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
/* ---------- definition CRUD (agents/<slug>/routines.yml) ---------- */
|
|
88
|
+
async function routinesFile(slug) {
|
|
89
|
+
return path.join(await teamRoot(), safeMemberSlug(slug), "routines.yml");
|
|
90
|
+
}
|
|
91
|
+
function normalizeRoutine(raw) {
|
|
92
|
+
const id = String(raw.id ?? "").trim();
|
|
93
|
+
if (!id)
|
|
94
|
+
return null;
|
|
95
|
+
return {
|
|
96
|
+
id,
|
|
97
|
+
name: String(raw.name ?? id),
|
|
98
|
+
schedule: String(raw.schedule ?? ""),
|
|
99
|
+
prompt: String(raw.prompt ?? ""),
|
|
100
|
+
enabled: raw.enabled !== false,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export async function listRoutines(slug) {
|
|
104
|
+
let raw;
|
|
105
|
+
try {
|
|
106
|
+
raw = parse(await fs.readFile(await routinesFile(slug), "utf8"));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
if (!Array.isArray(raw))
|
|
112
|
+
return [];
|
|
113
|
+
return raw
|
|
114
|
+
.map((r) => normalizeRoutine((r ?? {})))
|
|
115
|
+
.filter(Boolean);
|
|
116
|
+
}
|
|
117
|
+
async function writeRoutines(slug, routines) {
|
|
118
|
+
await fs.writeFile(await routinesFile(slug), stringify(routines));
|
|
119
|
+
}
|
|
120
|
+
export async function saveRoutine(slug, input) {
|
|
121
|
+
const name = input.name?.trim();
|
|
122
|
+
if (!name)
|
|
123
|
+
throw new Error("name is required");
|
|
124
|
+
if (!input.prompt?.trim())
|
|
125
|
+
throw new Error("prompt is required");
|
|
126
|
+
parseCron(input.schedule); // throws with a useful message on a bad schedule
|
|
127
|
+
const id = input.id?.trim() ||
|
|
128
|
+
name
|
|
129
|
+
.toLowerCase()
|
|
130
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
131
|
+
.replace(/^-+|-+$/g, "")
|
|
132
|
+
.slice(0, 48);
|
|
133
|
+
if (!id)
|
|
134
|
+
throw new Error("name must contain at least one letter or digit");
|
|
135
|
+
const routine = {
|
|
136
|
+
id,
|
|
137
|
+
name,
|
|
138
|
+
schedule: input.schedule.trim(),
|
|
139
|
+
prompt: input.prompt,
|
|
140
|
+
enabled: input.enabled !== false,
|
|
141
|
+
};
|
|
142
|
+
const routines = await listRoutines(slug);
|
|
143
|
+
const i = routines.findIndex((r) => r.id === id);
|
|
144
|
+
if (i >= 0)
|
|
145
|
+
routines[i] = routine;
|
|
146
|
+
else
|
|
147
|
+
routines.push(routine);
|
|
148
|
+
await writeRoutines(slug, routines);
|
|
149
|
+
return routine;
|
|
150
|
+
}
|
|
151
|
+
export async function deleteRoutine(slug, id) {
|
|
152
|
+
const routines = await listRoutines(slug);
|
|
153
|
+
await writeRoutines(slug, routines.filter((r) => r.id !== id));
|
|
154
|
+
}
|
|
155
|
+
const stateFile = () => path.join(getOutputDir(), "routines-state.json");
|
|
156
|
+
async function readState() {
|
|
157
|
+
try {
|
|
158
|
+
return JSON.parse(await fs.readFile(stateFile(), "utf8"));
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return {};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function writeState(state) {
|
|
165
|
+
await fs.mkdir(getOutputDir(), { recursive: true });
|
|
166
|
+
await fs.writeFile(stateFile(), JSON.stringify(state, null, 2));
|
|
167
|
+
}
|
|
168
|
+
export async function routineStatuses(slug) {
|
|
169
|
+
const [routines, state] = await Promise.all([listRoutines(slug), readState()]);
|
|
170
|
+
return routines.map((r) => {
|
|
171
|
+
const s = state[`${slug}/${r.id}`] ?? {};
|
|
172
|
+
return {
|
|
173
|
+
...r,
|
|
174
|
+
...(r.enabled ? { nextRunAt: nextRun(r.schedule) } : {}),
|
|
175
|
+
...(s.lastRunAt ? { lastRunAt: s.lastRunAt } : {}),
|
|
176
|
+
...(s.lastChatId ? { lastChatId: s.lastChatId } : {}),
|
|
177
|
+
...(s.lastError ? { lastError: s.lastError } : {}),
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/* ---------- firing ---------- */
|
|
182
|
+
/** Open a fresh session for the member and post the routine's prompt. */
|
|
183
|
+
async function fireRoutine(slug, routine) {
|
|
184
|
+
const member = await getMember(slug);
|
|
185
|
+
if (!member)
|
|
186
|
+
throw new Error(`team agent "${slug}" not found`);
|
|
187
|
+
const meta = await createChat({
|
|
188
|
+
agent: member.harness,
|
|
189
|
+
scope: { kind: "team", member: slug, routine: routine.id },
|
|
190
|
+
title: `⏰ ${routine.name}`,
|
|
191
|
+
});
|
|
192
|
+
const { error } = await postMessage(meta.id, routine.prompt);
|
|
193
|
+
if (error)
|
|
194
|
+
throw new Error(error);
|
|
195
|
+
return meta.id;
|
|
196
|
+
}
|
|
197
|
+
export async function runRoutineNow(slug, id) {
|
|
198
|
+
const routine = (await listRoutines(slug)).find((r) => r.id === id);
|
|
199
|
+
if (!routine)
|
|
200
|
+
throw new Error("routine not found");
|
|
201
|
+
const state = await readState();
|
|
202
|
+
const key = `${slug}/${id}`;
|
|
203
|
+
try {
|
|
204
|
+
const chatId = await fireRoutine(slug, routine);
|
|
205
|
+
state[key] = { ...state[key], lastRunAt: new Date().toISOString(), lastChatId: chatId };
|
|
206
|
+
delete state[key].lastError;
|
|
207
|
+
await writeState(state);
|
|
208
|
+
return { chatId };
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
state[key] = { ...state[key], lastError: err.message };
|
|
212
|
+
await writeState(state);
|
|
213
|
+
throw err;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/* ---------- scheduler ---------- */
|
|
217
|
+
let timer = null;
|
|
218
|
+
async function tick() {
|
|
219
|
+
const now = new Date();
|
|
220
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
221
|
+
const minuteKey = `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}T${p(now.getHours())}:${p(now.getMinutes())}`;
|
|
222
|
+
let members;
|
|
223
|
+
try {
|
|
224
|
+
members = await listMembers();
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const state = await readState();
|
|
230
|
+
let dirty = false;
|
|
231
|
+
for (const m of members) {
|
|
232
|
+
for (const r of await listRoutines(m.slug).catch(() => [])) {
|
|
233
|
+
if (!r.enabled)
|
|
234
|
+
continue;
|
|
235
|
+
let due = false;
|
|
236
|
+
try {
|
|
237
|
+
due = cronMatches(parseCron(r.schedule), now);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
continue; // invalid schedule — surfaced by validation on save, skip here
|
|
241
|
+
}
|
|
242
|
+
const key = `${m.slug}/${r.id}`;
|
|
243
|
+
if (!due || state[key]?.lastMinute === minuteKey)
|
|
244
|
+
continue;
|
|
245
|
+
state[key] = { ...state[key], lastMinute: minuteKey };
|
|
246
|
+
dirty = true;
|
|
247
|
+
try {
|
|
248
|
+
const chatId = await fireRoutine(m.slug, r);
|
|
249
|
+
state[key] = { ...state[key], lastRunAt: now.toISOString(), lastChatId: chatId };
|
|
250
|
+
delete state[key].lastError;
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
state[key] = { ...state[key], lastError: err.message };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (dirty)
|
|
258
|
+
await writeState(state);
|
|
259
|
+
}
|
|
260
|
+
/** Start the in-process scheduler; safe to call more than once. */
|
|
261
|
+
export function startRoutineScheduler() {
|
|
262
|
+
if (timer)
|
|
263
|
+
return;
|
|
264
|
+
timer = setInterval(() => void tick().catch(() => { }), 20_000);
|
|
265
|
+
timer.unref?.();
|
|
266
|
+
}
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow trigger for the web UI. The inspector stays decoupled from the
|
|
3
|
+
* framework runtime: it shells out to `npx kraftwerk run` in the consumer
|
|
4
|
+
* project, detached, with a pre-chosen --run-id so the browser can jump to
|
|
5
|
+
* /runs/<id> immediately and watch the (already polling) live timeline.
|
|
6
|
+
*
|
|
7
|
+
* Sandbox mode adds --sandbox: one Docker container per run, workflow
|
|
8
|
+
* mounted read-only, run dir bind-mounted back into output/ (see
|
|
9
|
+
* kraftwerk/src/runner/docker.ts).
|
|
10
|
+
*/
|
|
1
11
|
export declare function dockerStatus(): {
|
|
2
12
|
available: boolean;
|
|
3
13
|
image: boolean;
|
package/dist/inspector/runner.js
CHANGED
|
@@ -2,6 +2,8 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
2
2
|
import { mkdirSync, openSync, closeSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { getProjectRoot } from "./context.js";
|
|
5
|
+
import { RUN_ID_RE, safeRunDir } from "./runs.js";
|
|
6
|
+
import { newRunId } from "../workflow.js";
|
|
5
7
|
/**
|
|
6
8
|
* Workflow trigger for the web UI. The inspector stays decoupled from the
|
|
7
9
|
* framework runtime: it shells out to `npx kraftwerk run` in the consumer
|
|
@@ -12,10 +14,6 @@ import { getProjectRoot } from "./context.js";
|
|
|
12
14
|
* mounted read-only, run dir bind-mounted back into output/ (see
|
|
13
15
|
* kraftwerk/src/runner/docker.ts).
|
|
14
16
|
*/
|
|
15
|
-
function stamp(d = new Date()) {
|
|
16
|
-
const p = (n) => String(n).padStart(2, "0");
|
|
17
|
-
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}-${p(d.getSeconds())}`;
|
|
18
|
-
}
|
|
19
17
|
export function dockerStatus() {
|
|
20
18
|
const daemon = spawnSync("docker", ["version", "--format", "ok"], {
|
|
21
19
|
stdio: "ignore",
|
|
@@ -37,9 +35,9 @@ export function triggerRun(opts) {
|
|
|
37
35
|
if (!docker.image)
|
|
38
36
|
throw new Error('Image "kraftwerk-runner" missing — run `kraftwerk runner build`.');
|
|
39
37
|
}
|
|
40
|
-
const runId =
|
|
41
|
-
// Matches the CLI's --run-id resolution: output/<id
|
|
42
|
-
const runDir =
|
|
38
|
+
const runId = newRunId(opts.workflowName);
|
|
39
|
+
// Matches the CLI's --run-id resolution: <output dir>/runs/<id>.
|
|
40
|
+
const runDir = safeRunDir(runId);
|
|
43
41
|
mkdirSync(runDir, { recursive: true });
|
|
44
42
|
const log = openSync(path.join(runDir, "trigger.log"), "a");
|
|
45
43
|
const args = ["kraftwerk", "run", "--yes", "--run-id", runId];
|
|
@@ -60,7 +58,7 @@ export function triggerRun(opts) {
|
|
|
60
58
|
}
|
|
61
59
|
export function stopRun(runId) {
|
|
62
60
|
// Sandboxed runs run in container kw-<runId>; docker stop ends them.
|
|
63
|
-
if (
|
|
61
|
+
if (!RUN_ID_RE.test(runId))
|
|
64
62
|
return false;
|
|
65
63
|
return (spawnSync("docker", ["stop", `kw-${runId}`], { stdio: "ignore", timeout: 30_000 }).status === 0);
|
|
66
64
|
}
|
package/dist/inspector/runs.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface RunDetail extends RunListItem {
|
|
|
49
49
|
files: FileView[];
|
|
50
50
|
}
|
|
51
51
|
export declare function listRuns(): Promise<RunListItem[]>;
|
|
52
|
+
/** "2026-08-25-1432-07-hello" (runs/ subdir) or legacy "run-…" (output root). */
|
|
53
|
+
export declare const RUN_ID_RE: RegExp;
|
|
52
54
|
export declare function safeRunDir(id: string): string;
|
|
53
55
|
export declare function getRun(id: string): Promise<RunDetail | null>;
|
|
54
56
|
export declare function readRunFile(id: string, name: string): Promise<{
|
package/dist/inspector/runs.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { getOutputDir } from "./context.js";
|
|
4
4
|
/**
|
|
5
5
|
* Filesystem + trace.jsonl reading for the inspector. The output directory
|
|
6
|
-
* holds one folder per run (
|
|
6
|
+
* holds one folder per run (runs/<stamp>-<workflow>, legacy run-* at the root);
|
|
7
7
|
* trace.jsonl written by the framework plus the run's working files.
|
|
8
8
|
*/
|
|
9
9
|
/** A run with no trace update for this long and no summary counts as aborted. */
|
|
@@ -158,15 +158,14 @@ function shortenPath(p) {
|
|
|
158
158
|
return parts.length > 2 ? parts.slice(-2).join("/") : p;
|
|
159
159
|
}
|
|
160
160
|
export async function listRuns() {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
161
|
+
// New runs live under <output>/runs/; legacy run-* folders sit at the root.
|
|
162
|
+
const current = (await fs.readdir(path.join(getOutputDir(), "runs")).catch(() => []))
|
|
163
|
+
.filter((e) => RUN_ID_RE.test(e) && !e.startsWith("run-"));
|
|
164
|
+
const legacy = (await fs.readdir(getOutputDir()).catch(() => []))
|
|
165
|
+
.filter((e) => e.startsWith("run-") && RUN_ID_RE.test(e));
|
|
166
|
+
const entries = [...current, ...legacy];
|
|
168
167
|
const items = await Promise.all(entries.map(async (id) => {
|
|
169
|
-
const runDir =
|
|
168
|
+
const runDir = safeRunDir(id);
|
|
170
169
|
const st = await fs.stat(runDir).catch(() => null);
|
|
171
170
|
if (!st?.isDirectory())
|
|
172
171
|
return null;
|
|
@@ -188,12 +187,18 @@ export async function listRuns() {
|
|
|
188
187
|
costUsd: summary?.total?.costUsd,
|
|
189
188
|
};
|
|
190
189
|
}));
|
|
191
|
-
|
|
190
|
+
// Newest first; strip the legacy "run-" prefix so both id styles compare by stamp.
|
|
191
|
+
const stampOf = (r) => r.id.replace(/^run-/, "");
|
|
192
|
+
return items.filter(Boolean).sort((a, b) => stampOf(b).localeCompare(stampOf(a)));
|
|
192
193
|
}
|
|
194
|
+
/** "2026-08-25-1432-07-hello" (runs/ subdir) or legacy "run-…" (output root). */
|
|
195
|
+
export const RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
193
196
|
export function safeRunDir(id) {
|
|
194
|
-
if (
|
|
197
|
+
if (!RUN_ID_RE.test(id))
|
|
195
198
|
throw new Error("invalid run id");
|
|
196
|
-
return
|
|
199
|
+
return id.startsWith("run-")
|
|
200
|
+
? path.join(getOutputDir(), id)
|
|
201
|
+
: path.join(getOutputDir(), "runs", id);
|
|
197
202
|
}
|
|
198
203
|
export async function getRun(id) {
|
|
199
204
|
const runDir = safeRunDir(id);
|
|
@@ -3,6 +3,8 @@ export interface InspectorOptions {
|
|
|
3
3
|
outputDir: string;
|
|
4
4
|
staticDir: string;
|
|
5
5
|
port: number;
|
|
6
|
+
/** Consumer project root; defaults to the parent of outputDir. */
|
|
7
|
+
projectRoot?: string;
|
|
6
8
|
}
|
|
7
9
|
/** Start the server; resolves once it listens. Runs until the process ends. */
|
|
8
10
|
export declare function startInspector(opts: InspectorOptions): Promise<http.Server>;
|