@netnodeag/kraftwerk 0.79.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.
Files changed (86) hide show
  1. package/README.md +88 -2
  2. package/dist/cli/daemon.js +10 -9
  3. package/dist/cli/doctor.js +1 -1
  4. package/dist/cli/entities.d.ts +8 -0
  5. package/dist/cli/entities.js +222 -0
  6. package/dist/cli/init.js +6 -2
  7. package/dist/cli/kraftwerk.js +10 -0
  8. package/dist/cli/temp.d.ts +2 -0
  9. package/dist/cli/temp.js +62 -0
  10. package/dist/cli/ui.js +2 -1
  11. package/dist/client/routes.d.ts +18 -0
  12. package/dist/client/routes.js +18 -0
  13. package/dist/config.d.ts +25 -0
  14. package/dist/config.js +17 -6
  15. package/dist/core/chat/acp.js +2 -1
  16. package/dist/core/chat/backend.d.ts +2 -0
  17. package/dist/core/chat/context.js +15 -7
  18. package/dist/core/chat/pi.js +2 -1
  19. package/dist/core/chat/seats.js +3 -0
  20. package/dist/core/chat/sessions.js +3 -0
  21. package/dist/core/entities.d.ts +171 -0
  22. package/dist/core/entities.js +441 -0
  23. package/dist/core/files.js +1 -0
  24. package/dist/core/git.js +3 -1
  25. package/dist/core/journal.d.ts +12 -2
  26. package/dist/core/journal.js +50 -5
  27. package/dist/core/log.d.ts +18 -0
  28. package/dist/core/log.js +23 -0
  29. package/dist/core/projects.d.ts +22 -33
  30. package/dist/core/projects.js +41 -135
  31. package/dist/core/records.d.ts +28 -0
  32. package/dist/core/records.js +71 -0
  33. package/dist/core/refs.d.ts +74 -0
  34. package/dist/core/refs.js +143 -0
  35. package/dist/core/search.d.ts +10 -0
  36. package/dist/core/search.js +12 -0
  37. package/dist/core/settings.d.ts +4 -0
  38. package/dist/core/settings.js +24 -3
  39. package/dist/core/slug.d.ts +9 -0
  40. package/dist/core/slug.js +18 -0
  41. package/dist/core/temp.d.ts +73 -0
  42. package/dist/core/temp.js +154 -0
  43. package/dist/core/trash.d.ts +1 -1
  44. package/dist/harness.d.ts +2 -0
  45. package/dist/harnesses/acp.js +1 -1
  46. package/dist/harnesses/claude.js +1 -0
  47. package/dist/harnesses/codex.js +1 -0
  48. package/dist/harnesses/pi.js +1 -0
  49. package/dist/run.d.ts +7 -0
  50. package/dist/run.js +13 -1
  51. package/dist/server/api/devices.js +2 -1
  52. package/dist/server/api/entities.d.ts +74 -0
  53. package/dist/server/api/entities.js +60 -0
  54. package/dist/server/api/files.d.ts +9 -1
  55. package/dist/server/api/files.js +13 -0
  56. package/dist/server/api/index.d.ts +75 -5
  57. package/dist/server/api/index.js +5 -0
  58. package/dist/server/api/projects.d.ts +3 -0
  59. package/dist/server/api/projects.js +3 -0
  60. package/dist/server/api/workspace.d.ts +15 -0
  61. package/dist/server/api/workspace.js +6 -0
  62. package/dist/server/cloud.js +3 -2
  63. package/dist/server/relay.d.ts +3 -2
  64. package/dist/server/relay.js +7 -7
  65. package/dist/server/server.js +3 -2
  66. package/dist/yaml.js +1 -0
  67. package/package.json +1 -1
  68. package/schema/kraftwerk.schema.json +54 -0
  69. package/web/dist/assets/{dist-cNijUfNl.js → dist-8IOFzZx-.js} +1 -1
  70. package/web/dist/assets/{dist-E48sO9Gc.js → dist-AhnSw4hy.js} +1 -1
  71. package/web/dist/assets/{dist-BTD8k55t.js → dist-BK0VhAPQ.js} +1 -1
  72. package/web/dist/assets/{dist-A0LN_JdM.js → dist-Ba6WSVOB.js} +1 -1
  73. package/web/dist/assets/{dist-DgP31lcC.js → dist-BiU4F98O.js} +1 -1
  74. package/web/dist/assets/{dist-wtIUmbnn.js → dist-Dx3aajX1.js} +1 -1
  75. package/web/dist/assets/{dist-DdOsafun.js → dist-Dy1SvhKC.js} +1 -1
  76. package/web/dist/assets/{dist-D93lSM0a.js → dist-ENZYo2jZ.js} +1 -1
  77. package/web/dist/assets/{dist-BasQkRXA.js → dist-FfKYtQxT.js} +1 -1
  78. package/web/dist/assets/{dist-xLZN2eGO.js → dist-cewmjxFi.js} +1 -1
  79. package/web/dist/assets/dist-lgUK2Fjt.js +1 -0
  80. package/web/dist/assets/{editor-BQlHN7qT.js → editor-CaRa1NMC.js} +3 -3
  81. package/web/dist/assets/index-Dpzux3OC.js +231 -0
  82. package/web/dist/assets/index-Dvt-vE1h.css +2 -0
  83. package/web/dist/index.html +2 -2
  84. package/web/dist/assets/dist-B04mMuvH.js +0 -1
  85. package/web/dist/assets/index-BKDJocDd.css +0 -2
  86. package/web/dist/assets/index-C7ddpWNC.js +0 -231
@@ -51,6 +51,11 @@ export const ROUTES = {
51
51
  "cloud.status": ["GET", "/api/cloud"],
52
52
  "cloud.sync": ["POST", "/api/cloud/sync"],
53
53
  "cloud.unlink": ["POST", "/api/cloud/unlink"],
54
+ "contacts.create": ["POST", "/api/contacts"],
55
+ "contacts.delete": ["DELETE", "/api/contacts/:slug"],
56
+ "contacts.get": ["GET", "/api/contacts/:slug"],
57
+ "contacts.list": ["GET", "/api/contacts"],
58
+ "contacts.save": ["PUT", "/api/contacts/:slug"],
54
59
  "devices.code": ["POST", "/api/devices/code"],
55
60
  "devices.dropPush": ["DELETE", "/api/devices/push"],
56
61
  "devices.list": ["GET", "/api/devices"],
@@ -60,6 +65,11 @@ export const ROUTES = {
60
65
  "devices.self": ["GET", "/api/devices/self"],
61
66
  "devices.setPush": ["POST", "/api/devices/push"],
62
67
  "devices.setRelay": ["POST", "/api/devices/relay"],
68
+ "events.create": ["POST", "/api/events"],
69
+ "events.delete": ["DELETE", "/api/events/:slug"],
70
+ "events.get": ["GET", "/api/events/:slug"],
71
+ "events.list": ["GET", "/api/events"],
72
+ "events.save": ["PUT", "/api/events/:slug"],
63
73
  "files.delete": ["DELETE", "/api/files"],
64
74
  "files.list": ["GET", "/api/files/list"],
65
75
  "files.mkdir": ["POST", "/api/files/folder"],
@@ -90,6 +100,11 @@ export const ROUTES = {
90
100
  "notifications.diagnose": ["POST", "/api/notifications/:id/diagnose"],
91
101
  "notifications.list": ["GET", "/api/notifications"],
92
102
  "notifications.read": ["POST", "/api/notifications/read"],
103
+ "places.create": ["POST", "/api/places"],
104
+ "places.delete": ["DELETE", "/api/places/:slug"],
105
+ "places.get": ["GET", "/api/places/:slug"],
106
+ "places.list": ["GET", "/api/places"],
107
+ "places.save": ["PUT", "/api/places/:slug"],
93
108
  "projects.create": ["POST", "/api/projects"],
94
109
  "projects.delete": ["DELETE", "/api/projects/:slug"],
95
110
  "projects.get": ["GET", "/api/projects/:slug"],
@@ -99,6 +114,7 @@ export const ROUTES = {
99
114
  "projects.save": ["PUT", "/api/projects/:slug"],
100
115
  "projects.saveLayout": ["PUT", "/api/projects"],
101
116
  "protocol.get": ["GET", "/api/protocol"],
117
+ "refs.resolve": ["POST", "/api/refs/resolve"],
102
118
  "repos.add": ["POST", "/api/repos"],
103
119
  "repos.commit": ["GET", "/api/repos/:slug/commits/:hash"],
104
120
  "repos.diff": ["GET", "/api/repos/:slug/diff"],
@@ -121,6 +137,8 @@ export const ROUTES = {
121
137
  "settings.save": ["PUT", "/api/settings"],
122
138
  "skills.get": ["GET", "/api/skills/:name"],
123
139
  "skills.list": ["GET", "/api/skills"],
140
+ "temp.clean": ["DELETE", "/api/temp"],
141
+ "temp.list": ["GET", "/api/temp"],
124
142
  "trash.empty": ["DELETE", "/api/trash"],
125
143
  "trash.list": ["GET", "/api/trash"],
126
144
  "trash.purge": ["POST", "/api/trash/purge"],
package/dist/config.d.ts CHANGED
@@ -121,6 +121,25 @@ export interface ProjectsConfig {
121
121
  export declare const PROJECTS_DEFAULT_ROOT = "kraftwerk-data/projects";
122
122
  /** Absolute projects root when the feature is on, undefined otherwise. */
123
123
  export declare function projectsRootFor(project: ResolvedWorkspace): string | undefined;
124
+ /**
125
+ * Contacts, places and events: the workspace's own records of who, where
126
+ * and when — one Markdown file with YAML frontmatter per entry under the
127
+ * kind's root, so humans, agents and projects can reference them reliably
128
+ * (`contact:anna-meier`). Three features with one shape, each switched on
129
+ * by its own block; part of the workspace and synced by the workspace git.
130
+ */
131
+ export interface EntitiesConfig {
132
+ /** false keeps the block but turns the feature off. Default: true. */
133
+ enabled?: boolean;
134
+ /** Where the files live, relative to the project root. Default: kraftwerk-data/<kind> */
135
+ root?: string;
136
+ }
137
+ export declare const ENTITY_KINDS: readonly ["contacts", "places", "events"];
138
+ export type EntityKind = (typeof ENTITY_KINDS)[number];
139
+ /** Where a kind's files live when its `root` is not set. */
140
+ export declare const entityDefaultRoot: (kind: EntityKind) => string;
141
+ /** Absolute root of a kind when its feature is on, undefined otherwise. */
142
+ export declare function entitiesRootFor(project: ResolvedWorkspace, kind: EntityKind): string | undefined;
124
143
  /**
125
144
  * kraftwerk.yml `cloud:` — since the cloud knows machines (core/cloud.ts,
126
145
  * server/cloud.ts), this block only vetoes: `enabled: false` leaves the
@@ -193,6 +212,12 @@ export interface WorkspaceConfig {
193
212
  vibeables?: VibeablesConfig;
194
213
  /** Projects: goal-scoped folders the chats work in. Absent = off. */
195
214
  projects?: ProjectsConfig;
215
+ /** Contacts: people and organisations, one Markdown file each. Absent = off. */
216
+ contacts?: EntitiesConfig;
217
+ /** Places: addresses and points of interest, one Markdown file each. Absent = off. */
218
+ places?: EntitiesConfig;
219
+ /** Events: dated entries (one-off), one Markdown file each. Absent = off. */
220
+ events?: EntitiesConfig;
196
221
  /** The machine's cloud record lists this workspace. Absent = listed; `enabled: false` leaves it out. */
197
222
  cloud?: CloudConfig;
198
223
  }
package/dist/config.js CHANGED
@@ -96,6 +96,16 @@ export function projectsRootFor(project) {
96
96
  return undefined;
97
97
  return path.resolve(project.root, p.root ?? PROJECTS_DEFAULT_ROOT);
98
98
  }
99
+ export const ENTITY_KINDS = ["contacts", "places", "events"];
100
+ /** Where a kind's files live when its `root` is not set. */
101
+ export const entityDefaultRoot = (kind) => `kraftwerk-data/${kind}`;
102
+ /** Absolute root of a kind when its feature is on, undefined otherwise. */
103
+ export function entitiesRootFor(project, kind) {
104
+ const e = project.config[kind];
105
+ if (!e || e.enabled === false)
106
+ return undefined;
107
+ return path.resolve(project.root, e.root ?? entityDefaultRoot(kind));
108
+ }
99
109
  export const CLOUD_DEFAULT_URL = "https://srv.kraftwerk-cloud.netnode.cloud";
100
110
  /** A directory as a .gitignore entry: relative, forward slashes, no trailing slash; undefined outside the root. */
101
111
  export function ignoreEntryFor(projectRoot, dir) {
@@ -231,7 +241,7 @@ export async function addWorkspaceId(configPath) {
231
241
  await writeFile(configPath, out);
232
242
  return id;
233
243
  }
234
- const KNOWN_KEYS = ["id", "name", "slug", "icon", "color", "port", "workflows", "output", "knowledge", "agents", "skills", "switcher", "git", "repos", "vibeables", "projects", "cloud"];
244
+ const KNOWN_KEYS = ["id", "name", "slug", "icon", "color", "port", "workflows", "output", "knowledge", "agents", "skills", "switcher", "git", "repos", "vibeables", "projects", "contacts", "places", "events", "cloud"];
235
245
  /**
236
246
  * Keys an older kraftwerk understood and this one ignores, so the files
237
247
  * that still carry them keep loading: `public:` and `tunnel:` (the
@@ -298,10 +308,10 @@ async function loadConfig(configPath) {
298
308
  config[key] = {};
299
309
  validateRootBlock(configPath, "vibeables", config[key]);
300
310
  }
301
- else if (key === "projects") {
311
+ else if (key === "projects" || key === "contacts" || key === "places" || key === "events") {
302
312
  if (config[key] === null)
303
313
  config[key] = {};
304
- validateRootBlock(configPath, "projects", config[key]);
314
+ validateRootBlock(configPath, key, config[key]);
305
315
  }
306
316
  else if (key === "cloud") {
307
317
  if (config[key] === null)
@@ -403,9 +413,10 @@ function validateGit(configPath, value) {
403
413
  }
404
414
  }
405
415
  /**
406
- * `repos`, `vibeables` and `projects` share one shape: { enabled?, root? }. The root
407
- * must never be the project itself or anything outside it — it is what
408
- * remove/delete operate under, and what the workspace git may stage.
416
+ * `repos`, `vibeables`, `projects`, `contacts`, `places` and `events` share one
417
+ * shape: { enabled?, root? }. The root must never be the project itself or
418
+ * anything outside it — it is what remove/delete operate under, and what
419
+ * the workspace git may stage.
409
420
  */
410
421
  function validateRootBlock(configPath, block, value) {
411
422
  const file = path.basename(configPath);
@@ -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
@@ -7,10 +7,12 @@ import { getAgent, listAgents } from "../agents.js";
7
7
  import { listRepos } from "../repos.js";
8
8
  import { listVibeables, vibeableStatus, VIBEABLE_CONFIG_FILE } from "../vibeables.js";
9
9
  import { projectContext } from "../projects.js";
10
+ import { entitiesContext } from "../entities.js";
10
11
  import { unattendedTimeoutLabel } from "./permissions.js";
11
12
  import { getChannel } from "../channels.js";
12
13
  import { JOURNAL_FILE, journalExcerpt, readJournal } from "../journal.js";
13
14
  import { filesSummary } from "../files.js";
15
+ import { sessionTemp, tempContext } from "../temp.js";
14
16
  /**
15
17
  * What an agent is told about where it works: the skills it may use and
16
18
  * the scope's context block (the workspace, a run, a project, a channel,
@@ -84,8 +86,8 @@ async function agentKnowledgeContext(def) {
84
86
  `part of your job. Consult them before answering questions in their domain, and keep them current ` +
85
87
  `when you learn something durable:\n${lines}\n\n` +
86
88
  `Read with \`npx kraftwerk knowledge list <bundle>\`, \`get <bundle>/<path>\`, \`search <text>\`. ` +
87
- `Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file <tmp.md> --actor ${def.slug}/${def.harness}\` ` +
88
- `(write the markdown to a temp file first; the CLI stamps provenance and maintains index.md/log.md). ` +
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). ` +
89
91
  `Never hand-edit index.md or log.md, and never add \`verified\` yourself — verification is the human's ` +
90
92
  `click in the UI.\n\n`);
91
93
  }
@@ -131,7 +133,8 @@ export async function agentFilesContext(def) {
131
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"}). ` +
132
134
  (linked ? `The folders that are part of your job:\n${linked}\n` : "") +
133
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/. ` +
134
- `Delete nothing there unless asked.\n\n`);
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`);
135
138
  }
136
139
  /**
137
140
  * "## Your vibeables" block: the apps an agent builds and maintains, linked
@@ -309,7 +312,7 @@ async function baseScopeContext(scope, agent) {
309
312
  `- Favor structural markdown (headings, tables, lists) over prose.\n\n` +
310
313
  `ALWAYS write through the kraftwerk CLI so provenance is stamped and the bundle log/index stay maintained:\n` +
311
314
  `- \`npx kraftwerk knowledge init <bundle>\` — new bundle\n` +
312
- `- \`npx kraftwerk knowledge put <bundle>/<path> --file <tmp.md> --actor kraftwerk-chat/${agent}\` — create/update a concept ` +
315
+ `- \`npx kraftwerk knowledge put <bundle>/<path> --file $KRAFTWERK_TEMP/<name>.md --actor kraftwerk-chat/${agent}\` — create/update a concept ` +
313
316
  `(write the markdown to a temp file first; the CLI stamps generated.by/at, appends log.md, regenerates index.md)\n` +
314
317
  `- \`npx kraftwerk knowledge list [bundle]\`, \`get <bundle>/<path>\`, \`search <text>\`, \`validate\`\n` +
315
318
  `Do not hand-edit index.md or log.md (derived/maintained), and do not add \`verified\` yourself — ` +
@@ -348,7 +351,7 @@ async function baseScopeContext(scope, agent) {
348
351
  `## Recent runs\n${runLines || "(none)"}\n\n` +
349
352
  `## Knowledge\nOKF bundles in this project:\n${bundleLines || "(none)"}\n` +
350
353
  `Read with \`npx kraftwerk knowledge list [bundle]\`, \`get <bundle>/<path>\`, \`search <text>\`. ` +
351
- `Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file <tmp.md> --actor kraftwerk-chat/${agent}\` ` +
354
+ `Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file $KRAFTWERK_TEMP/<name>.md --actor kraftwerk-chat/${agent}\` ` +
352
355
  `(stamps provenance, maintains index.md/log.md — never hand-edit those).\n\n` +
353
356
  `## Agents\n${agentLines || "(none)"}\n` +
354
357
  `These are persistent agents (defined under agents/); the user talks to them on the ` +
@@ -391,7 +394,7 @@ export async function scopeContext(meta, seat) {
391
394
  const agent = seat.harness;
392
395
  // The repositories block reads every clone from git, so it runs alongside
393
396
  // the rest instead of adding its spawns to the first prompt's latency.
394
- const [base, repos, vibe, skills, channel, project] = await Promise.all([
397
+ const [base, repos, vibe, skills, channel, project, entities] = await Promise.all([
395
398
  baseScopeContext(scope, agent),
396
399
  scope.kind === "agent" || scope.kind === "kraftwerk" ? reposContext() : Promise.resolve(""),
397
400
  meta.vibeable ? vibeableContext(meta.vibeable, meta.cwd) : Promise.resolve(""),
@@ -399,8 +402,13 @@ export async function scopeContext(meta, seat) {
399
402
  meta.scope.kind === "channel" ? channelContext(meta.scope.slug, seat.key) : Promise.resolve(""),
400
403
  // A channel that works in a project: every member reads the project too, as a member, not as its assistant.
401
404
  meta.scope.kind === "channel" && meta.project ? projectContext(meta.project, `${seat.key}/${agent}`, { member: true }) : Promise.resolve(""),
405
+ // Who, where and when: the workspace's contacts, places and events, wherever the chat works (a run discusses its own artifacts only).
406
+ scope.kind === "run" ? Promise.resolve("") : entitiesContext().catch(() => ""),
402
407
  ]);
403
- return [base, channel, project, repos, vibe, RENDERING_BLOCK, skillsBlock(skills)].filter(Boolean).join("\n\n");
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");
404
412
  }
405
413
  /** The channel block a member agent gets once per process: who is here, how turns work, how to hand over. */
406
414
  async function channelContext(slug, self) {
@@ -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);
@@ -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. */
@@ -0,0 +1,171 @@
1
+ import { type EntityKind, type ResolvedWorkspace } from "../config.js";
2
+ import { recordLine, type SystemOfRecord } from "./records.js";
3
+ import { type LinkState, type RefKind } from "./refs.js";
4
+ /**
5
+ * Contacts, places and events: the workspace's own records of who, where
6
+ * and when. One Markdown file per entry under the kind's root (`contacts:`,
7
+ * `places:`, `events:` in kraftwerk.yml, default kraftwerk-data/<kind>/):
8
+ * the typed fields in YAML frontmatter, free notes in the body. Readable
9
+ * and editable by people and agents, versioned by the workspace git,
10
+ * deleted into the trash.
11
+ *
12
+ * contacts/anna-meier.md title, role, org, email, phone, place, url, tags, records + notes
13
+ * places/netnode-office.md title, address, lat, lng, url, tags, records + notes
14
+ * events/2026-11-03-kickoff.md title, start, end, place, contacts, project, url, tags, records + notes
15
+ *
16
+ * The three kinds share this store; each is a small definition (its
17
+ * fields, how its slug derives, how it sorts, how it reads in one line).
18
+ * Fields that point at other things (a contact's place, an event's place,
19
+ * contacts and project) hold slugs and are resolved through the reference
20
+ * registry (refs.ts), so a missing target is reported, never an error.
21
+ * Systems of record say where the truth lives outside kraftwerk (CRM,
22
+ * newsletter tool, calendar, Google Maps); a place without one gets a
23
+ * Google Maps link derived from its coordinates or address.
24
+ */
25
+ export interface EntityBase {
26
+ slug: string;
27
+ title: string;
28
+ tags: string[];
29
+ records: SystemOfRecord[];
30
+ /** Frontmatter keys this kind does not define; kept verbatim across saves. */
31
+ extra?: Record<string, unknown>;
32
+ }
33
+ export interface ContactDef extends EntityBase {
34
+ role?: string;
35
+ org?: string;
36
+ email?: string;
37
+ phone?: string;
38
+ /** A place's slug (place:<slug>). */
39
+ place?: string;
40
+ url?: string;
41
+ }
42
+ export interface PlaceDef extends EntityBase {
43
+ address?: string;
44
+ lat?: number;
45
+ lng?: number;
46
+ url?: string;
47
+ /** Derived, never written: the google-maps record's URL, else one from lat/lng or the address. */
48
+ mapsUrl?: string;
49
+ }
50
+ export interface EventDef extends EntityBase {
51
+ /** ISO date (2026-11-03) or date-time (2026-11-03T14:00, optionally with seconds and a zone). */
52
+ start: string;
53
+ end?: string;
54
+ /** A place's slug. */
55
+ place?: string;
56
+ /** Contact slugs. */
57
+ contacts: string[];
58
+ /** A project's slug. */
59
+ project?: string;
60
+ url?: string;
61
+ }
62
+ export type FieldType = "text" | "email" | "url" | "number" | "date" | "list" | "ref" | "refs";
63
+ /** One typed frontmatter field beyond the shared ones (title, tags, records); drives validation, the CLI options and the web form. */
64
+ export interface FieldSpec {
65
+ name: string;
66
+ label: string;
67
+ type: FieldType;
68
+ /** For ref/refs: what the slugs point at. */
69
+ refKind?: RefKind;
70
+ required?: boolean;
71
+ /** One line for the CLI option and the form's placeholder. */
72
+ hint: string;
73
+ }
74
+ export interface EntityKindDef<D extends EntityBase = EntityBase, K extends EntityKind = EntityKind> {
75
+ /** The literal kind, so a factory built on the definition yields `contacts.list` and `/api/contacts/:slug` as types. */
76
+ kind: K;
77
+ /** The token in references: contact in contact:anna-meier. */
78
+ singular: string;
79
+ /** Material icon name the UI shows for the kind. */
80
+ icon: string;
81
+ fields: FieldSpec[];
82
+ /** The default slug of a new entry. */
83
+ slugFor(def: D): string;
84
+ /** List order. */
85
+ sort(a: D, b: D): number;
86
+ /** What says more than the title (role and org, address, date); "" when there is nothing. */
87
+ detail(def: D): string;
88
+ /** One line that stands for the entry in lists, link labels and agent context: the title with its detail. */
89
+ describe(def: D): string;
90
+ /** Checks across fields (an event's end after its start); throws the one error to show. */
91
+ validate?(def: D): void;
92
+ /** Fields computed on read and never written (a place's maps URL); every derived key is present, undefined when there is no value. */
93
+ derive?(def: D): Partial<D>;
94
+ /** The days an entry covers, for a `from`/`to` filter on the list: an entry counts while it is still running. */
95
+ span?(def: D): {
96
+ start: string;
97
+ end: string;
98
+ };
99
+ }
100
+ export type EntitySummary<D extends EntityBase> = D & {
101
+ /** Absolute file path. */
102
+ path: string;
103
+ updatedAt?: string;
104
+ /** Set when the file's frontmatter is unusable; the rest shows defaults. */
105
+ configError?: string;
106
+ };
107
+ export type EntityDetail<D extends EntityBase> = EntitySummary<D> & {
108
+ /** The Markdown body. */
109
+ notes: string;
110
+ /** Per ref field (place, contacts, project): the referenced slugs and whether each exists. */
111
+ links: Record<string, LinkState[]>;
112
+ };
113
+ export interface EntitiesView<D extends EntityBase> {
114
+ enabled: boolean;
115
+ root?: string;
116
+ items: EntitySummary<D>[];
117
+ /** Set when the feature is off or the root cannot be read. */
118
+ error?: string;
119
+ }
120
+ /** Fields of a create or save: shared ones, the kind's, plus notes; every one optional. */
121
+ export type EntityInput = Record<string, unknown> & {
122
+ title?: string;
123
+ notes?: string;
124
+ slug?: string;
125
+ };
126
+ export declare const CONTACTS: EntityKindDef<ContactDef, "contacts">;
127
+ export declare const PLACES: EntityKindDef<PlaceDef, "places">;
128
+ /** The Google Maps link of a place: its google-maps record, else its coordinates, else its address; undefined with none of them. */
129
+ export declare function mapsUrl(p: PlaceDef): string | undefined;
130
+ export declare const EVENTS: EntityKindDef<EventDef, "events">;
131
+ /**
132
+ * When an event's ISO value happens, as a timestamp: a plain day is local
133
+ * midnight (its end, when asked, the last moment of that day), a date-time
134
+ * is what Date reads (local without a zone, else the zone it names) — so
135
+ * values with different offsets order by the instant, not by their text.
136
+ */
137
+ export declare function eventTime(iso: string, endOfDay?: boolean): number;
138
+ export declare const ENTITY_DEFS: {
139
+ [K in EntityKind]: EntityKindDef<any>;
140
+ };
141
+ export declare const entityDef: (kind: string) => EntityKindDef | undefined;
142
+ export declare function openEntities(kind: EntityKind): Promise<{
143
+ project?: ResolvedWorkspace;
144
+ root?: string;
145
+ off?: boolean;
146
+ error?: string;
147
+ }>;
148
+ /** Make sure the root exists; runs when the feature is switched on. */
149
+ export declare function ensureEntitiesRoot(kind: EntityKind): Promise<void>;
150
+ export declare function safeEntitySlug(def: EntityKindDef<any>, slug: string): string;
151
+ /** Every entry under the kind's root in the kind's order; `from`/`to` narrow kinds with a date (events) by it. */
152
+ export declare function listEntities<D extends EntityBase>(def: EntityKindDef<D>, range?: {
153
+ from?: string;
154
+ to?: string;
155
+ }): Promise<EntitiesView<D>>;
156
+ /** One entry with its notes and link states; null when there is no such file. */
157
+ export declare function getEntity<D extends EntityBase>(def: EntityKindDef<D>, slug: string): Promise<EntityDetail<D> | null>;
158
+ export declare function createEntity<D extends EntityBase>(def: EntityKindDef<D>, input: EntityInput): Promise<EntityDetail<D>>;
159
+ /** Merge the given fields into the frontmatter (and the notes when given); lists replace wholesale, "" clears a field. */
160
+ export declare function saveEntity<D extends EntityBase>(def: EntityKindDef<D>, slug: string, input: EntityInput): Promise<EntityDetail<D>>;
161
+ /** Move the file to the trash. */
162
+ export declare function deleteEntity(def: EntityKindDef<any>, slug: string): Promise<void>;
163
+ /** One line for an entry in a context block: its token, how it reads, its records. */
164
+ export declare function entityLine<D extends EntityBase>(def: EntityKindDef<D>, d: D): string;
165
+ /**
166
+ * The "## Contacts, places and events" block every chat starts with when at
167
+ * least one of the kinds is on: what exists (counts, the next events), how
168
+ * to reference an entry, and the CLI that reads and writes them.
169
+ */
170
+ export declare function entitiesContext(): Promise<string>;
171
+ export { recordLine };