@netnodeag/kraftwerk 0.48.0 → 0.49.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 +96 -5
- package/dist/cli/doctor.js +7 -1
- package/dist/cli/init.js +1 -0
- package/dist/cli/kraftwerk.js +5 -1
- package/dist/cli/projects.d.ts +0 -14
- package/dist/cli/projects.js +181 -107
- package/dist/cli/repos.js +1 -1
- package/dist/cli/ui.js +1 -1
- package/dist/cli/vibeables.js +1 -1
- package/dist/cli/workspaces.d.ts +16 -0
- package/dist/cli/workspaces.js +149 -0
- package/dist/config.d.ts +22 -1
- package/dist/config.js +19 -3
- package/dist/inspector/agents.js +2 -2
- package/dist/inspector/channels.d.ts +10 -1
- package/dist/inspector/channels.js +11 -2
- package/dist/inspector/chat/sessions.d.ts +8 -3
- package/dist/inspector/chat/sessions.js +58 -19
- package/dist/inspector/chat/types.d.ts +11 -0
- package/dist/inspector/git.js +5 -3
- package/dist/inspector/instances.d.ts +14 -12
- package/dist/inspector/instances.js +71 -31
- package/dist/inspector/notifications.js +2 -0
- package/dist/inspector/projects.d.ts +167 -0
- package/dist/inspector/projects.js +520 -0
- package/dist/inspector/search.d.ts +1 -1
- package/dist/inspector/search.js +3 -3
- package/dist/inspector/server.js +111 -20
- package/dist/inspector/settings.d.ts +3 -1
- package/dist/inspector/settings.js +14 -3
- package/inspector/dist/assets/{dist-Dk5ZcwMb.js → dist-B-8ArbIm.js} +1 -1
- package/inspector/dist/assets/{dist-DUq31xBw.js → dist-BFJWVdBm.js} +1 -1
- package/inspector/dist/assets/{dist-D6fPO3Jz.js → dist-BIvcKBWo.js} +1 -1
- package/inspector/dist/assets/{dist-n8GR1Slo.js → dist-BgYnJawU.js} +1 -1
- package/inspector/dist/assets/dist-Bl1ogZZM.js +1 -0
- package/inspector/dist/assets/{dist-O97r5Ket.js → dist-BrgwFqUR.js} +1 -1
- package/inspector/dist/assets/{dist-CmH9PvLb.js → dist-Bz0bwjja.js} +1 -1
- package/inspector/dist/assets/{dist-Dv40dCIn.js → dist-CFvGR87v.js} +1 -1
- package/inspector/dist/assets/{dist-Bz3XVWXl.js → dist-CjtxxsZ2.js} +1 -1
- package/inspector/dist/assets/{dist-BNGsfwfO.js → dist-QwwTJ2wk.js} +1 -1
- package/inspector/dist/assets/{dist-BWkBM49t.js → dist-tfmgozDB.js} +1 -1
- package/inspector/dist/assets/{editor-toVFopCO.js → editor-C6m40u-h.js} +3 -3
- package/inspector/dist/assets/{index-CC0tY40i.js → index-Cvq-RiV2.js} +11 -11
- package/inspector/dist/assets/{index-DQrsXEWt.css → index-DcQwGaV2.css} +1 -1
- package/inspector/dist/index.html +2 -2
- package/package.json +1 -1
- package/schema/kraftwerk.schema.json +18 -0
- package/inspector/dist/assets/dist-JaBY_kZV.js +0 -1
package/README.md
CHANGED
|
@@ -94,7 +94,7 @@ newer version is on disk, right after "update now" or after a manual install.
|
|
|
94
94
|
|
|
95
95
|
```bash
|
|
96
96
|
npm install -g @netnodeag/kraftwerk@latest
|
|
97
|
-
kraftwerk
|
|
97
|
+
kraftwerk workspaces # every workspace on this machine, running or not
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
## Consume
|
|
@@ -156,7 +156,8 @@ kraftwerk run # interactive: pick workflow, type the r
|
|
|
156
156
|
kraftwerk runs # past runs from output/*/trace.jsonl; runs show <id> for detail
|
|
157
157
|
kraftwerk knowledge # Knowledge: OKF bundles (list/get/put/verify/search/...)
|
|
158
158
|
kraftwerk ui # inspector web UI on http://localhost:1981; --port, --output
|
|
159
|
-
kraftwerk
|
|
159
|
+
kraftwerk workspaces # every workspace on this machine; workspaces start|stop|forget <ref>
|
|
160
|
+
kraftwerk projects # goal-scoped project folders; projects create|show|link|log|remove
|
|
160
161
|
kraftwerk doctor # preflight: harness CLIs, docker, workflows, declared env vars
|
|
161
162
|
kraftwerk validate # all discovered: schema + semantics + files, exit 1 on failure
|
|
162
163
|
kraftwerk validate src/workflows/pitch # specific paths
|
|
@@ -217,8 +218,8 @@ switcher:
|
|
|
217
218
|
letters of an agent's or channel's name, description or workspace and hit
|
|
218
219
|
enter to jump to it. It lists the active agents and the channels of every
|
|
219
220
|
workspace on this machine, grouped by workspace and running or not (a
|
|
220
|
-
stopped one is started on the way), read from the
|
|
221
|
-
`~/.kraftwerk/
|
|
221
|
+
stopped one is started on the way), read from the workspace registry under
|
|
222
|
+
`~/.kraftwerk/workspaces`, which every inspector keeps current with its
|
|
222
223
|
roster and channel list.
|
|
223
224
|
|
|
224
225
|
### Triggering from CI, cron, or webhooks
|
|
@@ -380,7 +381,7 @@ working without a login. `kraftwerk doctor` reports the tunnel, the Access
|
|
|
380
381
|
block and whether cloudflared is installed.
|
|
381
382
|
|
|
382
383
|
**Variants.** `kraftwerk tunnel` runs the configured tunnel alone, for an
|
|
383
|
-
inspector that already runs elsewhere (started by `kraftwerk
|
|
384
|
+
inspector that already runs elsewhere (started by `kraftwerk workspaces
|
|
384
385
|
start`, or in a container). For a tunnel created in the Zero Trust
|
|
385
386
|
dashboard instead (Networks → Tunnels), leave `name` out, route the
|
|
386
387
|
hostname to `http://localhost:<port>` there and export its token as
|
|
@@ -403,6 +404,96 @@ Quick tunnels (`trycloudflare.com`) are deliberately not supported: Access
|
|
|
403
404
|
cannot be attached to them, which would leave the UI open to anyone with
|
|
404
405
|
the URL.
|
|
405
406
|
|
|
407
|
+
## Projects
|
|
408
|
+
|
|
409
|
+
Turn on `projects:` in `kraftwerk.yml` (or the checkbox in settings) and
|
|
410
|
+
the workspace gets one folder per project — a goal with everything the
|
|
411
|
+
agents need to reach it:
|
|
412
|
+
|
|
413
|
+
```yaml
|
|
414
|
+
projects:
|
|
415
|
+
root: kraftwerk-data/projects # default; part of the workspace, synced like agents and knowledge
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
```
|
|
419
|
+
kraftwerk-data/projects/<slug>/
|
|
420
|
+
project.yml # title, status, goal, records, links
|
|
421
|
+
brief.md # the goal in full: what done looks like, constraints, stakeholders
|
|
422
|
+
state.md # current state, rewritten at the end of a session
|
|
423
|
+
log.md # append-only, newest first: decisions and milestones, dated and attributed
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
`project.yml` holds the one-line goal, a status (`active`, `paused`, `done`,
|
|
427
|
+
`archived`), the harness, model and effort its chats run on (the same three
|
|
428
|
+
fields an agent has; the harness decides, not the caller), the **systems of
|
|
429
|
+
record** and the **links**:
|
|
430
|
+
|
|
431
|
+
```yaml
|
|
432
|
+
title: Relaunch netnode.ch
|
|
433
|
+
status: active
|
|
434
|
+
goal: Ship the new site on NodeHive by 2026-11-30
|
|
435
|
+
harness: claude # claude | codex | pi — every chat in the project runs on it
|
|
436
|
+
model: sonnet # optional, like an agent's
|
|
437
|
+
effort: high # optional: low | medium | high | xhigh | max
|
|
438
|
+
records:
|
|
439
|
+
- kind: my-netnode # my-netnode | google-drive | github | bitbucket | notion | slack | url | anything
|
|
440
|
+
workspace: 22
|
|
441
|
+
url: https://my.netnode.ch/workspace/22
|
|
442
|
+
note: tickets, roadmap and meetings
|
|
443
|
+
- kind: google-drive
|
|
444
|
+
title: Contracts and briefs
|
|
445
|
+
url: https://drive.google.com/drive/folders/…
|
|
446
|
+
knowledge: [netnode-helpdesk] # OKF bundle names
|
|
447
|
+
vibeables: [launch-tracker] # folders under the vibeables root
|
|
448
|
+
repos: [netnode-frontend] # folders under the repos root
|
|
449
|
+
workflows: [website-check] # workflow slugs
|
|
450
|
+
agents: [max] # agent slugs
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
A system of record says where the truth of the project is managed outside
|
|
454
|
+
kraftwerk and what usually lives there. It is context, not a credential and
|
|
455
|
+
not a grant: the agent reaches it through the tools its harness already has
|
|
456
|
+
(a CLI, an MCP server, the browser), and the harness decides what it may
|
|
457
|
+
call. Kraftwerk knows a few kinds only to label them and phrase the context
|
|
458
|
+
better; any other kind passes through as a link with a note.
|
|
459
|
+
|
|
460
|
+
Links are one-directional lists of slugs. A target that does not exist is
|
|
461
|
+
shown as "not found" on the project page and told to the agent, never an
|
|
462
|
+
error — the same rule an agent's knowledge list follows.
|
|
463
|
+
|
|
464
|
+
**Working in a project is chat.** The Projects screen lists the projects,
|
|
465
|
+
the chats of the selected one, and the thread; opening a project opens its
|
|
466
|
+
latest chat. A session scoped to a project starts with the brief, the
|
|
467
|
+
current state, the records, every link with how to reach it, and the rule
|
|
468
|
+
for keeping the project current: `state.md` is rewritten at the end of a
|
|
469
|
+
session that changed something, so the next session (by anyone) continues
|
|
470
|
+
without the transcript; `log.md` is appended through
|
|
471
|
+
`kraftwerk projects log <slug> "<line>" --actor <who>` so every line is
|
|
472
|
+
dated and attributed; `brief.md` belongs to the user. The working
|
|
473
|
+
directory stays the workspace root, so linked repositories and knowledge
|
|
474
|
+
are reachable by path; opening a vibeable moves it into the app folder as
|
|
475
|
+
in any chat.
|
|
476
|
+
|
|
477
|
+
**Coworkers.** "Add coworker" on a project chat works as on an agent
|
|
478
|
+
session: the chat becomes a channel whose members are the agents you pick,
|
|
479
|
+
with everything said so far and the project's brief, state, records and
|
|
480
|
+
links as context for every member (`project:` in `channel.yml`). The
|
|
481
|
+
channel lives on the channels screen and stays listed under the project.
|
|
482
|
+
The first agent picked answers messages that mention nobody.
|
|
483
|
+
|
|
484
|
+
```bash
|
|
485
|
+
kraftwerk projects # list: title, status, goal, links
|
|
486
|
+
kraftwerk projects create "Relaunch netnode.ch" --goal "Ship by November"
|
|
487
|
+
kraftwerk projects show relaunch-netnode-ch # definition, records, link states, state, log
|
|
488
|
+
kraftwerk projects set relaunch-netnode-ch --harness codex --model gpt-5.6-sol --effort high
|
|
489
|
+
kraftwerk projects link relaunch-netnode-ch workflows website-check
|
|
490
|
+
kraftwerk projects log relaunch-netnode-ch "Decided on NodeHive." --actor human:lukas
|
|
491
|
+
kraftwerk projects remove relaunch-netnode-ch
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
The registry of workspaces on this machine, which answered to
|
|
495
|
+
`kraftwerk projects` until 0.48, is `kraftwerk workspaces` now.
|
|
496
|
+
|
|
406
497
|
## Persistent agents
|
|
407
498
|
|
|
408
499
|
The inspector's "agents" screen turns chat agents into persistent teammates. An
|
package/dist/cli/doctor.js
CHANGED
|
@@ -88,7 +88,13 @@ export async function runDoctor(cwd) {
|
|
|
88
88
|
else {
|
|
89
89
|
const cfg = project.config;
|
|
90
90
|
const keys = Object.keys(cfg);
|
|
91
|
-
|
|
91
|
+
// Blocks (git, repos, vibeables, projects, tunnel) print as their keys, not [object Object].
|
|
92
|
+
const show = (v) => typeof v === "object" && v !== null
|
|
93
|
+
? Array.isArray(v)
|
|
94
|
+
? `[${v.length}]`
|
|
95
|
+
: `{${Object.entries(v).map(([k, x]) => `${k}: ${show(x)}`).join(", ") || " "}}`
|
|
96
|
+
: String(v);
|
|
97
|
+
report("ok", `${path.basename(project.configPath)} well-formed`, keys.length ? keys.map((k) => `${k}: ${show(cfg[k])}`).join(", ") : "empty — defaults apply");
|
|
92
98
|
if (!cfg.name)
|
|
93
99
|
report("info", "name not set in kraftwerk.yml", "inspector header falls back to the folder name");
|
|
94
100
|
for (const key of ["workflows", "knowledge", "agents", "output"]) {
|
package/dist/cli/init.js
CHANGED
|
@@ -26,6 +26,7 @@ skills: ${DATA_DIR}/skills # workspace skills (shared instruction packag
|
|
|
26
26
|
# repos: # git repositories the agents work on (uncomment both lines to enable)
|
|
27
27
|
# root: ${DATA_DIR}/repos # clones land here (git-ignored); a bare \`repos:\` uses repos/ instead
|
|
28
28
|
# vibeables: # small apps built live in a chat with a preview pane (uncomment to enable; one folder per app under ${DATA_DIR}/vibeables, versioned with the workspace)
|
|
29
|
+
# projects: # goal-scoped folders — brief, systems of record, links to knowledge/vibeables/repos/workflows/agents — that chats work in (uncomment to enable; one folder per project under ${DATA_DIR}/projects)
|
|
29
30
|
# public: https://kw.example.com # hostname the inspector is reached at through a tunnel or reverse proxy
|
|
30
31
|
# tunnel: # Cloudflare Tunnel run by \`kraftwerk ui\` (needs public:; put a Cloudflare Access policy on the hostname — the UI has no login of its own)
|
|
31
32
|
# name: kraftwerk # locally-managed tunnel (cloudflared tunnel create/route dns); omit and export TUNNEL_TOKEN for a dashboard-managed one
|
package/dist/cli/kraftwerk.js
CHANGED
|
@@ -13,6 +13,7 @@ import { runDoctor } from "./doctor.js";
|
|
|
13
13
|
import { runInit } from "./init.js";
|
|
14
14
|
import { registerKnowledgeCommands } from "./knowledge.js";
|
|
15
15
|
import { registerProjectCommands } from "./projects.js";
|
|
16
|
+
import { registerWorkspaceCommands } from "./workspaces.js";
|
|
16
17
|
import { registerRepoCommands } from "./repos.js";
|
|
17
18
|
import { registerVibeableCommands } from "./vibeables.js";
|
|
18
19
|
import { registerTunnelCommands } from "./tunnel.js";
|
|
@@ -31,7 +32,9 @@ import { runUi } from "./ui.js";
|
|
|
31
32
|
* kraftwerk knowledge ... Knowledge: OKF bundles (list/get/put/verify/...)
|
|
32
33
|
* kraftwerk routines ... per-agent scheduled prompts (list/add/remove/enable/run)
|
|
33
34
|
* kraftwerk ui start the inspector web UI (localhost:1981)
|
|
34
|
-
* kraftwerk
|
|
35
|
+
* kraftwerk workspaces ... known workspaces on this machine (list/start/stop/forget)
|
|
36
|
+
* kraftwerk projects ... goal-scoped project folders (list/create/show/link/log/remove)
|
|
37
|
+
* kraftwerk vibeables ... small apps built live in a chat (list/create/remove)
|
|
35
38
|
* kraftwerk repos ... repositories the agents work on (list/add/update/remove)
|
|
36
39
|
* kraftwerk tunnel [setup <host>] Cloudflare Tunnel to the inspector: run it alone, or set one up
|
|
37
40
|
* kraftwerk doctor preflight: harness CLIs, docker, workflows, env
|
|
@@ -279,6 +282,7 @@ runs
|
|
|
279
282
|
});
|
|
280
283
|
registerKnowledgeCommands(program);
|
|
281
284
|
registerRoutineCommands(program);
|
|
285
|
+
registerWorkspaceCommands(program);
|
|
282
286
|
registerProjectCommands(program);
|
|
283
287
|
registerRepoCommands(program);
|
|
284
288
|
registerVibeableCommands(program);
|
package/dist/cli/projects.d.ts
CHANGED
|
@@ -1,16 +1,2 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
-
/**
|
|
3
|
-
* `kraftwerk projects` — every project that ever ran the inspector on this
|
|
4
|
-
* machine (~/.kraftwerk/projects), joined with what is running right now.
|
|
5
|
-
* The answer to "where did I start that UI, and how do I get it back":
|
|
6
|
-
*
|
|
7
|
-
* kraftwerk projects list: name, root, running/stopped
|
|
8
|
-
* kraftwerk projects start <ref> relaunch `kraftwerk ui` in that root, detached
|
|
9
|
-
* kraftwerk projects stop <ref> SIGTERM a running UI (also ones started in a terminal)
|
|
10
|
-
* kraftwerk projects forget <ref> drop the record (the folder stays)
|
|
11
|
-
*
|
|
12
|
-
* <ref> is a project name, the root's folder name, the root path, or a
|
|
13
|
-
* port / localhost:port for running instances that recorded no root.
|
|
14
|
-
*/
|
|
15
|
-
export declare const fmtAgo: (iso?: string) => string;
|
|
16
2
|
export declare function registerProjectCommands(program: Command): void;
|
package/dist/cli/projects.js
CHANGED
|
@@ -1,149 +1,223 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
1
|
import chalk from "chalk";
|
|
3
|
-
import { absolutePath } from "../config.js";
|
|
4
2
|
import Table from "cli-table3";
|
|
5
|
-
import {
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { appendProjectLog, createProject, deleteProject, getProject, linkProject, listProjects, LINK_KINDS, recordLine, saveProject, } from "../inspector/projects.js";
|
|
5
|
+
import { initContext as prepare } from "./routines.js";
|
|
6
|
+
import { fmtAgo } from "./workspaces.js";
|
|
6
7
|
/**
|
|
7
|
-
* `kraftwerk projects` —
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* `kraftwerk projects` — the goal-scoped folders under the projects root
|
|
9
|
+
* (`projects.root` in kraftwerk.yml): a brief, the systems of record, and
|
|
10
|
+
* links to knowledge, vibeables, repositories, workflows and agents. The
|
|
11
|
+
* same module the inspector uses, so a project created here shows up in
|
|
12
|
+
* the UI at once, and an agent in a project chat uses `log` and `link`
|
|
13
|
+
* to keep the project current:
|
|
10
14
|
*
|
|
11
|
-
* kraftwerk projects
|
|
12
|
-
* kraftwerk projects
|
|
13
|
-
* kraftwerk projects
|
|
14
|
-
* kraftwerk projects
|
|
15
|
+
* kraftwerk projects list: title, status, goal, links
|
|
16
|
+
* kraftwerk projects create <title> [--goal ..] new folder from the starter (project.yml, brief, state, log); --harness/--model/--effort
|
|
17
|
+
* kraftwerk projects set <slug> [--harness ..] change harness, model, effort, status or goal
|
|
18
|
+
* kraftwerk projects show <slug> definition, links (found / missing), records, state, log
|
|
19
|
+
* kraftwerk projects link <slug> <kind> <name> add a knowledge|vibeables|repos|workflows|agents link
|
|
20
|
+
* kraftwerk projects unlink <slug> <kind> <name> remove one
|
|
21
|
+
* kraftwerk projects log <slug> "<entry>" append a stamped line to log.md (--actor)
|
|
22
|
+
* kraftwerk projects remove <slug> delete the folder (its history stays in the workspace git)
|
|
15
23
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
24
|
+
* The workspace registry answered to this name until 0.48; it is
|
|
25
|
+
* `kraftwerk workspaces` now (workspaces.ts).
|
|
18
26
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const s = Math.max(0, (Date.now() - Date.parse(iso)) / 1000);
|
|
23
|
-
if (s < 90)
|
|
24
|
-
return "just now";
|
|
25
|
-
if (s < 5400)
|
|
26
|
-
return `${Math.round(s / 60)} min ago`;
|
|
27
|
-
if (s < 129600)
|
|
28
|
-
return `${Math.round(s / 3600)} h ago`;
|
|
29
|
-
return `${Math.round(s / 86400)} d ago`;
|
|
27
|
+
const die = (msg) => {
|
|
28
|
+
console.error(chalk.red(msg));
|
|
29
|
+
process.exit(1);
|
|
30
30
|
};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
e.url === port);
|
|
54
|
-
if (hits.length === 1)
|
|
55
|
-
return hits[0];
|
|
56
|
-
if (hits.length === 0) {
|
|
57
|
-
console.error(chalk.red(`No known project "${ref}". See \`kraftwerk projects\`.`));
|
|
58
|
-
process.exit(2);
|
|
31
|
+
const linkSummary = (p) => LINK_KINDS.map((k) => (p[k].length ? `${p[k].length} ${k}` : ""))
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join(", ");
|
|
34
|
+
function printDetail(p) {
|
|
35
|
+
console.log(`${chalk.bold(p.title)} ${chalk.dim(`(${p.slug}, ${p.status})`)}`);
|
|
36
|
+
console.log(p.goal ? p.goal : chalk.dim("(no goal yet)"));
|
|
37
|
+
console.log(chalk.dim(`runs on ${p.harness}${p.model ? ` · ${p.model}` : ""}${p.effort ? ` · effort ${p.effort}` : ""}`));
|
|
38
|
+
console.log(chalk.dim(p.path));
|
|
39
|
+
if (p.configError)
|
|
40
|
+
console.log(chalk.red(`project.yml: ${p.configError}`));
|
|
41
|
+
if (p.records.length) {
|
|
42
|
+
console.log(`\n${chalk.bold("systems of record")}`);
|
|
43
|
+
for (const r of p.records)
|
|
44
|
+
console.log(recordLine(r));
|
|
45
|
+
}
|
|
46
|
+
for (const kind of LINK_KINDS) {
|
|
47
|
+
if (!p.links[kind].length)
|
|
48
|
+
continue;
|
|
49
|
+
console.log(`\n${chalk.bold(kind)}`);
|
|
50
|
+
for (const l of p.links[kind]) {
|
|
51
|
+
console.log(l.found ? ` ${chalk.green("✔")} ${l.slug}${l.label ? chalk.dim(` ${l.label}`) : ""}` : ` ${chalk.yellow("?")} ${l.slug} ${chalk.dim("(not found)")}`);
|
|
52
|
+
}
|
|
59
53
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
if (p.state.trim())
|
|
55
|
+
console.log(`\n${chalk.bold("state.md")}\n${p.state.trim()}`);
|
|
56
|
+
if (p.log.trim())
|
|
57
|
+
console.log(`\n${chalk.bold("log.md")}\n${p.log.trim()}`);
|
|
64
58
|
}
|
|
65
59
|
export function registerProjectCommands(program) {
|
|
66
60
|
const projects = program
|
|
67
61
|
.command("projects")
|
|
68
|
-
.description("
|
|
62
|
+
.description("Projects: a goal with its brief, systems of record and links, worked on in chat — list, create, link, log");
|
|
69
63
|
projects
|
|
70
64
|
.command("list", { isDefault: true })
|
|
71
|
-
.description("List
|
|
65
|
+
.description("List the projects under the projects root")
|
|
72
66
|
.option("--json", "Machine-readable output")
|
|
73
67
|
.action(async (opts) => {
|
|
74
|
-
|
|
68
|
+
await prepare();
|
|
69
|
+
const view = await listProjects();
|
|
75
70
|
if (opts.json) {
|
|
76
|
-
console.log(JSON.stringify(
|
|
71
|
+
console.log(JSON.stringify(view, null, 2));
|
|
77
72
|
return;
|
|
78
73
|
}
|
|
79
|
-
if (
|
|
80
|
-
|
|
74
|
+
if (!view.enabled)
|
|
75
|
+
die(view.error ?? "projects are off");
|
|
76
|
+
if (view.projects.length === 0) {
|
|
77
|
+
console.log(chalk.dim(`No projects under ${view.root} yet — \`kraftwerk projects create "<title>"\` starts one.`));
|
|
81
78
|
return;
|
|
82
79
|
}
|
|
83
80
|
const table = new Table({
|
|
84
|
-
head: ["project", "
|
|
81
|
+
head: ["project", "status", "goal", "links", "changed"].map((h) => chalk.bold(h)),
|
|
85
82
|
wordWrap: true,
|
|
86
|
-
colWidths: [
|
|
83
|
+
colWidths: [26, 10, 40, 24, 14],
|
|
87
84
|
});
|
|
88
|
-
for (const
|
|
85
|
+
for (const p of view.projects) {
|
|
89
86
|
table.push([
|
|
90
|
-
`${
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
`${chalk.cyan(p.title)}\n${chalk.dim(p.slug)}`,
|
|
88
|
+
p.status === "active" ? chalk.green(p.status) : chalk.dim(p.status),
|
|
89
|
+
p.configError ? chalk.red(p.configError) : p.goal || chalk.dim("—"),
|
|
90
|
+
chalk.dim(linkSummary(p) || "—"),
|
|
91
|
+
chalk.dim(fmtAgo(p.updatedAt)),
|
|
94
92
|
]);
|
|
95
93
|
}
|
|
96
94
|
console.log(table.toString());
|
|
95
|
+
console.log(chalk.dim(view.root ?? ""));
|
|
97
96
|
});
|
|
98
97
|
projects
|
|
99
|
-
.command("
|
|
100
|
-
.description("
|
|
101
|
-
.argument("<
|
|
102
|
-
.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
98
|
+
.command("create")
|
|
99
|
+
.description("Create a project folder from the starter (project.yml, brief.md, state.md, log.md)")
|
|
100
|
+
.argument("<title>", "Display title; the folder name is derived from it unless --slug is given")
|
|
101
|
+
.option("--goal <text>", "One-line goal")
|
|
102
|
+
.option("--slug <slug>", "Folder name under the root (lowercase, digits, dashes)")
|
|
103
|
+
.option("--harness <harness>", "Harness every chat in the project runs on: claude | codex | pi (default: claude)")
|
|
104
|
+
.option("--model <model>", "Model for that harness (default: the harness default)")
|
|
105
|
+
.option("--effort <effort>", "Reasoning effort: low | medium | high | xhigh | max")
|
|
106
|
+
.option("--json", "Print the new project as JSON")
|
|
107
|
+
.action(async (title, opts) => {
|
|
108
|
+
await prepare();
|
|
109
|
+
try {
|
|
110
|
+
const p = await createProject({ title, goal: opts.goal, slug: opts.slug, harness: opts.harness, model: opts.model, effort: opts.effort });
|
|
111
|
+
if (opts.json)
|
|
112
|
+
console.log(JSON.stringify(p, null, 2));
|
|
113
|
+
else
|
|
114
|
+
console.log(`${chalk.green("✔")} ${chalk.cyan(p.slug)} → ${p.path} ${chalk.dim("(open it on the Projects screen, or `kraftwerk projects show " + p.slug + "`)")}`);
|
|
107
115
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
console.error(chalk.red(`Could not start ${entry.name}: ${r.error}`));
|
|
111
|
-
if (r.log)
|
|
112
|
-
console.error(chalk.dim(`log: ${tildify(r.log)}`));
|
|
113
|
-
process.exit(1);
|
|
116
|
+
catch (err) {
|
|
117
|
+
die(err.message);
|
|
114
118
|
}
|
|
115
|
-
console.log(`${chalk.green("✔")} ${entry.name} ${r.live ? "running" : "starting"}: ${chalk.cyan(r.url)}` +
|
|
116
|
-
chalk.dim(` (pid ${r.pid}${r.log ? `, log ${tildify(r.log)}` : ""})`));
|
|
117
119
|
});
|
|
118
120
|
projects
|
|
119
|
-
.command("
|
|
120
|
-
.description("
|
|
121
|
-
.argument("<
|
|
122
|
-
.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
121
|
+
.command("show")
|
|
122
|
+
.description("Show one project: definition, records, links with their state, state.md and log.md")
|
|
123
|
+
.argument("<slug>", "Folder name under the root")
|
|
124
|
+
.option("--json", "Machine-readable output")
|
|
125
|
+
.action(async (slug, opts) => {
|
|
126
|
+
await prepare();
|
|
127
|
+
try {
|
|
128
|
+
const p = await getProject(slug);
|
|
129
|
+
if (!p)
|
|
130
|
+
die(`no project "${slug}"`);
|
|
131
|
+
if (opts.json)
|
|
132
|
+
console.log(JSON.stringify(p, null, 2));
|
|
133
|
+
else
|
|
134
|
+
printDetail(p);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
die(err.message);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
projects
|
|
141
|
+
.command("set")
|
|
142
|
+
.description("Change the project's harness, model, effort, status or goal (like an agent's settings)")
|
|
143
|
+
.argument("<slug>", "Project folder name")
|
|
144
|
+
.option("--harness <harness>", "claude | codex | pi")
|
|
145
|
+
.option("--model <model>", 'Model for the harness; "" for the default')
|
|
146
|
+
.option("--effort <effort>", 'low | medium | high | xhigh | max; "" for the default')
|
|
147
|
+
.option("--status <status>", "active | paused | done | archived")
|
|
148
|
+
.option("--goal <text>", "One-line goal")
|
|
149
|
+
.option("--json", "Print the project as JSON")
|
|
150
|
+
.action(async (slug, opts) => {
|
|
151
|
+
await prepare();
|
|
152
|
+
const { json: asJson, ...fields } = opts;
|
|
153
|
+
if (Object.values(fields).every((v) => v === undefined))
|
|
154
|
+
die("nothing to set — pass --harness, --model, --effort, --status or --goal");
|
|
155
|
+
try {
|
|
156
|
+
const p = await saveProject(slug, fields);
|
|
157
|
+
if (asJson)
|
|
158
|
+
console.log(JSON.stringify(p, null, 2));
|
|
159
|
+
else
|
|
160
|
+
console.log(`${chalk.green("✔")} ${chalk.cyan(p.slug)} runs on ${p.harness}${p.model ? ` · ${p.model}` : ""}${p.effort ? ` · effort ${p.effort}` : ""} ${chalk.dim(`(${p.status})`)}`);
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
die(err.message);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
for (const verb of ["link", "unlink"]) {
|
|
167
|
+
projects
|
|
168
|
+
.command(verb)
|
|
169
|
+
.description(verb === "link" ? "Add a linked knowledge bundle, vibeable, repository, workflow or agent" : "Remove a link")
|
|
170
|
+
.argument("<slug>", "Project folder name")
|
|
171
|
+
.argument("<kind>", `One of ${LINK_KINDS.join(", ")}`)
|
|
172
|
+
.argument("<name>", "The target's name (bundle, folder, workflow or agent slug)")
|
|
173
|
+
.option("--json", "Print the project as JSON")
|
|
174
|
+
.action(async (slug, kind, name, opts) => {
|
|
175
|
+
await prepare();
|
|
176
|
+
try {
|
|
177
|
+
const p = await linkProject(slug, kind, name, verb === "unlink");
|
|
178
|
+
if (opts.json)
|
|
179
|
+
console.log(JSON.stringify(p, null, 2));
|
|
180
|
+
else {
|
|
181
|
+
const state = p.links[kind]?.find((l) => l.slug === name);
|
|
182
|
+
const note = verb === "link" && state && !state.found ? chalk.yellow(" (not found in this workspace yet)") : "";
|
|
183
|
+
console.log(`${chalk.green("✔")} ${verb === "link" ? "linked" : "unlinked"} ${kind} ${chalk.cyan(name)} ${verb === "link" ? "to" : "from"} ${p.slug}${note}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
die(err.message);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
projects
|
|
192
|
+
.command("log")
|
|
193
|
+
.description("Append a dated, attributed line to the project's log.md (newest first)")
|
|
194
|
+
.argument("<slug>", "Project folder name")
|
|
195
|
+
.argument("[entry]", "The line to record; omit with --file")
|
|
196
|
+
.option("--file <path>", "Read the entry from a file")
|
|
197
|
+
.option("--actor <actor>", "Who writes: human:<id>, <agent>/<harness>, process:<id>", "human:user")
|
|
198
|
+
.action(async (slug, entry, opts) => {
|
|
199
|
+
await prepare();
|
|
200
|
+
try {
|
|
201
|
+
const text = opts.file ? await readFile(opts.file, "utf8") : entry ?? "";
|
|
202
|
+
await appendProjectLog(slug, text, opts.actor);
|
|
203
|
+
console.log(`${chalk.green("✔")} logged to ${chalk.cyan(slug)} ${chalk.dim(`(${opts.actor})`)}`);
|
|
127
204
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
console.error(chalk.red(`Could not stop ${entry.name}: ${r.error}`));
|
|
131
|
-
process.exit(1);
|
|
205
|
+
catch (err) {
|
|
206
|
+
die(err.message);
|
|
132
207
|
}
|
|
133
|
-
console.log(`${chalk.green("✔")} ${entry.name} stopped ${chalk.dim(`(${entry.url.replace(/^https?:\/\//, "")})`)}`);
|
|
134
208
|
});
|
|
135
209
|
projects
|
|
136
|
-
.command("
|
|
137
|
-
.description("
|
|
138
|
-
.argument("<
|
|
139
|
-
.action(async (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
210
|
+
.command("remove")
|
|
211
|
+
.description("Delete a project folder; its history stays in the workspace git")
|
|
212
|
+
.argument("<slug>", "Folder name under the root")
|
|
213
|
+
.action(async (slug) => {
|
|
214
|
+
await prepare();
|
|
215
|
+
try {
|
|
216
|
+
await deleteProject(slug);
|
|
217
|
+
console.log(`${chalk.green("✔")} removed ${chalk.cyan(slug)}`);
|
|
143
218
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
process.exit(1);
|
|
219
|
+
catch (err) {
|
|
220
|
+
die(err.message);
|
|
147
221
|
}
|
|
148
222
|
});
|
|
149
223
|
}
|
package/dist/cli/repos.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import Table from "cli-table3";
|
|
3
3
|
import { addRepo, listRepos, removeRepo, updateRepo } from "../inspector/repos.js";
|
|
4
|
-
import { fmtAgo } from "./
|
|
4
|
+
import { fmtAgo } from "./workspaces.js";
|
|
5
5
|
import { initContext as prepare } from "./routines.js";
|
|
6
6
|
/**
|
|
7
7
|
* `kraftwerk repos` — the git repositories the agents work on, kept under
|
package/dist/cli/ui.js
CHANGED
|
@@ -69,7 +69,7 @@ export async function runUi(cwd, opts) {
|
|
|
69
69
|
* local node_modules, npx, and a file: dev checkout. Any exit other than
|
|
70
70
|
* RESTART_EXIT_CODE ends the loop. A SIGTERM/SIGINT to the supervisor is
|
|
71
71
|
* forwarded to the server, so `kill <supervisor pid>` (what `kraftwerk
|
|
72
|
-
*
|
|
72
|
+
* workspaces start` reports) takes the whole UI down; Ctrl-C signals the
|
|
73
73
|
* foreground group and reaches both anyway.
|
|
74
74
|
*/
|
|
75
75
|
async function superviseUi(cwd, opts) {
|
package/dist/cli/vibeables.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import Table from "cli-table3";
|
|
3
3
|
import { createVibeable, deleteVibeable, listVibeables } from "../inspector/vibeables.js";
|
|
4
|
-
import { fmtAgo } from "./
|
|
4
|
+
import { fmtAgo } from "./workspaces.js";
|
|
5
5
|
import { initContext as prepare } from "./routines.js";
|
|
6
6
|
/**
|
|
7
7
|
* `kraftwerk vibeables` — the small apps built live in a chat, one folder
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* `kraftwerk workspaces` — every workspace that ever ran the inspector on this
|
|
4
|
+
* machine (~/.kraftwerk/workspaces), joined with what is running right now.
|
|
5
|
+
* The answer to "where did I start that UI, and how do I get it back":
|
|
6
|
+
*
|
|
7
|
+
* kraftwerk workspaces list: name, root, running/stopped
|
|
8
|
+
* kraftwerk workspaces start <ref> relaunch `kraftwerk ui` in that root, detached
|
|
9
|
+
* kraftwerk workspaces stop <ref> SIGTERM a running UI (also ones started in a terminal)
|
|
10
|
+
* kraftwerk workspaces forget <ref> drop the record (the folder stays)
|
|
11
|
+
*
|
|
12
|
+
* <ref> is a workspace name, the root's folder name, the root path, or a
|
|
13
|
+
* port / localhost:port for running instances that recorded no root.
|
|
14
|
+
*/
|
|
15
|
+
export declare const fmtAgo: (iso?: string) => string;
|
|
16
|
+
export declare function registerWorkspaceCommands(program: Command): void;
|