@netnodeag/kraftwerk 0.75.1 → 0.77.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 +33 -27
- package/dist/cli/cloud.d.ts +2 -0
- package/dist/cli/cloud.js +262 -0
- package/dist/cli/daemon-control.js +4 -2
- package/dist/cli/daemon.js +9 -0
- package/dist/cli/doctor.d.ts +3 -1
- package/dist/cli/doctor.js +51 -9
- package/dist/cli/init.js +7 -4
- package/dist/cli/kraftwerk.js +13 -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 +7 -0
- package/dist/client/routes.js +7 -0
- package/dist/config.d.ts +35 -28
- package/dist/config.js +43 -22
- package/dist/core/attention.d.ts +8 -1
- package/dist/core/attention.js +38 -2
- package/dist/core/cloud.d.ts +115 -0
- package/dist/core/cloud.js +94 -0
- 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/cloud.d.ts +72 -0
- package/dist/server/api/cloud.js +38 -0
- package/dist/server/api/devices.d.ts +8 -8
- package/dist/server/api/index.d.ts +102 -13
- package/dist/server/api/index.js +2 -0
- package/dist/server/api/runs.d.ts +3 -1
- package/dist/server/api/runs.js +9 -1
- package/dist/server/api/workspace.d.ts +22 -2
- package/dist/server/api/workspace.js +7 -1
- package/dist/server/cloud.d.ts +65 -33
- package/dist/server/cloud.js +207 -188
- package/dist/server/hub-context.d.ts +1 -1
- package/dist/server/hub-context.js +3 -3
- package/dist/server/push.js +1 -1
- package/dist/server/relay.d.ts +13 -0
- package/dist/server/relay.js +29 -1
- package/dist/server/server.d.ts +3 -2
- package/dist/server/server.js +6 -6
- package/package.json +1 -1
- package/schema/kraftwerk.schema.json +9 -4
- package/web/dist/assets/{dist-CezfArgg.js → dist-BGHweZKi.js} +1 -1
- package/web/dist/assets/{dist-Y9aHncjU.js → dist-BVPxOqcx.js} +1 -1
- package/web/dist/assets/{dist-CLZjnhZZ.js → dist-CCNyRSAA.js} +1 -1
- package/web/dist/assets/{dist-QqFmuPda.js → dist-CXoAMU1b.js} +1 -1
- package/web/dist/assets/{dist-BI4umsk6.js → dist-DCFREMXk.js} +1 -1
- package/web/dist/assets/{dist-YNCNGGgK.js → dist-DT2Tjvwx.js} +1 -1
- package/web/dist/assets/{dist-CzM6tn0f.js → dist-DdA5PoVJ.js} +1 -1
- package/web/dist/assets/{dist-CYicby0c.js → dist-DoD3qX1P.js} +1 -1
- package/web/dist/assets/{dist-CODxhtAK.js → dist-FFA4-S6d.js} +1 -1
- package/web/dist/assets/{dist-DaGnyH_l.js → dist-yGx0qYbw.js} +1 -1
- package/web/dist/assets/dist-zjUUP5a9.js +1 -0
- package/web/dist/assets/{editor-DEb8z5-Q.js → editor-CyWapLQr.js} +3 -3
- package/web/dist/assets/index-CNXcNoKJ.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
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"];
|
|
@@ -44,6 +45,10 @@ export declare const ROUTES: {
|
|
|
44
45
|
readonly "chats.setVibeable": readonly ["POST", "/api/chats/:id/vibeable"];
|
|
45
46
|
readonly "chats.steer": readonly ["POST", "/api/chats/:id/steer"];
|
|
46
47
|
readonly "chats.stopTask": readonly ["POST", "/api/chats/:id/task-stop"];
|
|
48
|
+
readonly "cloud.set": readonly ["POST", "/api/cloud"];
|
|
49
|
+
readonly "cloud.status": readonly ["GET", "/api/cloud"];
|
|
50
|
+
readonly "cloud.sync": readonly ["POST", "/api/cloud/sync"];
|
|
51
|
+
readonly "cloud.unlink": readonly ["POST", "/api/cloud/unlink"];
|
|
47
52
|
readonly "devices.code": readonly ["POST", "/api/devices/code"];
|
|
48
53
|
readonly "devices.dropPush": readonly ["DELETE", "/api/devices/push"];
|
|
49
54
|
readonly "devices.list": readonly ["GET", "/api/devices"];
|
|
@@ -70,6 +75,7 @@ export declare const ROUTES: {
|
|
|
70
75
|
readonly "hub.close": readonly ["POST", "/api/hub/close"];
|
|
71
76
|
readonly "hub.list": readonly ["GET", "/api/hub"];
|
|
72
77
|
readonly "hub.open": readonly ["POST", "/api/hub/open"];
|
|
78
|
+
readonly "identity.get": readonly ["GET", "/api/identity"];
|
|
73
79
|
readonly "knowledge.concept": readonly ["GET", "/api/knowledge/:bundle/concept"];
|
|
74
80
|
readonly "knowledge.create": readonly ["POST", "/api/knowledge"];
|
|
75
81
|
readonly "knowledge.delete": readonly ["DELETE", "/api/knowledge/:bundle"];
|
|
@@ -98,6 +104,7 @@ export declare const ROUTES: {
|
|
|
98
104
|
readonly "repos.list": readonly ["GET", "/api/repos"];
|
|
99
105
|
readonly "repos.remove": readonly ["DELETE", "/api/repos/:slug"];
|
|
100
106
|
readonly "repos.update": readonly ["POST", "/api/repos/:slug/update"];
|
|
107
|
+
readonly "routines.adopt": readonly ["POST", "/api/routines/adopt"];
|
|
101
108
|
readonly "routines.list": readonly ["GET", "/api/routines"];
|
|
102
109
|
readonly "runs.decide": readonly ["POST", "/api/runs/:id/decision"];
|
|
103
110
|
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"],
|
|
@@ -45,6 +46,10 @@ export const ROUTES = {
|
|
|
45
46
|
"chats.setVibeable": ["POST", "/api/chats/:id/vibeable"],
|
|
46
47
|
"chats.steer": ["POST", "/api/chats/:id/steer"],
|
|
47
48
|
"chats.stopTask": ["POST", "/api/chats/:id/task-stop"],
|
|
49
|
+
"cloud.set": ["POST", "/api/cloud"],
|
|
50
|
+
"cloud.status": ["GET", "/api/cloud"],
|
|
51
|
+
"cloud.sync": ["POST", "/api/cloud/sync"],
|
|
52
|
+
"cloud.unlink": ["POST", "/api/cloud/unlink"],
|
|
48
53
|
"devices.code": ["POST", "/api/devices/code"],
|
|
49
54
|
"devices.dropPush": ["DELETE", "/api/devices/push"],
|
|
50
55
|
"devices.list": ["GET", "/api/devices"],
|
|
@@ -71,6 +76,7 @@ export const ROUTES = {
|
|
|
71
76
|
"hub.close": ["POST", "/api/hub/close"],
|
|
72
77
|
"hub.list": ["GET", "/api/hub"],
|
|
73
78
|
"hub.open": ["POST", "/api/hub/open"],
|
|
79
|
+
"identity.get": ["GET", "/api/identity"],
|
|
74
80
|
"knowledge.concept": ["GET", "/api/knowledge/:bundle/concept"],
|
|
75
81
|
"knowledge.create": ["POST", "/api/knowledge"],
|
|
76
82
|
"knowledge.delete": ["DELETE", "/api/knowledge/:bundle"],
|
|
@@ -99,6 +105,7 @@ export const ROUTES = {
|
|
|
99
105
|
"repos.list": ["GET", "/api/repos"],
|
|
100
106
|
"repos.remove": ["DELETE", "/api/repos/:slug"],
|
|
101
107
|
"repos.update": ["POST", "/api/repos/:slug/update"],
|
|
108
|
+
"routines.adopt": ["POST", "/api/routines/adopt"],
|
|
102
109
|
"routines.list": ["GET", "/api/routines"],
|
|
103
110
|
"runs.decide": ["POST", "/api/runs/:id/decision"],
|
|
104
111
|
"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)
|
|
@@ -33,11 +34,10 @@
|
|
|
33
34
|
* root: apps # one folder per app, part of the workspace. Default: kraftwerk-data/vibeables
|
|
34
35
|
* projects: # goal-scoped folders (brief, systems of record, links) that chats work in (absent = off, bare key = on)
|
|
35
36
|
* root: kraftwerk-data/projects # one folder per project, part of the workspace. Default: kraftwerk-data/projects
|
|
36
|
-
* cloud: # kraftwerk cloud
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* #
|
|
40
|
-
* # a server that should register under an account unattended puts KRAFTWERK_CLOUD_TOKEN into .env.
|
|
37
|
+
* cloud: # the machine's kraftwerk cloud record lists this workspace — `enabled: false` leaves it out
|
|
38
|
+
* enabled: false # a per-workspace veto; the machine's switch is `kraftwerk cloud on|off` (~/.kraftwerk/cloud.json)
|
|
39
|
+
* # No key here: the daemon registers the MACHINE (its relay id) and `kraftwerk login` claims it.
|
|
40
|
+
* # `url` and `interval` are deprecated: the cloud's origin is the machine's (cloud.json / KRAFTWERK_CLOUD_URL).
|
|
41
41
|
*/
|
|
42
42
|
/** Stable, versionless URL of the workflow JSON schema (editor validation). */
|
|
43
43
|
export declare const SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/workflow.schema.json";
|
|
@@ -122,35 +122,23 @@ export declare const PROJECTS_DEFAULT_ROOT = "kraftwerk-data/projects";
|
|
|
122
122
|
/** Absolute projects root when the feature is on, undefined otherwise. */
|
|
123
123
|
export declare function projectsRootFor(project: ResolvedWorkspace): string | undefined;
|
|
124
124
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* the tests and CI set). No credential lives in kraftwerk.yml — the
|
|
133
|
-
* identity comes from the cloud, and an account token for unattended
|
|
134
|
-
* servers is KRAFTWERK_CLOUD_TOKEN in .env.
|
|
125
|
+
* kraftwerk.yml `cloud:` — since the cloud knows machines (core/cloud.ts,
|
|
126
|
+
* server/cloud.ts), this block only vetoes: `enabled: false` leaves the
|
|
127
|
+
* workspace out of the machine's reports. The machine's switch and the
|
|
128
|
+
* cloud's origin are ~/.kraftwerk/cloud.json (`kraftwerk cloud on|off`) and
|
|
129
|
+
* KRAFTWERK_CLOUD_URL. `url` is still honored for one release (with a
|
|
130
|
+
* warning) when the machine names none; `interval` is ignored — the cloud
|
|
131
|
+
* sets the cadence.
|
|
135
132
|
*/
|
|
136
133
|
export interface CloudConfig {
|
|
137
|
-
/** false
|
|
134
|
+
/** false leaves this workspace out of the machine's cloud record. Default: true. */
|
|
138
135
|
enabled?: boolean;
|
|
139
|
-
/**
|
|
136
|
+
/** @deprecated The machine's (cloud.json / KRAFTWERK_CLOUD_URL); used only when the machine names none. */
|
|
140
137
|
url?: string;
|
|
141
|
-
/**
|
|
138
|
+
/** @deprecated Ignored: the cloud sets the heartbeat interval. */
|
|
142
139
|
interval?: number;
|
|
143
140
|
}
|
|
144
141
|
export declare const CLOUD_DEFAULT_URL = "https://srv.kraftwerk-cloud.netnode.cloud";
|
|
145
|
-
export declare const CLOUD_DEFAULT_INTERVAL = 60;
|
|
146
|
-
export interface CloudSettings {
|
|
147
|
-
url: string;
|
|
148
|
-
interval: number;
|
|
149
|
-
/** KRAFTWERK_CLOUD_TOKEN from the environment (.env), when set. */
|
|
150
|
-
token?: string;
|
|
151
|
-
}
|
|
152
|
-
/** The effective cloud settings when the feature is on; undefined when kraftwerk.yml or the environment turned it off. */
|
|
153
|
-
export declare function cloudFor(project: ResolvedWorkspace): CloudSettings | undefined;
|
|
154
142
|
/** A directory as a .gitignore entry: relative, forward slashes, no trailing slash; undefined outside the root. */
|
|
155
143
|
export declare function ignoreEntryFor(projectRoot: string, dir: string): string | undefined;
|
|
156
144
|
/** True when .gitignore text already covers the entry (with or without a leading or trailing slash). */
|
|
@@ -165,6 +153,12 @@ export interface SwitcherEntry {
|
|
|
165
153
|
icon?: string;
|
|
166
154
|
}
|
|
167
155
|
export interface WorkspaceConfig {
|
|
156
|
+
/**
|
|
157
|
+
* The workspace's identity wherever it is cloned: `ws_` + 10 base36
|
|
158
|
+
* characters, committed with kraftwerk.yml. Unlike the slug (an address on
|
|
159
|
+
* one machine) it is the same on every clone. Nothing reads it yet.
|
|
160
|
+
*/
|
|
161
|
+
id?: string;
|
|
168
162
|
/** Display name of the project ("environment"), shown in the inspector header. */
|
|
169
163
|
name?: string;
|
|
170
164
|
/**
|
|
@@ -199,7 +193,7 @@ export interface WorkspaceConfig {
|
|
|
199
193
|
vibeables?: VibeablesConfig;
|
|
200
194
|
/** Projects: goal-scoped folders the chats work in. Absent = off. */
|
|
201
195
|
projects?: ProjectsConfig;
|
|
202
|
-
/**
|
|
196
|
+
/** The machine's cloud record lists this workspace. Absent = listed; `enabled: false` leaves it out. */
|
|
203
197
|
cloud?: CloudConfig;
|
|
204
198
|
}
|
|
205
199
|
export interface ResolvedWorkspace {
|
|
@@ -209,6 +203,8 @@ export interface ResolvedWorkspace {
|
|
|
209
203
|
config: WorkspaceConfig;
|
|
210
204
|
/** Absolute path of the config file, if one exists. */
|
|
211
205
|
configPath?: string;
|
|
206
|
+
/** kraftwerk.yml `id`, when set. */
|
|
207
|
+
id?: string;
|
|
212
208
|
/** Absolute workflows root, if one exists. */
|
|
213
209
|
workflowsRoot?: string;
|
|
214
210
|
/** Absolute run-artifact directory (may not exist yet). */
|
|
@@ -250,3 +246,14 @@ export declare function workspaceSlug(ws: {
|
|
|
250
246
|
slug?: string;
|
|
251
247
|
};
|
|
252
248
|
}): string;
|
|
249
|
+
/** A workspace id: `ws_` and 10 lowercase base36 characters. */
|
|
250
|
+
export declare const WORKSPACE_ID_RE: RegExp;
|
|
251
|
+
/** A fresh workspace id for kraftwerk.yml. */
|
|
252
|
+
export declare function newWorkspaceId(): string;
|
|
253
|
+
/**
|
|
254
|
+
* Give an existing kraftwerk.yml an id, textually: one line inserted after
|
|
255
|
+
* the leading comments, the rest of the file byte for byte as it was (a
|
|
256
|
+
* re-serialised YAML document would lose the comments people keep there).
|
|
257
|
+
* Returns the id, or the one already there.
|
|
258
|
+
*/
|
|
259
|
+
export declare function addWorkspaceId(configPath: string): Promise<string>;
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomInt } from "node:crypto";
|
|
2
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { parse } from "yaml";
|
|
@@ -13,6 +14,7 @@ import { parse } from "yaml";
|
|
|
13
14
|
*
|
|
14
15
|
* All fields are optional — a valid kraftwerk.yml may be empty:
|
|
15
16
|
*
|
|
17
|
+
* id: ws_k3f9x2m7q1 # the workspace's identity across machines and clones (committed; `kraftwerk init` writes it)
|
|
16
18
|
* name: my-project # display name (inspector header, "environment")
|
|
17
19
|
* icon: "⚡" # emoji shown as inspector favicon
|
|
18
20
|
* color: "#c2410c" # accent for this workspace in the switcher (default: derived from the root)
|
|
@@ -37,11 +39,10 @@ import { parse } from "yaml";
|
|
|
37
39
|
* root: apps # one folder per app, part of the workspace. Default: kraftwerk-data/vibeables
|
|
38
40
|
* projects: # goal-scoped folders (brief, systems of record, links) that chats work in (absent = off, bare key = on)
|
|
39
41
|
* root: kraftwerk-data/projects # one folder per project, part of the workspace. Default: kraftwerk-data/projects
|
|
40
|
-
* cloud: # kraftwerk cloud
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* #
|
|
44
|
-
* # a server that should register under an account unattended puts KRAFTWERK_CLOUD_TOKEN into .env.
|
|
42
|
+
* cloud: # the machine's kraftwerk cloud record lists this workspace — `enabled: false` leaves it out
|
|
43
|
+
* enabled: false # a per-workspace veto; the machine's switch is `kraftwerk cloud on|off` (~/.kraftwerk/cloud.json)
|
|
44
|
+
* # No key here: the daemon registers the MACHINE (its relay id) and `kraftwerk login` claims it.
|
|
45
|
+
* # `url` and `interval` are deprecated: the cloud's origin is the machine's (cloud.json / KRAFTWERK_CLOUD_URL).
|
|
45
46
|
*/
|
|
46
47
|
/** Stable, versionless URL of the workflow JSON schema (editor validation). */
|
|
47
48
|
export const SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/workflow.schema.json";
|
|
@@ -96,19 +97,6 @@ export function projectsRootFor(project) {
|
|
|
96
97
|
return path.resolve(project.root, p.root ?? PROJECTS_DEFAULT_ROOT);
|
|
97
98
|
}
|
|
98
99
|
export const CLOUD_DEFAULT_URL = "https://srv.kraftwerk-cloud.netnode.cloud";
|
|
99
|
-
export const CLOUD_DEFAULT_INTERVAL = 60;
|
|
100
|
-
/** The effective cloud settings when the feature is on; undefined when kraftwerk.yml or the environment turned it off. */
|
|
101
|
-
export function cloudFor(project) {
|
|
102
|
-
const c = project.config.cloud ?? {};
|
|
103
|
-
const envUrl = process.env.KRAFTWERK_CLOUD_URL?.trim();
|
|
104
|
-
if (c.enabled === false || envUrl?.toLowerCase() === "off")
|
|
105
|
-
return undefined;
|
|
106
|
-
return {
|
|
107
|
-
url: (envUrl || c.url || CLOUD_DEFAULT_URL).replace(/\/+$/, ""),
|
|
108
|
-
interval: c.interval ?? CLOUD_DEFAULT_INTERVAL,
|
|
109
|
-
token: process.env.KRAFTWERK_CLOUD_TOKEN?.trim() || undefined,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
100
|
/** A directory as a .gitignore entry: relative, forward slashes, no trailing slash; undefined outside the root. */
|
|
113
101
|
export function ignoreEntryFor(projectRoot, dir) {
|
|
114
102
|
const rel = path.relative(projectRoot, path.resolve(projectRoot, dir)).split(path.sep).join("/");
|
|
@@ -174,7 +162,7 @@ export async function resolveWorkspace(cwd) {
|
|
|
174
162
|
const { config, legacyKeys } = loaded;
|
|
175
163
|
const workflowsRoot = await findWorkflowsDir(dir, config.workflows);
|
|
176
164
|
if (!bare || (workflowsRoot && path.resolve(workflowsRoot) === bare.workflowsRoot)) {
|
|
177
|
-
return { root: dir, config, configPath, workflowsRoot, outputDir: path.resolve(dir, config.output ?? "output"), ...(legacyKeys.length ? { legacyKeys } : {}) };
|
|
165
|
+
return { root: dir, config, configPath, ...(config.id ? { id: config.id } : {}), workflowsRoot, outputDir: path.resolve(dir, config.output ?? "output"), ...(legacyKeys.length ? { legacyKeys } : {}) };
|
|
178
166
|
}
|
|
179
167
|
return bare;
|
|
180
168
|
}
|
|
@@ -213,7 +201,37 @@ export function slugFromFolder(folder) {
|
|
|
213
201
|
export function workspaceSlug(ws) {
|
|
214
202
|
return ws.config.slug ?? slugFromFolder(path.basename(ws.root));
|
|
215
203
|
}
|
|
216
|
-
|
|
204
|
+
/** A workspace id: `ws_` and 10 lowercase base36 characters. */
|
|
205
|
+
export const WORKSPACE_ID_RE = /^ws_[a-z0-9]{10}$/;
|
|
206
|
+
/** A fresh workspace id for kraftwerk.yml. */
|
|
207
|
+
export function newWorkspaceId() {
|
|
208
|
+
const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
209
|
+
let id = "ws_";
|
|
210
|
+
for (let i = 0; i < 10; i++)
|
|
211
|
+
id += alphabet[randomInt(alphabet.length)];
|
|
212
|
+
return id;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Give an existing kraftwerk.yml an id, textually: one line inserted after
|
|
216
|
+
* the leading comments, the rest of the file byte for byte as it was (a
|
|
217
|
+
* re-serialised YAML document would lose the comments people keep there).
|
|
218
|
+
* Returns the id, or the one already there.
|
|
219
|
+
*/
|
|
220
|
+
export async function addWorkspaceId(configPath) {
|
|
221
|
+
const text = await readFile(configPath, "utf8");
|
|
222
|
+
const existing = /^id:\s*["']?(ws_[a-z0-9]{10})["']?\s*(#.*)?$/m.exec(text);
|
|
223
|
+
if (existing)
|
|
224
|
+
return existing[1];
|
|
225
|
+
const id = newWorkspaceId();
|
|
226
|
+
const line = `id: ${id} # this workspace's identity on every machine and clone — commit it, keep it`;
|
|
227
|
+
const lines = text.split("\n");
|
|
228
|
+
const first = lines.findIndex((l) => !/^\s*(#|$)/.test(l));
|
|
229
|
+
// Nothing but comments (or nothing at all): the id goes at the end.
|
|
230
|
+
const out = first === -1 ? `${text.replace(/\n*$/, text.trim() ? "\n" : "")}${line}\n` : [...lines.slice(0, first), line, ...lines.slice(first)].join("\n");
|
|
231
|
+
await writeFile(configPath, out);
|
|
232
|
+
return id;
|
|
233
|
+
}
|
|
234
|
+
const KNOWN_KEYS = ["id", "name", "slug", "icon", "color", "port", "workflows", "output", "knowledge", "agents", "skills", "switcher", "git", "repos", "vibeables", "projects", "cloud"];
|
|
217
235
|
/**
|
|
218
236
|
* Keys an older kraftwerk understood and this one ignores, so the files
|
|
219
237
|
* that still carry them keep loading: `public:` and `tunnel:` (the
|
|
@@ -243,6 +261,9 @@ async function loadConfig(configPath) {
|
|
|
243
261
|
if (!known.includes(key)) {
|
|
244
262
|
throw new Error(`${path.basename(configPath)}: unknown key "${key}" (allowed: ${known.join(", ")})`);
|
|
245
263
|
}
|
|
264
|
+
if (key === "id" && (typeof config[key] !== "string" || !WORKSPACE_ID_RE.test(config[key]))) {
|
|
265
|
+
throw new Error(`${path.basename(configPath)}: id must be "ws_" followed by 10 lowercase letters or digits (like ws_k3f9x2m7q1), got ${JSON.stringify(config[key])} — delete the line and run \`kraftwerk doctor --fix\` for a fresh one`);
|
|
266
|
+
}
|
|
246
267
|
if (key === "slug" && (typeof config[key] !== "string" || !SLUG_RE.test(config[key]))) {
|
|
247
268
|
throw new Error(`${path.basename(configPath)}: slug must be lowercase letters, digits and dashes, starting with a letter or digit (at most 63)`);
|
|
248
269
|
}
|
|
@@ -302,7 +323,7 @@ function validateCloud(configPath, value) {
|
|
|
302
323
|
const c = value;
|
|
303
324
|
for (const key of Object.keys(c)) {
|
|
304
325
|
if (key === "token") {
|
|
305
|
-
throw new Error(`${file}: cloud.token does not belong in kraftwerk.yml —
|
|
326
|
+
throw new Error(`${file}: cloud.token does not belong in kraftwerk.yml — the cloud knows the machine, and \`kraftwerk login\` claims it`);
|
|
306
327
|
}
|
|
307
328
|
if (!["enabled", "url", "interval"].includes(key)) {
|
|
308
329
|
throw new Error(`${file}: cloud.${key} is unknown (allowed: enabled, url, interval)`);
|
package/dist/core/attention.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { type RunnerRef } from "./routines.js";
|
|
1
2
|
/**
|
|
2
3
|
* What needs you: every place where work is stuck on a human, with whose it
|
|
3
4
|
* is. Two sources: requests waiting in a live chat (an approval, a question
|
|
4
5
|
* — the agent cannot go on until someone answers) and failures nobody has
|
|
5
6
|
* looked at yet (a routine or a workflow run that ended badly; unread
|
|
6
7
|
* failure notifications). Oldest first: what waits longest comes first.
|
|
8
|
+
* A third, about setup rather than one piece of work: routines nobody runs
|
|
9
|
+
* (enabled, no runs_on) and routines naming an identity this machine has
|
|
10
|
+
* retired — both fire nowhere until someone assigns them a runner.
|
|
7
11
|
*
|
|
8
12
|
* The inspector builds everything urgent from this one list: the counts on
|
|
9
13
|
* the rail, the "needs you" part of the bell, the tab title and "next",
|
|
@@ -29,7 +33,8 @@ export interface AttentionOwner {
|
|
|
29
33
|
export interface AttentionItem {
|
|
30
34
|
/** The request id (live requests) or the notification id (failures). */
|
|
31
35
|
id: string;
|
|
32
|
-
|
|
36
|
+
/** runner: routines without a runner, or naming a retired identity of this machine (see routines.ts). */
|
|
37
|
+
kind: "approval" | "question" | "failed" | "runner";
|
|
33
38
|
/** What is asked, or what went wrong. */
|
|
34
39
|
title: string;
|
|
35
40
|
/** The session it sits in. */
|
|
@@ -41,6 +46,8 @@ export interface AttentionItem {
|
|
|
41
46
|
owner: AttentionOwner;
|
|
42
47
|
/** Failures: the notification to mark read once the item was opened. */
|
|
43
48
|
notificationId?: string;
|
|
49
|
+
/** runner items: the routines concerned; `from` is the retired identity they name. */
|
|
50
|
+
routines?: RunnerRef[];
|
|
44
51
|
}
|
|
45
52
|
export interface AttentionView {
|
|
46
53
|
items: AttentionItem[];
|
package/dist/core/attention.js
CHANGED
|
@@ -2,11 +2,13 @@ import { listAgents } from "./agents.js";
|
|
|
2
2
|
import { waitingRequests } from "./chat/sessions.js";
|
|
3
3
|
import { chatHref, listNotifications } from "./notifications.js";
|
|
4
4
|
import { listProjects } from "./projects.js";
|
|
5
|
+
import { runnerFindings } from "./routines.js";
|
|
5
6
|
export async function listAttention() {
|
|
6
|
-
const [agents, projects, notes] = await Promise.all([
|
|
7
|
+
const [agents, projects, notes, runners] = await Promise.all([
|
|
7
8
|
listAgents().catch(() => []),
|
|
8
9
|
listProjects().catch(() => ({ projects: [] })),
|
|
9
10
|
listNotifications().catch(() => ({ items: [] })),
|
|
11
|
+
runnerFindings().catch(() => ({ unassigned: [], retired: [] })),
|
|
10
12
|
]);
|
|
11
13
|
const agentOf = (slug) => {
|
|
12
14
|
const a = agents.find((x) => x.slug === slug);
|
|
@@ -65,6 +67,40 @@ export async function listAttention() {
|
|
|
65
67
|
notificationId: n.id,
|
|
66
68
|
};
|
|
67
69
|
});
|
|
68
|
-
|
|
70
|
+
// One item per problem, not per routine: the fix ("run these here") is one action for all of them.
|
|
71
|
+
const since = runners.since ?? new Date(0).toISOString();
|
|
72
|
+
// The fix lives on an agent profile's routines panel (/agents/<slug>/info — /agents/<slug> redirects
|
|
73
|
+
// to a chat). Routines of one agent: that profile and its rail entry; of several: the first agent's
|
|
74
|
+
// profile, whose banner adopts across the whole workspace.
|
|
75
|
+
const placed = (these) => {
|
|
76
|
+
const slugs = [...new Set(these.map((r) => r.agent))];
|
|
77
|
+
return slugs.length === 1
|
|
78
|
+
? { href: `/agents/${slugs[0]}/info`, owner: { agent: agentOf(slugs[0]) } }
|
|
79
|
+
: { href: `/agents/${slugs[0]}/info`, owner: {} };
|
|
80
|
+
};
|
|
81
|
+
const runner = [];
|
|
82
|
+
const n = runners.unassigned.length;
|
|
83
|
+
if (n) {
|
|
84
|
+
runner.push({
|
|
85
|
+
id: "runner:unassigned",
|
|
86
|
+
kind: "runner",
|
|
87
|
+
title: `${n} routine${n === 1 ? " has" : "s have"} no runner — ${n === 1 ? "it fires" : "they fire"} nowhere until a machine runs ${n === 1 ? "it" : "them"}`,
|
|
88
|
+
since,
|
|
89
|
+
...placed(runners.unassigned),
|
|
90
|
+
routines: runners.unassigned,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
for (const old of [...new Set(runners.retired.map((r) => r.from))]) {
|
|
94
|
+
const these = runners.retired.filter((r) => r.from === old);
|
|
95
|
+
runner.push({
|
|
96
|
+
id: `runner:retired:${old}`,
|
|
97
|
+
kind: "runner",
|
|
98
|
+
title: `${these.length} routine${these.length === 1 ? " names" : "s name"} a retired identity ${old} — reassign`,
|
|
99
|
+
since,
|
|
100
|
+
...placed(these),
|
|
101
|
+
routines: these,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const items = [...waiting, ...failed, ...runner].sort((a, b) => a.since.localeCompare(b.since));
|
|
69
105
|
return { items };
|
|
70
106
|
}
|