@netnodeag/kraftwerk 0.80.0 → 0.81.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 +5 -2
- package/dist/cli/init.js +3 -2
- package/dist/cli/kraftwerk.js +3 -0
- package/dist/cli/temp.d.ts +2 -0
- package/dist/cli/temp.js +62 -0
- package/dist/client/routes.d.ts +2 -0
- package/dist/client/routes.js +2 -0
- package/dist/core/chat/acp.js +2 -1
- package/dist/core/chat/backend.d.ts +2 -0
- package/dist/core/chat/context.js +11 -6
- package/dist/core/chat/pi.js +2 -1
- package/dist/core/chat/seats.js +3 -0
- package/dist/core/chat/sessions.js +3 -0
- package/dist/core/files.js +1 -0
- package/dist/core/projects.js +1 -1
- package/dist/core/temp.d.ts +73 -0
- package/dist/core/temp.js +154 -0
- package/dist/harness.d.ts +2 -0
- package/dist/harnesses/acp.js +1 -1
- package/dist/harnesses/claude.js +1 -0
- package/dist/harnesses/codex.js +1 -0
- package/dist/harnesses/pi.js +1 -0
- package/dist/run.d.ts +7 -0
- package/dist/run.js +13 -1
- package/dist/server/api/files.d.ts +9 -1
- package/dist/server/api/files.js +13 -0
- package/dist/server/api/index.d.ts +9 -1
- package/dist/yaml.js +1 -0
- package/package.json +1 -1
- package/web/dist/assets/{dist-1PajaYPg.js → dist-8IOFzZx-.js} +1 -1
- package/web/dist/assets/{dist-K_3Y5Nbl.js → dist-AhnSw4hy.js} +1 -1
- package/web/dist/assets/{dist-B4dqwo7_.js → dist-BK0VhAPQ.js} +1 -1
- package/web/dist/assets/{dist-CK2u1OXb.js → dist-Ba6WSVOB.js} +1 -1
- package/web/dist/assets/{dist-NX4_yvho.js → dist-BiU4F98O.js} +1 -1
- package/web/dist/assets/{dist-DlZPeneT.js → dist-Dx3aajX1.js} +1 -1
- package/web/dist/assets/{dist-D-f0nszo.js → dist-Dy1SvhKC.js} +1 -1
- package/web/dist/assets/{dist-u_oAKIhZ.js → dist-ENZYo2jZ.js} +1 -1
- package/web/dist/assets/{dist-DgKgeYFJ.js → dist-FfKYtQxT.js} +1 -1
- package/web/dist/assets/{dist-DKduisGW.js → dist-cewmjxFi.js} +1 -1
- package/web/dist/assets/dist-lgUK2Fjt.js +1 -0
- package/web/dist/assets/{editor-CE9TlKkJ.js → editor-CaRa1NMC.js} +3 -3
- package/web/dist/assets/{index-IAuwD22j.js → index-Dpzux3OC.js} +3 -3
- package/web/dist/index.html +1 -1
- package/web/dist/assets/dist-Bp1iMuQ7.js +0 -1
package/README.md
CHANGED
|
@@ -300,7 +300,9 @@ Optional, at the project root, and also the root marker for the walk-up. All
|
|
|
300
300
|
fields are optional. `workflows:` sets the workflows root, `output:` the
|
|
301
301
|
run-artifact directory (default `output/`), `knowledge:` the OKF bundle root
|
|
302
302
|
(default `knowledge/`), and `agents:` the agent-definition root (default
|
|
303
|
-
`agents/`). `repos:` turns on the [repositories](#repositories) folder
|
|
303
|
+
`agents/`). `repos:` turns on the [repositories](#repositories) folder,
|
|
304
|
+
`projects:` the [projects](#projects), and `contacts:`, `places:` and
|
|
305
|
+
`events:` each one of the [contacts, places and events](#contacts-places-and-events).
|
|
304
306
|
`cloud: { enabled: false }` leaves the workspace out of this machine's
|
|
305
307
|
record in the [kraftwerk cloud](#this-machine-in-the-kraftwerk-cloud).
|
|
306
308
|
|
|
@@ -342,7 +344,8 @@ projects (all but archived ones) and the channels of every workspace on this
|
|
|
342
344
|
machine, grouped by workspace and running or not (a stopped one is started
|
|
343
345
|
on the way), read from the workspace registry under `~/.kraftwerk/workspaces`,
|
|
344
346
|
which every inspector keeps current with its roster, project and channel
|
|
345
|
-
lists
|
|
347
|
+
lists — and this workspace's contacts, places and events, by title, role,
|
|
348
|
+
organisation, address or date.
|
|
346
349
|
|
|
347
350
|
### Triggering from CI, cron, or webhooks
|
|
348
351
|
|
package/dist/cli/init.js
CHANGED
|
@@ -139,11 +139,12 @@ export async function runInit(cwd) {
|
|
|
139
139
|
created.push(bundleRel);
|
|
140
140
|
}
|
|
141
141
|
// .gitignore: the output dir, the repos root (clones must never become
|
|
142
|
-
// gitlinks of the workspace repo)
|
|
142
|
+
// gitlinks of the workspace repo), the temp root (session scratch, which
|
|
143
|
+
// also carries a .gitignore of its own) and .env (secrets). A missing file gets
|
|
143
144
|
// all in one write; an existing one only the entries it lacks.
|
|
144
145
|
const gitignorePath = path.join(cwd, ".gitignore");
|
|
145
146
|
const gitignore = (await readFile(gitignorePath, "utf8").catch(() => null)) ?? null;
|
|
146
|
-
const entries = [`${DATA_DIR}/output/`, `${DATA_DIR}/repos/`, DOTENV_FILE];
|
|
147
|
+
const entries = [`${DATA_DIR}/output/`, `${DATA_DIR}/repos/`, `${DATA_DIR}/temp/`, DOTENV_FILE];
|
|
147
148
|
if (gitignore === null) {
|
|
148
149
|
await writeFile(gitignorePath, entries.map((e) => `${e}\n`).join(""));
|
|
149
150
|
created.push(".gitignore");
|
package/dist/cli/kraftwerk.js
CHANGED
|
@@ -18,6 +18,7 @@ import { registerWorkspaceCommands } from "./workspaces.js";
|
|
|
18
18
|
import { registerRepoCommands } from "./repos.js";
|
|
19
19
|
import { registerVibeableCommands } from "./vibeables.js";
|
|
20
20
|
import { registerTrashCommands } from "./trash.js";
|
|
21
|
+
import { registerTempCommands } from "./temp.js";
|
|
21
22
|
import { registerDevicesCommands } from "./devices.js";
|
|
22
23
|
import { registerDaemonCommand } from "./daemon.js";
|
|
23
24
|
import { registerRemoteCommand } from "./remote.js";
|
|
@@ -49,6 +50,7 @@ import { runUi } from "./ui.js";
|
|
|
49
50
|
* kraftwerk repos ... repositories the agents work on (list/add/update/remove)
|
|
50
51
|
* kraftwerk journal <agent> [entry] an agent's memory across sessions: print it, add a line
|
|
51
52
|
* kraftwerk trash ... what was deleted: list, restore, purge, empty
|
|
53
|
+
* kraftwerk temp [clean] the sessions' scratch folders (TMPDIR of every agent): list, clean
|
|
52
54
|
* kraftwerk devices ... paired devices: list, pair one, revoke one
|
|
53
55
|
* kraftwerk daemon the machine's kraftwerk: every open workspace at <slug>.localhost
|
|
54
56
|
* kraftwerk cloud [on|off|status] this machine in the kraftwerk cloud (the daemon registers it)
|
|
@@ -309,6 +311,7 @@ registerRefCommand(program);
|
|
|
309
311
|
registerRepoCommands(program);
|
|
310
312
|
registerVibeableCommands(program);
|
|
311
313
|
registerTrashCommands(program);
|
|
314
|
+
registerTempCommands(program);
|
|
312
315
|
registerDevicesCommands(program);
|
|
313
316
|
registerDaemonCommand(program);
|
|
314
317
|
registerRemoteCommand(program);
|
package/dist/cli/temp.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import Table from "cli-table3";
|
|
3
|
+
import { cleanTemp, listTemp, SWEEP_AFTER_MS } from "../core/temp.js";
|
|
4
|
+
import { fmtAgo } from "./workspaces.js";
|
|
5
|
+
import { initContext as prepare } from "./routines.js";
|
|
6
|
+
/**
|
|
7
|
+
* `kraftwerk temp` — the sessions' scratch folders under kraftwerk-data/temp
|
|
8
|
+
* (one per chat session; TMPDIR of the agent process). Nothing in there is
|
|
9
|
+
* a deliverable: a clean deletes for good, no trash.
|
|
10
|
+
*
|
|
11
|
+
* kraftwerk temp list: session, files, size, last change
|
|
12
|
+
* kraftwerk temp clean remove folders idle for a week
|
|
13
|
+
* kraftwerk temp clean --older-than <d> … idle for <d> days
|
|
14
|
+
* kraftwerk temp clean --all remove every folder
|
|
15
|
+
*/
|
|
16
|
+
const fmtBytes = (n) => n < 1024 ? `${n} B` : n < 1024 * 1024 ? `${(n / 1024).toFixed(1)} KB` : n < 1024 ** 3 ? `${(n / 1024 ** 2).toFixed(1)} MB` : `${(n / 1024 ** 3).toFixed(2)} GB`;
|
|
17
|
+
export function registerTempCommands(program) {
|
|
18
|
+
const temp = program.command("temp").description("The sessions' scratch folders: list, clean");
|
|
19
|
+
temp
|
|
20
|
+
.command("list", { isDefault: true })
|
|
21
|
+
.description("List the scratch folders, newest first")
|
|
22
|
+
.option("--json", "Machine-readable output")
|
|
23
|
+
.action(async (opts) => {
|
|
24
|
+
await prepare();
|
|
25
|
+
const view = await listTemp();
|
|
26
|
+
if (opts.json) {
|
|
27
|
+
console.log(JSON.stringify(view, null, 2));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (view.entries.length === 0) {
|
|
31
|
+
console.log(chalk.dim("No scratch folders."));
|
|
32
|
+
console.log(chalk.dim(view.root));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const table = new Table({ head: ["session", "files", "size", "last change"].map((h) => chalk.bold(h)) });
|
|
36
|
+
for (const e of view.entries)
|
|
37
|
+
table.push([chalk.cyan(e.id), String(e.files), fmtBytes(e.bytes), chalk.dim(fmtAgo(e.modifiedAt))]);
|
|
38
|
+
console.log(table.toString());
|
|
39
|
+
console.log(chalk.dim(`${view.root} — ${fmtBytes(view.bytes)} in ${view.entries.length} folder${view.entries.length === 1 ? "" : "s"}`));
|
|
40
|
+
});
|
|
41
|
+
temp
|
|
42
|
+
.command("clean")
|
|
43
|
+
.description(`Remove scratch folders idle for ${SWEEP_AFTER_MS / 86_400_000} days (for good — nothing there is a deliverable)`)
|
|
44
|
+
.option("--older-than <days>", "Remove folders idle for this many days instead")
|
|
45
|
+
.option("--all", "Remove every folder, whatever its age")
|
|
46
|
+
.option("--json", "Machine-readable output")
|
|
47
|
+
.action(async (opts) => {
|
|
48
|
+
await prepare();
|
|
49
|
+
const days = opts.olderThan === undefined ? undefined : Number(opts.olderThan);
|
|
50
|
+
if (days !== undefined && (!Number.isFinite(days) || days < 0)) {
|
|
51
|
+
console.error(chalk.red(`--older-than wants a number of days, not "${opts.olderThan}"`));
|
|
52
|
+
process.exit(2);
|
|
53
|
+
}
|
|
54
|
+
const gone = await cleanTemp({ all: opts.all, ...(days === undefined ? {} : { olderThanMs: days * 86_400_000 }) });
|
|
55
|
+
const bytes = gone.reduce((n, e) => n + e.bytes, 0);
|
|
56
|
+
if (opts.json) {
|
|
57
|
+
console.log(JSON.stringify({ removed: gone.map((e) => e.id), bytes }, null, 2));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
console.log(`${chalk.green("✔")} removed ${gone.length} folder${gone.length === 1 ? "" : "s"} (${fmtBytes(bytes)})`);
|
|
61
|
+
});
|
|
62
|
+
}
|
package/dist/client/routes.d.ts
CHANGED
|
@@ -136,6 +136,8 @@ export declare const ROUTES: {
|
|
|
136
136
|
readonly "settings.save": readonly ["PUT", "/api/settings"];
|
|
137
137
|
readonly "skills.get": readonly ["GET", "/api/skills/:name"];
|
|
138
138
|
readonly "skills.list": readonly ["GET", "/api/skills"];
|
|
139
|
+
readonly "temp.clean": readonly ["DELETE", "/api/temp"];
|
|
140
|
+
readonly "temp.list": readonly ["GET", "/api/temp"];
|
|
139
141
|
readonly "trash.empty": readonly ["DELETE", "/api/trash"];
|
|
140
142
|
readonly "trash.list": readonly ["GET", "/api/trash"];
|
|
141
143
|
readonly "trash.purge": readonly ["POST", "/api/trash/purge"];
|
package/dist/client/routes.js
CHANGED
|
@@ -137,6 +137,8 @@ export const ROUTES = {
|
|
|
137
137
|
"settings.save": ["PUT", "/api/settings"],
|
|
138
138
|
"skills.get": ["GET", "/api/skills/:name"],
|
|
139
139
|
"skills.list": ["GET", "/api/skills"],
|
|
140
|
+
"temp.clean": ["DELETE", "/api/temp"],
|
|
141
|
+
"temp.list": ["GET", "/api/temp"],
|
|
140
142
|
"trash.empty": ["DELETE", "/api/trash"],
|
|
141
143
|
"trash.list": ["GET", "/api/trash"],
|
|
142
144
|
"trash.purge": ["POST", "/api/trash/purge"],
|
package/dist/core/chat/acp.js
CHANGED
|
@@ -3,6 +3,7 @@ import { promises as fs } from "node:fs";
|
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { RequestError, } from "@agentclientprotocol/sdk";
|
|
5
5
|
import { adapterEnv, airMeta, connectAcp, EXT_METHODS, EXT_NOTIFICATIONS, fileChangeReportMeta, openSession, withAdapter, } from "../../acp.js";
|
|
6
|
+
import { tempEnv } from "../temp.js";
|
|
6
7
|
import { unattendedMode } from "./permissions.js";
|
|
7
8
|
/**
|
|
8
9
|
* ACP-backed chat: spawn an adapter (claude-agent-acp / codex-acp) as a
|
|
@@ -389,7 +390,7 @@ export async function startAcpBackend(agent, cwd, hooks, tuning = {}) {
|
|
|
389
390
|
};
|
|
390
391
|
const { child, conn } = await connectAcp(agent, {
|
|
391
392
|
cwd,
|
|
392
|
-
env: adapterEnv(agent, tuning),
|
|
393
|
+
env: { ...adapterEnv(agent, tuning), ...(tuning.temp ? tempEnv(tuning.temp) : {}) },
|
|
393
394
|
client,
|
|
394
395
|
clientName: "kraftwerk-inspector",
|
|
395
396
|
elicitation: true,
|
|
@@ -37,6 +37,8 @@ export interface BackendTuning {
|
|
|
37
37
|
skillDirs?: string[];
|
|
38
38
|
/** claude: extra directories the session may work in (e.g. project root for run chats). */
|
|
39
39
|
addDirs?: string[];
|
|
40
|
+
/** The session's scratch folder (core/temp.ts): handed to the process as TMPDIR and KRAFTWERK_TEMP. */
|
|
41
|
+
temp?: string;
|
|
40
42
|
/**
|
|
41
43
|
* Nobody is watching this session live (routine runs). The backend selects
|
|
42
44
|
* the harness's own hands-off permission mode — the harness keeps deciding
|
|
@@ -12,6 +12,7 @@ import { unattendedTimeoutLabel } from "./permissions.js";
|
|
|
12
12
|
import { getChannel } from "../channels.js";
|
|
13
13
|
import { JOURNAL_FILE, journalExcerpt, readJournal } from "../journal.js";
|
|
14
14
|
import { filesSummary } from "../files.js";
|
|
15
|
+
import { sessionTemp, tempContext } from "../temp.js";
|
|
15
16
|
/**
|
|
16
17
|
* What an agent is told about where it works: the skills it may use and
|
|
17
18
|
* the scope's context block (the workspace, a run, a project, a channel,
|
|
@@ -85,8 +86,8 @@ async function agentKnowledgeContext(def) {
|
|
|
85
86
|
`part of your job. Consult them before answering questions in their domain, and keep them current ` +
|
|
86
87
|
`when you learn something durable:\n${lines}\n\n` +
|
|
87
88
|
`Read with \`npx kraftwerk knowledge list <bundle>\`, \`get <bundle>/<path>\`, \`search <text>\`. ` +
|
|
88
|
-
`Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file
|
|
89
|
-
`(write the markdown
|
|
89
|
+
`Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file $KRAFTWERK_TEMP/<name>.md --actor ${def.slug}/${def.harness}\` ` +
|
|
90
|
+
`(write the markdown into your temp folder first; the CLI stamps provenance and maintains index.md/log.md). ` +
|
|
90
91
|
`Never hand-edit index.md or log.md, and never add \`verified\` yourself — verification is the human's ` +
|
|
91
92
|
`click in the UI.\n\n`);
|
|
92
93
|
}
|
|
@@ -132,7 +133,8 @@ export async function agentFilesContext(def) {
|
|
|
132
133
|
return (`## Files\nThe workspace's shared files (any format: brand assets, templates, contracts, …) are in ${ws.root}/ (${ws.count} file${ws.count === 1 ? "" : "s"}). ` +
|
|
133
134
|
(linked ? `The folders that are part of your job:\n${linked}\n` : "") +
|
|
134
135
|
`Work you do for a project belongs in that project's files folder, not here. What you produce in a direct session goes to ${ws.root}/inbox/. ` +
|
|
135
|
-
`
|
|
136
|
+
`Only finished work and the user's own material belong in these folders — scratch, intermediate and downloaded files go to your ` +
|
|
137
|
+
`temp folder (below), never here. Delete nothing there unless asked.\n\n`);
|
|
136
138
|
}
|
|
137
139
|
/**
|
|
138
140
|
* "## Your vibeables" block: the apps an agent builds and maintains, linked
|
|
@@ -310,7 +312,7 @@ async function baseScopeContext(scope, agent) {
|
|
|
310
312
|
`- Favor structural markdown (headings, tables, lists) over prose.\n\n` +
|
|
311
313
|
`ALWAYS write through the kraftwerk CLI so provenance is stamped and the bundle log/index stay maintained:\n` +
|
|
312
314
|
`- \`npx kraftwerk knowledge init <bundle>\` — new bundle\n` +
|
|
313
|
-
`- \`npx kraftwerk knowledge put <bundle>/<path> --file
|
|
315
|
+
`- \`npx kraftwerk knowledge put <bundle>/<path> --file $KRAFTWERK_TEMP/<name>.md --actor kraftwerk-chat/${agent}\` — create/update a concept ` +
|
|
314
316
|
`(write the markdown to a temp file first; the CLI stamps generated.by/at, appends log.md, regenerates index.md)\n` +
|
|
315
317
|
`- \`npx kraftwerk knowledge list [bundle]\`, \`get <bundle>/<path>\`, \`search <text>\`, \`validate\`\n` +
|
|
316
318
|
`Do not hand-edit index.md or log.md (derived/maintained), and do not add \`verified\` yourself — ` +
|
|
@@ -349,7 +351,7 @@ async function baseScopeContext(scope, agent) {
|
|
|
349
351
|
`## Recent runs\n${runLines || "(none)"}\n\n` +
|
|
350
352
|
`## Knowledge\nOKF bundles in this project:\n${bundleLines || "(none)"}\n` +
|
|
351
353
|
`Read with \`npx kraftwerk knowledge list [bundle]\`, \`get <bundle>/<path>\`, \`search <text>\`. ` +
|
|
352
|
-
`Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file
|
|
354
|
+
`Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file $KRAFTWERK_TEMP/<name>.md --actor kraftwerk-chat/${agent}\` ` +
|
|
353
355
|
`(stamps provenance, maintains index.md/log.md — never hand-edit those).\n\n` +
|
|
354
356
|
`## Agents\n${agentLines || "(none)"}\n` +
|
|
355
357
|
`These are persistent agents (defined under agents/); the user talks to them on the ` +
|
|
@@ -403,7 +405,10 @@ export async function scopeContext(meta, seat) {
|
|
|
403
405
|
// Who, where and when: the workspace's contacts, places and events, wherever the chat works (a run discusses its own artifacts only).
|
|
404
406
|
scope.kind === "run" ? Promise.resolve("") : entitiesContext().catch(() => ""),
|
|
405
407
|
]);
|
|
406
|
-
|
|
408
|
+
// Every session has a scratch folder of its own (core/temp.ts), named by its absolute path: a run chat or a
|
|
409
|
+
// vibeable session works in another folder than the workspace root. The block comes after the files blocks it refers to.
|
|
410
|
+
const temp = await sessionTemp(meta.id).catch(() => null);
|
|
411
|
+
return [base, channel, project, repos, entities, vibe, temp ? tempContext(temp.abs) : "", RENDERING_BLOCK, skillsBlock(skills)].filter(Boolean).join("\n\n");
|
|
407
412
|
}
|
|
408
413
|
/** The channel block a member agent gets once per process: who is here, how turns work, how to hand over. */
|
|
409
414
|
async function channelContext(slug, self) {
|
package/dist/core/chat/pi.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { workspaceEnv } from "../env.js";
|
|
4
|
+
import { tempEnv } from "../temp.js";
|
|
4
5
|
const clip = (raw) => {
|
|
5
6
|
const oneLine = String(raw ?? "").replace(/\s+/g, " ").trim();
|
|
6
7
|
return oneLine.length > 120 ? `${oneLine.slice(0, 120)}…` : oneLine;
|
|
@@ -24,7 +25,7 @@ export function startPiBackend(cwd, hooks, tuning = {}) {
|
|
|
24
25
|
const proc = spawn("pi", ["-p", "--mode", "json", "--session-id", sessionId, ...tuningArgs], {
|
|
25
26
|
cwd,
|
|
26
27
|
stdio: ["pipe", "pipe", "pipe"],
|
|
27
|
-
env: workspaceEnv(),
|
|
28
|
+
env: { ...workspaceEnv(), ...(tuning.temp ? tempEnv(tuning.temp) : {}) },
|
|
28
29
|
});
|
|
29
30
|
child = proc;
|
|
30
31
|
proc.stdin.write(text);
|
package/dist/core/chat/seats.js
CHANGED
|
@@ -9,6 +9,7 @@ import { chatHref, dismissKey, pushNotification } from "../notifications.js";
|
|
|
9
9
|
import { writeMeta } from "./store.js";
|
|
10
10
|
import { availableSkills } from "./context.js";
|
|
11
11
|
import { effectiveCwd } from "./sessions.js";
|
|
12
|
+
import { sessionTemp } from "../temp.js";
|
|
12
13
|
import { emit, isUnattended, ownerLabel, seatAuthor } from "./state.js";
|
|
13
14
|
/**
|
|
14
15
|
* Starting an agent process for a seat: its harness, tuning (model,
|
|
@@ -57,6 +58,8 @@ async function backendTuning(meta, seat) {
|
|
|
57
58
|
if (skills.length > 0)
|
|
58
59
|
tuning.skillDirs = skills.map((s) => s.dir);
|
|
59
60
|
}
|
|
61
|
+
// Every seat gets the chat's scratch folder as its temp dir (core/temp.ts), so nothing temporary lands in files/ or /tmp.
|
|
62
|
+
tuning.temp = (await sessionTemp(meta.id)).abs;
|
|
60
63
|
return tuning;
|
|
61
64
|
}
|
|
62
65
|
export async function ensureBackend(state, seat) {
|
|
@@ -7,6 +7,7 @@ import { deleteAgentSession, listAgentSessions } from "./acp.js";
|
|
|
7
7
|
import { chatHref, pushNotification } from "../notifications.js";
|
|
8
8
|
import { readIdentity } from "../identity.js";
|
|
9
9
|
import { moveToTrash } from "../trash.js";
|
|
10
|
+
import { removeSessionTemp } from "../temp.js";
|
|
10
11
|
import { attachmentPath, copyChatFiles, listChatMetas, newChatId, readMetaAt, safeChatDir, writeMeta } from "./store.js";
|
|
11
12
|
import { postChannelMessage } from "./channel-turns.js";
|
|
12
13
|
import { expandSkillInvocation, scopeContext } from "./context.js";
|
|
@@ -526,6 +527,8 @@ export async function deleteChat(id) {
|
|
|
526
527
|
dropBackend(state);
|
|
527
528
|
chatStates().delete(id);
|
|
528
529
|
await moveToTrash("chats", id, safeChatDir(id));
|
|
530
|
+
// Scratch, not a deliverable: gone for good with the chat.
|
|
531
|
+
await removeSessionTemp(id).catch(() => { });
|
|
529
532
|
return {};
|
|
530
533
|
}
|
|
531
534
|
/** The agents' own transcripts of a chat folder (one in the trash), deleted for good. Best effort. */
|
package/dist/core/files.js
CHANGED
|
@@ -312,5 +312,6 @@ export async function filesContext(scope, heading, intro) {
|
|
|
312
312
|
return (`## ${heading}\n${intro} ${s.root}/ — ${s.count} file${s.count === 1 ? "" : "s"}` +
|
|
313
313
|
(recent ? `, most recently changed:\n${recent}\n` : " (empty so far)\n") +
|
|
314
314
|
`Read and write them there like any folder; put what you produce for the user in ${s.root}/deliverables/. ` +
|
|
315
|
+
`Only finished work and the material the user gave you belong here — scratch, intermediate and downloaded files go to your temp folder (see Temporary files), never into this folder. ` +
|
|
315
316
|
`Files over 25 MB stay out of git automatically. Delete nothing there unless asked.`);
|
|
316
317
|
}
|
package/dist/core/projects.js
CHANGED
|
@@ -441,7 +441,7 @@ export async function projectContext(slug, actor, opts = {}) {
|
|
|
441
441
|
? `## Knowledge\nOKF bundles linked to this project (markdown + YAML frontmatter under the knowledge root):\n` +
|
|
442
442
|
linkLines("knowledge", (l) => `- ${l.slug}${l.label ? ` (${l.label})` : ""}`) +
|
|
443
443
|
`\nRead with \`npx kraftwerk knowledge list <bundle>\`, \`get <bundle>/<path>\`, \`search <text>\`. ` +
|
|
444
|
-
`Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file
|
|
444
|
+
`Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file $KRAFTWERK_TEMP/<name>.md --actor ${actor}\` ` +
|
|
445
445
|
`(stamps provenance, maintains index.md/log.md — never hand-edit those).`
|
|
446
446
|
: "",
|
|
447
447
|
p.repos.length > 0
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The temp folder: where a session's scratch work goes — intermediate
|
|
3
|
+
* results, downloads, build output, a markdown written for a CLI to read —
|
|
4
|
+
* instead of the system's temp dir or, worse, the workspace's files.
|
|
5
|
+
*
|
|
6
|
+
* kraftwerk-data/temp/<session-id>/ one folder per chat session
|
|
7
|
+
* <run dir>/temp/ a workflow run's (inside its mount)
|
|
8
|
+
*
|
|
9
|
+
* Three things make it stick, so it does not depend on an agent reading a
|
|
10
|
+
* sentence: the folder is handed to every spawned agent as TMPDIR (and
|
|
11
|
+
* TMP, TEMP, KRAFTWERK_TEMP), so mktemp, Python's tempfile, Node's
|
|
12
|
+
* os.tmpdir() and the agent's own scratchpad land there by themselves; it
|
|
13
|
+
* sits inside the workspace, so writes there need no approval from the
|
|
14
|
+
* harness; and the session prompt names it next to the files folders,
|
|
15
|
+
* which say in turn that nothing temporary belongs in them.
|
|
16
|
+
*
|
|
17
|
+
* The temp root is local: a .gitignore of its own keeps everything in it
|
|
18
|
+
* out of the workspace git (and `kraftwerk init` lists it too). Nothing in
|
|
19
|
+
* it is a deliverable, so nothing goes to the trash: a chat's folder is
|
|
20
|
+
* removed with the chat, and folders nobody touched for SWEEP_AFTER_MS are
|
|
21
|
+
* swept when a session starts (at most once an hour) or on demand
|
|
22
|
+
* (`kraftwerk temp clean`, DELETE /api/temp).
|
|
23
|
+
*/
|
|
24
|
+
export declare const TEMP_DIR = "kraftwerk-data/temp";
|
|
25
|
+
/** A folder idle this long is swept. */
|
|
26
|
+
export declare const SWEEP_AFTER_MS: number;
|
|
27
|
+
export interface TempEntry {
|
|
28
|
+
/** The folder name: the session's id. */
|
|
29
|
+
id: string;
|
|
30
|
+
/** Relative to the workspace root, forward slashes. */
|
|
31
|
+
path: string;
|
|
32
|
+
files: number;
|
|
33
|
+
bytes: number;
|
|
34
|
+
/** The newest change inside (the folder's own when empty). */
|
|
35
|
+
modifiedAt: string;
|
|
36
|
+
}
|
|
37
|
+
export interface TempView {
|
|
38
|
+
root: string;
|
|
39
|
+
entries: TempEntry[];
|
|
40
|
+
bytes: number;
|
|
41
|
+
}
|
|
42
|
+
/** Absolute temp root of the current workspace. */
|
|
43
|
+
export declare function tempRoot(): Promise<string>;
|
|
44
|
+
/** The environment that points a process at `dir` for everything temporary. */
|
|
45
|
+
export declare function tempEnv(dir: string): Record<string, string>;
|
|
46
|
+
/** Create a folder outside the workspace's temp root (a workflow run's) and return the env for it. */
|
|
47
|
+
export declare function tempEnvFor(dir: string): Promise<Record<string, string>>;
|
|
48
|
+
/**
|
|
49
|
+
* A session's own folder, created: absolute path and the path relative to
|
|
50
|
+
* the workspace root (for the prompt). Starting a session is also when
|
|
51
|
+
* folders nobody uses any more are swept, at most once an hour.
|
|
52
|
+
*/
|
|
53
|
+
export declare function sessionTemp(id: string): Promise<{
|
|
54
|
+
abs: string;
|
|
55
|
+
rel: string;
|
|
56
|
+
env: Record<string, string>;
|
|
57
|
+
}>;
|
|
58
|
+
/** Remove one session's folder for good (nothing there is a deliverable). */
|
|
59
|
+
export declare function removeSessionTemp(id: string): Promise<void>;
|
|
60
|
+
/** Every session folder in the temp root, newest first. */
|
|
61
|
+
export declare function listTemp(): Promise<TempView>;
|
|
62
|
+
/**
|
|
63
|
+
* Remove session folders: all of them, or those idle longer than
|
|
64
|
+
* `olderThanMs` (default SWEEP_AFTER_MS). `keep` names folders that stay
|
|
65
|
+
* whatever their age (the session being started). Returns what went.
|
|
66
|
+
*/
|
|
67
|
+
export declare function cleanTemp(opts?: {
|
|
68
|
+
all?: boolean;
|
|
69
|
+
olderThanMs?: number;
|
|
70
|
+
keep?: string[];
|
|
71
|
+
}): Promise<TempEntry[]>;
|
|
72
|
+
/** The "## Temporary files" block of a session's context. */
|
|
73
|
+
export declare function tempContext(rel: string): string;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveWorkspace } from "../config.js";
|
|
4
|
+
import { workspaceRoot } from "./context.js";
|
|
5
|
+
import { perWorkspace } from "./workspace.js";
|
|
6
|
+
/**
|
|
7
|
+
* The temp folder: where a session's scratch work goes — intermediate
|
|
8
|
+
* results, downloads, build output, a markdown written for a CLI to read —
|
|
9
|
+
* instead of the system's temp dir or, worse, the workspace's files.
|
|
10
|
+
*
|
|
11
|
+
* kraftwerk-data/temp/<session-id>/ one folder per chat session
|
|
12
|
+
* <run dir>/temp/ a workflow run's (inside its mount)
|
|
13
|
+
*
|
|
14
|
+
* Three things make it stick, so it does not depend on an agent reading a
|
|
15
|
+
* sentence: the folder is handed to every spawned agent as TMPDIR (and
|
|
16
|
+
* TMP, TEMP, KRAFTWERK_TEMP), so mktemp, Python's tempfile, Node's
|
|
17
|
+
* os.tmpdir() and the agent's own scratchpad land there by themselves; it
|
|
18
|
+
* sits inside the workspace, so writes there need no approval from the
|
|
19
|
+
* harness; and the session prompt names it next to the files folders,
|
|
20
|
+
* which say in turn that nothing temporary belongs in them.
|
|
21
|
+
*
|
|
22
|
+
* The temp root is local: a .gitignore of its own keeps everything in it
|
|
23
|
+
* out of the workspace git (and `kraftwerk init` lists it too). Nothing in
|
|
24
|
+
* it is a deliverable, so nothing goes to the trash: a chat's folder is
|
|
25
|
+
* removed with the chat, and folders nobody touched for SWEEP_AFTER_MS are
|
|
26
|
+
* swept when a session starts (at most once an hour) or on demand
|
|
27
|
+
* (`kraftwerk temp clean`, DELETE /api/temp).
|
|
28
|
+
*/
|
|
29
|
+
export const TEMP_DIR = "kraftwerk-data/temp";
|
|
30
|
+
/** A folder idle this long is swept. */
|
|
31
|
+
export const SWEEP_AFTER_MS = 7 * 24 * 60 * 60 * 1000;
|
|
32
|
+
const SWEEP_EVERY_MS = 60 * 60 * 1000;
|
|
33
|
+
/** A session id as a folder name: what newChatId() produces, nothing that could leave the root. */
|
|
34
|
+
const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,120}$/;
|
|
35
|
+
async function resolvedRoot() {
|
|
36
|
+
const ws = await resolveWorkspace(workspaceRoot()).catch(() => null);
|
|
37
|
+
return ws?.root ?? workspaceRoot();
|
|
38
|
+
}
|
|
39
|
+
/** Absolute temp root of the current workspace. */
|
|
40
|
+
export async function tempRoot() {
|
|
41
|
+
return path.join(await resolvedRoot(), TEMP_DIR);
|
|
42
|
+
}
|
|
43
|
+
/** Create the root and keep it out of the workspace git: a .gitignore of its own ignores everything in it, itself included. */
|
|
44
|
+
async function prepare(root) {
|
|
45
|
+
await fs.mkdir(root, { recursive: true });
|
|
46
|
+
await fs.writeFile(path.join(root, ".gitignore"), "*\n", { flag: "wx" }).catch(() => { });
|
|
47
|
+
}
|
|
48
|
+
/** The environment that points a process at `dir` for everything temporary. */
|
|
49
|
+
export function tempEnv(dir) {
|
|
50
|
+
return { KRAFTWERK_TEMP: dir, TMPDIR: dir, TMP: dir, TEMP: dir };
|
|
51
|
+
}
|
|
52
|
+
/** Create a folder outside the workspace's temp root (a workflow run's) and return the env for it. */
|
|
53
|
+
export async function tempEnvFor(dir) {
|
|
54
|
+
await fs.mkdir(dir, { recursive: true });
|
|
55
|
+
return tempEnv(dir);
|
|
56
|
+
}
|
|
57
|
+
const lastSweep = perWorkspace(() => ({ at: 0 }));
|
|
58
|
+
/**
|
|
59
|
+
* A session's own folder, created: absolute path and the path relative to
|
|
60
|
+
* the workspace root (for the prompt). Starting a session is also when
|
|
61
|
+
* folders nobody uses any more are swept, at most once an hour.
|
|
62
|
+
*/
|
|
63
|
+
export async function sessionTemp(id) {
|
|
64
|
+
if (!ID_RE.test(id))
|
|
65
|
+
throw new Error("invalid session id");
|
|
66
|
+
const root = await tempRoot();
|
|
67
|
+
await prepare(root);
|
|
68
|
+
const abs = path.join(root, id);
|
|
69
|
+
await fs.mkdir(abs, { recursive: true });
|
|
70
|
+
const sweep = lastSweep();
|
|
71
|
+
if (Date.now() - sweep.at > SWEEP_EVERY_MS) {
|
|
72
|
+
sweep.at = Date.now();
|
|
73
|
+
void cleanTemp({ keep: [id] }).catch(() => { });
|
|
74
|
+
}
|
|
75
|
+
return { abs, rel: `${TEMP_DIR}/${id}`, env: tempEnv(abs) };
|
|
76
|
+
}
|
|
77
|
+
/** Remove one session's folder for good (nothing there is a deliverable). */
|
|
78
|
+
export async function removeSessionTemp(id) {
|
|
79
|
+
if (!ID_RE.test(id))
|
|
80
|
+
return;
|
|
81
|
+
await fs.rm(path.join(await tempRoot(), id), { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
/** Files, bytes and the newest mtime below a folder. */
|
|
84
|
+
async function measure(dir) {
|
|
85
|
+
const st = await fs.stat(dir);
|
|
86
|
+
const out = { files: 0, bytes: 0, newest: st.mtimeMs };
|
|
87
|
+
const walk = async (d) => {
|
|
88
|
+
if (out.files > 20000)
|
|
89
|
+
return;
|
|
90
|
+
for (const k of await fs.readdir(d, { withFileTypes: true }).catch(() => [])) {
|
|
91
|
+
const abs = path.join(d, k.name);
|
|
92
|
+
if (k.isDirectory())
|
|
93
|
+
await walk(abs);
|
|
94
|
+
else if (k.isFile()) {
|
|
95
|
+
const s = await fs.stat(abs).catch(() => null);
|
|
96
|
+
if (!s)
|
|
97
|
+
continue;
|
|
98
|
+
out.files++;
|
|
99
|
+
out.bytes += s.size;
|
|
100
|
+
if (s.mtimeMs > out.newest)
|
|
101
|
+
out.newest = s.mtimeMs;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
await walk(dir);
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
/** Every session folder in the temp root, newest first. */
|
|
109
|
+
export async function listTemp() {
|
|
110
|
+
const root = await tempRoot();
|
|
111
|
+
const kids = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
|
|
112
|
+
const entries = [];
|
|
113
|
+
for (const k of kids) {
|
|
114
|
+
if (!k.isDirectory() || k.name.startsWith("."))
|
|
115
|
+
continue;
|
|
116
|
+
const m = await measure(path.join(root, k.name)).catch(() => null);
|
|
117
|
+
if (!m)
|
|
118
|
+
continue;
|
|
119
|
+
entries.push({ id: k.name, path: `${TEMP_DIR}/${k.name}`, files: m.files, bytes: m.bytes, modifiedAt: new Date(m.newest).toISOString() });
|
|
120
|
+
}
|
|
121
|
+
entries.sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt));
|
|
122
|
+
return { root, entries, bytes: entries.reduce((n, e) => n + e.bytes, 0) };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Remove session folders: all of them, or those idle longer than
|
|
126
|
+
* `olderThanMs` (default SWEEP_AFTER_MS). `keep` names folders that stay
|
|
127
|
+
* whatever their age (the session being started). Returns what went.
|
|
128
|
+
*/
|
|
129
|
+
export async function cleanTemp(opts = {}) {
|
|
130
|
+
const view = await listTemp();
|
|
131
|
+
const cutoff = Date.now() - (opts.olderThanMs ?? SWEEP_AFTER_MS);
|
|
132
|
+
const keep = new Set(opts.keep ?? []);
|
|
133
|
+
const gone = [];
|
|
134
|
+
for (const e of view.entries) {
|
|
135
|
+
if (keep.has(e.id))
|
|
136
|
+
continue;
|
|
137
|
+
if (!opts.all && Date.parse(e.modifiedAt) > cutoff)
|
|
138
|
+
continue;
|
|
139
|
+
await fs.rm(path.join(view.root, e.id), { recursive: true, force: true });
|
|
140
|
+
gone.push(e);
|
|
141
|
+
}
|
|
142
|
+
return gone;
|
|
143
|
+
}
|
|
144
|
+
/** The "## Temporary files" block of a session's context. */
|
|
145
|
+
export function tempContext(rel) {
|
|
146
|
+
return (`## Temporary files\n` +
|
|
147
|
+
`Your scratch folder is ${rel}/ — TMPDIR (and KRAFTWERK_TEMP) point there, so \`mktemp\`, Python's tempfile, ` +
|
|
148
|
+
`os.tmpdir() and your own scratchpad land in it by themselves. Put everything that is not a result there: ` +
|
|
149
|
+
`intermediate output, downloads, extracted archives, build output, one-off scripts, a markdown written for a CLI ` +
|
|
150
|
+
`to read. It is private to this session, kept out of git, and removed when the session is deleted or after a week ` +
|
|
151
|
+
`unused — never store anything there you or the user want to keep. The files folders named above hold only what ` +
|
|
152
|
+
`the user keeps: what you produce for them, and the material they gave you. Never create temporary or ` +
|
|
153
|
+
`intermediate files there, and never in the system's temp dir.`);
|
|
154
|
+
}
|
package/dist/harness.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface AgentInvocation {
|
|
|
47
47
|
clis?: string[];
|
|
48
48
|
/** MCP servers available in this phase, keyed by server name. */
|
|
49
49
|
mcpServers?: Record<string, McpServerConfig>;
|
|
50
|
+
/** Extra environment for the agent process (the run's temp folder as TMPDIR, see core/temp.ts); on top of the workspace's. */
|
|
51
|
+
env?: Record<string, string>;
|
|
50
52
|
resume?: string;
|
|
51
53
|
onToolUse?: (tool: string, target: string) => void;
|
|
52
54
|
onText?: (text: string) => void;
|
package/dist/harnesses/acp.js
CHANGED
|
@@ -83,7 +83,7 @@ async function open(agent, inv, resume) {
|
|
|
83
83
|
};
|
|
84
84
|
const { child, conn } = await connectAcp(agent, {
|
|
85
85
|
cwd: inv.cwd,
|
|
86
|
-
env: adapterEnv(agent, { model: inv.model, effort: inv.effort }),
|
|
86
|
+
env: { ...adapterEnv(agent, { model: inv.model, effort: inv.effort }), ...inv.env },
|
|
87
87
|
client,
|
|
88
88
|
clientName: "kraftwerk",
|
|
89
89
|
onError: (err) => end(`could not start the ${agent} adapter: ${err.message}`),
|
package/dist/harnesses/claude.js
CHANGED
package/dist/harnesses/codex.js
CHANGED
package/dist/harnesses/pi.js
CHANGED
package/dist/run.d.ts
CHANGED
|
@@ -39,6 +39,13 @@ export declare class Run {
|
|
|
39
39
|
private readonly verbose;
|
|
40
40
|
private readonly maxGateRetries;
|
|
41
41
|
constructor(options: RunOptions);
|
|
42
|
+
/**
|
|
43
|
+
* The run's scratch folder, <run dir>/temp, as TMPDIR for every phase
|
|
44
|
+
* (core/temp.ts): inside the run dir so a sandboxed run finds it in its
|
|
45
|
+
* mount, created when the first phase starts.
|
|
46
|
+
*/
|
|
47
|
+
private temp?;
|
|
48
|
+
private tempEnv;
|
|
42
49
|
trace(event: string, data?: Record<string, unknown>): Promise<void>;
|
|
43
50
|
agentPhase(params: {
|
|
44
51
|
name: string;
|