@netnodeag/kraftwerk 0.33.0 → 0.34.1
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 +317 -242
- package/dist/cli/projects.js +30 -8
- package/dist/inspector/instances.d.ts +40 -0
- package/dist/inspector/instances.js +120 -1
- package/dist/inspector/server.js +18 -1
- package/inspector/dist/assets/{editor-BlAeAqt0.js → editor-CXrDJOv_.js} +3 -3
- package/inspector/dist/assets/{index-Dnqjpy0h.js → index-7J4zAS6J.js} +1 -1
- package/inspector/dist/assets/index-B7rwNdVj.css +1 -0
- package/inspector/dist/assets/index-BBGjdXk1.js +213 -0
- package/inspector/dist/assets/{index-e-11MUIo.js → index-BIDsfw8C.js} +1 -1
- package/inspector/dist/assets/{index--wwp-KD0.js → index-BNcXQYbv.js} +1 -1
- package/inspector/dist/assets/{index-BvCAG0mQ.js → index-BQMgADlc.js} +1 -1
- package/inspector/dist/assets/{index-AR7uGZPa.js → index-BULB2rC_.js} +1 -1
- package/inspector/dist/assets/{index-BKnMNAOc.js → index-Bumwg9SM.js} +1 -1
- package/inspector/dist/assets/{index-DcXZX6dj.js → index-BxEG-IdY.js} +1 -1
- package/inspector/dist/assets/{index--9LQl_DL.js → index-C3hpUVfp.js} +1 -1
- package/inspector/dist/assets/{index-LBoNKP4N.js → index-CMMYSoL3.js} +1 -1
- package/inspector/dist/assets/{index-D3O4auYk.js → index-CNJyAniG.js} +1 -1
- package/inspector/dist/assets/{index-9rElziKS.js → index-CUCCMUMY.js} +1 -1
- package/inspector/dist/assets/{index-C3ugp_Wf.js → index-ChuqpBnv.js} +1 -1
- package/inspector/dist/assets/{index-CdkhUV5I.js → index-CszaAR9T.js} +1 -1
- package/inspector/dist/assets/{index-CV4Aiss-.js → index-DAi1VtJ8.js} +1 -1
- package/inspector/dist/assets/{index-B0qGMqfM.js → index-DrSWAGft.js} +1 -1
- package/inspector/dist/assets/{index-BvGhLr64.js → index-Upf-BEsx.js} +1 -1
- package/inspector/dist/assets/{index-BxPjb1aG.js → index-x5G6Np7T.js} +1 -1
- package/inspector/dist/index.html +2 -2
- package/package.json +1 -1
- package/inspector/dist/assets/index-B7W4QbA2.css +0 -1
- package/inspector/dist/assets/index-C5IBvIVa.js +0 -213
package/dist/cli/projects.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import Table from "cli-table3";
|
|
4
|
-
import { discoverWorkspaces, forgetProject, startProject, tildify, } from "../inspector/instances.js";
|
|
4
|
+
import { discoverWorkspaces, forgetProject, startProject, stopProject, tildify, } from "../inspector/instances.js";
|
|
5
5
|
/**
|
|
6
6
|
* `kraftwerk projects` — every project that ever ran the inspector on this
|
|
7
7
|
* machine (~/.kraftwerk/projects), joined with what is running right now.
|
|
@@ -9,9 +9,11 @@ import { discoverWorkspaces, forgetProject, startProject, tildify, } from "../in
|
|
|
9
9
|
*
|
|
10
10
|
* kraftwerk projects list: name, root, running/stopped
|
|
11
11
|
* kraftwerk projects start <ref> relaunch `kraftwerk ui` in that root, detached
|
|
12
|
+
* kraftwerk projects stop <ref> SIGTERM a running UI (also ones started in a terminal)
|
|
12
13
|
* kraftwerk projects forget <ref> drop the record (the folder stays)
|
|
13
14
|
*
|
|
14
|
-
* <ref> is a project name, the root's folder name,
|
|
15
|
+
* <ref> is a project name, the root's folder name, the root path, or a
|
|
16
|
+
* port / localhost:port for running instances that recorded no root.
|
|
15
17
|
*/
|
|
16
18
|
const fmtAgo = (iso) => {
|
|
17
19
|
if (!iso)
|
|
@@ -38,28 +40,31 @@ const status = (e) => {
|
|
|
38
40
|
const clean = !!e.lastStopped && !!e.lastStarted && e.lastStopped >= e.lastStarted;
|
|
39
41
|
return clean ? chalk.dim("stopped") : chalk.yellow("died");
|
|
40
42
|
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
/** Match a ref against known workspaces; `needRoot` excludes rootless live instances (start/forget need a root). */
|
|
44
|
+
async function resolveRef(ref, needRoot = true) {
|
|
45
|
+
const all = (await discoverWorkspaces()).filter((e) => e.root || !needRoot);
|
|
43
46
|
const abs = path.resolve(ref);
|
|
47
|
+
const port = /^\d+$/.test(ref) ? `http://localhost:${ref}` : `http://${ref.replace(/^https?:\/\//, "")}`;
|
|
44
48
|
const hits = all.filter((e) => e.root === abs ||
|
|
45
49
|
e.root === ref ||
|
|
46
50
|
e.name === ref ||
|
|
47
|
-
path.basename(e.root) === ref)
|
|
51
|
+
(e.root && path.basename(e.root) === ref) ||
|
|
52
|
+
e.url === port);
|
|
48
53
|
if (hits.length === 1)
|
|
49
54
|
return hits[0];
|
|
50
55
|
if (hits.length === 0) {
|
|
51
56
|
console.error(chalk.red(`No known project "${ref}". See \`kraftwerk projects\`.`));
|
|
52
57
|
process.exit(2);
|
|
53
58
|
}
|
|
54
|
-
console.error(chalk.red(`"${ref}" is ambiguous — use the root path:`));
|
|
59
|
+
console.error(chalk.red(`"${ref}" is ambiguous — use the root path or port:`));
|
|
55
60
|
for (const h of hits)
|
|
56
|
-
console.error(` ${h.root}`);
|
|
61
|
+
console.error(` ${h.root ?? h.url}`);
|
|
57
62
|
process.exit(2);
|
|
58
63
|
}
|
|
59
64
|
export function registerProjectCommands(program) {
|
|
60
65
|
const projects = program
|
|
61
66
|
.command("projects")
|
|
62
|
-
.description("Known kraftwerk projects on this machine: list, start a stopped UI, forget");
|
|
67
|
+
.description("Known kraftwerk projects on this machine: list, start a stopped UI, stop a running one, forget");
|
|
63
68
|
projects
|
|
64
69
|
.command("list", { isDefault: true })
|
|
65
70
|
.description("List known projects with their root and whether the UI is running")
|
|
@@ -109,6 +114,23 @@ export function registerProjectCommands(program) {
|
|
|
109
114
|
console.log(`${chalk.green("✔")} ${entry.name} ${r.live ? "running" : "starting"}: ${chalk.cyan(r.url)}` +
|
|
110
115
|
chalk.dim(` (pid ${r.pid}${r.log ? `, log ${tildify(r.log)}` : ""})`));
|
|
111
116
|
});
|
|
117
|
+
projects
|
|
118
|
+
.command("stop")
|
|
119
|
+
.description("Stop a running UI (SIGTERM to its server; works for terminal-started ones too)")
|
|
120
|
+
.argument("<ref>", "Project name, folder name, root path, or port")
|
|
121
|
+
.action(async (ref) => {
|
|
122
|
+
const entry = await resolveRef(ref, false);
|
|
123
|
+
if (!entry.live) {
|
|
124
|
+
console.log(chalk.dim(`${entry.name} is not running.`));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const r = await stopProject({ root: entry.root, url: entry.url });
|
|
128
|
+
if (!r.ok) {
|
|
129
|
+
console.error(chalk.red(`Could not stop ${entry.name}: ${r.error}`));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
console.log(`${chalk.green("✔")} ${entry.name} stopped ${chalk.dim(`(${entry.url.replace(/^https?:\/\//, "")})`)}`);
|
|
133
|
+
});
|
|
112
134
|
projects
|
|
113
135
|
.command("forget")
|
|
114
136
|
.description("Remove a project from the registry (its folder is untouched)")
|
|
@@ -14,6 +14,8 @@ export interface DiscoveredInstance {
|
|
|
14
14
|
icon?: string;
|
|
15
15
|
live: true;
|
|
16
16
|
root?: string;
|
|
17
|
+
/** Server process id (what to signal to stop it). */
|
|
18
|
+
pid: number;
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
19
21
|
* One workspace as the switcher shows it: a known project (running or
|
|
@@ -56,6 +58,31 @@ export declare function forgetProject(root: string): Promise<boolean>;
|
|
|
56
58
|
* discoverInstances — /api/meta is polled and this stats every root.
|
|
57
59
|
*/
|
|
58
60
|
export declare function discoverWorkspaces(): Promise<WorkspaceEntry[]>;
|
|
61
|
+
export type WorkspaceState = "running" | "stopped" | "died" | "missing" | "orphaned";
|
|
62
|
+
/** A workspace with everything the admin screen shows. */
|
|
63
|
+
export interface WorkspaceDetail extends WorkspaceEntry {
|
|
64
|
+
/** The instance answering this request. */
|
|
65
|
+
current?: boolean;
|
|
66
|
+
/** kraftwerk.yml present in the root itself (not inherited from an ancestor). */
|
|
67
|
+
hasConfig?: boolean;
|
|
68
|
+
/** The root directory itself is gone (vs. still there but no longer a project). */
|
|
69
|
+
dirGone?: boolean;
|
|
70
|
+
state: WorkspaceState;
|
|
71
|
+
firstSeen?: string;
|
|
72
|
+
startCount?: number;
|
|
73
|
+
counts?: {
|
|
74
|
+
agents: number;
|
|
75
|
+
workflows: number;
|
|
76
|
+
runs: number;
|
|
77
|
+
chats: number;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Every workspace including this one, with config presence, counts of
|
|
82
|
+
* what lives in the root, and a single derived state. Not cached — the
|
|
83
|
+
* admin screen polls slowly and wants fresh counts.
|
|
84
|
+
*/
|
|
85
|
+
export declare function listWorkspacesDetailed(): Promise<WorkspaceDetail[]>;
|
|
59
86
|
export interface StartResult {
|
|
60
87
|
ok: boolean;
|
|
61
88
|
url?: string;
|
|
@@ -74,3 +101,16 @@ export interface StartResult {
|
|
|
74
101
|
* straight to it.
|
|
75
102
|
*/
|
|
76
103
|
export declare function startProject(root: string): Promise<StartResult>;
|
|
104
|
+
/**
|
|
105
|
+
* Stop a running workspace: SIGTERM to its server process, which
|
|
106
|
+
* unregisters, stamps lastStopped and exits; the `kraftwerk ui` supervisor
|
|
107
|
+
* follows. Matched by root, or by url for instances from older versions
|
|
108
|
+
* that registered no root. Waits up to ~5s for the port to go quiet.
|
|
109
|
+
*/
|
|
110
|
+
export declare function stopProject(target: {
|
|
111
|
+
root?: string;
|
|
112
|
+
url?: string;
|
|
113
|
+
}): Promise<{
|
|
114
|
+
ok: boolean;
|
|
115
|
+
error?: string;
|
|
116
|
+
}>;
|
|
@@ -109,7 +109,7 @@ export async function discoverInstances() {
|
|
|
109
109
|
await fs.unlink(path.join(INSTANCES_DIR, f)).catch(() => { }); // stale — prune
|
|
110
110
|
return null;
|
|
111
111
|
}
|
|
112
|
-
return { ...found, root: rec.root };
|
|
112
|
+
return { ...found, root: rec.root, pid: rec.pid };
|
|
113
113
|
}));
|
|
114
114
|
const entries = probed
|
|
115
115
|
.filter((e) => e != null)
|
|
@@ -264,6 +264,92 @@ export async function discoverWorkspaces() {
|
|
|
264
264
|
wsCache = { at: Date.now(), entries };
|
|
265
265
|
return entries;
|
|
266
266
|
}
|
|
267
|
+
const countDirs = async (dir, marker) => {
|
|
268
|
+
try {
|
|
269
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
270
|
+
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith("."));
|
|
271
|
+
if (!marker)
|
|
272
|
+
return dirs.length;
|
|
273
|
+
const flags = await Promise.all(dirs.map((d) => fs.stat(path.join(dir, d.name, marker)).then(() => true, () => false)));
|
|
274
|
+
return flags.filter(Boolean).length;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const countEntries = async (dir) => {
|
|
281
|
+
try {
|
|
282
|
+
return (await fs.readdir(dir)).filter((n) => !n.startsWith(".")).length;
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* Every workspace including this one, with config presence, counts of
|
|
290
|
+
* what lives in the root, and a single derived state. Not cached — the
|
|
291
|
+
* admin screen polls slowly and wants fresh counts.
|
|
292
|
+
*/
|
|
293
|
+
export async function listWorkspacesDetailed() {
|
|
294
|
+
const others = await discoverWorkspaces();
|
|
295
|
+
const all = [...others];
|
|
296
|
+
if (selfRoot && selfPort) {
|
|
297
|
+
const d = await describeRoot(selfRoot);
|
|
298
|
+
const rec = await readProject(projectFile(selfRoot));
|
|
299
|
+
all.unshift({
|
|
300
|
+
name: d.name,
|
|
301
|
+
icon: d.icon,
|
|
302
|
+
url: `http://localhost:${selfPort}`,
|
|
303
|
+
live: true,
|
|
304
|
+
root: selfRoot,
|
|
305
|
+
rootLabel: tildify(selfRoot),
|
|
306
|
+
exists: true,
|
|
307
|
+
lastStarted: rec?.lastStarted,
|
|
308
|
+
lastStopped: rec?.lastStopped,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return Promise.all(all.map(async (e) => {
|
|
312
|
+
const current = !!selfRoot && e.root === selfRoot;
|
|
313
|
+
const base = { ...e, current, state: e.live ? "running" : "stopped" };
|
|
314
|
+
if (!e.root)
|
|
315
|
+
return base;
|
|
316
|
+
const rec = await readProject(projectFile(e.root));
|
|
317
|
+
base.firstSeen = rec?.firstSeen;
|
|
318
|
+
base.startCount = rec?.startCount;
|
|
319
|
+
// "missing" = the folder is gone; "orphaned" = it is still there but
|
|
320
|
+
// no longer a project of its own (kraftwerk.yml removed, or it now
|
|
321
|
+
// resolves to an ancestor). Both are removable from the registry.
|
|
322
|
+
const dirGone = !(await isDir(e.root));
|
|
323
|
+
base.dirGone = dirGone;
|
|
324
|
+
if (!e.live) {
|
|
325
|
+
if (dirGone)
|
|
326
|
+
base.state = "missing";
|
|
327
|
+
else if (e.exists === false)
|
|
328
|
+
base.state = "orphaned";
|
|
329
|
+
else if (!(rec?.lastStopped && rec.lastStarted && rec.lastStopped >= rec.lastStarted))
|
|
330
|
+
base.state = "died";
|
|
331
|
+
}
|
|
332
|
+
if (e.exists === false) {
|
|
333
|
+
base.hasConfig = false;
|
|
334
|
+
return base;
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const project = await resolveProject(e.root);
|
|
338
|
+
base.hasConfig = !!project.configPath && path.dirname(project.configPath) === path.resolve(e.root);
|
|
339
|
+
const [agents, workflows, runs, chats] = await Promise.all([
|
|
340
|
+
countDirs(path.resolve(project.root, project.config.agents ?? "agents"), "agent.yml"),
|
|
341
|
+
project.workflowsRoot ? countDirs(project.workflowsRoot, "workflow.yml") : Promise.resolve(0),
|
|
342
|
+
countDirs(path.join(project.outputDir, "runs")),
|
|
343
|
+
countEntries(path.join(project.outputDir, "chats")),
|
|
344
|
+
]);
|
|
345
|
+
base.counts = { agents, workflows, runs, chats };
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
base.hasConfig = false;
|
|
349
|
+
}
|
|
350
|
+
return base;
|
|
351
|
+
}));
|
|
352
|
+
}
|
|
267
353
|
/**
|
|
268
354
|
* Launch `kraftwerk ui` for a root as a detached process (selfCommand:
|
|
269
355
|
* the bin this process runs from), logging to ~/.kraftwerk/logs/<key>.log.
|
|
@@ -334,3 +420,36 @@ export async function startProject(root) {
|
|
|
334
420
|
}
|
|
335
421
|
return { ok: true, url, live: false, pid: child.pid, log };
|
|
336
422
|
}
|
|
423
|
+
/* ---------- stop a project ---------- */
|
|
424
|
+
/**
|
|
425
|
+
* Stop a running workspace: SIGTERM to its server process, which
|
|
426
|
+
* unregisters, stamps lastStopped and exits; the `kraftwerk ui` supervisor
|
|
427
|
+
* follows. Matched by root, or by url for instances from older versions
|
|
428
|
+
* that registered no root. Waits up to ~5s for the port to go quiet.
|
|
429
|
+
*/
|
|
430
|
+
export async function stopProject(target) {
|
|
431
|
+
if (target.root && path.resolve(target.root) === selfRoot) {
|
|
432
|
+
return { ok: false, error: "that is this workspace — stop it from its own terminal or pid" };
|
|
433
|
+
}
|
|
434
|
+
cache = wsCache = null;
|
|
435
|
+
const live = await discoverInstances();
|
|
436
|
+
const inst = live.find((i) => (target.root && i.root === path.resolve(target.root)) || (target.url && i.url === target.url));
|
|
437
|
+
if (!inst)
|
|
438
|
+
return { ok: false, error: "not running" };
|
|
439
|
+
try {
|
|
440
|
+
process.kill(inst.pid, "SIGTERM");
|
|
441
|
+
}
|
|
442
|
+
catch (err) {
|
|
443
|
+
return { ok: false, error: `could not signal pid ${inst.pid}: ${err.message}` };
|
|
444
|
+
}
|
|
445
|
+
const port = Number(new URL(inst.url).port);
|
|
446
|
+
const deadline = Date.now() + 5_000;
|
|
447
|
+
while (Date.now() < deadline) {
|
|
448
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
449
|
+
if (!(await probe(port))) {
|
|
450
|
+
cache = wsCache = null;
|
|
451
|
+
return { ok: true };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return { ok: false, error: `pid ${inst.pid} still answers on ${inst.url} after 5s` };
|
|
455
|
+
}
|
package/dist/inspector/server.js
CHANGED
|
@@ -10,7 +10,7 @@ import { cancelChat, createChat, deleteChat, disposeAllBackends, getChat, listCh
|
|
|
10
10
|
import { bundleDetail, conceptDetail, createBundle, knowledgeIndex, putConcept, verifyFromUi, } from "./knowledge.js";
|
|
11
11
|
import { deleteMember, getMember, listMembers, saveMember, setMemberArchived, teamRoot, } from "./team.js";
|
|
12
12
|
import { deleteAgentSkill, getSkill, listAgentSkills, listSkills, readSkill, saveAgentSkill, skillsRoot, } from "./skills.js";
|
|
13
|
-
import { discoverWorkspaces, forgetProject, markProjectStopped, registerInstance, registerProject, startProject, tildify, unregisterInstance, } from "./instances.js";
|
|
13
|
+
import { discoverWorkspaces, forgetProject, listWorkspacesDetailed, markProjectStopped, registerInstance, registerProject, startProject, stopProject, tildify, unregisterInstance, } from "./instances.js";
|
|
14
14
|
import { getSettings, saveSettings } from "./settings.js";
|
|
15
15
|
import { deleteRoutine, routineStatuses, runRoutineNow, saveRoutine, startRoutineScheduler, } from "./routines.js";
|
|
16
16
|
/**
|
|
@@ -123,6 +123,10 @@ async function handleApi(req, res, url) {
|
|
|
123
123
|
switcher,
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
|
+
// GET /api/projects — every known workspace incl. this one, with state + counts (admin screen)
|
|
127
|
+
if (seg.length === 2 && seg[1] === "projects" && method === "GET") {
|
|
128
|
+
return json(res, await listWorkspacesDetailed());
|
|
129
|
+
}
|
|
126
130
|
// POST /api/projects/start {root} — launch `kraftwerk ui` for a known
|
|
127
131
|
// project as a detached process (the switcher's Start button).
|
|
128
132
|
if (seg.length === 3 && seg[1] === "projects" && seg[2] === "start" && method === "POST") {
|
|
@@ -137,6 +141,19 @@ async function handleApi(req, res, url) {
|
|
|
137
141
|
return json(res, { error: err.message }, 400);
|
|
138
142
|
}
|
|
139
143
|
}
|
|
144
|
+
// POST /api/projects/stop {root | url} — SIGTERM a running workspace's server
|
|
145
|
+
if (seg.length === 3 && seg[1] === "projects" && seg[2] === "stop" && method === "POST") {
|
|
146
|
+
try {
|
|
147
|
+
const target = JSON.parse(await readBody(req));
|
|
148
|
+
if (!target.root && !target.url)
|
|
149
|
+
return json(res, { error: "root or url required" }, 400);
|
|
150
|
+
const result = await stopProject(target);
|
|
151
|
+
return json(res, result, result.ok ? 200 : 409);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
return json(res, { error: err.message }, 400);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
140
157
|
// POST /api/projects/forget {root} — drop a project from the registry
|
|
141
158
|
if (seg.length === 3 && seg[1] === "projects" && seg[2] === "forget" && method === "POST") {
|
|
142
159
|
try {
|