@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.
@@ -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,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerRoutineCommands(program: Command): void;
@@ -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.18.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",