@netnodeag/kraftwerk 0.36.0 → 0.37.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 +39 -1
- package/dist/cli/doctor.js +18 -1
- package/dist/cli/init.js +16 -9
- package/dist/cli/kraftwerk.js +3 -0
- package/dist/cli/projects.d.ts +14 -0
- package/dist/cli/projects.js +1 -1
- package/dist/cli/repos.d.ts +2 -0
- package/dist/cli/repos.js +115 -0
- package/dist/cli/routines.d.ts +10 -0
- package/dist/cli/routines.js +1 -1
- package/dist/config.d.ts +27 -0
- package/dist/config.js +55 -1
- package/dist/inspector/chat/sessions.js +29 -2
- package/dist/inspector/git.d.ts +14 -0
- package/dist/inspector/git.js +11 -9
- package/dist/inspector/repos.d.ts +118 -0
- package/dist/inspector/repos.js +249 -0
- package/dist/inspector/server.js +43 -1
- package/dist/inspector/settings.d.ts +7 -1
- package/dist/inspector/settings.js +32 -1
- package/inspector/dist/assets/{editor-7ImnDC3D.js → editor-vM6JSj5h.js} +3 -3
- package/inspector/dist/assets/{index-DbmSSHvr.js → index-BAAjszPI.js} +1 -1
- package/inspector/dist/assets/{index-DjRBLY9N.js → index-BKcOp35r.js} +1 -1
- package/inspector/dist/assets/index-Ba8VU6Wz.js +217 -0
- package/inspector/dist/assets/{index-C3Ob1O7W.js → index-Bb08wK-l.js} +1 -1
- package/inspector/dist/assets/{index-Ct_jZSyD.js → index-BrCwDkJZ.js} +1 -1
- package/inspector/dist/assets/{index-DKrBjjGQ.js → index-Bs1U0rWW.js} +1 -1
- package/inspector/dist/assets/{index-BC7aIJ-h.js → index-C1UXlW32.js} +1 -1
- package/inspector/dist/assets/{index-CIjv7yxE.js → index-CQ2XKqmD.js} +1 -1
- package/inspector/dist/assets/{index-BuRajvjQ.js → index-CReepirH.js} +1 -1
- package/inspector/dist/assets/{index-YZtBJhWD.js → index-CUGfxoYt.js} +1 -1
- package/inspector/dist/assets/{index-B_CIhKa4.js → index-CaKmxcD6.js} +1 -1
- package/inspector/dist/assets/{index-BZeYO5wT.js → index-CtMGH62C.js} +1 -1
- package/inspector/dist/assets/{index-BA7nx4V5.css → index-D4_mLTSH.css} +1 -1
- package/inspector/dist/assets/{index-DPBAv_kj.js → index-D_11IZBV.js} +1 -1
- package/inspector/dist/assets/{index-CRAwmMfk.js → index-DfZT0IoM.js} +1 -1
- package/inspector/dist/assets/{index-DL_bJiOj.js → index-DykDNrYm.js} +1 -1
- package/inspector/dist/assets/{index-BIXYBiw7.js → index-MWv5H1oP.js} +1 -1
- package/inspector/dist/assets/{index-fWUFhIGR.js → index-oIQqfbwm.js} +1 -1
- package/inspector/dist/assets/{index--2I7vpcS.js → index-yGZsomcy.js} +1 -1
- package/inspector/dist/index.html +2 -2
- package/package.json +1 -1
- package/schema/kraftwerk.schema.json +15 -0
- package/inspector/dist/assets/index-B2ThRS2S.js +0 -217
package/README.md
CHANGED
|
@@ -180,7 +180,7 @@ Optional, at the project root, and also the root marker for the walk-up. All
|
|
|
180
180
|
fields are optional. `workflows:` sets the workflows root, `output:` the
|
|
181
181
|
run-artifact directory (default `output/`), `knowledge:` the OKF bundle root
|
|
182
182
|
(default `knowledge/`), and `agents:` the agent-definition root (default
|
|
183
|
-
`agents/`).
|
|
183
|
+
`agents/`). `repos:` turns on the [repositories](#repositories) folder.
|
|
184
184
|
|
|
185
185
|
`switcher:` links other kraftwerk workspaces from the inspector header. The
|
|
186
186
|
workspace name becomes a dropdown listing them:
|
|
@@ -296,6 +296,44 @@ the model via ACP session options and the thinking budget via
|
|
|
296
296
|
are created and edited in the UI, or by editing the files, since the
|
|
297
297
|
definition is read fresh for each new session.
|
|
298
298
|
|
|
299
|
+
### Repositories
|
|
300
|
+
|
|
301
|
+
Turn on `repos:` in `kraftwerk.yml` (or the checkbox in settings) and the
|
|
302
|
+
workspace gets one folder for the git repositories its agents work on:
|
|
303
|
+
|
|
304
|
+
```yaml
|
|
305
|
+
repos:
|
|
306
|
+
root: kraftwerk-data/repos # default: repos/ — git-ignored, never synced
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
The folder is the registry. Whatever has a `.git` directly under the root is
|
|
310
|
+
a repository, whether the "repositories" screen cloned it, `kraftwerk repos
|
|
311
|
+
add <url>` did, or an agent ran `git clone` there. Every entry is read live
|
|
312
|
+
from git: origin, branch, head, uncommitted changes, ahead/behind. The
|
|
313
|
+
screen clones by url, fetches and fast-forwards clean clones, and removes
|
|
314
|
+
them (refusing while they hold unpushed or uncommitted work). Cloning uses
|
|
315
|
+
your own git credentials and never prompts, so a private remote has to work
|
|
316
|
+
from a terminal first.
|
|
317
|
+
|
|
318
|
+
Every agent, and the inspector assistant, gets the list as context: where
|
|
319
|
+
the root is, what is cloned, and how to add more with the CLI. So "look at
|
|
320
|
+
the widgets repo" works as soon as it is cloned, and "clone
|
|
321
|
+
github:org/widgets and look at it" works before.
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
kraftwerk repos # name, branch, head, state
|
|
325
|
+
kraftwerk repos add github:org/widgets # or any https / ssh url
|
|
326
|
+
kraftwerk repos add <url> --name tools --branch dev
|
|
327
|
+
kraftwerk repos add <url> --depth 1 # shallow clone of a large repository
|
|
328
|
+
kraftwerk repos update widgets # fetch, fast-forward when clean
|
|
329
|
+
kraftwerk repos remove widgets [--force]
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
`GET /api/repos` returns the same list for automation. The root must be a
|
|
333
|
+
folder inside the project (never the project itself): it is excluded from the
|
|
334
|
+
workspace git sync, the first clone adds it to `.gitignore`, and `kraftwerk
|
|
335
|
+
doctor` warns when git does not ignore it.
|
|
336
|
+
|
|
299
337
|
### Skills in chat
|
|
300
338
|
|
|
301
339
|
Chats, both general ones and agent sessions, can use skills. These are
|
package/dist/cli/doctor.js
CHANGED
|
@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
import { isDir, resolveProject } from "../config.js";
|
|
5
|
+
import { ignoreEntryFor, isDir, reposRootFor, resolveProject } from "../config.js";
|
|
6
6
|
import { discoverWorkflows } from "../discover.js";
|
|
7
7
|
import { missingEnv } from "../yaml.js";
|
|
8
8
|
const ICONS = {
|
|
@@ -106,6 +106,23 @@ export async function runDoctor(cwd) {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
+
// Repositories: the clones root must stay out of the workspace git. git
|
|
110
|
+
// itself decides — that covers worktrees (.git is a file), a workspace
|
|
111
|
+
// nested in a larger repo, a .gitignore at the toplevel, global excludes.
|
|
112
|
+
const reposRoot = reposRootFor(project);
|
|
113
|
+
if (reposRoot) {
|
|
114
|
+
const entry = ignoreEntryFor(project.root, reposRoot) ?? path.relative(project.root, reposRoot);
|
|
115
|
+
const label = `repos: ${path.relative(cwd, reposRoot) || "."}`;
|
|
116
|
+
const check = spawnSync("git", ["check-ignore", "-q", "--", reposRoot], { cwd: project.root, encoding: "utf8" });
|
|
117
|
+
if (check.error)
|
|
118
|
+
report("info", label, "git not found — cannot check .gitignore");
|
|
119
|
+
else if (check.status === 0)
|
|
120
|
+
report("ok", label, "git-ignored");
|
|
121
|
+
else if (check.status === 1)
|
|
122
|
+
report("warn", label, `${entry}/ is not git-ignored — clones would show up as untracked gitlinks; add it to .gitignore`);
|
|
123
|
+
else
|
|
124
|
+
report("ok", label, "not inside a git repository");
|
|
125
|
+
}
|
|
109
126
|
const found = project.workflowsRoot ? await discoverWorkflows(cwd) : [];
|
|
110
127
|
if (!project.workflowsRoot) {
|
|
111
128
|
report("warn", "no workflows root", "expected src/workflows/ or workflows/ — `kraftwerk init` scaffolds one");
|
package/dist/cli/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { appendFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import { CONFIG_SCHEMA_URL, SCHEMA_URL } from "../config.js";
|
|
4
|
+
import { CONFIG_SCHEMA_URL, gitignoreHas, SCHEMA_URL } from "../config.js";
|
|
5
5
|
import { initBundle, writeConcept } from "../okf.js";
|
|
6
6
|
/**
|
|
7
7
|
* `kraftwerk init` — make any repository a kraftwerk consumer in one
|
|
@@ -22,6 +22,8 @@ output: ${DATA_DIR}/output # where run artifacts land (git-ignored)
|
|
|
22
22
|
knowledge: ${DATA_DIR}/knowledge # OKF knowledge bundles
|
|
23
23
|
agents: ${DATA_DIR}/agents # agent definitions
|
|
24
24
|
skills: ${DATA_DIR}/skills # workspace skills (shared instruction packages)
|
|
25
|
+
# repos: # git repositories the agents work on (uncomment both lines to enable)
|
|
26
|
+
# root: ${DATA_DIR}/repos # clones land here (git-ignored); a bare \`repos:\` uses repos/ instead
|
|
25
27
|
`;
|
|
26
28
|
const WORKFLOW_TEMPLATE = `# yaml-language-server: $schema=${SCHEMA_URL}
|
|
27
29
|
name: hello
|
|
@@ -124,20 +126,25 @@ export async function runInit(cwd) {
|
|
|
124
126
|
await writeConcept(knowledgeRoot, DEMO_BUNDLE, "playbooks/refunds", DEMO_CONCEPT, "kraftwerk-init");
|
|
125
127
|
created.push(bundleRel);
|
|
126
128
|
}
|
|
127
|
-
// .gitignore:
|
|
128
|
-
|
|
129
|
+
// .gitignore: the output dir and the repos root (clones must never become
|
|
130
|
+
// gitlinks of the workspace repo). A missing file gets both in one write;
|
|
131
|
+
// an existing one only the entries it lacks.
|
|
129
132
|
const gitignorePath = path.join(cwd, ".gitignore");
|
|
130
133
|
const gitignore = (await readFile(gitignorePath, "utf8").catch(() => null)) ?? null;
|
|
134
|
+
const entries = [`${DATA_DIR}/output`, `${DATA_DIR}/repos`];
|
|
131
135
|
if (gitignore === null) {
|
|
132
|
-
await writeFile(gitignorePath, `${
|
|
136
|
+
await writeFile(gitignorePath, entries.map((e) => `${e}/\n`).join(""));
|
|
133
137
|
created.push(".gitignore");
|
|
134
138
|
}
|
|
135
|
-
else if (!gitignore.split("\n").some((l) => l.trim().replace(/\/$/, "") === outputEntry)) {
|
|
136
|
-
await appendFile(gitignorePath, `${gitignore.endsWith("\n") ? "" : "\n"}${outputEntry}/\n`);
|
|
137
|
-
created.push(`.gitignore (${outputEntry}/ added)`);
|
|
138
|
-
}
|
|
139
139
|
else {
|
|
140
|
-
|
|
140
|
+
const missing = entries.filter((e) => !gitignoreHas(gitignore, e));
|
|
141
|
+
if (missing.length === 0) {
|
|
142
|
+
skipped.push(".gitignore");
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
await appendFile(gitignorePath, `${gitignore.endsWith("\n") ? "" : "\n"}${missing.map((e) => `${e}/\n`).join("")}`);
|
|
146
|
+
created.push(`.gitignore (${missing.map((e) => `${e}/`).join(", ")} added)`);
|
|
147
|
+
}
|
|
141
148
|
}
|
|
142
149
|
for (const f of created)
|
|
143
150
|
console.log(`${chalk.green("✔")} ${f}`);
|
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 { registerRepoCommands } from "./repos.js";
|
|
16
17
|
import { registerRoutineCommands } from "./routines.js";
|
|
17
18
|
import { listRuns, showRun } from "./runs.js";
|
|
18
19
|
import { runUi } from "./ui.js";
|
|
@@ -27,6 +28,7 @@ import { runUi } from "./ui.js";
|
|
|
27
28
|
* kraftwerk routines ... per-agent scheduled prompts (list/add/remove/enable/run)
|
|
28
29
|
* kraftwerk ui start the inspector web UI (localhost:1981)
|
|
29
30
|
* kraftwerk projects ... known projects on this machine (list/start/forget)
|
|
31
|
+
* kraftwerk repos ... repositories the agents work on (list/add/update/remove)
|
|
30
32
|
* kraftwerk doctor preflight: harness CLIs, docker, workflows, env
|
|
31
33
|
* kraftwerk validate [paths...] validate without executing
|
|
32
34
|
*
|
|
@@ -259,6 +261,7 @@ runs
|
|
|
259
261
|
registerKnowledgeCommands(program);
|
|
260
262
|
registerRoutineCommands(program);
|
|
261
263
|
registerProjectCommands(program);
|
|
264
|
+
registerRepoCommands(program);
|
|
262
265
|
program
|
|
263
266
|
.command("ui")
|
|
264
267
|
.description("Start the inspector web UI for this project's runs and workflows")
|
package/dist/cli/projects.d.ts
CHANGED
|
@@ -1,2 +1,16 @@
|
|
|
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;
|
|
2
16
|
export declare function registerProjectCommands(program: Command): void;
|
package/dist/cli/projects.js
CHANGED
|
@@ -15,7 +15,7 @@ import { discoverWorkspaces, forgetProject, startProject, stopProject, tildify,
|
|
|
15
15
|
* <ref> is a project name, the root's folder name, the root path, or a
|
|
16
16
|
* port / localhost:port for running instances that recorded no root.
|
|
17
17
|
*/
|
|
18
|
-
const fmtAgo = (iso) => {
|
|
18
|
+
export const fmtAgo = (iso) => {
|
|
19
19
|
if (!iso)
|
|
20
20
|
return "—";
|
|
21
21
|
const s = Math.max(0, (Date.now() - Date.parse(iso)) / 1000);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import Table from "cli-table3";
|
|
3
|
+
import { addRepo, listRepos, removeRepo, updateRepo } from "../inspector/repos.js";
|
|
4
|
+
import { fmtAgo } from "./projects.js";
|
|
5
|
+
import { initContext as prepare } from "./routines.js";
|
|
6
|
+
/**
|
|
7
|
+
* `kraftwerk repos` — the git repositories the agents work on, kept under
|
|
8
|
+
* the project's repos root (`repos.root` in kraftwerk.yml). The same module
|
|
9
|
+
* the inspector uses, so a clone made here shows up on the repositories
|
|
10
|
+
* screen and in every agent's context at once:
|
|
11
|
+
*
|
|
12
|
+
* kraftwerk repos list: name, branch, head, state
|
|
13
|
+
* kraftwerk repos add <url> [--name x] [--branch b] [--depth n]
|
|
14
|
+
* kraftwerk repos update <name> fetch, fast-forward when clean
|
|
15
|
+
* kraftwerk repos remove <name> [--force]
|
|
16
|
+
*/
|
|
17
|
+
const die = (msg) => {
|
|
18
|
+
console.error(chalk.red(msg));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
};
|
|
21
|
+
export function registerRepoCommands(program) {
|
|
22
|
+
const repos = program
|
|
23
|
+
.command("repos")
|
|
24
|
+
.description("Repositories the agents work on: list, clone one into the repos root, update, remove");
|
|
25
|
+
repos
|
|
26
|
+
.command("list", { isDefault: true })
|
|
27
|
+
.description("List the clones under the repos root with branch, head and state")
|
|
28
|
+
.option("--json", "Machine-readable output")
|
|
29
|
+
.action(async (opts) => {
|
|
30
|
+
await prepare();
|
|
31
|
+
const view = await listRepos();
|
|
32
|
+
if (opts.json) {
|
|
33
|
+
console.log(JSON.stringify(view, null, 2));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (!view.enabled)
|
|
37
|
+
die(view.error ?? "repositories are off");
|
|
38
|
+
if (view.repos.length === 0) {
|
|
39
|
+
console.log(chalk.dim(`No repositories under ${view.root} yet — \`kraftwerk repos add <url>\` clones one.`));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const table = new Table({
|
|
43
|
+
head: ["name", "branch", "head", "state", "url"].map((h) => chalk.bold(h)),
|
|
44
|
+
wordWrap: true,
|
|
45
|
+
colWidths: [22, 16, 30, 18, 40],
|
|
46
|
+
});
|
|
47
|
+
for (const r of view.repos) {
|
|
48
|
+
const state = r.error
|
|
49
|
+
? chalk.red(r.error)
|
|
50
|
+
: [
|
|
51
|
+
r.dirty ? chalk.yellow(`${r.dirty} changed`) : "",
|
|
52
|
+
r.ahead ? chalk.green(`${r.ahead}↑`) : "",
|
|
53
|
+
r.behind ? chalk.cyan(`${r.behind}↓`) : "",
|
|
54
|
+
]
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.join(" ") || chalk.dim("clean");
|
|
57
|
+
table.push([
|
|
58
|
+
chalk.cyan(r.slug),
|
|
59
|
+
r.branch ?? "—",
|
|
60
|
+
r.head ? `${r.head} ${chalk.dim(r.subject ?? "")}\n${chalk.dim(fmtAgo(r.committedAt))}` : "—",
|
|
61
|
+
state,
|
|
62
|
+
r.url ?? chalk.dim("(no origin)"),
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
console.log(table.toString());
|
|
66
|
+
console.log(chalk.dim(view.root ?? ""));
|
|
67
|
+
});
|
|
68
|
+
repos
|
|
69
|
+
.command("add")
|
|
70
|
+
.description("Clone a repository into the repos root")
|
|
71
|
+
.argument("<url>", "git url, or github:org/repo")
|
|
72
|
+
.option("--name <name>", "Folder name under the root (default: last url segment)")
|
|
73
|
+
.option("--branch <branch>", "Branch to check out")
|
|
74
|
+
.option("--depth <n>", "Shallow clone with n commits of history (large repositories)")
|
|
75
|
+
.option("--json", "Print the new entry as JSON")
|
|
76
|
+
.action(async (url, opts) => {
|
|
77
|
+
await prepare();
|
|
78
|
+
try {
|
|
79
|
+
const depth = opts.depth === undefined ? undefined : Number(opts.depth);
|
|
80
|
+
const repo = await addRepo({ url, name: opts.name, branch: opts.branch, depth });
|
|
81
|
+
if (opts.json)
|
|
82
|
+
console.log(JSON.stringify(repo, null, 2));
|
|
83
|
+
else
|
|
84
|
+
console.log(`${chalk.green("✔")} ${chalk.cyan(repo.slug)} → ${repo.path}${repo.branch ? chalk.dim(` (${repo.branch})`) : ""}`);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
die(err.message);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
repos
|
|
91
|
+
.command("update")
|
|
92
|
+
.description("Fetch a repository and fast-forward it when it is clean")
|
|
93
|
+
.argument("<name>", "Folder name under the root")
|
|
94
|
+
.action(async (name) => {
|
|
95
|
+
await prepare();
|
|
96
|
+
const r = await updateRepo(name).catch((err) => ({ ok: false, error: err.message, repo: undefined }));
|
|
97
|
+
if (!r.ok)
|
|
98
|
+
die(r.error ?? "update failed");
|
|
99
|
+
const repo = r.repo;
|
|
100
|
+
const tail = r.error ? chalk.yellow(` — ${r.error}`) : "";
|
|
101
|
+
console.log(`${chalk.green("✔")} ${chalk.cyan(name)} at ${repo.head} ${chalk.dim(repo.subject ?? "")}${tail}`);
|
|
102
|
+
});
|
|
103
|
+
repos
|
|
104
|
+
.command("remove")
|
|
105
|
+
.description("Delete a clone from the repos root (refused while it holds unpushed or uncommitted work)")
|
|
106
|
+
.argument("<name>", "Folder name under the root")
|
|
107
|
+
.option("--force", "Delete even with local changes")
|
|
108
|
+
.action(async (name, opts) => {
|
|
109
|
+
await prepare();
|
|
110
|
+
const r = await removeRepo(name, !!opts.force).catch((err) => ({ ok: false, error: err.message }));
|
|
111
|
+
if (!r.ok)
|
|
112
|
+
die(r.error ?? "remove failed");
|
|
113
|
+
console.log(`${chalk.green("✔")} removed ${chalk.cyan(name)}`);
|
|
114
|
+
});
|
|
115
|
+
}
|
package/dist/cli/routines.d.ts
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* `kraftwerk routines` — CLI surface over per-agent scheduled prompts
|
|
4
|
+
* (agents/<slug>/routines.yml). Definition CRUD works directly on the
|
|
5
|
+
* files; `run` fires through the running inspector server, because the
|
|
6
|
+
* session (chat) and the scheduler live there.
|
|
7
|
+
*/
|
|
8
|
+
/** Point the inspector context at this project so agent/routine paths resolve. */
|
|
9
|
+
export declare function initContext(): Promise<{
|
|
10
|
+
port: number;
|
|
11
|
+
}>;
|
|
2
12
|
export declare function registerRoutineCommands(program: Command): void;
|
package/dist/cli/routines.js
CHANGED
|
@@ -12,7 +12,7 @@ import { getAgent, listAgents } from "../inspector/agents.js";
|
|
|
12
12
|
* session (chat) and the scheduler live there.
|
|
13
13
|
*/
|
|
14
14
|
/** Point the inspector context at this project so agent/routine paths resolve. */
|
|
15
|
-
async function initContext() {
|
|
15
|
+
export async function initContext() {
|
|
16
16
|
const project = await resolveProject(process.cwd());
|
|
17
17
|
setProjectRoot(project.root);
|
|
18
18
|
setOutputDir(project.outputDir);
|
package/dist/config.d.ts
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
* remote: origin
|
|
26
26
|
* interval: 300 # seconds between background fetches
|
|
27
27
|
* autosync: pull # off | pull
|
|
28
|
+
* repos: # git repositories the agents work on (absent = off, bare key = on)
|
|
29
|
+
* root: kraftwerk-data/repos # where clones land, relative to the file. Default: repos
|
|
28
30
|
*/
|
|
29
31
|
/** Stable, versionless URL of the workflow JSON schema (editor validation). */
|
|
30
32
|
export declare const SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/workflow.schema.json";
|
|
@@ -49,6 +51,29 @@ export interface GitConfig {
|
|
|
49
51
|
/** off = fetch only. pull = also fast-forward when behind and clean. Default: pull. */
|
|
50
52
|
autosync?: "off" | "pull";
|
|
51
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Repositories: git clones the agents work on, kept under one root inside
|
|
56
|
+
* the project so every agent finds them at a known path. The root is never
|
|
57
|
+
* synced by the workspace git and should be git-ignored.
|
|
58
|
+
*/
|
|
59
|
+
export interface ReposConfig {
|
|
60
|
+
/** false keeps the block but turns the feature off. Default: true. */
|
|
61
|
+
enabled?: boolean;
|
|
62
|
+
/** Where clones land, relative to the project root. Default: repos */
|
|
63
|
+
root?: string;
|
|
64
|
+
}
|
|
65
|
+
/** Where clones land when `repos.root` is not set. */
|
|
66
|
+
export declare const REPOS_DEFAULT_ROOT = "repos";
|
|
67
|
+
/**
|
|
68
|
+
* Absolute repos root when the feature is on, undefined otherwise. The one
|
|
69
|
+
* place that reads the block, so the sync exclude, the doctor check, the
|
|
70
|
+
* settings save and the module itself cannot disagree.
|
|
71
|
+
*/
|
|
72
|
+
export declare function reposRootFor(project: Project): string | undefined;
|
|
73
|
+
/** A directory as a .gitignore entry: relative, forward slashes, no trailing slash; undefined outside the root. */
|
|
74
|
+
export declare function ignoreEntryFor(projectRoot: string, dir: string): string | undefined;
|
|
75
|
+
/** True when .gitignore text already covers the entry (with or without a leading or trailing slash). */
|
|
76
|
+
export declare function gitignoreHas(text: string, entry: string): boolean;
|
|
52
77
|
/** One entry of the workspace switcher: another kraftwerk instance to link to. */
|
|
53
78
|
export interface SwitcherEntry {
|
|
54
79
|
/** Display name of the other workspace. */
|
|
@@ -79,6 +104,8 @@ export interface ProjectConfig {
|
|
|
79
104
|
switcher?: SwitcherEntry[];
|
|
80
105
|
/** Workspace git sync. Absent = off. */
|
|
81
106
|
git?: GitConfig;
|
|
107
|
+
/** Repositories the agents work on. Absent = off. */
|
|
108
|
+
repos?: ReposConfig;
|
|
82
109
|
}
|
|
83
110
|
export interface Project {
|
|
84
111
|
/** Absolute project root the CLI operates on. */
|
package/dist/config.js
CHANGED
|
@@ -28,6 +28,8 @@ import { parse } from "yaml";
|
|
|
28
28
|
* remote: origin
|
|
29
29
|
* interval: 300 # seconds between background fetches
|
|
30
30
|
* autosync: pull # off | pull
|
|
31
|
+
* repos: # git repositories the agents work on (absent = off, bare key = on)
|
|
32
|
+
* root: kraftwerk-data/repos # where clones land, relative to the file. Default: repos
|
|
31
33
|
*/
|
|
32
34
|
/** Stable, versionless URL of the workflow JSON schema (editor validation). */
|
|
33
35
|
export const SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/workflow.schema.json";
|
|
@@ -35,6 +37,30 @@ export const SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk
|
|
|
35
37
|
export const CONFIG_SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/kraftwerk.schema.json";
|
|
36
38
|
export const CONFIG_FILENAMES = ["kraftwerk.yml", "kraftwerk.yaml"];
|
|
37
39
|
const WORKFLOW_ROOT_CANDIDATES = ["src/workflows", "workflows"];
|
|
40
|
+
/** Where clones land when `repos.root` is not set. */
|
|
41
|
+
export const REPOS_DEFAULT_ROOT = "repos";
|
|
42
|
+
/**
|
|
43
|
+
* Absolute repos root when the feature is on, undefined otherwise. The one
|
|
44
|
+
* place that reads the block, so the sync exclude, the doctor check, the
|
|
45
|
+
* settings save and the module itself cannot disagree.
|
|
46
|
+
*/
|
|
47
|
+
export function reposRootFor(project) {
|
|
48
|
+
const r = project.config.repos;
|
|
49
|
+
if (!r || r.enabled === false)
|
|
50
|
+
return undefined;
|
|
51
|
+
return path.resolve(project.root, r.root ?? REPOS_DEFAULT_ROOT);
|
|
52
|
+
}
|
|
53
|
+
/** A directory as a .gitignore entry: relative, forward slashes, no trailing slash; undefined outside the root. */
|
|
54
|
+
export function ignoreEntryFor(projectRoot, dir) {
|
|
55
|
+
const rel = path.relative(projectRoot, path.resolve(projectRoot, dir)).split(path.sep).join("/");
|
|
56
|
+
if (!rel || rel === "." || rel.startsWith(".."))
|
|
57
|
+
return undefined;
|
|
58
|
+
return rel;
|
|
59
|
+
}
|
|
60
|
+
/** True when .gitignore text already covers the entry (with or without a leading or trailing slash). */
|
|
61
|
+
export function gitignoreHas(text, entry) {
|
|
62
|
+
return text.split("\n").some((l) => l.trim().replace(/^\//, "").replace(/\/$/, "") === entry);
|
|
63
|
+
}
|
|
38
64
|
const exists = async (p) => !!(await stat(p).catch(() => null));
|
|
39
65
|
/** True when the path exists and is a directory. */
|
|
40
66
|
export const isDir = async (p) => (await stat(p).catch(() => null))?.isDirectory() ?? false;
|
|
@@ -87,7 +113,7 @@ export async function resolveProject(cwd) {
|
|
|
87
113
|
const root = gitFallback ?? start;
|
|
88
114
|
return { root, config: {}, outputDir: path.join(root, "output") };
|
|
89
115
|
}
|
|
90
|
-
const KNOWN_KEYS = ["name", "icon", "port", "workflows", "output", "knowledge", "agents", "skills", "switcher", "git"];
|
|
116
|
+
const KNOWN_KEYS = ["name", "icon", "port", "workflows", "output", "knowledge", "agents", "skills", "switcher", "git", "repos"];
|
|
91
117
|
async function loadConfig(configPath) {
|
|
92
118
|
let raw;
|
|
93
119
|
try {
|
|
@@ -123,6 +149,11 @@ async function loadConfig(configPath) {
|
|
|
123
149
|
config[key] = {};
|
|
124
150
|
validateGit(configPath, config[key]);
|
|
125
151
|
}
|
|
152
|
+
else if (key === "repos") {
|
|
153
|
+
if (config[key] === null)
|
|
154
|
+
config[key] = {};
|
|
155
|
+
validateRepos(configPath, config[key]);
|
|
156
|
+
}
|
|
126
157
|
else if (typeof config[key] !== "string") {
|
|
127
158
|
throw new Error(`${path.basename(configPath)}: ${key} must be a string`);
|
|
128
159
|
}
|
|
@@ -182,3 +213,26 @@ function validateGit(configPath, value) {
|
|
|
182
213
|
throw new Error(`${file}: git.autosync must be "off" or "pull"`);
|
|
183
214
|
}
|
|
184
215
|
}
|
|
216
|
+
function validateRepos(configPath, value) {
|
|
217
|
+
const file = path.basename(configPath);
|
|
218
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
219
|
+
throw new Error(`${file}: repos must be a mapping (enabled, root)`);
|
|
220
|
+
}
|
|
221
|
+
const r = value;
|
|
222
|
+
for (const key of Object.keys(r)) {
|
|
223
|
+
if (!["enabled", "root"].includes(key)) {
|
|
224
|
+
throw new Error(`${file}: repos.${key} is unknown (allowed: enabled, root)`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (r.enabled !== undefined && typeof r.enabled !== "boolean") {
|
|
228
|
+
throw new Error(`${file}: repos.enabled must be true or false`);
|
|
229
|
+
}
|
|
230
|
+
if (r.root !== undefined && (typeof r.root !== "string" || !r.root.trim())) {
|
|
231
|
+
throw new Error(`${file}: repos.root must be a non-empty string`);
|
|
232
|
+
}
|
|
233
|
+
// The root is what `repos remove --force` deletes under, so it must never
|
|
234
|
+
// be the project itself or anything outside it.
|
|
235
|
+
if (typeof r.root === "string" && !ignoreEntryFor(path.dirname(configPath), r.root.trim())) {
|
|
236
|
+
throw new Error(`${file}: repos.root must be a directory inside the project (not ".", ".." or an absolute path outside it)`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -6,6 +6,7 @@ import { getRun, listRuns, safeRunDir } from "../runs.js";
|
|
|
6
6
|
import { listSkills, readSkill } from "../skills.js";
|
|
7
7
|
import { listWorkflows } from "../workflows.js";
|
|
8
8
|
import { getAgent, listAgents } from "../agents.js";
|
|
9
|
+
import { listRepos } from "../repos.js";
|
|
9
10
|
import { startAcpBackend } from "./acp.js";
|
|
10
11
|
import { startPiBackend } from "./pi.js";
|
|
11
12
|
import { appendEvent, listChatMetas, newChatId, readEvents, readMeta, safeChatDir, writeMeta, } from "./store.js";
|
|
@@ -144,6 +145,29 @@ async function agentKnowledgeContext(def) {
|
|
|
144
145
|
`Never hand-edit index.md or log.md, and never add \`verified\` yourself — verification is the human's ` +
|
|
145
146
|
`click in the UI.\n\n`);
|
|
146
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* "## Repositories" block: the clones under the repos root, when the
|
|
150
|
+
* feature is on. Empty when it is off, so agents in a workspace without
|
|
151
|
+
* it never hear about it.
|
|
152
|
+
*/
|
|
153
|
+
async function reposContext() {
|
|
154
|
+
const view = await listRepos().catch(() => null);
|
|
155
|
+
if (!view?.enabled || !view.root)
|
|
156
|
+
return "";
|
|
157
|
+
const lines = view.repos
|
|
158
|
+
.map((r) => {
|
|
159
|
+
const state = r.error ? `unreadable: ${r.error}` : [r.dirty ? `${r.dirty} uncommitted` : "clean", r.ahead ? `${r.ahead} ahead` : "", r.behind ? `${r.behind} behind` : ""].filter(Boolean).join(", ");
|
|
160
|
+
return `- ${r.slug} — ${r.path}${r.url ? ` (${r.url})` : ""}${r.branch ? `, branch ${r.branch}` : ""}${r.head ? ` @ ${r.head}` : ""}, ${state}`;
|
|
161
|
+
})
|
|
162
|
+
.join("\n");
|
|
163
|
+
return (`## Repositories\nGit repositories this workspace works on live under ${view.root} (one clone per folder). ` +
|
|
164
|
+
`When the user names one of them, work inside its folder: read its README and structure first, keep commits on a ` +
|
|
165
|
+
`branch unless told otherwise, and never push or force-push without asking.\n${lines || "(none cloned yet)"}\n\n` +
|
|
166
|
+
`Clone a new one with \`npx kraftwerk repos add <url> [--name <folder>] [--branch <b>] [--depth <n>]\` so it lands in ` +
|
|
167
|
+
`that root (\`--depth 1\` for a large repository; a plain \`git clone\` into that folder works too); ` +
|
|
168
|
+
`\`npx kraftwerk repos\` lists them, \`update <name>\` fetches and fast-forwards a clean clone, \`remove <name>\` ` +
|
|
169
|
+
`deletes one. A repository the user mentions that is not listed is not cloned yet — offer to add it.`);
|
|
170
|
+
}
|
|
147
171
|
/** Base context per scope; scopeContext() appends the shared skills block. */
|
|
148
172
|
async function baseScopeContext(scope, agent) {
|
|
149
173
|
if (scope.kind === "agent") {
|
|
@@ -278,11 +302,14 @@ const RENDERING_BLOCK = `## Chat rendering\n` +
|
|
|
278
302
|
`Prefer these relative /api/... URLs over file paths when showing results: they render directly ` +
|
|
279
303
|
`in this chat and keep working wherever the inspector is reachable.`;
|
|
280
304
|
async function scopeContext(scope, agent) {
|
|
281
|
-
|
|
305
|
+
// The repositories block reads every clone from git, so it runs alongside
|
|
306
|
+
// the rest instead of adding its spawns to the first prompt's latency.
|
|
307
|
+
const [base, repos, skills] = await Promise.all([
|
|
282
308
|
baseScopeContext(scope, agent),
|
|
309
|
+
scope.kind === "agent" || scope.kind === "kraftwerk" ? reposContext() : Promise.resolve(""),
|
|
283
310
|
availableSkills(scope),
|
|
284
311
|
]);
|
|
285
|
-
return [base, RENDERING_BLOCK, skillsBlock(skills)].filter(Boolean).join("\n\n");
|
|
312
|
+
return [base, repos, RENDERING_BLOCK, skillsBlock(skills)].filter(Boolean).join("\n\n");
|
|
286
313
|
}
|
|
287
314
|
/* ---------- backend lifecycle ---------- */
|
|
288
315
|
/** Agent sessions carry the agent's model/effort; resolved live so edits apply to new backends. */
|
package/dist/inspector/git.d.ts
CHANGED
|
@@ -40,6 +40,20 @@ export interface GitDiff {
|
|
|
40
40
|
/** The diff was cut at MAX_DIFF; the rest is available in a terminal. */
|
|
41
41
|
truncated?: boolean;
|
|
42
42
|
}
|
|
43
|
+
export interface GitResult {
|
|
44
|
+
ok: boolean;
|
|
45
|
+
/** Exit status, or null when git never ran (spawn error) or was killed. */
|
|
46
|
+
code: number | null;
|
|
47
|
+
stdout: string;
|
|
48
|
+
stderr: string;
|
|
49
|
+
}
|
|
50
|
+
export declare const NET_TIMEOUT = 60000;
|
|
51
|
+
export declare function git(args: string[], cwd: string, timeoutMs?: number, env?: NodeJS.ProcessEnv): Promise<GitResult>;
|
|
52
|
+
export declare function sshCommandFor(repoRoot: string): Promise<string>;
|
|
53
|
+
/** A git call that may touch the network: longer timeout, non-interactive ssh. */
|
|
54
|
+
export declare function gitNet(args: string[], repoRoot: string): Promise<GitResult>;
|
|
55
|
+
/** Repo root for a directory, or null when it is not inside a git repo. */
|
|
56
|
+
export declare function repoRootFor(dir: string): Promise<string | null>;
|
|
43
57
|
/**
|
|
44
58
|
* Everything the git screen renders. Each call shells out to git a handful
|
|
45
59
|
* of times, and the nav badge polls it from every open tab, so results are
|
package/dist/inspector/git.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { resolveProject } from "../config.js";
|
|
4
|
+
import { reposRootFor, resolveProject } from "../config.js";
|
|
5
5
|
import { ENV_FILE } from "../runner/docker.js";
|
|
6
6
|
import { getOutputDir, getProjectRoot } from "./context.js";
|
|
7
7
|
/**
|
|
@@ -44,7 +44,7 @@ const DENY = [
|
|
|
44
44
|
const DEFAULTS = { remote: "origin", interval: 300, autosync: "pull" };
|
|
45
45
|
/** Local operations are quick; anything touching the network gets longer. */
|
|
46
46
|
const LOCAL_TIMEOUT = 15_000;
|
|
47
|
-
const NET_TIMEOUT = 60_000;
|
|
47
|
+
export const NET_TIMEOUT = 60_000;
|
|
48
48
|
const MAX_OUTPUT = 32 * 1024 * 1024;
|
|
49
49
|
/** Longest diff the screen gets; a dumped database under knowledge/ is not something to read in a browser. */
|
|
50
50
|
const MAX_DIFF = 512 * 1024;
|
|
@@ -70,7 +70,7 @@ const gitEnv = (extra) => ({
|
|
|
70
70
|
GIT_OPTIONAL_LOCKS: "0",
|
|
71
71
|
...extra,
|
|
72
72
|
});
|
|
73
|
-
function git(args, cwd, timeoutMs = LOCAL_TIMEOUT, env) {
|
|
73
|
+
export function git(args, cwd, timeoutMs = LOCAL_TIMEOUT, env) {
|
|
74
74
|
return new Promise((resolve) => {
|
|
75
75
|
let child;
|
|
76
76
|
try {
|
|
@@ -115,7 +115,7 @@ function git(args, cwd, timeoutMs = LOCAL_TIMEOUT, env) {
|
|
|
115
115
|
* is left alone: whoever set it owns it.
|
|
116
116
|
*/
|
|
117
117
|
const sshCache = new Map();
|
|
118
|
-
async function sshCommandFor(repoRoot) {
|
|
118
|
+
export async function sshCommandFor(repoRoot) {
|
|
119
119
|
if (process.env.GIT_SSH_COMMAND)
|
|
120
120
|
return process.env.GIT_SSH_COMMAND;
|
|
121
121
|
const hit = sshCache.get(repoRoot);
|
|
@@ -127,7 +127,7 @@ async function sshCommandFor(repoRoot) {
|
|
|
127
127
|
return cmd;
|
|
128
128
|
}
|
|
129
129
|
/** A git call that may touch the network: longer timeout, non-interactive ssh. */
|
|
130
|
-
async function gitNet(args, repoRoot) {
|
|
130
|
+
export async function gitNet(args, repoRoot) {
|
|
131
131
|
return git(args, repoRoot, NET_TIMEOUT, { GIT_SSH_COMMAND: await sshCommandFor(repoRoot) });
|
|
132
132
|
}
|
|
133
133
|
/**
|
|
@@ -136,7 +136,7 @@ async function gitNet(args, repoRoot) {
|
|
|
136
136
|
*/
|
|
137
137
|
const literal = (p) => `:(literal)${p}`;
|
|
138
138
|
/** Repo root for a directory, or null when it is not inside a git repo. */
|
|
139
|
-
async function repoRootFor(dir) {
|
|
139
|
+
export async function repoRootFor(dir) {
|
|
140
140
|
const r = await git(["rev-parse", "--show-toplevel"], dir);
|
|
141
141
|
return r.ok ? path.resolve(r.stdout.trim()) : null;
|
|
142
142
|
}
|
|
@@ -220,10 +220,12 @@ function scopeFor({ project, repoRoot }) {
|
|
|
220
220
|
project.configPath,
|
|
221
221
|
];
|
|
222
222
|
const mapped = roots.filter((r) => !!r).map(rel).filter((r) => !!r);
|
|
223
|
-
|
|
223
|
+
// Clones under the repos root are other people's history, never this
|
|
224
|
+
// repo's: excluded like run artifacts, whatever the configured roots say.
|
|
225
|
+
const excluded = [getOutputDir(), reposRootFor(project)].filter((r) => !!r);
|
|
224
226
|
return {
|
|
225
227
|
include: [...new Set(mapped.filter((r) => r !== "."))],
|
|
226
|
-
exclude:
|
|
228
|
+
exclude: excluded.map(rel).filter((r) => !!r && r !== "."),
|
|
227
229
|
wholeRepo: mapped.includes("."),
|
|
228
230
|
};
|
|
229
231
|
}
|
|
@@ -242,7 +244,7 @@ function reasonFor(file, code, scope) {
|
|
|
242
244
|
if (DENY.some((re) => re.test(file)))
|
|
243
245
|
return "never synced (secret or key)";
|
|
244
246
|
if (scope.exclude.some((e) => within(file, e)))
|
|
245
|
-
return "run artifacts, not synced";
|
|
247
|
+
return "run artifacts or repositories, not synced";
|
|
246
248
|
if (!scope.include.some((r) => within(file, r)))
|
|
247
249
|
return "outside the workspace paths";
|
|
248
250
|
if (/[RC]/.test(code))
|