@netnodeag/kraftwerk 0.18.0 → 0.19.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/kraftwerk.js +3 -0
- package/dist/cli/routines.d.ts +2 -0
- package/dist/cli/routines.js +177 -0
- package/package.json +1 -1
package/dist/cli/kraftwerk.js
CHANGED
|
@@ -12,6 +12,7 @@ import { renderCreateBrief } from "./create-brief.js";
|
|
|
12
12
|
import { runDoctor } from "./doctor.js";
|
|
13
13
|
import { runInit } from "./init.js";
|
|
14
14
|
import { registerKnowledgeCommands } from "./knowledge.js";
|
|
15
|
+
import { registerRoutineCommands } from "./routines.js";
|
|
15
16
|
import { listRuns, showRun } from "./runs.js";
|
|
16
17
|
import { runUi } from "./ui.js";
|
|
17
18
|
/**
|
|
@@ -22,6 +23,7 @@ import { runUi } from "./ui.js";
|
|
|
22
23
|
* kraftwerk run [workflow] [text] run one (prompts interactively if omitted)
|
|
23
24
|
* kraftwerk runs [show <id>] inspect past runs from their traces
|
|
24
25
|
* kraftwerk knowledge ... Context & Knowledge: OKF bundles (list/get/put/verify/...)
|
|
26
|
+
* kraftwerk routines ... per-agent scheduled prompts (list/add/remove/enable/run)
|
|
25
27
|
* kraftwerk ui start the inspector web UI (localhost:1981)
|
|
26
28
|
* kraftwerk doctor preflight: harness CLIs, docker, workflows, env
|
|
27
29
|
* kraftwerk validate [paths...] validate without executing
|
|
@@ -253,6 +255,7 @@ runs
|
|
|
253
255
|
await showRun(process.cwd(), runId, opts);
|
|
254
256
|
});
|
|
255
257
|
registerKnowledgeCommands(program);
|
|
258
|
+
registerRoutineCommands(program);
|
|
256
259
|
program
|
|
257
260
|
.command("ui")
|
|
258
261
|
.description("Start the inspector web UI for this project's runs and workflows")
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { resolveProject } from "../config.js";
|
|
5
|
+
import { setOutputDir, setProjectRoot } from "../inspector/context.js";
|
|
6
|
+
import { deleteRoutine, listRoutines, routineStatuses, saveRoutine, } from "../inspector/routines.js";
|
|
7
|
+
import { getMember, listMembers } from "../inspector/team.js";
|
|
8
|
+
/**
|
|
9
|
+
* `kraftwerk routines` — CLI surface over per-agent scheduled prompts
|
|
10
|
+
* (agents/<slug>/routines.yml). Definition CRUD works directly on the
|
|
11
|
+
* files; `run` fires through the running inspector server, because the
|
|
12
|
+
* session (chat) and the scheduler live there.
|
|
13
|
+
*/
|
|
14
|
+
/** Point the inspector context at this project so team/routine paths resolve. */
|
|
15
|
+
async function initContext() {
|
|
16
|
+
const project = await resolveProject(process.cwd());
|
|
17
|
+
setProjectRoot(project.root);
|
|
18
|
+
setOutputDir(project.outputDir);
|
|
19
|
+
return { port: project.config.port ?? 1981 };
|
|
20
|
+
}
|
|
21
|
+
async function requireMember(slug) {
|
|
22
|
+
if (!(await getMember(slug))) {
|
|
23
|
+
console.error(chalk.red(`Agent "${slug}" not found (expected agents/${slug}/agent.yml).`));
|
|
24
|
+
process.exit(2);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function readStdin() {
|
|
28
|
+
let data = "";
|
|
29
|
+
for await (const chunk of process.stdin)
|
|
30
|
+
data += chunk;
|
|
31
|
+
return data;
|
|
32
|
+
}
|
|
33
|
+
/** ISO timestamp → local "YYYY-MM-DD HH:mm" (schedules are server-local time). */
|
|
34
|
+
const fmtTime = (iso) => {
|
|
35
|
+
if (!iso)
|
|
36
|
+
return "—";
|
|
37
|
+
const d = new Date(iso);
|
|
38
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
39
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
40
|
+
};
|
|
41
|
+
function routineLine(r) {
|
|
42
|
+
const state = r.enabled ? chalk.green("on ") : chalk.dim("off");
|
|
43
|
+
const flags = r.lastError ? ` ${chalk.red(`error: ${r.lastError}`)}` : "";
|
|
44
|
+
return (` ${chalk.cyan(r.id.padEnd(24))} ${state} ${r.schedule.padEnd(14)} ` +
|
|
45
|
+
chalk.dim(`next ${fmtTime(r.nextRunAt).padEnd(17)} last ${fmtTime(r.lastRunAt)}`) +
|
|
46
|
+
flags);
|
|
47
|
+
}
|
|
48
|
+
export function registerRoutineCommands(program) {
|
|
49
|
+
const routines = program
|
|
50
|
+
.command("routines")
|
|
51
|
+
.description("Per-agent scheduled prompts (agents/<slug>/routines.yml): list, add, run, ...");
|
|
52
|
+
routines
|
|
53
|
+
.command("list", { isDefault: true })
|
|
54
|
+
.description("List routines — all agents, or one agent with schedule state")
|
|
55
|
+
.argument("[agent]", "Agent slug (folder under agents/)")
|
|
56
|
+
.option("--json", "Machine-readable output")
|
|
57
|
+
.action(async (agent, opts) => {
|
|
58
|
+
await initContext();
|
|
59
|
+
const slugs = agent ? [agent] : (await listMembers()).map((m) => m.slug);
|
|
60
|
+
if (agent)
|
|
61
|
+
await requireMember(agent);
|
|
62
|
+
const all = {};
|
|
63
|
+
for (const slug of slugs)
|
|
64
|
+
all[slug] = await routineStatuses(slug);
|
|
65
|
+
if (opts.json)
|
|
66
|
+
return void console.log(JSON.stringify(all, null, 2));
|
|
67
|
+
let any = false;
|
|
68
|
+
for (const [slug, rs] of Object.entries(all)) {
|
|
69
|
+
if (rs.length === 0)
|
|
70
|
+
continue;
|
|
71
|
+
any = true;
|
|
72
|
+
console.log(chalk.bold(slug));
|
|
73
|
+
for (const r of rs)
|
|
74
|
+
console.log(routineLine(r));
|
|
75
|
+
}
|
|
76
|
+
if (!any) {
|
|
77
|
+
console.log(chalk.yellow("No routines yet — add one with `kraftwerk routines add <agent> --name ... --schedule ... --prompt ...`."));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
routines
|
|
81
|
+
.command("add")
|
|
82
|
+
.description("Create or update a routine (same id = update)")
|
|
83
|
+
.argument("<agent>", "Agent slug")
|
|
84
|
+
.requiredOption("--name <name>", 'Display name, e.g. "Morning triage"')
|
|
85
|
+
.requiredOption("--schedule <cron>", '5-field cron ("0 9 * * 1-5") or @hourly/@daily/@weekly/@monthly')
|
|
86
|
+
.option("--prompt <text>", "The prompt to post (or pipe it on stdin / use --file)")
|
|
87
|
+
.option("--file <path>", "Read the prompt from a file")
|
|
88
|
+
.option("--id <id>", "Routine id (default: derived from the name)")
|
|
89
|
+
.option("--disabled", "Create it switched off")
|
|
90
|
+
.action(async (agent, opts) => {
|
|
91
|
+
await initContext();
|
|
92
|
+
await requireMember(agent);
|
|
93
|
+
const prompt = opts.prompt ??
|
|
94
|
+
(opts.file ? await readFile(path.resolve(opts.file), "utf8") : process.stdin.isTTY ? "" : await readStdin());
|
|
95
|
+
if (!prompt.trim()) {
|
|
96
|
+
console.error(chalk.red("No prompt (--prompt, --file, or pipe it on stdin)."));
|
|
97
|
+
process.exit(2);
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const existing = new Set((await listRoutines(agent)).map((r) => r.id));
|
|
101
|
+
const routine = await saveRoutine(agent, {
|
|
102
|
+
...(opts.id ? { id: opts.id } : {}),
|
|
103
|
+
name: opts.name,
|
|
104
|
+
schedule: opts.schedule,
|
|
105
|
+
prompt,
|
|
106
|
+
enabled: !opts.disabled,
|
|
107
|
+
});
|
|
108
|
+
const verb = existing.has(routine.id) ? "updated" : "created";
|
|
109
|
+
console.log(`${chalk.green("✔")} ${verb} ${chalk.cyan(`${agent}/${routine.id}`)} ` +
|
|
110
|
+
chalk.dim(`(${routine.schedule}${routine.enabled ? "" : ", disabled"})`));
|
|
111
|
+
if (routine.enabled) {
|
|
112
|
+
console.log(chalk.dim(" fires from the inspector server — make sure `kraftwerk ui` is running."));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
console.error(chalk.red(err.message));
|
|
117
|
+
process.exit(2);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
routines
|
|
121
|
+
.command("remove")
|
|
122
|
+
.description("Delete a routine")
|
|
123
|
+
.argument("<agent>", "Agent slug")
|
|
124
|
+
.argument("<id>", "Routine id (see `kraftwerk routines <agent>`)")
|
|
125
|
+
.action(async (agent, id) => {
|
|
126
|
+
await initContext();
|
|
127
|
+
await requireMember(agent);
|
|
128
|
+
if (!(await listRoutines(agent)).some((r) => r.id === id)) {
|
|
129
|
+
console.error(chalk.red(`Routine "${agent}/${id}" not found.`));
|
|
130
|
+
process.exit(2);
|
|
131
|
+
}
|
|
132
|
+
await deleteRoutine(agent, id);
|
|
133
|
+
console.log(`${chalk.green("✔")} removed ${chalk.cyan(`${agent}/${id}`)}`);
|
|
134
|
+
});
|
|
135
|
+
for (const [verb, enabled] of [["enable", true], ["disable", false]]) {
|
|
136
|
+
routines
|
|
137
|
+
.command(verb)
|
|
138
|
+
.description(`${verb === "enable" ? "Enable" : "Disable"} a routine`)
|
|
139
|
+
.argument("<agent>", "Agent slug")
|
|
140
|
+
.argument("<id>", "Routine id")
|
|
141
|
+
.action(async (agent, id) => {
|
|
142
|
+
await initContext();
|
|
143
|
+
const routine = (await listRoutines(agent)).find((r) => r.id === id);
|
|
144
|
+
if (!routine) {
|
|
145
|
+
console.error(chalk.red(`Routine "${agent}/${id}" not found.`));
|
|
146
|
+
process.exit(2);
|
|
147
|
+
}
|
|
148
|
+
await saveRoutine(agent, { ...routine, enabled });
|
|
149
|
+
console.log(`${chalk.green("✔")} ${chalk.cyan(`${agent}/${id}`)} ${verb}d`);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
routines
|
|
153
|
+
.command("run")
|
|
154
|
+
.description("Fire a routine now (opens a session via the running `kraftwerk ui` server)")
|
|
155
|
+
.argument("<agent>", "Agent slug")
|
|
156
|
+
.argument("<id>", "Routine id")
|
|
157
|
+
.option("--port <port>", "Inspector port (default: kraftwerk.yml `port`, else 1981)")
|
|
158
|
+
.action(async (agent, id, opts) => {
|
|
159
|
+
const { port } = await initContext();
|
|
160
|
+
const p = opts.port ? Number(opts.port) : port;
|
|
161
|
+
const url = `http://localhost:${p}/api/team/${encodeURIComponent(agent)}/routines/${encodeURIComponent(id)}/run`;
|
|
162
|
+
let body;
|
|
163
|
+
try {
|
|
164
|
+
body = (await (await fetch(url, { method: "POST" })).json());
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
console.error(chalk.red(`Inspector not reachable on localhost:${p} — routines fire through the server; start it with \`kraftwerk ui\`.`));
|
|
168
|
+
process.exit(2);
|
|
169
|
+
}
|
|
170
|
+
if (body.error || !body.chatId) {
|
|
171
|
+
console.error(chalk.red(body.error ?? "run failed"));
|
|
172
|
+
process.exit(2);
|
|
173
|
+
}
|
|
174
|
+
console.log(`${chalk.green("✔")} ${chalk.cyan(`${agent}/${id}`)} fired → session ${body.chatId}\n` +
|
|
175
|
+
chalk.dim(` http://localhost:${p}/#/agents/${encodeURIComponent(agent)}/chat/${body.chatId}`));
|
|
176
|
+
});
|
|
177
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netnodeag/kraftwerk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Deterministic workflow-as-code framework: agents (persona + model + tools + harness) run in bounded phases on headless CLI harnesses (claude -p, codex exec, pi); code owns the control flow, envelopes + gates judge the results",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|