@netnodeag/kraftwerk 0.47.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 +106 -5
- package/dist/cli/doctor.js +18 -1
- package/dist/cli/init.js +10 -8
- package/dist/cli/kraftwerk.js +16 -2
- 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/dotenv.d.ts +40 -0
- package/dist/dotenv.js +84 -0
- 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-D8pjVMTl.js → dist-B-8ArbIm.js} +1 -1
- package/inspector/dist/assets/{dist-DfAFwiGQ.js → dist-BFJWVdBm.js} +1 -1
- package/inspector/dist/assets/{dist-C2Mu5bT-.js → dist-BIvcKBWo.js} +1 -1
- package/inspector/dist/assets/{dist-CdyUhzAt.js → dist-BgYnJawU.js} +1 -1
- package/inspector/dist/assets/dist-Bl1ogZZM.js +1 -0
- package/inspector/dist/assets/{dist-BNUGzZS7.js → dist-BrgwFqUR.js} +1 -1
- package/inspector/dist/assets/{dist-DnmDnMK2.js → dist-Bz0bwjja.js} +1 -1
- package/inspector/dist/assets/{dist-CjMETDfE.js → dist-CFvGR87v.js} +1 -1
- package/inspector/dist/assets/{dist-CUeUDZ9j.js → dist-CjtxxsZ2.js} +1 -1
- package/inspector/dist/assets/{dist-CmOlZvwa.js → dist-QwwTJ2wk.js} +1 -1
- package/inspector/dist/assets/{dist-BOQhQPN9.js → dist-tfmgozDB.js} +1 -1
- package/inspector/dist/assets/{editor-DJbeG376.js → editor-C6m40u-h.js} +3 -3
- package/inspector/dist/assets/{index-DgEUOA11.js → index-Cvq-RiV2.js} +14 -14
- 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-rrIMDK_-.js +0 -1
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;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { absolutePath } from "../config.js";
|
|
4
|
+
import Table from "cli-table3";
|
|
5
|
+
import { discoverWorkspaces, forgetWorkspace, startWorkspace, stopWorkspace, tildify, } from "../inspector/instances.js";
|
|
6
|
+
/**
|
|
7
|
+
* `kraftwerk workspaces` — every workspace that ever ran the inspector on this
|
|
8
|
+
* machine (~/.kraftwerk/workspaces), joined with what is running right now.
|
|
9
|
+
* The answer to "where did I start that UI, and how do I get it back":
|
|
10
|
+
*
|
|
11
|
+
* kraftwerk workspaces list: name, root, running/stopped
|
|
12
|
+
* kraftwerk workspaces start <ref> relaunch `kraftwerk ui` in that root, detached
|
|
13
|
+
* kraftwerk workspaces stop <ref> SIGTERM a running UI (also ones started in a terminal)
|
|
14
|
+
* kraftwerk workspaces forget <ref> drop the record (the folder stays)
|
|
15
|
+
*
|
|
16
|
+
* <ref> is a workspace name, the root's folder name, the root path, or a
|
|
17
|
+
* port / localhost:port for running instances that recorded no root.
|
|
18
|
+
*/
|
|
19
|
+
export const fmtAgo = (iso) => {
|
|
20
|
+
if (!iso)
|
|
21
|
+
return "—";
|
|
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`;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Running → its url. Not running: "stopped" after a clean shutdown, "died"
|
|
33
|
+
* when the last start has no matching stop (killed, crashed, terminal
|
|
34
|
+
* closed) — the case this command exists for.
|
|
35
|
+
*/
|
|
36
|
+
const status = (e) => {
|
|
37
|
+
if (e.live)
|
|
38
|
+
return chalk.green(`● ${e.url.replace(/^https?:\/\//, "")}`);
|
|
39
|
+
if (e.exists === false)
|
|
40
|
+
return chalk.red("path missing");
|
|
41
|
+
const clean = !!e.lastStopped && !!e.lastStarted && e.lastStopped >= e.lastStarted;
|
|
42
|
+
return clean ? chalk.dim("stopped") : chalk.yellow("died");
|
|
43
|
+
};
|
|
44
|
+
/** Match a ref against known workspaces; `needRoot` excludes rootless live instances (start/forget need a root). */
|
|
45
|
+
async function resolveRef(ref, needRoot = true) {
|
|
46
|
+
const all = (await discoverWorkspaces()).filter((e) => e.root || !needRoot);
|
|
47
|
+
const abs = absolutePath(ref);
|
|
48
|
+
const port = /^\d+$/.test(ref) ? `http://localhost:${ref}` : `http://${ref.replace(/^https?:\/\//, "")}`;
|
|
49
|
+
const hits = all.filter((e) => e.root === abs ||
|
|
50
|
+
e.root === ref ||
|
|
51
|
+
e.name === ref ||
|
|
52
|
+
(e.root && path.basename(e.root) === ref) ||
|
|
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 workspace "${ref}". See \`kraftwerk workspaces\`.`));
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
60
|
+
console.error(chalk.red(`"${ref}" is ambiguous — use the root path or port:`));
|
|
61
|
+
for (const h of hits)
|
|
62
|
+
console.error(` ${h.root ?? h.url}`);
|
|
63
|
+
process.exit(2);
|
|
64
|
+
}
|
|
65
|
+
export function registerWorkspaceCommands(program) {
|
|
66
|
+
const workspaces = program
|
|
67
|
+
.command("workspaces")
|
|
68
|
+
.description("Known kraftwerk workspaces on this machine: list, start a stopped UI, stop a running one, forget");
|
|
69
|
+
workspaces
|
|
70
|
+
.command("list", { isDefault: true })
|
|
71
|
+
.description("List known workspaces with their root and whether the UI is running")
|
|
72
|
+
.option("--json", "Machine-readable output")
|
|
73
|
+
.action(async (opts) => {
|
|
74
|
+
const entries = await discoverWorkspaces();
|
|
75
|
+
if (opts.json) {
|
|
76
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (entries.length === 0) {
|
|
80
|
+
console.log(chalk.dim("No workspaces known yet — `kraftwerk ui` registers the current one."));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const table = new Table({
|
|
84
|
+
head: ["workspace", "root", "status", "last started"].map((h) => chalk.bold(h)),
|
|
85
|
+
wordWrap: true,
|
|
86
|
+
colWidths: [22, 48, 24, 14],
|
|
87
|
+
});
|
|
88
|
+
for (const e of entries) {
|
|
89
|
+
table.push([
|
|
90
|
+
`${e.icon ? e.icon + " " : ""}${chalk.cyan(e.name)}`,
|
|
91
|
+
e.rootLabel ?? chalk.dim("(no root recorded)"),
|
|
92
|
+
status(e),
|
|
93
|
+
fmtAgo(e.lastStarted),
|
|
94
|
+
]);
|
|
95
|
+
}
|
|
96
|
+
console.log(table.toString());
|
|
97
|
+
});
|
|
98
|
+
workspaces
|
|
99
|
+
.command("start")
|
|
100
|
+
.description("Relaunch the UI of a stopped workspace (detached; logs in ~/.kraftwerk/logs)")
|
|
101
|
+
.argument("<ref>", "Workspace name, folder name, or root path")
|
|
102
|
+
.action(async (ref) => {
|
|
103
|
+
const entry = await resolveRef(ref);
|
|
104
|
+
if (entry.live) {
|
|
105
|
+
console.log(`${chalk.green("✔")} ${entry.name} already running: ${chalk.cyan(entry.url)}`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const r = await startWorkspace(entry.root);
|
|
109
|
+
if (!r.ok) {
|
|
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);
|
|
114
|
+
}
|
|
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
|
+
});
|
|
118
|
+
workspaces
|
|
119
|
+
.command("stop")
|
|
120
|
+
.description("Stop a running UI (SIGTERM to its server; works for terminal-started ones too)")
|
|
121
|
+
.argument("<ref>", "Workspace name, folder name, root path, or port")
|
|
122
|
+
.action(async (ref) => {
|
|
123
|
+
const entry = await resolveRef(ref, false);
|
|
124
|
+
if (!entry.live) {
|
|
125
|
+
console.log(chalk.dim(`${entry.name} is not running.`));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const r = await stopWorkspace({ root: entry.root, url: entry.url });
|
|
129
|
+
if (!r.ok) {
|
|
130
|
+
console.error(chalk.red(`Could not stop ${entry.name}: ${r.error}`));
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
console.log(`${chalk.green("✔")} ${entry.name} stopped ${chalk.dim(`(${entry.url.replace(/^https?:\/\//, "")})`)}`);
|
|
134
|
+
});
|
|
135
|
+
workspaces
|
|
136
|
+
.command("forget")
|
|
137
|
+
.description("Remove a workspace from the registry (its folder is untouched)")
|
|
138
|
+
.argument("<ref>", "Workspace name, folder name, or root path")
|
|
139
|
+
.action(async (ref) => {
|
|
140
|
+
const entry = await resolveRef(ref);
|
|
141
|
+
if (await forgetWorkspace(entry.root)) {
|
|
142
|
+
console.log(`${chalk.green("✔")} forgot ${entry.name} (${entry.rootLabel})`);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
console.error(chalk.red(`No record for ${entry.root}`));
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
* repos: # git repositories the agents work on (absent = off, bare key = on)
|
|
30
30
|
* root: kraftwerk-data/repos # where clones land, relative to the file. Default: repos
|
|
31
31
|
* vibeables: # small apps built live in a chat, rendered in the inspector (absent = off, bare key = on)
|
|
32
|
-
|
|
32
|
+
* root: apps # one folder per app, part of the workspace. Default: kraftwerk-data/vibeables
|
|
33
|
+
* projects: # goal-scoped folders (brief, systems of record, links) that chats work in (absent = off, bare key = on)
|
|
34
|
+
* root: kraftwerk-data/projects # one folder per project, part of the workspace. Default: kraftwerk-data/projects
|
|
33
35
|
* public: https://kw.example.com # hostname the inspector is reached at through a tunnel or reverse proxy
|
|
34
36
|
* tunnel: # Cloudflare Tunnel run by `kraftwerk ui` (absent = off, bare key = on)
|
|
35
37
|
* name: kraftwerk # locally-managed tunnel (cloudflared tunnel create); absent: TUNNEL_TOKEN env, dashboard-managed
|
|
@@ -102,6 +104,23 @@ export interface VibeablesConfig {
|
|
|
102
104
|
export declare const VIBEABLES_DEFAULT_ROOT = "kraftwerk-data/vibeables";
|
|
103
105
|
/** Absolute vibeables root when the feature is on, undefined otherwise. */
|
|
104
106
|
export declare function vibeablesRootFor(project: Project): string | undefined;
|
|
107
|
+
/**
|
|
108
|
+
* Projects: a goal with everything the agents need to reach it in one
|
|
109
|
+
* folder — a brief, the systems of record where the truth lives, and links
|
|
110
|
+
* to the workspace's knowledge, vibeables, repositories, workflows and
|
|
111
|
+
* agents. Each is one folder under the root; part of the workspace and
|
|
112
|
+
* synced by the workspace git like agents and knowledge.
|
|
113
|
+
*/
|
|
114
|
+
export interface ProjectsConfig {
|
|
115
|
+
/** false keeps the block but turns the feature off. Default: true. */
|
|
116
|
+
enabled?: boolean;
|
|
117
|
+
/** Where the projects live, relative to the project root. Default: kraftwerk-data/projects */
|
|
118
|
+
root?: string;
|
|
119
|
+
}
|
|
120
|
+
/** Where projects live when `projects.root` is not set. */
|
|
121
|
+
export declare const PROJECTS_DEFAULT_ROOT = "kraftwerk-data/projects";
|
|
122
|
+
/** Absolute projects root when the feature is on, undefined otherwise. */
|
|
123
|
+
export declare function projectsRootFor(project: Project): string | undefined;
|
|
105
124
|
/**
|
|
106
125
|
* Cloudflare Access in front of the public hostname. Access puts a login
|
|
107
126
|
* page on the hostname at Cloudflare's edge and hands the origin a signed
|
|
@@ -183,6 +202,8 @@ export interface ProjectConfig {
|
|
|
183
202
|
repos?: ReposConfig;
|
|
184
203
|
/** Vibeables: apps built live in a chat. Absent = off. */
|
|
185
204
|
vibeables?: VibeablesConfig;
|
|
205
|
+
/** Projects: goal-scoped folders the chats work in. Absent = off. */
|
|
206
|
+
projects?: ProjectsConfig;
|
|
186
207
|
/**
|
|
187
208
|
* Hostname the inspector is reached at through a tunnel or reverse proxy,
|
|
188
209
|
* e.g. "https://kw.example.com". The loopback bind then answers requests
|
package/dist/config.js
CHANGED
|
@@ -33,7 +33,9 @@ import { parse } from "yaml";
|
|
|
33
33
|
* repos: # git repositories the agents work on (absent = off, bare key = on)
|
|
34
34
|
* root: kraftwerk-data/repos # where clones land, relative to the file. Default: repos
|
|
35
35
|
* vibeables: # small apps built live in a chat, rendered in the inspector (absent = off, bare key = on)
|
|
36
|
-
|
|
36
|
+
* root: apps # one folder per app, part of the workspace. Default: kraftwerk-data/vibeables
|
|
37
|
+
* projects: # goal-scoped folders (brief, systems of record, links) that chats work in (absent = off, bare key = on)
|
|
38
|
+
* root: kraftwerk-data/projects # one folder per project, part of the workspace. Default: kraftwerk-data/projects
|
|
37
39
|
* public: https://kw.example.com # hostname the inspector is reached at through a tunnel or reverse proxy
|
|
38
40
|
* tunnel: # Cloudflare Tunnel run by `kraftwerk ui` (absent = off, bare key = on)
|
|
39
41
|
* name: kraftwerk # locally-managed tunnel (cloudflared tunnel create); absent: TUNNEL_TOKEN env, dashboard-managed
|
|
@@ -84,6 +86,15 @@ export function vibeablesRootFor(project) {
|
|
|
84
86
|
return undefined;
|
|
85
87
|
return path.resolve(project.root, v.root ?? VIBEABLES_DEFAULT_ROOT);
|
|
86
88
|
}
|
|
89
|
+
/** Where projects live when `projects.root` is not set. */
|
|
90
|
+
export const PROJECTS_DEFAULT_ROOT = "kraftwerk-data/projects";
|
|
91
|
+
/** Absolute projects root when the feature is on, undefined otherwise. */
|
|
92
|
+
export function projectsRootFor(project) {
|
|
93
|
+
const p = project.config.projects;
|
|
94
|
+
if (!p || p.enabled === false)
|
|
95
|
+
return undefined;
|
|
96
|
+
return path.resolve(project.root, p.root ?? PROJECTS_DEFAULT_ROOT);
|
|
97
|
+
}
|
|
87
98
|
/** The public hostname (lowercase, no port) when `public` is set, undefined otherwise. */
|
|
88
99
|
export function publicHostFor(project) {
|
|
89
100
|
return project.config.public ? parsePublic(project.config.public)?.hostname : undefined;
|
|
@@ -187,7 +198,7 @@ export async function resolveProject(cwd) {
|
|
|
187
198
|
const root = gitFallback ?? start;
|
|
188
199
|
return { root, config: {}, outputDir: path.join(root, "output") };
|
|
189
200
|
}
|
|
190
|
-
const KNOWN_KEYS = ["name", "icon", "color", "port", "workflows", "output", "knowledge", "agents", "skills", "switcher", "git", "repos", "vibeables", "public", "tunnel"];
|
|
201
|
+
const KNOWN_KEYS = ["name", "icon", "color", "port", "workflows", "output", "knowledge", "agents", "skills", "switcher", "git", "repos", "vibeables", "projects", "public", "tunnel"];
|
|
191
202
|
async function loadConfig(configPath) {
|
|
192
203
|
let raw;
|
|
193
204
|
try {
|
|
@@ -238,6 +249,11 @@ async function loadConfig(configPath) {
|
|
|
238
249
|
config[key] = {};
|
|
239
250
|
validateRootBlock(configPath, "vibeables", config[key]);
|
|
240
251
|
}
|
|
252
|
+
else if (key === "projects") {
|
|
253
|
+
if (config[key] === null)
|
|
254
|
+
config[key] = {};
|
|
255
|
+
validateRootBlock(configPath, "projects", config[key]);
|
|
256
|
+
}
|
|
241
257
|
else if (key === "public") {
|
|
242
258
|
if (typeof config[key] !== "string" || !parsePublic(config[key])) {
|
|
243
259
|
throw new Error(`${path.basename(configPath)}: public must be a hostname or https URL like "https://kw.example.com"`);
|
|
@@ -353,7 +369,7 @@ function validateGit(configPath, value) {
|
|
|
353
369
|
}
|
|
354
370
|
}
|
|
355
371
|
/**
|
|
356
|
-
* `repos` and `
|
|
372
|
+
* `repos`, `vibeables` and `projects` share one shape: { enabled?, root? }. The root
|
|
357
373
|
* must never be the project itself or anything outside it — it is what
|
|
358
374
|
* remove/delete operate under, and what the workspace git may stage.
|
|
359
375
|
*/
|