@netnodeag/kraftwerk 0.57.3 → 0.58.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.
Files changed (84) hide show
  1. package/README.md +85 -2
  2. package/dist/acp.js +2 -1
  3. package/dist/cli/journal.d.ts +10 -0
  4. package/dist/cli/journal.js +36 -0
  5. package/dist/cli/knowledge.js +17 -0
  6. package/dist/cli/kraftwerk.js +6 -0
  7. package/dist/cli/projects.js +3 -3
  8. package/dist/cli/repos.js +1 -1
  9. package/dist/cli/trash.d.ts +2 -0
  10. package/dist/cli/trash.js +70 -0
  11. package/dist/cli/vibeables.js +3 -3
  12. package/dist/inspector/agent-status.d.ts +21 -0
  13. package/dist/inspector/agent-status.js +25 -0
  14. package/dist/inspector/agents.d.ts +5 -0
  15. package/dist/inspector/agents.js +10 -1
  16. package/dist/inspector/attention.d.ts +48 -0
  17. package/dist/inspector/attention.js +66 -0
  18. package/dist/inspector/channels.js +2 -1
  19. package/dist/inspector/chat/sessions.d.ts +52 -1
  20. package/dist/inspector/chat/sessions.js +160 -27
  21. package/dist/inspector/chat/store.d.ts +2 -0
  22. package/dist/inspector/chat/store.js +5 -1
  23. package/dist/inspector/files.d.ts +87 -0
  24. package/dist/inspector/files.js +316 -0
  25. package/dist/inspector/git.js +3 -1
  26. package/dist/inspector/journal.d.ts +31 -0
  27. package/dist/inspector/journal.js +72 -0
  28. package/dist/inspector/knowledge.d.ts +2 -0
  29. package/dist/inspector/knowledge.js +7 -0
  30. package/dist/inspector/projects.d.ts +1 -1
  31. package/dist/inspector/projects.js +5 -2
  32. package/dist/inspector/repos.js +2 -1
  33. package/dist/inspector/runs.d.ts +1 -1
  34. package/dist/inspector/runs.js +3 -2
  35. package/dist/inspector/server.js +111 -1
  36. package/dist/inspector/skills.d.ts +1 -1
  37. package/dist/inspector/skills.js +3 -2
  38. package/dist/inspector/status-line.d.ts +14 -0
  39. package/dist/inspector/status-line.js +44 -0
  40. package/dist/inspector/trash.d.ts +37 -0
  41. package/dist/inspector/trash.js +148 -0
  42. package/dist/inspector/vibeables.d.ts +1 -1
  43. package/dist/inspector/vibeables.js +3 -2
  44. package/inspector/dist/assets/dist-B2o7tVdJ.js +23 -0
  45. package/inspector/dist/assets/{dist-Ceimhh7q.js → dist-BA7UaMHM.js} +1 -1
  46. package/inspector/dist/assets/dist-BXQtebmM.js +1 -0
  47. package/inspector/dist/assets/dist-BYDYk38f.js +1 -0
  48. package/inspector/dist/assets/{dist-CPlSMtdY.js → dist-BdZ5cNRp.js} +1 -1
  49. package/inspector/dist/assets/{dist-DGoW6poO.js → dist-BgakrfmB.js} +1 -1
  50. package/inspector/dist/assets/{dist-CprubYd5.js → dist-BpUghI2_.js} +1 -1
  51. package/inspector/dist/assets/{dist-BdimISeT.js → dist-BpYRdsio.js} +1 -1
  52. package/inspector/dist/assets/{dist-C9wdXHwY.js → dist-CHSqB1rM.js} +1 -1
  53. package/inspector/dist/assets/{dist-B1xH7Mxz.js → dist-CVpPKkOb.js} +1 -1
  54. package/inspector/dist/assets/{dist-H3rerbMg.js → dist-CYzOYk_e.js} +9 -9
  55. package/inspector/dist/assets/dist-CzkTIkYD.js +1 -0
  56. package/inspector/dist/assets/{dist-rE2oRtJp.js → dist-D7O3CloR.js} +1 -1
  57. package/inspector/dist/assets/dist-DJzkq2t-.js +1 -0
  58. package/inspector/dist/assets/dist-Dctj7Mr3.js +1 -0
  59. package/inspector/dist/assets/{dist-BA9oX2Qo.js → dist-DtEBn0Pj.js} +1 -1
  60. package/inspector/dist/assets/{dist-CmUexHfa.js → dist-DwpQHmP7.js} +1 -1
  61. package/inspector/dist/assets/{dist-CF7An11X.js → dist-KG4lrraG.js} +1 -1
  62. package/inspector/dist/assets/{dist-5wk4daHl.js → dist-Ko3fuaif.js} +1 -1
  63. package/inspector/dist/assets/{dist-ZMc9InB9.js → dist-f7v359XP.js} +1 -1
  64. package/inspector/dist/assets/{dist-yFKhRrHN.js → dist-itDgDZqI.js} +1 -1
  65. package/inspector/dist/assets/{dist-CXS6W3P9.js → dist-p_qW90fR.js} +1 -1
  66. package/inspector/dist/assets/{dist-DNBDudvB.js → dist-xi1nJU9x.js} +1 -1
  67. package/inspector/dist/assets/editor-BkGiMzB_.js +120 -0
  68. package/inspector/dist/assets/editor-CZMrhTTL.css +1 -0
  69. package/inspector/dist/assets/index-Bb27CwDb.css +2 -0
  70. package/inspector/dist/assets/index-D4h-VE-4.js +229 -0
  71. package/inspector/dist/assets/material-symbols-rounded-Dni8jOUD.woff2 +0 -0
  72. package/inspector/dist/index.html +2 -2
  73. package/package.json +1 -1
  74. package/inspector/dist/assets/dist-9zOw_mLO.js +0 -1
  75. package/inspector/dist/assets/dist-D3a-4Le2.js +0 -1
  76. package/inspector/dist/assets/dist-D8nmL5S9.js +0 -1
  77. package/inspector/dist/assets/dist-DN5yqrtf.js +0 -1
  78. package/inspector/dist/assets/dist-DaG9QYNU.js +0 -1
  79. package/inspector/dist/assets/dist-_9tJHb24.js +0 -23
  80. package/inspector/dist/assets/editor-B1Jq9YIq.css +0 -1
  81. package/inspector/dist/assets/editor-DtVtHids.js +0 -117
  82. package/inspector/dist/assets/index-B-sri0As.js +0 -225
  83. package/inspector/dist/assets/index-Da4dQf-y.css +0 -1
  84. package/inspector/dist/assets/material-symbols-rounded-DzDTtRam.woff2 +0 -0
@@ -0,0 +1,316 @@
1
+ import { createReadStream, createWriteStream, promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { pipeline } from "node:stream/promises";
4
+ import { resolveProject } from "../config.js";
5
+ import { getProjectRoot } from "./context.js";
6
+ import { listProjects, openProjects, safeProjectSlug } from "./projects.js";
7
+ import { moveToTrash } from "./trash.js";
8
+ /**
9
+ * Files: the material of the work, any format, as plain folders. Two
10
+ * owners, by how long a file lives:
11
+ *
12
+ * kraftwerk-data/files/ workspace files: shared, outlive any project
13
+ * kraftwerk-data/projects/<slug>/files/ a project's files: briefing, drafts, deliverables
14
+ *
15
+ * Agents own none (they link folders; see agentFilesContext). Being plain
16
+ * folders, agents read and write them like any directory, the workspace
17
+ * git keeps their history, and a delete goes to the trash. Files over
18
+ * LOCAL_OVER_BYTES stay out of git: listed in the root's own .gitignore,
19
+ * shown as "local only". Knowledge is separate: a file becomes evidence in
20
+ * a bundle's references/ through "learn from this" (later).
21
+ */
22
+ export const WORKSPACE_FILES_DIR = "kraftwerk-data/files";
23
+ export const PROJECT_FILES_DIR = "files";
24
+ /** Bigger files stay local: kept out of the workspace git. */
25
+ export const LOCAL_OVER_BYTES = 25 * 1024 * 1024;
26
+ /** Upload cap: a guard against a runaway request, not a policy. */
27
+ export const MAX_UPLOAD_BYTES = 2 * 1024 * 1024 * 1024;
28
+ const IGNORE_HEAD = "# kraftwerk: files over 25 MB stay local (not in git)";
29
+ const MIME = {
30
+ ".pdf": "application/pdf",
31
+ ".png": "image/png",
32
+ ".jpg": "image/jpeg",
33
+ ".jpeg": "image/jpeg",
34
+ ".gif": "image/gif",
35
+ ".webp": "image/webp",
36
+ ".svg": "image/svg+xml",
37
+ ".avif": "image/avif",
38
+ ".heic": "image/heic",
39
+ ".mp4": "video/mp4",
40
+ ".webm": "video/webm",
41
+ ".mov": "video/quicktime",
42
+ ".mp3": "audio/mpeg",
43
+ ".m4a": "audio/mp4",
44
+ ".wav": "audio/wav",
45
+ ".txt": "text/plain; charset=utf-8",
46
+ ".md": "text/markdown; charset=utf-8",
47
+ ".csv": "text/csv; charset=utf-8",
48
+ ".tsv": "text/tab-separated-values; charset=utf-8",
49
+ ".json": "application/json; charset=utf-8",
50
+ ".yml": "text/yaml; charset=utf-8",
51
+ ".yaml": "text/yaml; charset=utf-8",
52
+ ".xml": "text/xml; charset=utf-8",
53
+ ".html": "text/html; charset=utf-8",
54
+ ".log": "text/plain; charset=utf-8",
55
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
56
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
57
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
58
+ ".zip": "application/zip",
59
+ };
60
+ export const mimeOf = (name) => MIME[path.extname(name).toLowerCase()] ?? "application/octet-stream";
61
+ /** The root folder of a scope, its display label, and the root relative to the workspace. */
62
+ export async function scopeRoot(scope) {
63
+ const project = await resolveProject(getProjectRoot());
64
+ const workspace = project.root;
65
+ if (scope === "workspace") {
66
+ const abs = path.join(workspace, WORKSPACE_FILES_DIR);
67
+ return { abs, rel: WORKSPACE_FILES_DIR, label: "Workspace", workspace };
68
+ }
69
+ const m = /^project:(.+)$/.exec(scope);
70
+ if (!m)
71
+ throw new Error(`invalid files scope "${scope}"`);
72
+ const slug = safeProjectSlug(m[1]);
73
+ const opened = await openProjects();
74
+ if (!opened.root)
75
+ throw new Error(opened.error ?? "projects are off");
76
+ const dir = path.join(opened.root, slug);
77
+ if (!(await fs.stat(path.join(dir, "project.yml")).catch(() => null)))
78
+ throw new Error(`no project "${slug}"`);
79
+ const abs = path.join(dir, PROJECT_FILES_DIR);
80
+ const title = (await listProjects()).projects.find((p) => p.slug === slug)?.title ?? slug;
81
+ return { abs, rel: path.relative(workspace, abs).split(path.sep).join("/"), label: title, workspace };
82
+ }
83
+ /** A path inside a root: no escaping it, no hidden segments (.gitignore and friends are kraftwerk's). */
84
+ function inside(root, rel) {
85
+ const clean = rel.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
86
+ if (clean.split("/").some((s) => s === ".." || (s.startsWith(".") && s !== "")))
87
+ throw new Error("invalid path");
88
+ const abs = path.resolve(root, clean);
89
+ if (abs !== root && !abs.startsWith(root + path.sep))
90
+ throw new Error("invalid path");
91
+ return abs;
92
+ }
93
+ const toPosix = (p) => p.split(path.sep).join("/");
94
+ /** Number of files below a folder, stopping at `cap`. */
95
+ async function countFiles(dir, cap = 5000) {
96
+ let n = 0;
97
+ const walk = async (d) => {
98
+ if (n >= cap)
99
+ return;
100
+ const kids = await fs.readdir(d, { withFileTypes: true }).catch(() => []);
101
+ for (const k of kids) {
102
+ if (k.name.startsWith("."))
103
+ continue;
104
+ if (k.isDirectory())
105
+ await walk(path.join(d, k.name));
106
+ else if (k.isFile())
107
+ n++;
108
+ if (n >= cap)
109
+ return;
110
+ }
111
+ };
112
+ await walk(dir);
113
+ return n;
114
+ }
115
+ /* ---------- local-only (over the size limit) ---------- */
116
+ async function readIgnore(root) {
117
+ const raw = await fs.readFile(path.join(root, ".gitignore"), "utf8").catch(() => "");
118
+ return raw.split("\n").filter((l) => l.startsWith("/"));
119
+ }
120
+ /** Keep `rel` out of git (on) or let it back in (off), in the root's own .gitignore. */
121
+ async function markLocal(root, rel, on) {
122
+ const line = `/${rel}`;
123
+ const lines = await readIgnore(root);
124
+ const has = lines.includes(line);
125
+ if (on === has)
126
+ return;
127
+ const next = on ? [...lines, line] : lines.filter((l) => l !== line);
128
+ const file = path.join(root, ".gitignore");
129
+ if (next.length === 0)
130
+ await fs.rm(file, { force: true });
131
+ else
132
+ await fs.writeFile(file, `${IGNORE_HEAD}\n${next.sort().join("\n")}\n`);
133
+ }
134
+ /* ---------- reading ---------- */
135
+ export async function listScopes() {
136
+ const ws = await scopeRoot("workspace");
137
+ const out = [{ scope: "workspace", label: ws.label, root: ws.rel, count: await countFiles(ws.abs) }];
138
+ const projects = await listProjects().catch(() => ({ projects: [] }));
139
+ for (const p of projects.projects) {
140
+ const r = await scopeRoot(`project:${p.slug}`).catch(() => null);
141
+ if (!r)
142
+ continue;
143
+ out.push({ scope: `project:${p.slug}`, label: p.title, root: r.rel, count: await countFiles(r.abs), status: p.status });
144
+ }
145
+ return out;
146
+ }
147
+ export async function listFiles(scope, rel = "") {
148
+ const root = await scopeRoot(scope);
149
+ const dir = inside(root.abs, rel);
150
+ const st = await fs.stat(dir).catch(() => null);
151
+ if (rel && !st?.isDirectory())
152
+ throw new Error(`no folder "${rel}"`);
153
+ const local = new Set(await readIgnore(root.abs));
154
+ const kids = st ? await fs.readdir(dir, { withFileTypes: true }) : [];
155
+ const entries = [];
156
+ for (const k of kids) {
157
+ if (k.name.startsWith("."))
158
+ continue;
159
+ const abs = path.join(dir, k.name);
160
+ const s = await fs.stat(abs).catch(() => null);
161
+ if (!s)
162
+ continue;
163
+ const p = toPosix(path.relative(root.abs, abs));
164
+ if (s.isDirectory()) {
165
+ entries.push({ name: k.name, path: p, type: "dir", modifiedAt: s.mtime.toISOString(), count: await countFiles(abs, 1000) });
166
+ }
167
+ else if (s.isFile()) {
168
+ // A big file dropped in by hand (Finder, an agent) is kept out of git the moment it is seen.
169
+ const big = s.size > LOCAL_OVER_BYTES;
170
+ if (big && !local.has(`/${p}`))
171
+ await markLocal(root.abs, p, true);
172
+ entries.push({ name: k.name, path: p, type: "file", size: s.size, modifiedAt: s.mtime.toISOString(), mime: mimeOf(k.name), ...(big ? { local: true } : {}) });
173
+ }
174
+ }
175
+ entries.sort((a, b) => (a.type === b.type ? a.name.localeCompare(b.name, undefined, { numeric: true }) : a.type === "dir" ? -1 : 1));
176
+ return { scope, label: root.label, path: toPosix(path.relative(root.abs, dir)), root: root.rel, entries };
177
+ }
178
+ /** A file to stream to the browser. */
179
+ export async function openFile(scope, rel) {
180
+ const root = await scopeRoot(scope);
181
+ const abs = inside(root.abs, rel);
182
+ const st = await fs.stat(abs).catch(() => null);
183
+ if (!st?.isFile())
184
+ throw new Error(`no file "${rel}"`);
185
+ return { stream: createReadStream(abs), size: st.size, mime: mimeOf(abs), name: path.basename(abs) };
186
+ }
187
+ /* ---------- writing ---------- */
188
+ /** A name for an upload: no folders, no control characters, no leading dot. */
189
+ function cleanName(name) {
190
+ const base = path.basename(name.replace(/\\/g, "/")).replace(/[\u0000-\u001f\u007f/]/g, "").trim().replace(/^\.+/, "");
191
+ if (!base)
192
+ throw new Error("invalid file name");
193
+ return base.slice(0, 200);
194
+ }
195
+ /** "report.pdf" taken -> "report (2).pdf". */
196
+ async function freeName(dir, name) {
197
+ const ext = path.extname(name);
198
+ const stem = name.slice(0, name.length - ext.length);
199
+ let candidate = name;
200
+ for (let n = 2; await fs.stat(path.join(dir, candidate)).then(() => true, () => false); n++)
201
+ candidate = `${stem} (${n})${ext}`;
202
+ return candidate;
203
+ }
204
+ /** Stream an upload into a folder; a name already taken gets " (2)". */
205
+ export async function saveUpload(scope, dirRel, name, body) {
206
+ const root = await scopeRoot(scope);
207
+ const dir = inside(root.abs, dirRel);
208
+ await fs.mkdir(dir, { recursive: true });
209
+ const final = await freeName(dir, cleanName(name));
210
+ const abs = path.join(dir, final);
211
+ const tmp = path.join(dir, `.upload-${process.pid}-${Date.now()}`);
212
+ let size = 0;
213
+ body.on("data", (c) => {
214
+ size += c.length;
215
+ if (size > MAX_UPLOAD_BYTES)
216
+ body.destroy(new Error("file too large"));
217
+ });
218
+ try {
219
+ await pipeline(body, createWriteStream(tmp));
220
+ if (size === 0)
221
+ throw new Error("empty file");
222
+ await fs.rename(tmp, abs);
223
+ }
224
+ catch (err) {
225
+ await fs.rm(tmp, { force: true });
226
+ throw err;
227
+ }
228
+ const p = toPosix(path.relative(root.abs, abs));
229
+ if (size > LOCAL_OVER_BYTES)
230
+ await markLocal(root.abs, p, true);
231
+ return { name: final, path: p, type: "file", size, modifiedAt: new Date().toISOString(), mime: mimeOf(final), ...(size > LOCAL_OVER_BYTES ? { local: true } : {}) };
232
+ }
233
+ export async function makeFolder(scope, rel) {
234
+ const root = await scopeRoot(scope);
235
+ const abs = inside(root.abs, rel);
236
+ if (abs === root.abs)
237
+ throw new Error("folder name is required");
238
+ if (await fs.stat(abs).catch(() => null))
239
+ throw new Error(`"${path.basename(abs)}" already exists`);
240
+ await fs.mkdir(abs, { recursive: true });
241
+ return { name: path.basename(abs), path: toPosix(path.relative(root.abs, abs)), type: "dir", modifiedAt: new Date().toISOString(), count: 0 };
242
+ }
243
+ /** Rename or move inside the scope. Refused when the target exists. */
244
+ export async function moveFile(scope, fromRel, toRel) {
245
+ const root = await scopeRoot(scope);
246
+ const from = inside(root.abs, fromRel);
247
+ const to = inside(root.abs, toRel);
248
+ if (from === root.abs || to === root.abs)
249
+ throw new Error("invalid path");
250
+ const st = await fs.stat(from).catch(() => null);
251
+ if (!st)
252
+ throw new Error(`no file "${fromRel}"`);
253
+ if (await fs.stat(to).catch(() => null))
254
+ throw new Error(`"${toRel}" already exists`);
255
+ if (st.isDirectory() && (to + path.sep).startsWith(from + path.sep))
256
+ throw new Error("cannot move a folder into itself");
257
+ await fs.mkdir(path.dirname(to), { recursive: true });
258
+ await fs.rename(from, to);
259
+ if (st.isFile() && st.size > LOCAL_OVER_BYTES) {
260
+ await markLocal(root.abs, toPosix(path.relative(root.abs, from)), false);
261
+ await markLocal(root.abs, toPosix(path.relative(root.abs, to)), true);
262
+ }
263
+ }
264
+ /** Move a file or folder to the trash (trash/files/<workspace | projects/slug>/<path>). */
265
+ export async function deleteFile(scope, rel) {
266
+ const root = await scopeRoot(scope);
267
+ const abs = inside(root.abs, rel);
268
+ if (abs === root.abs)
269
+ throw new Error("invalid path");
270
+ const st = await fs.stat(abs).catch(() => null);
271
+ if (!st)
272
+ throw new Error(`no file "${rel}"`);
273
+ const p = toPosix(path.relative(root.abs, abs));
274
+ const owner = scope === "workspace" ? "workspace" : `projects/${scope.slice("project:".length)}`;
275
+ await moveToTrash("files", `${owner}/${p}`, abs);
276
+ if (st.isFile() && st.size > LOCAL_OVER_BYTES)
277
+ await markLocal(root.abs, p, false);
278
+ }
279
+ /* ---------- context for agents ---------- */
280
+ /** "files/ (23 files; recent: designs/home-v3.png 2026-10-05, …)" — where the files are, not every file. */
281
+ export async function filesSummary(scope, recent = 5) {
282
+ const root = await scopeRoot(scope).catch(() => null);
283
+ if (!root)
284
+ return null;
285
+ const all = [];
286
+ const walk = async (d) => {
287
+ if (all.length > 5000)
288
+ return;
289
+ for (const k of await fs.readdir(d, { withFileTypes: true }).catch(() => [])) {
290
+ if (k.name.startsWith("."))
291
+ continue;
292
+ const abs = path.join(d, k.name);
293
+ if (k.isDirectory())
294
+ await walk(abs);
295
+ else if (k.isFile()) {
296
+ const s = await fs.stat(abs).catch(() => null);
297
+ if (s)
298
+ all.push({ path: toPosix(path.relative(root.abs, abs)), at: s.mtime.toISOString() });
299
+ }
300
+ }
301
+ };
302
+ await walk(root.abs);
303
+ all.sort((a, b) => b.at.localeCompare(a.at));
304
+ return { root: root.rel, count: all.length, recent: all.slice(0, recent) };
305
+ }
306
+ /** The "## Files" block of a session's context: the folder to use, what is in it lately. */
307
+ export async function filesContext(scope, heading, intro) {
308
+ const s = await filesSummary(scope);
309
+ if (!s)
310
+ return "";
311
+ const recent = s.recent.map((r) => ` - ${r.path} (${r.at.slice(0, 10)})`).join("\n");
312
+ return (`## ${heading}\n${intro} ${s.root}/ — ${s.count} file${s.count === 1 ? "" : "s"}` +
313
+ (recent ? `, most recently changed:\n${recent}\n` : " (empty so far)\n") +
314
+ `Read and write them there like any folder; put what you produce for the user in ${s.root}/deliverables/. ` +
315
+ `Files over 25 MB stay out of git automatically. Delete nothing there unless asked.`);
316
+ }
@@ -204,7 +204,7 @@ async function checkedOut(repo) {
204
204
  }
205
205
  /**
206
206
  * The roots that may be committed, as repo-relative paths: the workflows,
207
- * knowledge, agents, skills, channels, vibeables and projects roots this project
207
+ * knowledge, agents, skills, channels, vibeables, projects and files roots this project
208
208
  * declares, plus kraftwerk.yml and the project README. The output directory
209
209
  * is excluded even when it lives inside one of them.
210
210
  */
@@ -237,6 +237,8 @@ function scopeFor({ project, repoRoot }) {
237
237
  vibeablesRootFor(project),
238
238
  // Projects too: briefs, state and logs are the workspace's own record.
239
239
  projectsRootFor(project),
240
+ // Workspace files: shared material, versioned (files over 25 MB ignore themselves, see files.ts).
241
+ path.resolve(project.root, "kraftwerk-data/files"),
240
242
  project.configPath,
241
243
  path.resolve(project.root, "README.md"),
242
244
  ];
@@ -0,0 +1,31 @@
1
+ /**
2
+ * An agent's journal: agents/<slug>/journal.md, the agent's own memory
3
+ * across sessions. What it learned, decided, promised and finished, one
4
+ * line per entry under a dated heading, newest first (like a project's
5
+ * log.md). The agent writes it through `kraftwerk journal` and reads the
6
+ * recent part back at the start of every session, so next week it is the
7
+ * same colleague; a human can add a line from the inspector.
8
+ */
9
+ export declare const JOURNAL_FILE = "journal.md";
10
+ export declare const JOURNAL_KINDS: readonly ["learned", "decided", "promised", "done", "note"];
11
+ export type JournalKind = (typeof JOURNAL_KINDS)[number];
12
+ /** The whole journal; empty when nothing was written yet. */
13
+ export declare function readJournal(slug: string): Promise<string>;
14
+ /**
15
+ * Prepend one entry under today's heading. `actor` is stamped unless it is
16
+ * the agent itself: in its own journal, its own lines need no signature,
17
+ * and a human's stand out.
18
+ */
19
+ export declare function appendJournal(slug: string, entry: string, opts?: {
20
+ kind?: string;
21
+ actor?: string;
22
+ }): Promise<string>;
23
+ /**
24
+ * The recent part of a journal for a session's context: whole days, newest
25
+ * first, until the budget is spent. Older days stay in the file, where the
26
+ * agent can read them when it needs to.
27
+ */
28
+ export declare function journalExcerpt(raw: string, budget?: number): {
29
+ text: string;
30
+ truncated: boolean;
31
+ };
@@ -0,0 +1,72 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { agentsRoot, getAgent, safeAgentSlug } from "./agents.js";
4
+ /**
5
+ * An agent's journal: agents/<slug>/journal.md, the agent's own memory
6
+ * across sessions. What it learned, decided, promised and finished, one
7
+ * line per entry under a dated heading, newest first (like a project's
8
+ * log.md). The agent writes it through `kraftwerk journal` and reads the
9
+ * recent part back at the start of every session, so next week it is the
10
+ * same colleague; a human can add a line from the inspector.
11
+ */
12
+ export const JOURNAL_FILE = "journal.md";
13
+ export const JOURNAL_KINDS = ["learned", "decided", "promised", "done", "note"];
14
+ async function journalPath(slug) {
15
+ return path.join(await agentsRoot(), safeAgentSlug(slug), JOURNAL_FILE);
16
+ }
17
+ /** The whole journal; empty when nothing was written yet. */
18
+ export async function readJournal(slug) {
19
+ return fs.readFile(await journalPath(slug), "utf8").catch(() => "");
20
+ }
21
+ /**
22
+ * Prepend one entry under today's heading. `actor` is stamped unless it is
23
+ * the agent itself: in its own journal, its own lines need no signature,
24
+ * and a human's stand out.
25
+ */
26
+ export async function appendJournal(slug, entry, opts = {}) {
27
+ const def = await getAgent(slug);
28
+ if (!def)
29
+ throw new Error(`no agent "${slug}"`);
30
+ const text = entry.trim().replace(/\s*\n\s*/g, " ");
31
+ if (!text)
32
+ throw new Error("entry is required");
33
+ const kind = (opts.kind ?? "note").trim().toLowerCase();
34
+ if (!JOURNAL_KINDS.includes(kind))
35
+ throw new Error(`kind must be one of: ${JOURNAL_KINDS.join(", ")}`);
36
+ const actor = opts.actor?.trim();
37
+ const own = !actor || actor === slug || actor.startsWith(`${slug}/`);
38
+ const line = `* ${kind === "note" ? "" : `**${kind[0].toUpperCase()}${kind.slice(1)}**: `}${text}${own ? "" : ` (${actor})`}`;
39
+ const file = await journalPath(slug);
40
+ const today = new Date().toISOString().slice(0, 10);
41
+ const raw = (await fs.readFile(file, "utf8").catch(() => null)) ?? `# ${def.name}'s journal\n`;
42
+ const lines = raw.split("\n");
43
+ const todayIdx = lines.findIndex((l) => l.trim() === `## ${today}`);
44
+ if (todayIdx >= 0) {
45
+ lines.splice(todayIdx + 1, 0, line);
46
+ }
47
+ else {
48
+ const titleIdx = lines.findIndex((l) => l.startsWith("# "));
49
+ lines.splice(titleIdx + 1, 0, "", `## ${today}`, line);
50
+ }
51
+ const out = lines.join("\n").replace(/\n{3,}/g, "\n\n");
52
+ await fs.writeFile(file, out);
53
+ return out;
54
+ }
55
+ /**
56
+ * The recent part of a journal for a session's context: whole days, newest
57
+ * first, until the budget is spent. Older days stay in the file, where the
58
+ * agent can read them when it needs to.
59
+ */
60
+ export function journalExcerpt(raw, budget = 4000) {
61
+ const days = raw.split(/\n(?=## )/).filter((d) => d.startsWith("## "));
62
+ const kept = [];
63
+ let used = 0;
64
+ for (const d of days) {
65
+ const day = d.trim();
66
+ if (used + day.length > budget && kept.length > 0)
67
+ break;
68
+ kept.push(day.length > budget ? `${day.slice(0, budget)}…` : day);
69
+ used += day.length;
70
+ }
71
+ return { text: kept.join("\n\n"), truncated: kept.length < days.length };
72
+ }
@@ -18,6 +18,8 @@ export interface BundleDetail {
18
18
  }
19
19
  export declare function knowledgeIndex(): Promise<KnowledgeIndex>;
20
20
  export declare function createBundle(name: string): Promise<BundleInfo>;
21
+ /** Move a bundle to the trash. */
22
+ export declare function deleteBundle(name: string): Promise<void>;
21
23
  export declare function bundleDetail(name: string): Promise<BundleDetail | null>;
22
24
  export declare function conceptDetail(bundle: string, id: string): Promise<ConceptDetail | null>;
23
25
  export declare function putConcept(bundle: string, id: string, content: string): Promise<ConceptDetail>;
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { resolveProject } from "../config.js";
4
4
  import { getConcept, initBundle, knowledgeRoot, listBundles, listConcepts, safeBundleName, verifyConcept, writeConcept, } from "../okf.js";
5
5
  import { getProjectRoot } from "./context.js";
6
+ import { moveToTrash } from "./trash.js";
6
7
  /**
7
8
  * Knowledge for the inspector: a thin API layer over the OKF
8
9
  * core (src/okf.ts). Writes from the UI are stamped as `human:user` — the
@@ -22,6 +23,12 @@ export async function createBundle(name) {
22
23
  await initBundle(await root(), name);
23
24
  return { name, concepts: 0 };
24
25
  }
26
+ /** Move a bundle to the trash. */
27
+ export async function deleteBundle(name) {
28
+ const dir = path.join(await root(), safeBundleName(name));
29
+ if (!(await moveToTrash("knowledge", name, dir)))
30
+ throw new Error(`no bundle "${name}"`);
31
+ }
25
32
  export async function bundleDetail(name) {
26
33
  const kroot = await root();
27
34
  const dir = path.join(kroot, safeBundleName(name));
@@ -161,7 +161,7 @@ export declare function linkProject(slug: string, kind: string, target: string,
161
161
  * agent reads as such next to a human's.
162
162
  */
163
163
  export declare function appendProjectLog(slug: string, entry: string, actor: string): Promise<string>;
164
- /** Remove the folder; its history stays in the workspace git. */
164
+ /** Move the folder to the trash. */
165
165
  export declare function deleteProject(slug: string): Promise<void>;
166
166
  /** One line per system of record: what it is, where, and what usually lives there. */
167
167
  export declare function recordLine(r: SystemOfRecord): string;
@@ -8,8 +8,10 @@ import { syncWorkspaceProjects } from "./instances.js";
8
8
  import { knowledgeIndex } from "./knowledge.js";
9
9
  import { newestMtime } from "./mtime.js";
10
10
  import { listRepos } from "./repos.js";
11
+ import { moveToTrash } from "./trash.js";
11
12
  import { listVibeables } from "./vibeables.js";
12
13
  import { listWorkflows } from "./workflows.js";
14
+ import { filesContext } from "./files.js";
13
15
  /**
14
16
  * Projects: a goal with everything the agents need to reach it gathered in
15
17
  * one folder — the brief, the systems of record where the truth is managed
@@ -427,13 +429,13 @@ export async function appendProjectLog(slug, entry, actor) {
427
429
  await fs.writeFile(file, out);
428
430
  return out;
429
431
  }
430
- /** Remove the folder; its history stays in the workspace git. */
432
+ /** Move the folder to the trash. */
431
433
  export async function deleteProject(slug) {
432
434
  const root = await requireRoot();
433
435
  const dir = path.join(root, safeProjectSlug(slug));
434
436
  if (!(await fs.stat(path.join(dir, PROJECT_FILE)).catch(() => null)))
435
437
  throw new Error(`no project "${slug}"`);
436
- await fs.rm(dir, { recursive: true, force: true });
438
+ await moveToTrash("projects", slug, dir);
437
439
  }
438
440
  /* ---------- chat context ---------- */
439
441
  /** One line per system of record: what it is, where, and what usually lives there. */
@@ -500,6 +502,7 @@ export async function projectContext(slug, actor, opts = {}) {
500
502
  `\`npx kraftwerk repos\` lists every clone, \`repos add <url>\` clones a new one into the root.`
501
503
  : `\nRepositories are switched off in this workspace's settings, so these clones are not reachable right now — tell the user.`)
502
504
  : "",
505
+ await filesContext(`project:${p.slug}`, "Files", "The project's files (briefings, drafts, designs, deliverables) are in"),
503
506
  vibeables
504
507
  ? `## Vibeables\nSmall apps of this project (one folder each under the vibeables root):\n` +
505
508
  linkLines("vibeables", (l) => `- ${l.slug}${l.label ? ` (${l.label})` : ""}`) +
@@ -2,6 +2,7 @@ import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { gitignoreHas, ignoreEntryFor, reposRootFor, resolveProject } from "../config.js";
4
4
  import { getProjectRoot } from "./context.js";
5
+ import { moveToTrash } from "./trash.js";
5
6
  import { newestMtime } from "./mtime.js";
6
7
  import { capDiff, DENY, diffAgainstHead, git, gitNet, label, literal, parseStatus, sshCommandFor } from "./git.js";
7
8
  /**
@@ -340,7 +341,7 @@ export async function removeRepo(slug, force = false) {
340
341
  if (repo.ahead)
341
342
  return { ok: false, conflict: true, error: `"${slug}" has ${repo.ahead} unpushed commit(s)` };
342
343
  }
343
- await fs.rm(dir, { recursive: true, force: true });
344
+ await moveToTrash("repos", slug, dir);
344
345
  return { ok: true };
345
346
  }
346
347
  /**
@@ -66,7 +66,7 @@ export declare function readRunFile(id: string, name: string): Promise<{
66
66
  size: number;
67
67
  } | null>;
68
68
  /**
69
- * Remove a run folder. A run that still looks live is refused: its process
69
+ * Move a run folder to the trash. A run that still looks live is refused: its process
70
70
  * would keep writing into a folder that no longer exists — stop it first.
71
71
  */
72
72
  export declare function deleteRun(id: string): Promise<"deleted" | "running" | "missing">;
@@ -1,6 +1,7 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { getOutputDir } from "./context.js";
4
+ import { moveToTrash } from "./trash.js";
4
5
  import { awaitingDecision, readDecision } from "./decisions.js";
5
6
  /**
6
7
  * Filesystem + trace.jsonl reading for the inspector. The output directory
@@ -306,7 +307,7 @@ export async function readRunFile(id, name) {
306
307
  return { absPath, size: st.size };
307
308
  }
308
309
  /**
309
- * Remove a run folder. A run that still looks live is refused: its process
310
+ * Move a run folder to the trash. A run that still looks live is refused: its process
310
311
  * would keep writing into a folder that no longer exists — stop it first.
311
312
  */
312
313
  export async function deleteRun(id) {
@@ -315,6 +316,6 @@ export async function deleteRun(id) {
315
316
  return "missing";
316
317
  if (run.status === "running")
317
318
  return "running";
318
- await fs.rm(safeRunDir(id), { recursive: true, force: true });
319
+ await moveToTrash("runs", id, safeRunDir(id));
319
320
  return "deleted";
320
321
  }