@netnodeag/kraftwerk 0.75.0 → 0.76.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/dist/cli/daemon-control.js +32 -13
- package/dist/cli/daemon-login.js +3 -3
- package/dist/cli/daemon.js +5 -4
- package/dist/cli/doctor.d.ts +3 -1
- package/dist/cli/doctor.js +40 -5
- package/dist/cli/init.js +5 -2
- package/dist/cli/kraftwerk.js +9 -5
- package/dist/cli/routines.d.ts +2 -1
- package/dist/cli/routines.js +40 -4
- package/dist/cli/whoami.d.ts +9 -0
- package/dist/cli/whoami.js +48 -0
- package/dist/client/routes.d.ts +3 -0
- package/dist/client/routes.js +3 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +38 -3
- package/dist/core/attention.d.ts +8 -1
- package/dist/core/attention.js +38 -2
- package/dist/core/daemon.d.ts +15 -1
- package/dist/core/daemon.js +46 -8
- package/dist/core/identity.d.ts +46 -0
- package/dist/core/identity.js +106 -0
- package/dist/core/routines.d.ts +81 -0
- package/dist/core/routines.js +174 -33
- package/dist/server/api/agents.d.ts +5 -1
- package/dist/server/api/agents.js +19 -2
- package/dist/server/api/index.d.ts +14 -3
- package/dist/server/api/runs.d.ts +3 -1
- package/dist/server/api/runs.js +9 -1
- package/dist/server/api/workspace.d.ts +6 -1
- package/dist/server/api/workspace.js +5 -0
- package/dist/server/push.js +1 -1
- package/package.json +1 -1
- package/schema/kraftwerk.schema.json +5 -0
- package/web/dist/assets/{dist-YNCNGGgK.js → dist-BQpWYkxm.js} +1 -1
- package/web/dist/assets/{dist-DaGnyH_l.js → dist-BTVP-6On.js} +1 -1
- package/web/dist/assets/dist-BURf1l-A.js +1 -0
- package/web/dist/assets/{dist-CLZjnhZZ.js → dist-BYkcY-Vl.js} +1 -1
- package/web/dist/assets/{dist-CzM6tn0f.js → dist-BwIca_Uj.js} +1 -1
- package/web/dist/assets/{dist-Y9aHncjU.js → dist-DFXmaDc5.js} +1 -1
- package/web/dist/assets/{dist-CezfArgg.js → dist-T1sNLC62.js} +1 -1
- package/web/dist/assets/{dist-CYicby0c.js → dist-fgB7OEJy.js} +1 -1
- package/web/dist/assets/{dist-QqFmuPda.js → dist-flnLXEzU.js} +1 -1
- package/web/dist/assets/{dist-BI4umsk6.js → dist-l7foWlFx.js} +1 -1
- package/web/dist/assets/{dist-CODxhtAK.js → dist-vuciRCSz.js} +1 -1
- package/web/dist/assets/{editor-DEb8z5-Q.js → editor-C6Ok8NPo.js} +3 -3
- package/web/dist/assets/index-z4tpCCzk.js +231 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/dist-DtktO5hc.js +0 -1
- package/web/dist/assets/index-kmyK4tFm.js +0 -231
|
@@ -2,9 +2,10 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { existsSync, mkdirSync, openSync, promises as fs } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
import { daemonLogPath, findDaemon, readDaemonFile } from "../core/daemon.js";
|
|
5
|
+
import { daemonLogPath, daemonState, findDaemon, readDaemonFile } from "../core/daemon.js";
|
|
6
6
|
import { tildify } from "../core/instances.js";
|
|
7
7
|
import { selfCommand } from "../core/self-command.js";
|
|
8
|
+
import { localIdentity } from "../core/identity.js";
|
|
8
9
|
import { getPkgVersion } from "../server/version.js";
|
|
9
10
|
import { daemonLoginItem, startLoginItem } from "./daemon-login.js";
|
|
10
11
|
/**
|
|
@@ -42,7 +43,7 @@ const ago = (iso) => {
|
|
|
42
43
|
};
|
|
43
44
|
async function getJson(port, p) {
|
|
44
45
|
try {
|
|
45
|
-
const r = await fetch(`http://127.0.0.1:${port}${p}`, { signal: AbortSignal.timeout(
|
|
46
|
+
const r = await fetch(`http://127.0.0.1:${port}${p}`, { signal: AbortSignal.timeout(3000) });
|
|
46
47
|
return r.ok ? (await r.json()) : null;
|
|
47
48
|
}
|
|
48
49
|
catch {
|
|
@@ -62,10 +63,14 @@ async function tail(n) {
|
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
async function status() {
|
|
65
|
-
const [
|
|
66
|
-
const base = { installed, loginItem: daemonLoginItem() !== null, log: daemonLogPath() };
|
|
67
|
-
if (
|
|
66
|
+
const [found, rec, installed, identity] = await Promise.all([daemonState(), readDaemonFile(), getPkgVersion(), localIdentity()]);
|
|
67
|
+
const base = { installed, loginItem: daemonLoginItem() !== null, log: daemonLogPath(), identity };
|
|
68
|
+
if (found.state === "stopped")
|
|
68
69
|
return { running: false, ...base };
|
|
70
|
+
const running = found;
|
|
71
|
+
if (found.state === "busy") {
|
|
72
|
+
return { running: true, busy: true, ...base, pid: found.pid, port: found.port, ...(rec?.version ? { version: rec.version } : {}), ...(rec?.startedAt ? { startedAt: rec.startedAt } : {}) };
|
|
73
|
+
}
|
|
69
74
|
const [hub, relay] = await Promise.all([
|
|
70
75
|
getJson(running.port, "/api/hub"),
|
|
71
76
|
getJson(running.port, "/api/devices/relay"),
|
|
@@ -87,7 +92,12 @@ export async function daemonStatus(opts) {
|
|
|
87
92
|
console.log(JSON.stringify(s, null, 2));
|
|
88
93
|
process.exit(s.running ? 0 : NOT_RUNNING);
|
|
89
94
|
}
|
|
90
|
-
if (s.
|
|
95
|
+
if (s.busy) {
|
|
96
|
+
const up = ago(s.startedAt);
|
|
97
|
+
console.log(`${chalk.yellow("●")} kraftwerk daemon ${chalk.yellow("running, but slow to answer")} ${chalk.dim(`pid ${s.pid}, port ${s.port}${up ? `, up ${up}` : ""}${s.version ? `, ${s.version}` : ""}`)}`);
|
|
98
|
+
console.log(chalk.dim(" its process lives and holds its port — busy (agents at work, a loaded machine); ask again in a moment"));
|
|
99
|
+
}
|
|
100
|
+
else if (s.running) {
|
|
91
101
|
const up = ago(s.startedAt);
|
|
92
102
|
const behind = s.version && s.version !== s.installed ? chalk.yellow(` — ${s.installed} is installed: \`kraftwerk daemon restart\``) : "";
|
|
93
103
|
console.log(`${chalk.green("●")} kraftwerk daemon ${chalk.green("running")} ${chalk.dim(`pid ${s.pid}${up ? `, up ${up}` : ""}${s.version ? `, ${s.version}` : ""}`)}${behind}`);
|
|
@@ -110,6 +120,7 @@ export async function daemonStatus(opts) {
|
|
|
110
120
|
console.log(chalk.dim(` last trouble in the log: ${crash.trim().slice(0, 200)}`));
|
|
111
121
|
console.log(chalk.dim(" `kraftwerk daemon start` starts it in the background"));
|
|
112
122
|
}
|
|
123
|
+
console.log(chalk.dim(` identity: ${s.identity} · routines with runs_on: ${s.identity} fire here (\`kraftwerk whoami\`)`));
|
|
113
124
|
console.log(chalk.dim(` login item: ${s.loginItem ? "starts at login" : "not installed (`kraftwerk daemon install` starts it at login)"}`));
|
|
114
125
|
console.log(chalk.dim(` log: ${tildify(s.log)} · \`kraftwerk daemon logs -f\` follows it`));
|
|
115
126
|
if (!s.running)
|
|
@@ -138,9 +149,10 @@ async function launch(port) {
|
|
|
138
149
|
return "background";
|
|
139
150
|
}
|
|
140
151
|
export async function startDaemon(opts) {
|
|
141
|
-
const running = await
|
|
142
|
-
if (running) {
|
|
143
|
-
|
|
152
|
+
const running = await daemonState();
|
|
153
|
+
if (running.state !== "stopped") {
|
|
154
|
+
const busy = running.state === "busy" ? ", busy — slow to answer right now" : "";
|
|
155
|
+
console.log(`${chalk.green("✔")} the daemon already runs ${chalk.dim(`(pid ${running.pid}, http://localhost:${running.port}${busy})`)}`);
|
|
144
156
|
return;
|
|
145
157
|
}
|
|
146
158
|
const how = await launch(opts.port);
|
|
@@ -157,9 +169,10 @@ export async function startDaemon(opts) {
|
|
|
157
169
|
}
|
|
158
170
|
/** Stop it: its supervisor first (else it would start the daemon again), then wait until it is gone. */
|
|
159
171
|
async function stopRunning() {
|
|
160
|
-
const [
|
|
161
|
-
if (
|
|
172
|
+
const [found, rec] = await Promise.all([daemonState(), readDaemonFile()]);
|
|
173
|
+
if (found.state === "stopped")
|
|
162
174
|
return false;
|
|
175
|
+
const running = found;
|
|
163
176
|
const target = rec?.pid === running.pid && alive(rec.supervisor) ? rec.supervisor : running.pid;
|
|
164
177
|
process.kill(target, "SIGTERM");
|
|
165
178
|
const gone = await until(async () => !alive(running.pid) && !alive(target) && (await findDaemon()) === null, 20_000);
|
|
@@ -182,9 +195,15 @@ export async function stopDaemon() {
|
|
|
182
195
|
}
|
|
183
196
|
/** Restart: the running daemon relaunches itself with the current install (its supervisor stays); none running — start one. */
|
|
184
197
|
export async function restartDaemon(opts) {
|
|
185
|
-
const
|
|
186
|
-
if (
|
|
198
|
+
const found = await daemonState();
|
|
199
|
+
if (found.state === "stopped")
|
|
187
200
|
return startDaemon(opts);
|
|
201
|
+
if (found.state === "busy") {
|
|
202
|
+
// It does not answer, so it cannot be asked to relaunch: stopping it is the only way (its agents are interrupted).
|
|
203
|
+
await stopRunning();
|
|
204
|
+
return startDaemon(opts);
|
|
205
|
+
}
|
|
206
|
+
const running = found;
|
|
188
207
|
// The restart route lives in every workspace (the daemon's root answers machine routes only); any open one will do.
|
|
189
208
|
const hub = await getJson(running.port, "/api/hub");
|
|
190
209
|
const slug = hub?.workspaces?.[0]?.slug;
|
package/dist/cli/daemon-login.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, promises as fs } from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import chalk from "chalk";
|
|
6
|
-
import { daemonLogPath,
|
|
6
|
+
import { daemonLogPath, daemonState } from "../core/daemon.js";
|
|
7
7
|
import { selfCommand } from "../core/self-command.js";
|
|
8
8
|
import { tildify } from "../core/instances.js";
|
|
9
9
|
/**
|
|
@@ -72,8 +72,8 @@ export async function installDaemon(opts) {
|
|
|
72
72
|
await fs.writeFile(file, renderPlist([cmd, ...args], env, logPath()));
|
|
73
73
|
// An older version of the item is replaced: unload it first (an error just means it was not loaded).
|
|
74
74
|
spawnSync(launchctl(), ["bootout", `${domain()}/${LAUNCH_LABEL}`], { stdio: "ignore" });
|
|
75
|
-
const running = await
|
|
76
|
-
if (running) {
|
|
75
|
+
const running = await daemonState();
|
|
76
|
+
if (running.state !== "stopped") {
|
|
77
77
|
// Loading now would start a second daemon, which refuses to run. launchd loads the file at the next login.
|
|
78
78
|
console.log(`${chalk.green("✔")} login item installed: ${chalk.dim(tildify(file))}`);
|
|
79
79
|
console.log(chalk.dim(` a daemon already runs (pid ${running.pid}); from the next login on, launchd starts it`));
|
package/dist/cli/daemon.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import { DAEMON_PORT,
|
|
2
|
+
import { DAEMON_PORT, daemonState, lastOpenRoots, setDaemonVersion, writeDaemonFile } from "../core/daemon.js";
|
|
3
3
|
import { tildify } from "../core/instances.js";
|
|
4
4
|
import { networkUrls } from "../server/bind.js";
|
|
5
5
|
import { startPushWatcher } from "../server/push.js";
|
|
@@ -22,9 +22,10 @@ export async function runDaemon(opts) {
|
|
|
22
22
|
process.env.KRAFTWERK_UI_HOST ||= "0.0.0.0";
|
|
23
23
|
if (process.env.KRAFTWERK_UI_SUPERVISED !== "1")
|
|
24
24
|
return supervise(["daemon", ...(opts.port ? ["--port", opts.port] : [])], "the daemon");
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
// A busy one counts too: a second daemon would only crash on its port.
|
|
26
|
+
const running = await daemonState();
|
|
27
|
+
if (running.state !== "stopped" && running.pid !== process.pid) {
|
|
28
|
+
console.error(chalk.red(`✖ a kraftwerk daemon already runs (pid ${running.pid}, port ${running.port}${running.state === "busy" ? ", busy right now" : ""})`));
|
|
28
29
|
process.exit(1);
|
|
29
30
|
}
|
|
30
31
|
const port = Number(opts.port ?? process.env.KRAFTWERK_DAEMON_PORT ?? DAEMON_PORT);
|
package/dist/cli/doctor.d.ts
CHANGED
package/dist/cli/doctor.js
CHANGED
|
@@ -2,16 +2,19 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
import { cloudFor, ignoreEntryFor, isDir, reposRootFor, resolveWorkspace, workspaceSlug } from "../config.js";
|
|
5
|
+
import { addWorkspaceId, cloudFor, ignoreEntryFor, isDir, reposRootFor, resolveWorkspace, workspaceSlug } from "../config.js";
|
|
6
6
|
import { discoverWorkflows } from "../discover.js";
|
|
7
7
|
import { missingEnv } from "../yaml.js";
|
|
8
8
|
import { applyDotenv, DOTENV_FILE } from "../dotenv.js";
|
|
9
9
|
import { legacyFindings, versionLt } from "./doctor-legacy.js";
|
|
10
10
|
import { listDevices } from "../core/devices.js";
|
|
11
11
|
import { slugClashes } from "../core/instances.js";
|
|
12
|
-
import {
|
|
12
|
+
import { daemonState } from "../core/daemon.js";
|
|
13
13
|
import { daemonLoginItem } from "./daemon-login.js";
|
|
14
14
|
import { readRelaySettings } from "../server/relay.js";
|
|
15
|
+
import { readIdentity, identityString } from "../core/identity.js";
|
|
16
|
+
import { adoptUnassigned, runnerFindings } from "../core/routines.js";
|
|
17
|
+
import { setDefaultWorkspace, Workspace } from "../core/workspace.js";
|
|
15
18
|
const ICONS = {
|
|
16
19
|
ok: chalk.green("✔"),
|
|
17
20
|
warn: chalk.yellow("⚠"),
|
|
@@ -42,7 +45,7 @@ function cliVersion(command) {
|
|
|
42
45
|
return undefined;
|
|
43
46
|
return (r.stdout || r.stderr).trim().split("\n")[0];
|
|
44
47
|
}
|
|
45
|
-
export async function runDoctor(cwd) {
|
|
48
|
+
export async function runDoctor(cwd, opts = {}) {
|
|
46
49
|
let failures = 0;
|
|
47
50
|
// Runtime.
|
|
48
51
|
const [major] = process.versions.node.split(".").map(Number);
|
|
@@ -100,6 +103,12 @@ export async function runDoctor(cwd) {
|
|
|
100
103
|
report("ok", `${path.basename(project.configPath)} well-formed`, keys.length ? keys.map((k) => `${k}: ${show(cfg[k])}`).join(", ") : "empty — defaults apply");
|
|
101
104
|
if (!cfg.name)
|
|
102
105
|
report("info", "name not set in kraftwerk.yml", "inspector header falls back to the folder name");
|
|
106
|
+
if (cfg.id)
|
|
107
|
+
report("ok", `workspace id: ${cfg.id}`);
|
|
108
|
+
else if (opts.fix)
|
|
109
|
+
report("ok", `workspace id: ${await addWorkspaceId(project.configPath)}`, `added to ${path.basename(project.configPath)} — commit it`);
|
|
110
|
+
else
|
|
111
|
+
report("warn", "no workspace id in kraftwerk.yml", "the same workspace on every clone is known by it — `kraftwerk doctor --fix` adds one");
|
|
103
112
|
for (const key of ["workflows", "knowledge", "agents", "output"]) {
|
|
104
113
|
const value = cfg[key];
|
|
105
114
|
if (!value)
|
|
@@ -143,6 +152,31 @@ export async function runDoctor(cwd) {
|
|
|
143
152
|
else
|
|
144
153
|
report("ok", label, "not inside a git repository");
|
|
145
154
|
}
|
|
155
|
+
// Routine runners: a routine fires only on the machine its runs_on names.
|
|
156
|
+
setDefaultWorkspace(new Workspace({ root: project.root, outputDir: project.outputDir }));
|
|
157
|
+
const me = identityString(await readIdentity());
|
|
158
|
+
const runners = await runnerFindings().catch(() => ({ unassigned: [], retired: [] }));
|
|
159
|
+
const names = (rs) => rs.map((r) => `${r.agent}/${r.id}`).join(", ");
|
|
160
|
+
if (!runners.unassigned.length && !runners.retired.length) {
|
|
161
|
+
report("ok", `routines: runner identity ${me}`, "every enabled routine names its runner");
|
|
162
|
+
}
|
|
163
|
+
else if (opts.fix) {
|
|
164
|
+
const r = await adoptUnassigned({ retired: true });
|
|
165
|
+
report("ok", `${r.adopted.length} routine(s) now run here (${me})`, names(r.adopted));
|
|
166
|
+
if (r.commit?.ok)
|
|
167
|
+
report("ok", `committed ${r.files.join(", ")}`, "push so the other clones see who runs them");
|
|
168
|
+
else if (r.commit)
|
|
169
|
+
report("warn", `written, not committed: ${r.commit.error}`, `commit ${r.files.join(", ")} yourself`);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
if (runners.unassigned.length) {
|
|
173
|
+
report("warn", `${runners.unassigned.length} enabled routine(s) have no runner — they fire nowhere`, `${names(runners.unassigned)} · \`kraftwerk doctor --fix\` (or \`kraftwerk routines adopt\`) runs them here: ${me}`);
|
|
174
|
+
}
|
|
175
|
+
for (const old of [...new Set(runners.retired.map((r) => r.from))]) {
|
|
176
|
+
const these = runners.retired.filter((r) => r.from === old);
|
|
177
|
+
report("warn", `${these.length} routine(s) name the retired identity ${old}`, `${names(these)} · \`kraftwerk doctor --fix\` moves them to ${me} and commits`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
146
180
|
// Cloud manager: registration is best-effort at runtime, so doctor only says what would happen.
|
|
147
181
|
const cloud = cloudFor(project);
|
|
148
182
|
if (cloud) {
|
|
@@ -214,9 +248,10 @@ export async function runDoctor(cwd) {
|
|
|
214
248
|
report("warn", "KRAFTWERK_UI_TRUST_NETWORK=1", "anyone who reaches the UI over the network uses it without pairing — unset it and pair devices instead (`kraftwerk devices pair`)");
|
|
215
249
|
report("info", devices.length ? `${devices.length} paired device(s)` : "no paired devices", devices.length ? devices.map((d) => d.name).join(", ") : "a phone or another computer can use kraftwerk once paired: `kraftwerk ui --lan`, then `kraftwerk devices pair`");
|
|
216
250
|
// The daemon: whether one runs, and whether it starts at login.
|
|
217
|
-
const
|
|
251
|
+
const state = await daemonState();
|
|
252
|
+
const daemon = state.state === "stopped" ? null : state;
|
|
218
253
|
const loginItem = daemonLoginItem();
|
|
219
|
-
report("info", daemon ? `kraftwerk daemon running on :${daemon.port}` :
|
|
254
|
+
report("info", !daemon ? "no kraftwerk daemon running" : daemon.state === "busy" ? `kraftwerk daemon running on :${daemon.port}, but slow to answer (busy)` : `kraftwerk daemon running on :${daemon.port}`, loginItem
|
|
220
255
|
? daemon
|
|
221
256
|
? "starts at login · `kraftwerk daemon status` for more"
|
|
222
257
|
: "starts at login, but not running now · `kraftwerk daemon start`, `kraftwerk daemon logs` says why it stopped"
|
package/dist/cli/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { appendFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import { CONFIG_SCHEMA_URL, gitignoreHas, SCHEMA_URL } from "../config.js";
|
|
4
|
+
import { CONFIG_SCHEMA_URL, gitignoreHas, newWorkspaceId, SCHEMA_URL } from "../config.js";
|
|
5
5
|
import { DOTENV_FILE } from "../dotenv.js";
|
|
6
6
|
import { initBundle, writeConcept } from "../okf.js";
|
|
7
7
|
/**
|
|
@@ -9,12 +9,15 @@ import { initBundle, writeConcept } from "../okf.js";
|
|
|
9
9
|
* command: kraftwerk.yml (root marker + config), a kraftwerk-data/ tree with
|
|
10
10
|
* a runnable example workflow, a starter agent ("max"), a demo
|
|
11
11
|
* knowledge bundle, and an output ignore entry. Everything is idempotent:
|
|
12
|
-
* existing files are left untouched and reported.
|
|
12
|
+
* existing files are left untouched and reported. A new kraftwerk.yml gets
|
|
13
|
+
* a fresh workspace id (`id: ws_…`); an existing one is not touched —
|
|
14
|
+
* `kraftwerk doctor --fix` adds an id to a file that has none.
|
|
13
15
|
*/
|
|
14
16
|
const DATA_DIR = "kraftwerk-data";
|
|
15
17
|
const configTemplate = (name) => `# yaml-language-server: $schema=${CONFIG_SCHEMA_URL}
|
|
16
18
|
# kraftwerk project config — also marks the project root for the CLI.
|
|
17
19
|
# All fields optional. Docs: https://github.com/NETNODEAG/kraftwerk
|
|
20
|
+
id: ${newWorkspaceId()} # this workspace's identity on every machine and clone — commit it, keep it (a copy of this workspace needs a new one)
|
|
18
21
|
name: ${JSON.stringify(name)} # display name, shown in the inspector header + browser tab
|
|
19
22
|
# slug: my-team # this workspace's address on this machine (my-team.localhost); default: the folder name
|
|
20
23
|
icon: "⚡" # emoji shown as the inspector favicon
|
package/dist/cli/kraftwerk.js
CHANGED
|
@@ -21,6 +21,7 @@ import { registerDevicesCommands } from "./devices.js";
|
|
|
21
21
|
import { registerDaemonCommand } from "./daemon.js";
|
|
22
22
|
import { registerRemoteCommand } from "./remote.js";
|
|
23
23
|
import { registerJournalCommands } from "./journal.js";
|
|
24
|
+
import { registerWhoamiCommand } from "./whoami.js";
|
|
24
25
|
import { applyDotenv } from "../dotenv.js";
|
|
25
26
|
import { resolveWorkspace } from "../config.js";
|
|
26
27
|
import { registerRoutineCommands } from "./routines.js";
|
|
@@ -34,7 +35,8 @@ import { runUi } from "./ui.js";
|
|
|
34
35
|
* kraftwerk run [workflow] [text] run one (prompts interactively if omitted)
|
|
35
36
|
* kraftwerk runs [show <id>] inspect past runs from their traces
|
|
36
37
|
* kraftwerk knowledge ... Knowledge: OKF bundles (list/get/put/verify/...)
|
|
37
|
-
* kraftwerk routines ... per-agent scheduled prompts (list/add/remove/enable/run)
|
|
38
|
+
* kraftwerk routines ... per-agent scheduled prompts (list/add/remove/enable/run/adopt)
|
|
39
|
+
* kraftwerk whoami this machine's identity (handle@machine), the runner routines name
|
|
38
40
|
* kraftwerk ui start the inspector web UI (localhost:1981)
|
|
39
41
|
* kraftwerk workspaces ... known workspaces on this machine (list/start/stop/forget)
|
|
40
42
|
* kraftwerk projects ... goal-scoped project folders (list/create/show/link/log/remove)
|
|
@@ -44,7 +46,7 @@ import { runUi } from "./ui.js";
|
|
|
44
46
|
* kraftwerk trash ... what was deleted: list, restore, purge, empty
|
|
45
47
|
* kraftwerk devices ... paired devices: list, pair one, revoke one
|
|
46
48
|
* kraftwerk daemon the machine's kraftwerk: every open workspace at <slug>.localhost
|
|
47
|
-
* kraftwerk doctor
|
|
49
|
+
* kraftwerk doctor [--fix] preflight: harness CLIs, docker, workflows, env, routine runners
|
|
48
50
|
* kraftwerk validate [paths...] validate without executing
|
|
49
51
|
*
|
|
50
52
|
* Workflows are auto-discovered under src/workflows/ (or workflows/), from
|
|
@@ -265,9 +267,10 @@ program
|
|
|
265
267
|
});
|
|
266
268
|
program
|
|
267
269
|
.command("doctor")
|
|
268
|
-
.description("Preflight: harness CLIs, docker, workflows, declared environment variables")
|
|
269
|
-
.
|
|
270
|
-
|
|
270
|
+
.description("Preflight: harness CLIs, docker, workflows, declared environment variables, routine runners, workspace id")
|
|
271
|
+
.option("--fix", "Apply the fixes doctor offers: run unassigned routines here, move routines off a retired identity (both committed), add a workspace id")
|
|
272
|
+
.action(async (opts) => {
|
|
273
|
+
await runDoctor(process.cwd(), { fix: !!opts.fix });
|
|
271
274
|
});
|
|
272
275
|
const runs = program
|
|
273
276
|
.command("runs")
|
|
@@ -298,6 +301,7 @@ registerDevicesCommands(program);
|
|
|
298
301
|
registerDaemonCommand(program);
|
|
299
302
|
registerRemoteCommand(program);
|
|
300
303
|
registerJournalCommands(program);
|
|
304
|
+
registerWhoamiCommand(program);
|
|
301
305
|
program
|
|
302
306
|
.command("ui")
|
|
303
307
|
.description("Start the inspector web UI for this project's runs and workflows")
|
package/dist/cli/routines.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ import type { Command } from "commander";
|
|
|
3
3
|
* `kraftwerk routines` — CLI surface over per-agent scheduled prompts
|
|
4
4
|
* (agents/<slug>/routines.yml). Definition CRUD works directly on the
|
|
5
5
|
* files; `run` fires through the running inspector server, because the
|
|
6
|
-
* session (chat) and the scheduler live there.
|
|
6
|
+
* session (chat) and the scheduler live there. `adopt` makes this machine
|
|
7
|
+
* the runner (runs_on) of every unassigned routine and commits routines.yml.
|
|
7
8
|
*/
|
|
8
9
|
/** Point the inspector context at this project so agent/routine paths resolve. */
|
|
9
10
|
export declare function initContext(): Promise<{
|
package/dist/cli/routines.js
CHANGED
|
@@ -3,13 +3,14 @@ import path from "node:path";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { resolveWorkspace } from "../config.js";
|
|
5
5
|
import { setDefaultWorkspace, Workspace } from "../core/workspace.js";
|
|
6
|
-
import { deleteRoutine, listRoutines, routineStatuses, saveRoutine, } from "../core/routines.js";
|
|
6
|
+
import { adoptUnassigned, deleteRoutine, listRoutines, routineStatuses, saveRoutine, } from "../core/routines.js";
|
|
7
7
|
import { getAgent, listAgents } from "../core/agents.js";
|
|
8
8
|
/**
|
|
9
9
|
* `kraftwerk routines` — CLI surface over per-agent scheduled prompts
|
|
10
10
|
* (agents/<slug>/routines.yml). Definition CRUD works directly on the
|
|
11
11
|
* files; `run` fires through the running inspector server, because the
|
|
12
|
-
* session (chat) and the scheduler live there.
|
|
12
|
+
* session (chat) and the scheduler live there. `adopt` makes this machine
|
|
13
|
+
* the runner (runs_on) of every unassigned routine and commits routines.yml.
|
|
13
14
|
*/
|
|
14
15
|
/** Point the inspector context at this project so agent/routine paths resolve. */
|
|
15
16
|
export async function initContext() {
|
|
@@ -37,11 +38,19 @@ const fmtTime = (iso) => {
|
|
|
37
38
|
const p = (n) => String(n).padStart(2, "0");
|
|
38
39
|
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
39
40
|
};
|
|
41
|
+
function runnerLabel(r) {
|
|
42
|
+
if (r.runner === "here")
|
|
43
|
+
return chalk.green("runs here");
|
|
44
|
+
if (r.runner === "unassigned")
|
|
45
|
+
return chalk.yellow("unassigned");
|
|
46
|
+
return r.retired ? chalk.yellow(`runs on ${r.runsOn} (retired)`) : chalk.dim(`runs on ${r.runsOn}`);
|
|
47
|
+
}
|
|
40
48
|
function routineLine(r) {
|
|
41
49
|
const state = r.enabled ? chalk.green("on ") : chalk.dim("off");
|
|
42
50
|
const flags = r.lastError ? ` ${chalk.red(`error: ${r.lastError}`)}` : "";
|
|
43
51
|
return (` ${chalk.cyan(r.id.padEnd(24))} ${state} ${r.schedule.padEnd(14)} ` +
|
|
44
52
|
chalk.dim(`next ${fmtTime(r.nextRunAt).padEnd(17)} last ${fmtTime(r.lastRunAt)}`) +
|
|
53
|
+
` ${runnerLabel(r)}` +
|
|
45
54
|
flags);
|
|
46
55
|
}
|
|
47
56
|
export function registerRoutineCommands(program) {
|
|
@@ -86,6 +95,7 @@ export function registerRoutineCommands(program) {
|
|
|
86
95
|
.option("--file <path>", "Read the prompt from a file")
|
|
87
96
|
.option("--id <id>", "Routine id (default: derived from the name)")
|
|
88
97
|
.option("--disabled", "Create it switched off")
|
|
98
|
+
.option("--runs-on <identity>", "The machine that fires it, handle@machine (default: this one for a new routine, unchanged for an update)")
|
|
89
99
|
.action(async (agent, opts) => {
|
|
90
100
|
await initContext();
|
|
91
101
|
await requireAgent(agent);
|
|
@@ -103,12 +113,16 @@ export function registerRoutineCommands(program) {
|
|
|
103
113
|
schedule: opts.schedule,
|
|
104
114
|
prompt,
|
|
105
115
|
enabled: !opts.disabled,
|
|
116
|
+
...(opts.runsOn !== undefined ? { runsOn: opts.runsOn } : {}),
|
|
106
117
|
});
|
|
107
118
|
const verb = existing.has(routine.id) ? "updated" : "created";
|
|
108
119
|
console.log(`${chalk.green("✔")} ${verb} ${chalk.cyan(`${agent}/${routine.id}`)} ` +
|
|
109
120
|
chalk.dim(`(${routine.schedule}${routine.enabled ? "" : ", disabled"})`));
|
|
110
|
-
if (routine.
|
|
111
|
-
console.log(chalk.
|
|
121
|
+
if (!routine.runsOn) {
|
|
122
|
+
console.log(chalk.yellow(" unassigned: it fires nowhere until a machine runs it (`kraftwerk routines adopt`)."));
|
|
123
|
+
}
|
|
124
|
+
else if (routine.enabled) {
|
|
125
|
+
console.log(chalk.dim(` fires from the inspector server on ${routine.runsOn} — make sure \`kraftwerk ui\` runs there.`));
|
|
112
126
|
}
|
|
113
127
|
}
|
|
114
128
|
catch (err) {
|
|
@@ -148,6 +162,28 @@ export function registerRoutineCommands(program) {
|
|
|
148
162
|
console.log(`${chalk.green("✔")} ${chalk.cyan(`${agent}/${id}`)} ${verb}d`);
|
|
149
163
|
});
|
|
150
164
|
}
|
|
165
|
+
routines
|
|
166
|
+
.command("adopt")
|
|
167
|
+
.description("Run these here: every enabled routine without runs_on gets this machine's identity; routines.yml is committed")
|
|
168
|
+
.option("--retired", "Also move routines that name a retired identity of this machine (see `kraftwerk whoami`)")
|
|
169
|
+
.option("--json", "Machine-readable output")
|
|
170
|
+
.action(async (opts) => {
|
|
171
|
+
await initContext();
|
|
172
|
+
const r = await adoptUnassigned({ retired: !!opts.retired });
|
|
173
|
+
if (opts.json)
|
|
174
|
+
return void console.log(JSON.stringify(r, null, 2));
|
|
175
|
+
if (!r.adopted.length) {
|
|
176
|
+
console.log(`${chalk.green("✔")} nothing to adopt — every enabled routine has a runner ${chalk.dim(`(this machine: ${r.runsOn})`)}`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
for (const a of r.adopted) {
|
|
180
|
+
console.log(`${chalk.green("✔")} ${chalk.cyan(`${a.agent}/${a.id}`)} runs on ${r.runsOn}${a.from ? chalk.dim(` (was ${a.from})`) : ""}`);
|
|
181
|
+
}
|
|
182
|
+
if (r.commit?.ok)
|
|
183
|
+
console.log(chalk.dim(` committed ${r.files.join(", ")} — push so the other clones see it`));
|
|
184
|
+
else
|
|
185
|
+
console.log(chalk.yellow(` written, not committed: ${r.commit?.error ?? "unknown error"} — commit ${r.files.join(", ")} yourself`));
|
|
186
|
+
});
|
|
151
187
|
routines
|
|
152
188
|
.command("run")
|
|
153
189
|
.description("Fire a routine now (opens a session via the running `kraftwerk ui` server)")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* `kraftwerk whoami` — this machine's identity (~/.kraftwerk/identity.json):
|
|
4
|
+
* the handle, the machine name, and `handle@machine`, which routines name in
|
|
5
|
+
* `runs_on:` so only one clone of a workspace fires them. `--handle` /
|
|
6
|
+
* `--machine` rename; the old pair is kept in `previous`, and doctor offers
|
|
7
|
+
* to move routines that still name it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerWhoamiCommand(program: Command): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { identityPartError, identityString, readIdentity, setIdentity } from "../core/identity.js";
|
|
3
|
+
/**
|
|
4
|
+
* `kraftwerk whoami` — this machine's identity (~/.kraftwerk/identity.json):
|
|
5
|
+
* the handle, the machine name, and `handle@machine`, which routines name in
|
|
6
|
+
* `runs_on:` so only one clone of a workspace fires them. `--handle` /
|
|
7
|
+
* `--machine` rename; the old pair is kept in `previous`, and doctor offers
|
|
8
|
+
* to move routines that still name it.
|
|
9
|
+
*/
|
|
10
|
+
export function registerWhoamiCommand(program) {
|
|
11
|
+
program
|
|
12
|
+
.command("whoami")
|
|
13
|
+
.description("This machine's identity (handle@machine) — the runner routines name in runs_on; --handle / --machine rename it")
|
|
14
|
+
.option("--handle <handle>", "Set your handle (lowercase letters, digits, . _ -)")
|
|
15
|
+
.option("--machine <machine>", "Set this machine's name (lowercase letters, digits, . _ -)")
|
|
16
|
+
.option("--json", "Machine-readable output")
|
|
17
|
+
.addHelpText("after", `
|
|
18
|
+
A routine fires only on the machine whose identity its runs_on names:
|
|
19
|
+
kraftwerk whoami print handle, machine and handle@machine
|
|
20
|
+
kraftwerk whoami --machine studio rename this machine (the old pair is kept as previous)
|
|
21
|
+
kraftwerk routines adopt make this machine the runner of every unassigned routine`)
|
|
22
|
+
.action(async (opts) => {
|
|
23
|
+
const before = identityString(await readIdentity());
|
|
24
|
+
for (const label of ["handle", "machine"]) {
|
|
25
|
+
const v = opts[label];
|
|
26
|
+
const err = v === undefined ? undefined : identityPartError(label, v);
|
|
27
|
+
if (err) {
|
|
28
|
+
console.error(chalk.red(err));
|
|
29
|
+
process.exit(2);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const changing = opts.handle !== undefined || opts.machine !== undefined;
|
|
33
|
+
const id = changing ? await setIdentity({ handle: opts.handle, machine: opts.machine }) : await readIdentity();
|
|
34
|
+
const now = identityString(id);
|
|
35
|
+
if (opts.json)
|
|
36
|
+
return void console.log(JSON.stringify({ handle: id.handle, machine: id.machine, identity: now, previous: id.previous }, null, 2));
|
|
37
|
+
if (changing && before !== now)
|
|
38
|
+
console.log(`${chalk.green("✔")} ${chalk.dim(before)} → ${chalk.cyan(now)}`);
|
|
39
|
+
console.log(`handle: ${id.handle}`);
|
|
40
|
+
console.log(`machine: ${id.machine}`);
|
|
41
|
+
console.log(`identity: ${chalk.cyan(now)}`);
|
|
42
|
+
if (id.previous.length)
|
|
43
|
+
console.log(chalk.dim(`previous: ${id.previous.join(", ")}`));
|
|
44
|
+
if (changing && before !== now) {
|
|
45
|
+
console.log(chalk.dim(` routines that name ${before} no longer fire here — \`kraftwerk doctor --fix\` moves them to ${now}`));
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
package/dist/client/routes.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Route name → [method, path pattern, and "raw" (answers a stream or a file) or "upload" (sends file bytes): both HTTP only]. */
|
|
2
2
|
export declare const ROUTES: {
|
|
3
3
|
readonly "agents.addJournal": readonly ["POST", "/api/agents/:slug/journal"];
|
|
4
|
+
readonly "agents.adoptRoutine": readonly ["POST", "/api/agents/:slug/routines/:id/adopt"];
|
|
4
5
|
readonly "agents.archive": readonly ["POST", "/api/agents/:slug/archive"];
|
|
5
6
|
readonly "agents.create": readonly ["POST", "/api/agents"];
|
|
6
7
|
readonly "agents.delete": readonly ["DELETE", "/api/agents/:slug"];
|
|
@@ -70,6 +71,7 @@ export declare const ROUTES: {
|
|
|
70
71
|
readonly "hub.close": readonly ["POST", "/api/hub/close"];
|
|
71
72
|
readonly "hub.list": readonly ["GET", "/api/hub"];
|
|
72
73
|
readonly "hub.open": readonly ["POST", "/api/hub/open"];
|
|
74
|
+
readonly "identity.get": readonly ["GET", "/api/identity"];
|
|
73
75
|
readonly "knowledge.concept": readonly ["GET", "/api/knowledge/:bundle/concept"];
|
|
74
76
|
readonly "knowledge.create": readonly ["POST", "/api/knowledge"];
|
|
75
77
|
readonly "knowledge.delete": readonly ["DELETE", "/api/knowledge/:bundle"];
|
|
@@ -98,6 +100,7 @@ export declare const ROUTES: {
|
|
|
98
100
|
readonly "repos.list": readonly ["GET", "/api/repos"];
|
|
99
101
|
readonly "repos.remove": readonly ["DELETE", "/api/repos/:slug"];
|
|
100
102
|
readonly "repos.update": readonly ["POST", "/api/repos/:slug/update"];
|
|
103
|
+
readonly "routines.adopt": readonly ["POST", "/api/routines/adopt"];
|
|
101
104
|
readonly "routines.list": readonly ["GET", "/api/routines"];
|
|
102
105
|
readonly "runs.decide": readonly ["POST", "/api/runs/:id/decision"];
|
|
103
106
|
readonly "runs.delete": readonly ["DELETE", "/api/runs/:id"];
|
package/dist/client/routes.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
/** Route name → [method, path pattern, and "raw" (answers a stream or a file) or "upload" (sends file bytes): both HTTP only]. */
|
|
3
3
|
export const ROUTES = {
|
|
4
4
|
"agents.addJournal": ["POST", "/api/agents/:slug/journal"],
|
|
5
|
+
"agents.adoptRoutine": ["POST", "/api/agents/:slug/routines/:id/adopt"],
|
|
5
6
|
"agents.archive": ["POST", "/api/agents/:slug/archive"],
|
|
6
7
|
"agents.create": ["POST", "/api/agents"],
|
|
7
8
|
"agents.delete": ["DELETE", "/api/agents/:slug"],
|
|
@@ -71,6 +72,7 @@ export const ROUTES = {
|
|
|
71
72
|
"hub.close": ["POST", "/api/hub/close"],
|
|
72
73
|
"hub.list": ["GET", "/api/hub"],
|
|
73
74
|
"hub.open": ["POST", "/api/hub/open"],
|
|
75
|
+
"identity.get": ["GET", "/api/identity"],
|
|
74
76
|
"knowledge.concept": ["GET", "/api/knowledge/:bundle/concept"],
|
|
75
77
|
"knowledge.create": ["POST", "/api/knowledge"],
|
|
76
78
|
"knowledge.delete": ["DELETE", "/api/knowledge/:bundle"],
|
|
@@ -99,6 +101,7 @@ export const ROUTES = {
|
|
|
99
101
|
"repos.list": ["GET", "/api/repos"],
|
|
100
102
|
"repos.remove": ["DELETE", "/api/repos/:slug"],
|
|
101
103
|
"repos.update": ["POST", "/api/repos/:slug/update"],
|
|
104
|
+
"routines.adopt": ["POST", "/api/routines/adopt"],
|
|
102
105
|
"routines.list": ["GET", "/api/routines"],
|
|
103
106
|
"runs.decide": ["POST", "/api/runs/:id/decision"],
|
|
104
107
|
"runs.delete": ["DELETE", "/api/runs/:id"],
|
package/dist/config.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* All fields are optional — a valid kraftwerk.yml may be empty:
|
|
11
11
|
*
|
|
12
|
+
* id: ws_k3f9x2m7q1 # the workspace's identity across machines and clones (committed; `kraftwerk init` writes it)
|
|
12
13
|
* name: my-project # display name (inspector header, "environment")
|
|
13
14
|
* icon: "⚡" # emoji shown as inspector favicon
|
|
14
15
|
* color: "#c2410c" # accent for this workspace in the switcher (default: derived from the root)
|
|
@@ -165,6 +166,12 @@ export interface SwitcherEntry {
|
|
|
165
166
|
icon?: string;
|
|
166
167
|
}
|
|
167
168
|
export interface WorkspaceConfig {
|
|
169
|
+
/**
|
|
170
|
+
* The workspace's identity wherever it is cloned: `ws_` + 10 base36
|
|
171
|
+
* characters, committed with kraftwerk.yml. Unlike the slug (an address on
|
|
172
|
+
* one machine) it is the same on every clone. Nothing reads it yet.
|
|
173
|
+
*/
|
|
174
|
+
id?: string;
|
|
168
175
|
/** Display name of the project ("environment"), shown in the inspector header. */
|
|
169
176
|
name?: string;
|
|
170
177
|
/**
|
|
@@ -209,6 +216,8 @@ export interface ResolvedWorkspace {
|
|
|
209
216
|
config: WorkspaceConfig;
|
|
210
217
|
/** Absolute path of the config file, if one exists. */
|
|
211
218
|
configPath?: string;
|
|
219
|
+
/** kraftwerk.yml `id`, when set. */
|
|
220
|
+
id?: string;
|
|
212
221
|
/** Absolute workflows root, if one exists. */
|
|
213
222
|
workflowsRoot?: string;
|
|
214
223
|
/** Absolute run-artifact directory (may not exist yet). */
|
|
@@ -250,3 +259,14 @@ export declare function workspaceSlug(ws: {
|
|
|
250
259
|
slug?: string;
|
|
251
260
|
};
|
|
252
261
|
}): string;
|
|
262
|
+
/** A workspace id: `ws_` and 10 lowercase base36 characters. */
|
|
263
|
+
export declare const WORKSPACE_ID_RE: RegExp;
|
|
264
|
+
/** A fresh workspace id for kraftwerk.yml. */
|
|
265
|
+
export declare function newWorkspaceId(): string;
|
|
266
|
+
/**
|
|
267
|
+
* Give an existing kraftwerk.yml an id, textually: one line inserted after
|
|
268
|
+
* the leading comments, the rest of the file byte for byte as it was (a
|
|
269
|
+
* re-serialised YAML document would lose the comments people keep there).
|
|
270
|
+
* Returns the id, or the one already there.
|
|
271
|
+
*/
|
|
272
|
+
export declare function addWorkspaceId(configPath: string): Promise<string>;
|