@netnodeag/kraftwerk 0.34.2 → 0.36.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 +40 -19
- package/dist/cli/init.js +3 -3
- package/dist/cli/routines.js +9 -9
- package/dist/config.d.ts +26 -2
- package/dist/config.js +43 -3
- package/dist/inspector/{team.d.ts → agents.d.ts} +21 -12
- package/dist/inspector/{team.js → agents.js} +34 -19
- package/dist/inspector/chat/backend.d.ts +1 -1
- package/dist/inspector/chat/sessions.js +43 -43
- package/dist/inspector/chat/store.js +13 -1
- package/dist/inspector/chat/types.d.ts +2 -2
- package/dist/inspector/git.d.ts +83 -0
- package/dist/inspector/git.js +651 -0
- package/dist/inspector/instances.d.ts +12 -0
- package/dist/inspector/instances.js +37 -1
- package/dist/inspector/routines.d.ts +4 -4
- package/dist/inspector/routines.js +11 -11
- package/dist/inspector/search.d.ts +25 -0
- package/dist/inspector/search.js +31 -0
- package/dist/inspector/server.js +166 -32
- package/dist/inspector/settings.d.ts +11 -1
- package/dist/inspector/settings.js +45 -0
- package/dist/inspector/skills.d.ts +6 -6
- package/dist/inspector/skills.js +8 -8
- package/dist/runner/docker.d.ts +1 -0
- package/dist/runner/docker.js +1 -1
- package/inspector/dist/assets/{editor-CXrDJOv_.js → editor-7ImnDC3D.js} +3 -3
- package/inspector/dist/assets/{index-BxEG-IdY.js → index--2I7vpcS.js} +1 -1
- package/inspector/dist/assets/index-B2ThRS2S.js +217 -0
- package/inspector/dist/assets/index-BA7nx4V5.css +1 -0
- package/inspector/dist/assets/{index-Bumwg9SM.js → index-BC7aIJ-h.js} +1 -1
- package/inspector/dist/assets/{index-BULB2rC_.js → index-BIXYBiw7.js} +1 -1
- package/inspector/dist/assets/{index-C3hpUVfp.js → index-BZeYO5wT.js} +1 -1
- package/inspector/dist/assets/{index-BQMgADlc.js → index-B_CIhKa4.js} +1 -1
- package/inspector/dist/assets/{index-DrSWAGft.js → index-BuRajvjQ.js} +1 -1
- package/inspector/dist/assets/{index-BNcXQYbv.js → index-C3Ob1O7W.js} +1 -1
- package/inspector/dist/assets/{index-BIDsfw8C.js → index-CIjv7yxE.js} +1 -1
- package/inspector/dist/assets/{index-ChuqpBnv.js → index-CRAwmMfk.js} +1 -1
- package/inspector/dist/assets/{index-x5G6Np7T.js → index-Ct_jZSyD.js} +1 -1
- package/inspector/dist/assets/{index-7J4zAS6J.js → index-DKrBjjGQ.js} +1 -1
- package/inspector/dist/assets/{index-CUCCMUMY.js → index-DL_bJiOj.js} +1 -1
- package/inspector/dist/assets/{index-DAi1VtJ8.js → index-DPBAv_kj.js} +1 -1
- package/inspector/dist/assets/{index-Upf-BEsx.js → index-DbmSSHvr.js} +1 -1
- package/inspector/dist/assets/{index-CNJyAniG.js → index-DjRBLY9N.js} +1 -1
- package/inspector/dist/assets/{index-CszaAR9T.js → index-YZtBJhWD.js} +1 -1
- package/inspector/dist/assets/{index-CMMYSoL3.js → index-fWUFhIGR.js} +1 -1
- package/inspector/dist/index.html +2 -2
- package/package.json +5 -2
- package/schema/kraftwerk.schema.json +35 -1
- package/inspector/dist/assets/index-B7rwNdVj.css +0 -1
- package/inspector/dist/assets/index-BBGjdXk1.js +0 -213
package/README.md
CHANGED
|
@@ -160,12 +160,26 @@ kraftwerk run --sandbox website-check "https://..." # isolated container per r
|
|
|
160
160
|
kraftwerk runner ps / stop <run-id> # see / stop running sandbox containers
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
+
The inspector binds `127.0.0.1` — it has no authentication of its own and
|
|
164
|
+
its chat runs coding agents against the repo, so it stays off the LAN, and
|
|
165
|
+
while bound there it only answers requests whose `Host` is a loopback name
|
|
166
|
+
(so a page on another site cannot reach it by re-pointing its own DNS at
|
|
167
|
+
127.0.0.1). Inside a container it binds all interfaces instead, because the
|
|
168
|
+
port mapping is the boundary there (`deploy-starter/` publishes to
|
|
169
|
+
localhost, and its traefik override adds basic-auth). `KRAFTWERK_UI_HOST`
|
|
170
|
+
overrides the default either way. A reverse proxy in front of it must set
|
|
171
|
+
`X-Forwarded-Host` to the host the browser addressed (Caddy and traefik do
|
|
172
|
+
by default; nginx needs `proxy_set_header X-Forwarded-Host $host;`): the
|
|
173
|
+
loopback bind answers a non-loopback `Host` only when that header is
|
|
174
|
+
present, and state-changing requests are refused when `Origin` names a
|
|
175
|
+
different host.
|
|
176
|
+
|
|
163
177
|
### The kraftwerk.yml project config
|
|
164
178
|
|
|
165
179
|
Optional, at the project root, and also the root marker for the walk-up. All
|
|
166
180
|
fields are optional. `workflows:` sets the workflows root, `output:` the
|
|
167
181
|
run-artifact directory (default `output/`), `knowledge:` the OKF bundle root
|
|
168
|
-
(default `knowledge/`), and `agents:` the
|
|
182
|
+
(default `knowledge/`), and `agents:` the agent-definition root (default
|
|
169
183
|
`agents/`).
|
|
170
184
|
|
|
171
185
|
`switcher:` links other kraftwerk workspaces from the inspector header. The
|
|
@@ -178,6 +192,13 @@ switcher:
|
|
|
178
192
|
icon: "🛰" # optional
|
|
179
193
|
```
|
|
180
194
|
|
|
195
|
+
**⌘K** (Ctrl K) anywhere in the inspector opens the agent palette: type a
|
|
196
|
+
few letters of an agent's name, description or workspace and hit enter to
|
|
197
|
+
jump to it. It lists the active agents of every workspace on this machine,
|
|
198
|
+
running or not (a stopped one is started on the way), read from the
|
|
199
|
+
project registry under `~/.kraftwerk/projects`, which every inspector keeps
|
|
200
|
+
current with its roster.
|
|
201
|
+
|
|
181
202
|
### Triggering from CI, cron, or webhooks
|
|
182
203
|
|
|
183
204
|
`run --json` is the machine mode. It runs non-interactively, prints one JSON
|
|
@@ -235,15 +256,15 @@ repo. Agent logins made inside the container persist in the `agent-home`
|
|
|
235
256
|
volume. This is a different image from the `kraftwerk-runner` sandbox
|
|
236
257
|
(`runner/Dockerfile`) used by `run --sandbox`.
|
|
237
258
|
|
|
238
|
-
## Persistent
|
|
259
|
+
## Persistent agents
|
|
239
260
|
|
|
240
|
-
The inspector's "
|
|
241
|
-
|
|
261
|
+
The inspector's "agents" screen turns chat agents into persistent teammates. An
|
|
262
|
+
agent is one folder under the project's `agents/` root:
|
|
242
263
|
|
|
243
264
|
```
|
|
244
265
|
agents/max/
|
|
245
266
|
agent.yml # name, emoji, description, harness, model, effort, workflows
|
|
246
|
-
system.md # the
|
|
267
|
+
system.md # the agent's system prompt (its role)
|
|
247
268
|
```
|
|
248
269
|
|
|
249
270
|
```yaml
|
|
@@ -255,43 +276,43 @@ harness: claude # claude | codex | pi (which chat backend runs it)
|
|
|
255
276
|
model: sonnet # optional; harness default when omitted
|
|
256
277
|
effort: medium # optional: low | medium | high | xhigh | max
|
|
257
278
|
workflows: [tagline, website-check]
|
|
258
|
-
knowledge: [customer-support] # OKF bundles the
|
|
279
|
+
knowledge: [customer-support] # OKF bundles the agent consults & maintains
|
|
259
280
|
skills: [report-html] # optional allowlist; omit = all skills, [] = none
|
|
260
281
|
```
|
|
261
282
|
|
|
262
|
-
Sessions with
|
|
263
|
-
listed per
|
|
283
|
+
Sessions with an agent are ordinary chats scoped `{ kind: "agent", slug }`,
|
|
284
|
+
listed per agent in a second sidebar. On the first message the agent gets
|
|
264
285
|
its role plus its connected workflows and knowledge bundles injected as
|
|
265
286
|
context. That context includes how to run workflows
|
|
266
287
|
(`KRAFTWERK_YES=1 npx kraftwerk run <workflow> "<request>"`) and how to read
|
|
267
288
|
and write knowledge through `kraftwerk knowledge`, with writes stamped with
|
|
268
|
-
the
|
|
289
|
+
the agent's own actor, `<slug>/<harness>`. So the agent triggers its own
|
|
269
290
|
workflows when a request matches, and keeps its bundles current.
|
|
270
291
|
|
|
271
292
|
Model and effort ride on backend-specific channels. The claude adapter takes
|
|
272
293
|
the model via ACP session options and the thinking budget via
|
|
273
294
|
`MAX_THINKING_TOKENS`. Codex gets a `CODEX_CONFIG` env override (`model`,
|
|
274
|
-
`model_reasoning_effort`). Pi gets `--model` and `--thinking` flags.
|
|
295
|
+
`model_reasoning_effort`). Pi gets `--model` and `--thinking` flags. Agents
|
|
275
296
|
are created and edited in the UI, or by editing the files, since the
|
|
276
297
|
definition is read fresh for each new session.
|
|
277
298
|
|
|
278
299
|
### Skills in chat
|
|
279
300
|
|
|
280
|
-
Chats, both general ones and
|
|
301
|
+
Chats, both general ones and agent sessions, can use skills. These are
|
|
281
302
|
Claude-style instruction packages, one folder per skill with a `SKILL.md`
|
|
282
303
|
holding YAML frontmatter (`name`, `description`) and then the instructions.
|
|
283
304
|
Four roots are discovered, each shadowing same-named skills in the roots below
|
|
284
305
|
it:
|
|
285
306
|
|
|
286
307
|
```
|
|
287
|
-
<project>/agents/<slug>/skills/<name>/SKILL.md # private to that one
|
|
308
|
+
<project>/agents/<slug>/skills/<name>/SKILL.md # private to that one agent
|
|
288
309
|
<project>/<skills root>/<name>/SKILL.md # workspace, git-tracked (kraftwerk.yml `skills`, default skills/)
|
|
289
310
|
<project>/.claude/skills/<name>/SKILL.md # git-tracked, per project
|
|
290
311
|
~/.claude/skills/<name>/SKILL.md # personal, per user
|
|
291
312
|
```
|
|
292
313
|
|
|
293
314
|
Agent skills are visible only to that agent's sessions and always apply. A
|
|
294
|
-
|
|
315
|
+
agent's `skills:` allowlist narrows the shared roots only. Manage them in the
|
|
295
316
|
agent profile under "own skills", or by editing the files.
|
|
296
317
|
|
|
297
318
|
Every chat lists its visible skills as context under "## Your skills", so the
|
|
@@ -299,16 +320,16 @@ agent reaches for one when the request matches. Typing `/` in the composer
|
|
|
299
320
|
opens an autocomplete over them. Sending `/<name> <args>` expands the skill's
|
|
300
321
|
SKILL.md into the prompt, which is what makes this work identically on claude,
|
|
301
322
|
codex, and pi. On top of that, claude discovers `.claude/skills` natively (a
|
|
302
|
-
|
|
303
|
-
loads each visible skill folder via `--skill`.
|
|
323
|
+
agent's `skills:` allowlist narrows that via ACP session options) and pi
|
|
324
|
+
loads each visible skill folder via `--skill`. Agents take an optional
|
|
304
325
|
`skills:` list in `agent.yml`. Omitted means all discovered skills, an empty
|
|
305
326
|
list means none, otherwise it is the allowlist. `GET /api/skills` returns
|
|
306
327
|
what's discovered.
|
|
307
328
|
|
|
308
329
|
### Routines, or scheduled prompts
|
|
309
330
|
|
|
310
|
-
|
|
311
|
-
orders for an employee. Definitions live next to the
|
|
331
|
+
An agent can have routines, cron-scheduled prompts that work like standing
|
|
332
|
+
orders for an employee. Definitions live next to the agent in
|
|
312
333
|
`agents/<slug>/routines.yml` and are git-tracked. Run state (last run, last
|
|
313
334
|
session, errors) lives in `<output>/routines-state.json`.
|
|
314
335
|
|
|
@@ -324,12 +345,12 @@ session, errors) lives in `<output>/routines-state.json`.
|
|
|
324
345
|
```
|
|
325
346
|
|
|
326
347
|
The inspector server runs the scheduler in-process, so there is no external
|
|
327
|
-
cron to set up. Every due routine opens a fresh session for the
|
|
348
|
+
cron to set up. Every due routine opens a fresh session for the agent, posts
|
|
328
349
|
the prompt, and shows up in the sessions sidebar titled "⏰ <name>". Routine
|
|
329
350
|
sessions run unattended, so tool-permission requests are auto-approved, with
|
|
330
351
|
the request and approval pair left in the thread as an audit trail. Give
|
|
331
352
|
routine prompts the same trust you would give a `KRAFTWERK_YES=1` workflow
|
|
332
|
-
run. Manage routines on the
|
|
353
|
+
run. Manage routines on the agent page, where you can create, edit and
|
|
333
354
|
delete them, toggle enabled, hit "run now", and jump to the last run's
|
|
334
355
|
session. Schedules missed while the server is down are skipped, not replayed.
|
|
335
356
|
|
package/dist/cli/init.js
CHANGED
|
@@ -6,7 +6,7 @@ import { initBundle, writeConcept } from "../okf.js";
|
|
|
6
6
|
/**
|
|
7
7
|
* `kraftwerk init` — make any repository a kraftwerk consumer in one
|
|
8
8
|
* command: kraftwerk.yml (root marker + config), a kraftwerk-data/ tree with
|
|
9
|
-
* a runnable example workflow, a starter
|
|
9
|
+
* a runnable example workflow, a starter agent ("max"), a demo
|
|
10
10
|
* knowledge bundle, and an output ignore entry. Everything is idempotent:
|
|
11
11
|
* existing files are left untouched and reported.
|
|
12
12
|
*/
|
|
@@ -20,7 +20,7 @@ port: 1981 # port \`kraftwerk ui\` listens on
|
|
|
20
20
|
workflows: ${DATA_DIR}/workflows # where workflows live
|
|
21
21
|
output: ${DATA_DIR}/output # where run artifacts land (git-ignored)
|
|
22
22
|
knowledge: ${DATA_DIR}/knowledge # OKF knowledge bundles
|
|
23
|
-
agents: ${DATA_DIR}/agents #
|
|
23
|
+
agents: ${DATA_DIR}/agents # agent definitions
|
|
24
24
|
skills: ${DATA_DIR}/skills # workspace skills (shared instruction packages)
|
|
25
25
|
`;
|
|
26
26
|
const WORKFLOW_TEMPLATE = `# yaml-language-server: $schema=${SCHEMA_URL}
|
|
@@ -107,7 +107,7 @@ export async function runInit(cwd) {
|
|
|
107
107
|
await put(`${DATA_DIR}/workflows/hello/workflow.yml`, WORKFLOW_TEMPLATE);
|
|
108
108
|
await put(`${DATA_DIR}/workflows/hello/prompts/assistant.md`, PERSONA_TEMPLATE);
|
|
109
109
|
await put(`${DATA_DIR}/workflows/hello/prompts/answer.md`, PROMPT_TEMPLATE);
|
|
110
|
-
//
|
|
110
|
+
// Agent "max".
|
|
111
111
|
await put(`${DATA_DIR}/agents/max/agent.yml`, AGENT_YML_TEMPLATE);
|
|
112
112
|
await put(`${DATA_DIR}/agents/max/system.md`, AGENT_SYSTEM_TEMPLATE);
|
|
113
113
|
// Example workspace skill.
|
package/dist/cli/routines.js
CHANGED
|
@@ -4,22 +4,22 @@ import chalk from "chalk";
|
|
|
4
4
|
import { resolveProject } from "../config.js";
|
|
5
5
|
import { setOutputDir, setProjectRoot } from "../inspector/context.js";
|
|
6
6
|
import { deleteRoutine, listRoutines, routineStatuses, saveRoutine, } from "../inspector/routines.js";
|
|
7
|
-
import {
|
|
7
|
+
import { getAgent, listAgents } from "../inspector/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
12
|
* session (chat) and the scheduler live there.
|
|
13
13
|
*/
|
|
14
|
-
/** Point the inspector context at this project so
|
|
14
|
+
/** Point the inspector context at this project so agent/routine paths resolve. */
|
|
15
15
|
async function initContext() {
|
|
16
16
|
const project = await resolveProject(process.cwd());
|
|
17
17
|
setProjectRoot(project.root);
|
|
18
18
|
setOutputDir(project.outputDir);
|
|
19
19
|
return { port: project.config.port ?? 1981 };
|
|
20
20
|
}
|
|
21
|
-
async function
|
|
22
|
-
if (!(await
|
|
21
|
+
async function requireAgent(slug) {
|
|
22
|
+
if (!(await getAgent(slug))) {
|
|
23
23
|
console.error(chalk.red(`Agent "${slug}" not found (expected agents/${slug}/agent.yml).`));
|
|
24
24
|
process.exit(2);
|
|
25
25
|
}
|
|
@@ -56,9 +56,9 @@ export function registerRoutineCommands(program) {
|
|
|
56
56
|
.option("--json", "Machine-readable output")
|
|
57
57
|
.action(async (agent, opts) => {
|
|
58
58
|
await initContext();
|
|
59
|
-
const slugs = agent ? [agent] : (await
|
|
59
|
+
const slugs = agent ? [agent] : (await listAgents()).map((m) => m.slug);
|
|
60
60
|
if (agent)
|
|
61
|
-
await
|
|
61
|
+
await requireAgent(agent);
|
|
62
62
|
const all = {};
|
|
63
63
|
for (const slug of slugs)
|
|
64
64
|
all[slug] = await routineStatuses(slug);
|
|
@@ -89,7 +89,7 @@ export function registerRoutineCommands(program) {
|
|
|
89
89
|
.option("--disabled", "Create it switched off")
|
|
90
90
|
.action(async (agent, opts) => {
|
|
91
91
|
await initContext();
|
|
92
|
-
await
|
|
92
|
+
await requireAgent(agent);
|
|
93
93
|
const prompt = opts.prompt ??
|
|
94
94
|
(opts.file ? await readFile(path.resolve(opts.file), "utf8") : process.stdin.isTTY ? "" : await readStdin());
|
|
95
95
|
if (!prompt.trim()) {
|
|
@@ -124,7 +124,7 @@ export function registerRoutineCommands(program) {
|
|
|
124
124
|
.argument("<id>", "Routine id (see `kraftwerk routines <agent>`)")
|
|
125
125
|
.action(async (agent, id) => {
|
|
126
126
|
await initContext();
|
|
127
|
-
await
|
|
127
|
+
await requireAgent(agent);
|
|
128
128
|
if (!(await listRoutines(agent)).some((r) => r.id === id)) {
|
|
129
129
|
console.error(chalk.red(`Routine "${agent}/${id}" not found.`));
|
|
130
130
|
process.exit(2);
|
|
@@ -158,7 +158,7 @@ export function registerRoutineCommands(program) {
|
|
|
158
158
|
.action(async (agent, id, opts) => {
|
|
159
159
|
const { port } = await initContext();
|
|
160
160
|
const p = opts.port ? Number(opts.port) : port;
|
|
161
|
-
const url = `http://localhost:${p}/api/
|
|
161
|
+
const url = `http://localhost:${p}/api/agents/${encodeURIComponent(agent)}/routines/${encodeURIComponent(id)}/run`;
|
|
162
162
|
let body;
|
|
163
163
|
try {
|
|
164
164
|
body = (await (await fetch(url, { method: "POST" })).json());
|
package/dist/config.d.ts
CHANGED
|
@@ -15,18 +15,40 @@
|
|
|
15
15
|
* workflows: src/workflows # workflows root, relative to the file
|
|
16
16
|
* output: output # run-artifact directory, relative to the file
|
|
17
17
|
* knowledge: knowledge # OKF knowledge-bundle root, relative to the file
|
|
18
|
-
* agents: agents #
|
|
18
|
+
* agents: agents # agent-definition root, relative to the file
|
|
19
19
|
* skills: skills # workspace skill root, relative to the file
|
|
20
20
|
* switcher: # other kraftwerk workspaces, linked from the header dropdown
|
|
21
21
|
* - name: other space
|
|
22
22
|
* url: https://localhost:1985
|
|
23
23
|
* icon: "🛰" # optional emoji shown next to the entry
|
|
24
|
+
* git: # workspace git sync (absent = off, bare key = on with defaults)
|
|
25
|
+
* remote: origin
|
|
26
|
+
* interval: 300 # seconds between background fetches
|
|
27
|
+
* autosync: pull # off | pull
|
|
24
28
|
*/
|
|
25
29
|
/** Stable, versionless URL of the workflow JSON schema (editor validation). */
|
|
26
30
|
export declare const SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/workflow.schema.json";
|
|
27
31
|
/** Stable, versionless URL of the kraftwerk.yml JSON schema (editor validation). */
|
|
28
32
|
export declare const CONFIG_SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/kraftwerk.schema.json";
|
|
29
33
|
export declare const CONFIG_FILENAMES: string[];
|
|
34
|
+
/**
|
|
35
|
+
* Git sync for the workspace. The synced paths are not configurable: they
|
|
36
|
+
* are the roots this file already declares (workflows, knowledge, agents,
|
|
37
|
+
* skills) plus kraftwerk.yml itself. Commit and push stay manual; the
|
|
38
|
+
* interval only fetches, and pulls when `autosync` allows it.
|
|
39
|
+
*/
|
|
40
|
+
export interface GitConfig {
|
|
41
|
+
/** false keeps the block but turns the feature off. Default: true. */
|
|
42
|
+
enabled?: boolean;
|
|
43
|
+
/** Remote to fetch, pull and push. Default: origin. */
|
|
44
|
+
remote?: string;
|
|
45
|
+
/** Branch to sync. Default: whatever is checked out. */
|
|
46
|
+
branch?: string;
|
|
47
|
+
/** Seconds between background fetches. 0 disables the timer. Default: 300. */
|
|
48
|
+
interval?: number;
|
|
49
|
+
/** off = fetch only. pull = also fast-forward when behind and clean. Default: pull. */
|
|
50
|
+
autosync?: "off" | "pull";
|
|
51
|
+
}
|
|
30
52
|
/** One entry of the workspace switcher: another kraftwerk instance to link to. */
|
|
31
53
|
export interface SwitcherEntry {
|
|
32
54
|
/** Display name of the other workspace. */
|
|
@@ -49,12 +71,14 @@ export interface ProjectConfig {
|
|
|
49
71
|
output?: string;
|
|
50
72
|
/** OKF knowledge-bundle root relative to the project root. Default: knowledge */
|
|
51
73
|
knowledge?: string;
|
|
52
|
-
/**
|
|
74
|
+
/** Agent-definition root relative to the project root. Default: agents */
|
|
53
75
|
agents?: string;
|
|
54
76
|
/** Workspace skill root relative to the project root. Default: skills */
|
|
55
77
|
skills?: string;
|
|
56
78
|
/** Other kraftwerk workspaces, shown as a switcher dropdown in the inspector header. */
|
|
57
79
|
switcher?: SwitcherEntry[];
|
|
80
|
+
/** Workspace git sync. Absent = off. */
|
|
81
|
+
git?: GitConfig;
|
|
58
82
|
}
|
|
59
83
|
export interface Project {
|
|
60
84
|
/** Absolute project root the CLI operates on. */
|
package/dist/config.js
CHANGED
|
@@ -18,12 +18,16 @@ import { parse } from "yaml";
|
|
|
18
18
|
* workflows: src/workflows # workflows root, relative to the file
|
|
19
19
|
* output: output # run-artifact directory, relative to the file
|
|
20
20
|
* knowledge: knowledge # OKF knowledge-bundle root, relative to the file
|
|
21
|
-
* agents: agents #
|
|
21
|
+
* agents: agents # agent-definition root, relative to the file
|
|
22
22
|
* skills: skills # workspace skill root, relative to the file
|
|
23
23
|
* switcher: # other kraftwerk workspaces, linked from the header dropdown
|
|
24
24
|
* - name: other space
|
|
25
25
|
* url: https://localhost:1985
|
|
26
26
|
* icon: "🛰" # optional emoji shown next to the entry
|
|
27
|
+
* git: # workspace git sync (absent = off, bare key = on with defaults)
|
|
28
|
+
* remote: origin
|
|
29
|
+
* interval: 300 # seconds between background fetches
|
|
30
|
+
* autosync: pull # off | pull
|
|
27
31
|
*/
|
|
28
32
|
/** Stable, versionless URL of the workflow JSON schema (editor validation). */
|
|
29
33
|
export const SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/workflow.schema.json";
|
|
@@ -83,6 +87,7 @@ export async function resolveProject(cwd) {
|
|
|
83
87
|
const root = gitFallback ?? start;
|
|
84
88
|
return { root, config: {}, outputDir: path.join(root, "output") };
|
|
85
89
|
}
|
|
90
|
+
const KNOWN_KEYS = ["name", "icon", "port", "workflows", "output", "knowledge", "agents", "skills", "switcher", "git"];
|
|
86
91
|
async function loadConfig(configPath) {
|
|
87
92
|
let raw;
|
|
88
93
|
try {
|
|
@@ -94,10 +99,10 @@ async function loadConfig(configPath) {
|
|
|
94
99
|
if (raw === null || raw === undefined)
|
|
95
100
|
return {};
|
|
96
101
|
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
97
|
-
throw new Error(`${path.basename(configPath)}: expected a mapping (
|
|
102
|
+
throw new Error(`${path.basename(configPath)}: expected a mapping (${KNOWN_KEYS.join(", ")})`);
|
|
98
103
|
}
|
|
99
104
|
const config = raw;
|
|
100
|
-
const known =
|
|
105
|
+
const known = KNOWN_KEYS;
|
|
101
106
|
for (const key of Object.keys(config)) {
|
|
102
107
|
if (!known.includes(key)) {
|
|
103
108
|
throw new Error(`${path.basename(configPath)}: unknown key "${key}" (allowed: ${known.join(", ")})`);
|
|
@@ -110,6 +115,14 @@ async function loadConfig(configPath) {
|
|
|
110
115
|
else if (key === "switcher") {
|
|
111
116
|
validateSwitcher(configPath, config[key]);
|
|
112
117
|
}
|
|
118
|
+
else if (key === "git") {
|
|
119
|
+
// A bare `git:` line parses as null. Every field has a default, so read
|
|
120
|
+
// it as "on, with defaults" instead of failing the whole config load
|
|
121
|
+
// and taking the rest of the inspector down with it.
|
|
122
|
+
if (config[key] === null)
|
|
123
|
+
config[key] = {};
|
|
124
|
+
validateGit(configPath, config[key]);
|
|
125
|
+
}
|
|
113
126
|
else if (typeof config[key] !== "string") {
|
|
114
127
|
throw new Error(`${path.basename(configPath)}: ${key} must be a string`);
|
|
115
128
|
}
|
|
@@ -142,3 +155,30 @@ function validateSwitcher(configPath, value) {
|
|
|
142
155
|
}
|
|
143
156
|
});
|
|
144
157
|
}
|
|
158
|
+
function validateGit(configPath, value) {
|
|
159
|
+
const file = path.basename(configPath);
|
|
160
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
161
|
+
throw new Error(`${file}: git must be a mapping (enabled, remote, branch, interval, autosync)`);
|
|
162
|
+
}
|
|
163
|
+
const g = value;
|
|
164
|
+
const known = ["enabled", "remote", "branch", "interval", "autosync"];
|
|
165
|
+
for (const key of Object.keys(g)) {
|
|
166
|
+
if (!known.includes(key)) {
|
|
167
|
+
throw new Error(`${file}: git.${key} is unknown (allowed: ${known.join(", ")})`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (g.enabled !== undefined && typeof g.enabled !== "boolean") {
|
|
171
|
+
throw new Error(`${file}: git.enabled must be true or false`);
|
|
172
|
+
}
|
|
173
|
+
for (const key of ["remote", "branch"]) {
|
|
174
|
+
if (g[key] !== undefined && (typeof g[key] !== "string" || !g[key].trim())) {
|
|
175
|
+
throw new Error(`${file}: git.${key} must be a non-empty string`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (g.interval !== undefined && (typeof g.interval !== "number" || !Number.isInteger(g.interval) || g.interval < 0)) {
|
|
179
|
+
throw new Error(`${file}: git.interval must be a whole number of seconds (0 disables the timer)`);
|
|
180
|
+
}
|
|
181
|
+
if (g.autosync !== undefined && g.autosync !== "off" && g.autosync !== "pull") {
|
|
182
|
+
throw new Error(`${file}: git.autosync must be "off" or "pull"`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import type { ChatAgentId } from "./chat/types.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Agents: persistent agents, defined on the filesystem. Each one
|
|
4
4
|
* is one folder under the project's agents/ root:
|
|
5
5
|
*
|
|
6
6
|
* agents/<slug>/agent.yml # name, emoji, harness, model, effort, workflows
|
|
7
|
-
* agents/<slug>/system.md # the
|
|
7
|
+
* agents/<slug>/system.md # the agent's system prompt / role description
|
|
8
8
|
*
|
|
9
9
|
* Definitions are git-tracked project config (like workflows/), not run
|
|
10
10
|
* state — sessions with a member are ordinary chats scoped to it.
|
|
11
11
|
*/
|
|
12
12
|
export declare const HARNESSES: ChatAgentId[];
|
|
13
13
|
export declare const EFFORTS: readonly ["low", "medium", "high", "xhigh", "max"];
|
|
14
|
-
export interface
|
|
14
|
+
export interface Agent {
|
|
15
15
|
slug: string;
|
|
16
16
|
name: string;
|
|
17
17
|
emoji: string;
|
|
@@ -33,17 +33,26 @@ export interface TeamMember {
|
|
|
33
33
|
/** Archived: hidden from the active roster, restorable any time. */
|
|
34
34
|
archived?: boolean;
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
/** What the ⌘K palette needs to find and show an agent — kept in the project registry. */
|
|
37
|
+
export interface AgentSummary {
|
|
38
|
+
slug: string;
|
|
39
|
+
name: string;
|
|
40
|
+
emoji: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
group?: string;
|
|
43
|
+
}
|
|
44
|
+
export declare const toSummary: (a: Agent) => AgentSummary;
|
|
45
|
+
export interface AgentDetail extends Agent {
|
|
37
46
|
/** Contents of system.md — the member's system prompt. */
|
|
38
47
|
system: string;
|
|
39
48
|
}
|
|
40
|
-
export declare function
|
|
41
|
-
export declare function
|
|
49
|
+
export declare function agentsRoot(): Promise<string>;
|
|
50
|
+
export declare function safeAgentSlug(slug: string): string;
|
|
42
51
|
/** Derive a slug from a display name: "Max Müller" -> "max-mueller"-ish. */
|
|
43
52
|
export declare function slugFromName(name: string): string;
|
|
44
|
-
export declare function
|
|
45
|
-
export declare function
|
|
46
|
-
export interface
|
|
53
|
+
export declare function listAgents(): Promise<Agent[]>;
|
|
54
|
+
export declare function getAgent(slug: string): Promise<AgentDetail | null>;
|
|
55
|
+
export interface SaveAgentInput {
|
|
47
56
|
slug?: string;
|
|
48
57
|
name: string;
|
|
49
58
|
emoji?: string;
|
|
@@ -59,7 +68,7 @@ export interface SaveMemberInput {
|
|
|
59
68
|
skills?: string[];
|
|
60
69
|
system?: string;
|
|
61
70
|
}
|
|
62
|
-
export declare function
|
|
71
|
+
export declare function saveAgent(input: SaveAgentInput): Promise<AgentDetail>;
|
|
63
72
|
/** Archive/unarchive a member: toggles `archived:` in agent.yml, nothing else. */
|
|
64
|
-
export declare function
|
|
65
|
-
export declare function
|
|
73
|
+
export declare function setAgentArchived(slug: string, archived: boolean): Promise<AgentDetail>;
|
|
74
|
+
export declare function deleteAgent(slug: string): Promise<void>;
|
|
@@ -3,24 +3,32 @@ import path from "node:path";
|
|
|
3
3
|
import { parse, stringify } from "yaml";
|
|
4
4
|
import { resolveProject } from "../config.js";
|
|
5
5
|
import { getProjectRoot } from "./context.js";
|
|
6
|
+
import { syncProjectAgents } from "./instances.js";
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
+
* Agents: persistent agents, defined on the filesystem. Each one
|
|
8
9
|
* is one folder under the project's agents/ root:
|
|
9
10
|
*
|
|
10
11
|
* agents/<slug>/agent.yml # name, emoji, harness, model, effort, workflows
|
|
11
|
-
* agents/<slug>/system.md # the
|
|
12
|
+
* agents/<slug>/system.md # the agent's system prompt / role description
|
|
12
13
|
*
|
|
13
14
|
* Definitions are git-tracked project config (like workflows/), not run
|
|
14
15
|
* state — sessions with a member are ordinary chats scoped to it.
|
|
15
16
|
*/
|
|
16
17
|
export const HARNESSES = ["claude", "codex", "pi"];
|
|
17
18
|
export const EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
18
|
-
export
|
|
19
|
+
export const toSummary = (a) => ({
|
|
20
|
+
slug: a.slug,
|
|
21
|
+
name: a.name,
|
|
22
|
+
emoji: a.emoji,
|
|
23
|
+
...(a.description ? { description: a.description } : {}),
|
|
24
|
+
...(a.group ? { group: a.group } : {}),
|
|
25
|
+
});
|
|
26
|
+
export async function agentsRoot() {
|
|
19
27
|
const project = await resolveProject(getProjectRoot());
|
|
20
28
|
return path.resolve(project.root, project.config.agents ?? "agents");
|
|
21
29
|
}
|
|
22
30
|
const SLUG_RE = /^[a-z0-9][a-z0-9-]{0,48}$/;
|
|
23
|
-
export function
|
|
31
|
+
export function safeAgentSlug(slug) {
|
|
24
32
|
if (!SLUG_RE.test(slug))
|
|
25
33
|
throw new Error(`invalid agent slug "${slug}"`);
|
|
26
34
|
return slug;
|
|
@@ -55,8 +63,8 @@ function normalize(slug, raw) {
|
|
|
55
63
|
...(raw.archived === true ? { archived: true } : {}),
|
|
56
64
|
};
|
|
57
65
|
}
|
|
58
|
-
export async function
|
|
59
|
-
const root = await
|
|
66
|
+
export async function listAgents() {
|
|
67
|
+
const root = await agentsRoot();
|
|
60
68
|
let entries;
|
|
61
69
|
try {
|
|
62
70
|
entries = (await fs.readdir(root, { withFileTypes: true }))
|
|
@@ -66,7 +74,7 @@ export async function listMembers() {
|
|
|
66
74
|
catch {
|
|
67
75
|
return [];
|
|
68
76
|
}
|
|
69
|
-
const
|
|
77
|
+
const agents = await Promise.all(entries.map(async (slug) => {
|
|
70
78
|
try {
|
|
71
79
|
const raw = parse(await fs.readFile(path.join(root, slug, "agent.yml"), "utf8"));
|
|
72
80
|
return normalize(slug, (raw ?? {}));
|
|
@@ -75,10 +83,17 @@ export async function listMembers() {
|
|
|
75
83
|
return null;
|
|
76
84
|
}
|
|
77
85
|
}));
|
|
78
|
-
|
|
86
|
+
const list = agents.filter(Boolean).sort((a, b) => a.name.localeCompare(b.name));
|
|
87
|
+
// Keep the registry's copy of the roster current for the other instances'
|
|
88
|
+
// palettes. Awaited so a read that follows a save sees the record updated;
|
|
89
|
+
// it only writes when the roster changed.
|
|
90
|
+
await resolveProject(getProjectRoot())
|
|
91
|
+
.then((p) => syncProjectAgents(p.root, list.filter((a) => !a.archived).map(toSummary)))
|
|
92
|
+
.catch(() => { });
|
|
93
|
+
return list;
|
|
79
94
|
}
|
|
80
|
-
export async function
|
|
81
|
-
const dir = path.join(await
|
|
95
|
+
export async function getAgent(slug) {
|
|
96
|
+
const dir = path.join(await agentsRoot(), safeAgentSlug(slug));
|
|
82
97
|
let raw;
|
|
83
98
|
try {
|
|
84
99
|
raw = parse(await fs.readFile(path.join(dir, "agent.yml"), "utf8"));
|
|
@@ -89,7 +104,7 @@ export async function getMember(slug) {
|
|
|
89
104
|
const system = await fs.readFile(path.join(dir, "system.md"), "utf8").catch(() => "");
|
|
90
105
|
return { ...normalize(slug, (raw ?? {})), system: system.trim() };
|
|
91
106
|
}
|
|
92
|
-
export async function
|
|
107
|
+
export async function saveAgent(input) {
|
|
93
108
|
const name = input.name?.trim();
|
|
94
109
|
if (!name)
|
|
95
110
|
throw new Error("name is required");
|
|
@@ -99,8 +114,8 @@ export async function saveMember(input) {
|
|
|
99
114
|
if (input.effort && !EFFORTS.includes(input.effort)) {
|
|
100
115
|
throw new Error(`effort must be one of: ${EFFORTS.join(", ")}`);
|
|
101
116
|
}
|
|
102
|
-
const slug = input.slug ?
|
|
103
|
-
const dir = path.join(await
|
|
117
|
+
const slug = input.slug ? safeAgentSlug(input.slug) : slugFromName(name);
|
|
118
|
+
const dir = path.join(await agentsRoot(), slug);
|
|
104
119
|
await fs.mkdir(dir, { recursive: true });
|
|
105
120
|
// Profile edits must not silently unarchive: carry the flag over.
|
|
106
121
|
const existing = await fs
|
|
@@ -122,11 +137,11 @@ export async function saveMember(input) {
|
|
|
122
137
|
};
|
|
123
138
|
await fs.writeFile(path.join(dir, "agent.yml"), stringify(yml));
|
|
124
139
|
await fs.writeFile(path.join(dir, "system.md"), (input.system ?? "").trim() + "\n");
|
|
125
|
-
return (await
|
|
140
|
+
return (await getAgent(slug));
|
|
126
141
|
}
|
|
127
142
|
/** Archive/unarchive a member: toggles `archived:` in agent.yml, nothing else. */
|
|
128
|
-
export async function
|
|
129
|
-
const dir = path.join(await
|
|
143
|
+
export async function setAgentArchived(slug, archived) {
|
|
144
|
+
const dir = path.join(await agentsRoot(), safeAgentSlug(slug));
|
|
130
145
|
let raw;
|
|
131
146
|
try {
|
|
132
147
|
raw = (parse(await fs.readFile(path.join(dir, "agent.yml"), "utf8")) ?? {});
|
|
@@ -140,9 +155,9 @@ export async function setMemberArchived(slug, archived) {
|
|
|
140
155
|
else
|
|
141
156
|
delete yml.archived;
|
|
142
157
|
await fs.writeFile(path.join(dir, "agent.yml"), stringify(yml));
|
|
143
|
-
return (await
|
|
158
|
+
return (await getAgent(slug));
|
|
144
159
|
}
|
|
145
|
-
export async function
|
|
146
|
-
const dir = path.join(await
|
|
160
|
+
export async function deleteAgent(slug) {
|
|
161
|
+
const dir = path.join(await agentsRoot(), safeAgentSlug(slug));
|
|
147
162
|
await fs.rm(dir, { recursive: true, force: true });
|
|
148
163
|
}
|
|
@@ -19,7 +19,7 @@ export interface BackendHooks {
|
|
|
19
19
|
}>): Promise<string | null>;
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
|
-
* Optional per-chat model/effort overrides (from
|
|
22
|
+
* Optional per-chat model/effort overrides (from an agent definition).
|
|
23
23
|
* Each backend maps them onto whatever its agent actually supports.
|
|
24
24
|
*/
|
|
25
25
|
export interface BackendTuning {
|