@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
package/README.md CHANGED
@@ -130,6 +130,9 @@ set up the rest. From there:
130
130
  Open one and chat with it; its sessions continue where they ended.
131
131
  - **Projects** gather a goal, a brief and links; every chat in a project
132
132
  starts with that context.
133
+ - **Contacts, places and events** are the workspace's records of who, where
134
+ and when — one Markdown file each, referenced anywhere as `contact:<slug>`,
135
+ `place:<slug>`, `event:<slug>`.
133
136
  - **Workflows** are repeatable processes with checked results; run one from
134
137
  its page or with `kraftwerk run <workflow> "<request>"`.
135
138
  - **Knowledge** is the shared, human-verified context agents read.
@@ -256,6 +259,8 @@ kraftwerk workspaces # every workspace on this machine; works
256
259
  kraftwerk devices # paired devices (a phone, another computer); devices pair|revoke
257
260
  kraftwerk remote on|off|pair # reach this machine from anywhere through the relay, end to end encrypted
258
261
  kraftwerk projects # goal-scoped project folders; projects create|show|link|log|remove
262
+ kraftwerk contacts|places|events # who, where and when, one Markdown file each; … show|create|set|remove, events --from/--to
263
+ kraftwerk ref <kind:slug> # resolve a reference (contact:anna-meier, project:relaunch, …); --file <md> resolves every one in a file
259
264
  kraftwerk trash # what was deleted; trash restore|purge <id>, trash empty
260
265
  kraftwerk journal <agent> ["<entry>"] # an agent's memory across sessions: print it, add a line (--kind)
261
266
  kraftwerk doctor # preflight: harness CLIs, docker, workflows, declared env vars, legacy leftovers
@@ -295,7 +300,9 @@ Optional, at the project root, and also the root marker for the walk-up. All
295
300
  fields are optional. `workflows:` sets the workflows root, `output:` the
296
301
  run-artifact directory (default `output/`), `knowledge:` the OKF bundle root
297
302
  (default `knowledge/`), and `agents:` the agent-definition root (default
298
- `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).
299
306
  `cloud: { enabled: false }` leaves the workspace out of this machine's
300
307
  record in the [kraftwerk cloud](#this-machine-in-the-kraftwerk-cloud).
301
308
 
@@ -337,7 +344,8 @@ projects (all but archived ones) and the channels of every workspace on this
337
344
  machine, grouped by workspace and running or not (a stopped one is started
338
345
  on the way), read from the workspace registry under `~/.kraftwerk/workspaces`,
339
346
  which every inspector keeps current with its roster, project and channel
340
- lists.
347
+ lists — and this workspace's contacts, places and events, by title, role,
348
+ organisation, address or date.
341
349
 
342
350
  ### Triggering from CI, cron, or webhooks
343
351
 
@@ -626,6 +634,84 @@ kraftwerk projects remove relaunch-netnode-ch
626
634
  The registry of workspaces on this machine, which answered to
627
635
  `kraftwerk projects` until 0.48, is `kraftwerk workspaces` now.
628
636
 
637
+ ## Contacts, places and events
638
+
639
+ Who, where and when, kept by the workspace itself so people and agents can
640
+ point at them reliably. Three features with one shape — turn each on in
641
+ `kraftwerk.yml` (or its checkbox in settings):
642
+
643
+ ```yaml
644
+ contacts: # people and organisations
645
+ places: # addresses and points of interest
646
+ events: # dated one-off entries
647
+ root: kraftwerk-data/events # default kraftwerk-data/<kind>; part of the workspace, synced like agents and knowledge
648
+ ```
649
+
650
+ One Markdown file per entry: the typed fields in YAML frontmatter, free
651
+ notes in the body. Readable and editable by hand, by agents and by the UI;
652
+ versioned with the workspace, deleted into the trash.
653
+
654
+ ```
655
+ kraftwerk-data/contacts/anna-meier.md # title, role, org, email, phone, place, url, tags, records + notes
656
+ kraftwerk-data/places/netnode-office.md # title, address, lat, lng, url, tags, records + notes
657
+ kraftwerk-data/events/2026-11-03-kickoff.md # title, start, end, place, contacts, project, url, tags, records + notes
658
+ ```
659
+
660
+ ```markdown
661
+ ---
662
+ title: Kickoff
663
+ start: 2026-11-03T14:00 # ISO date or date-time; events are one-off (recurrence lives in the calendar entry)
664
+ end: 2026-11-03T16:00
665
+ place: netnode-office # a place's slug
666
+ contacts: [anna-meier] # contact slugs
667
+ project: relaunch # a project's slug
668
+ records:
669
+ - kind: calendar
670
+ url: https://calendar.google.com/event?eid=…
671
+ ---
672
+ Agenda: scope, timeline, who does what.
673
+ ```
674
+
675
+ **References.** Fields that point at other entries hold slugs, and anything
676
+ in the workspace is referenced in text as `kind:slug` — `contact:anna-meier`,
677
+ `place:netnode-office`, `event:2026-11-03-kickoff`, and likewise `project:`,
678
+ `agent:`, `workflow:`, `repo:`, `vibeable:`, `knowledge:`. The same resolver
679
+ serves a project's links, an event's place and contacts, a token in notes or
680
+ in a chat reply, the ⌘K palette and the context an agent starts with: a
681
+ target that does not exist is reported ("not found"), never an error, and
682
+ only these tokens match, so `https:` never does. Projects link to contacts,
683
+ places and events like to anything else (`kraftwerk projects link <slug>
684
+ contacts anna-meier`), and a project chat starts with them as one-liners.
685
+
686
+ **Systems of record.** As on a project: where the truth lives outside
687
+ kraftwerk — a contact's CRM record and newsletter subscription (`crm`,
688
+ `newsletter`), an event's calendar entry (`calendar`), a place on Google
689
+ Maps (`google-maps`). A place without a maps record gets a Google Maps link
690
+ derived from its coordinates or its address.
691
+
692
+ **Agents** get a "Contacts, places and events" block in every chat: what
693
+ exists, the next events, the token syntax and the CLI — they read and write
694
+ the files through it, never by hand:
695
+
696
+ ```bash
697
+ kraftwerk contacts # list (--json); places, events the same
698
+ kraftwerk contacts create "Anna Meier" --org ACME --email anna@acme.ch --place netnode-office
699
+ kraftwerk contacts show anna-meier # fields, records, references with their state, notes
700
+ kraftwerk contacts set anna-meier --role CTO --email "" # "" clears a field; --notes-file replaces the notes
701
+ kraftwerk places create "NETNODE Office" --address "Zentralstrasse 1, 6003 Luzern" --lat 47.05 --lng 8.3
702
+ kraftwerk places show netnode-office # prints the Google Maps link
703
+ kraftwerk events create "Kickoff" --start 2026-11-03T14:00 --place netnode-office --contacts anna-meier --project relaunch
704
+ kraftwerk events --from 2026-11-01 --to 2026-11-30 # the month's events
705
+ kraftwerk ref contact:anna-meier # ✔ with its label, or ? not found (exit 1); --file notes.md checks a whole file
706
+ kraftwerk contacts remove anna-meier # to the trash
707
+ ```
708
+
709
+ Over the API each kind is its own route family — `contacts.list|get|create|save|delete`,
710
+ `places.*`, `events.*` (`GET /api/events?from=&to=`) — and `refs.resolve`
711
+ resolves tokens or every reference found in a text. In the inspector each
712
+ kind is a page of the workspace (expert mode) with the list beside the open
713
+ entry; a project's context column shows its contacts, places and events.
714
+
629
715
  ## Files
630
716
 
631
717
  Files are the material of the work, in any format: briefings, contracts,
@@ -1,6 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { DAEMON_PORT, daemonState, lastOpenRoots, setDaemonVersion, writeDaemonFile } from "../core/daemon.js";
3
3
  import { tildify } from "../core/instances.js";
4
+ import { log } from "../core/log.js";
4
5
  import { networkUrls } from "../server/bind.js";
5
6
  import { startMachineCloud } from "../server/cloud.js";
6
7
  import { startPushWatcher } from "../server/push.js";
@@ -44,31 +45,31 @@ export async function runDaemon(opts) {
44
45
  await hub.open(root);
45
46
  }
46
47
  catch (err) {
47
- console.log(`${chalk.yellow("⚠")} not reopened: ${tildify(root)} — ${err.message}`);
48
+ log("warn", "workspace", `not reopened: ${tildify(root)} — ${err.message}`);
48
49
  }
49
50
  }
50
- console.log(`${chalk.green("✔")} kraftwerk daemon on ${chalk.cyan(`http://localhost:${hub.port}`)} ${chalk.dim(`(pid ${process.pid}, ${new Date().toISOString()})`)}`);
51
+ log("info", "daemon", `listening on http://localhost:${hub.port} (pid ${process.pid}, v${await getPkgVersion()})`);
51
52
  for (const o of hub.list())
52
- console.log(` ${chalk.cyan(o.url)} ${chalk.dim(o.name)}${o.aliasPort ? chalk.dim(` · also :${o.aliasPort}`) : ""}`);
53
+ log("info", "workspace", `open: ${o.url} (${o.name})${o.aliasPort ? ` · also :${o.aliasPort}` : ""}`);
53
54
  if (!hub.list().length)
54
- console.log(chalk.dim(" no workspace open yet — `kraftwerk ui` in a workspace folder opens it here"));
55
+ log("info", "daemon", "no workspace open yet — `kraftwerk ui` in a workspace folder opens it here");
55
56
  // The machine in the kraftwerk cloud: registered once, the open workspaces as its children (`kraftwerk cloud`, `kraftwerk login`).
56
57
  const cloud = await startMachineCloud({ version: await getPkgVersion(), workspaces: () => hub.list(), owner: hub });
57
58
  if (cloud.state === "connected") {
58
- console.log(`${chalk.green("✔")} Cloud: ${chalk.cyan(cloud.url)} ${chalk.dim(cloud.account ? `(claimed by ${cloud.account})` : `(unclaimed — \`kraftwerk login\` claims this machine)`)}`);
59
+ log("info", "cloud", `registered at ${cloud.url} ${cloud.account ? `(claimed by ${cloud.account})` : "(unclaimed — `kraftwerk login` claims this machine)"}`);
59
60
  }
60
61
  else if (cloud.state === "error") {
61
- console.log(`${chalk.yellow("⚠")} Cloud: ${cloud.url} unreachable — ${cloud.error ?? "no answer"} ${chalk.dim("(retrying; nothing else depends on it)")}`);
62
+ log("warn", "cloud", `${cloud.url} unreachable — ${cloud.error ?? "no answer"} (retrying; nothing else depends on it)`);
62
63
  }
63
64
  // The relay (`kraftwerk remote on`): devices reach this machine from anywhere, end to end encrypted.
64
- const relay = await applyRelay(hub.port, await getPkgVersion(), (line) => console.log(chalk.dim(line)));
65
+ const relay = await applyRelay(hub.port, await getPkgVersion(), (level, line) => log(level, "relay", line));
65
66
  // What needs you, pushed to the phones that asked (through the relay, sealed per device).
66
67
  startPushWatcher(hub, relayPush);
67
68
  if (relay.state !== "off")
68
- console.log(`${chalk.green("✔")} Reachable from anywhere through ${chalk.cyan(relay.url ?? "")} ${chalk.dim("(`kraftwerk remote pair` pairs a device)")}`);
69
+ log("info", "relay", `reachable from anywhere through ${relay.url ?? ""} (\`kraftwerk remote pair\` pairs a device)`);
69
70
  const lan = networkUrls(hub.port);
70
71
  if (lan.length)
71
- console.log(`${chalk.green("✔")} On your network: ${lan.map((u) => chalk.cyan(u)).join(", ")} ${chalk.dim("(add /w/<slug>/; devices pair first)")}`);
72
+ log("info", "daemon", `on your network: ${lan.join(", ")} (add /w/<slug>/; devices pair first)`);
72
73
  }
73
74
  export function registerDaemonCommand(program) {
74
75
  const daemon = program
@@ -95,7 +95,7 @@ export async function runDoctor(cwd, opts = {}) {
95
95
  else {
96
96
  const cfg = project.config;
97
97
  const keys = Object.keys(cfg);
98
- // Blocks (git, repos, vibeables, projects, cloud) print as their keys, not [object Object].
98
+ // Blocks (git, repos, vibeables, projects, contacts, places, events, cloud) print as their keys, not [object Object].
99
99
  const show = (v) => typeof v === "object" && v !== null
100
100
  ? Array.isArray(v)
101
101
  ? `[${v.length}]`
@@ -0,0 +1,8 @@
1
+ import type { Command } from "commander";
2
+ import { type EntityKindDef } from "../core/entities.js";
3
+ export declare function registerEntityCommands(program: Command, def: EntityKindDef<any>): void;
4
+ export declare const registerContactCommands: (program: Command) => void;
5
+ export declare const registerPlaceCommands: (program: Command) => void;
6
+ export declare const registerEventCommands: (program: Command) => void;
7
+ /** `kraftwerk ref` — resolve one reference, or every reference in a file. */
8
+ export declare function registerRefCommand(program: Command): void;
@@ -0,0 +1,222 @@
1
+ import chalk from "chalk";
2
+ import Table from "cli-table3";
3
+ import { readFile } from "node:fs/promises";
4
+ import { CONTACTS, createEntity, deleteEntity, EVENTS, getEntity, listEntities, PLACES, saveEntity } from "../core/entities.js";
5
+ import { recordLine } from "../core/records.js";
6
+ import { findRefs, resolveRef, resolveTokens } from "../core/refs.js";
7
+ import { initContext as prepare } from "./routines.js";
8
+ import { fmtAgo } from "./workspaces.js";
9
+ /**
10
+ * `kraftwerk contacts|places|events` — the workspace's records of who,
11
+ * where and when, one Markdown file each (core/entities.ts). One command
12
+ * group per kind, built from the kind's definition so the options are its
13
+ * fields; the same module the inspector uses, so an entry created here
14
+ * shows up in the UI at once, and an agent uses these to keep them current:
15
+ *
16
+ * kraftwerk contacts list (--json)
17
+ * kraftwerk contacts show <slug> fields, records, links (found / missing), notes
18
+ * kraftwerk contacts create <title> [--org ..] [--email ..] [--place <slug>] [--tags a,b] [--notes ..]
19
+ * kraftwerk contacts set <slug> [--role ..] [--notes-file <md>] change fields; "" clears one
20
+ * kraftwerk contacts remove <slug> move the file to the trash
21
+ * kraftwerk places ... --address, --lat, --lng, --url; show prints the Google Maps link
22
+ * kraftwerk events [--from <date>] [--to <date>] --start (required), --end, --place, --contacts a,b, --project
23
+ * kraftwerk ref <kind:slug> | --file <md> resolve references (exit 1 when a target is missing)
24
+ */
25
+ const die = (msg) => {
26
+ console.error(chalk.red(msg));
27
+ process.exit(1);
28
+ };
29
+ const mark = (found) => (found ? chalk.green("✔") : chalk.yellow("?"));
30
+ function printDetail(def, e) {
31
+ const rec = e;
32
+ console.log(`${chalk.bold(e.title)} ${chalk.dim(`(${def.singular}:${e.slug})`)}`);
33
+ console.log(chalk.dim(e.path));
34
+ if (e.configError)
35
+ console.log(chalk.red(`frontmatter: ${e.configError}`));
36
+ for (const f of def.fields) {
37
+ const v = rec[f.name];
38
+ if (v === undefined || (Array.isArray(v) && v.length === 0))
39
+ continue;
40
+ const links = e.links[f.name];
41
+ const shown = links ? links.map((l) => `${mark(l.found)} ${l.slug}${l.label ? chalk.dim(` ${l.label}`) : l.found ? "" : chalk.dim(" (not found)")}`).join(", ") : Array.isArray(v) ? v.join(", ") : String(v);
42
+ console.log(`${chalk.dim(f.label.padEnd(12))} ${shown}`);
43
+ }
44
+ if (def.kind === "places" && rec.mapsUrl)
45
+ console.log(`${chalk.dim("map".padEnd(12))} ${rec.mapsUrl}`);
46
+ if (e.tags.length)
47
+ console.log(`${chalk.dim("tags".padEnd(12))} ${e.tags.join(", ")}`);
48
+ for (const [k, v] of Object.entries(e.extra ?? {}))
49
+ console.log(`${chalk.dim(k.padEnd(12))} ${typeof v === "string" ? v : JSON.stringify(v)}`);
50
+ if (e.records.length) {
51
+ console.log(`\n${chalk.bold("systems of record")}`);
52
+ for (const r of e.records)
53
+ console.log(recordLine(r));
54
+ }
55
+ if (e.notes.trim())
56
+ console.log(`\n${chalk.bold("notes")}\n${e.notes.trim()}`);
57
+ }
58
+ /** The fields given on the command line, as the store takes them (comma lists split there; "" clears). */
59
+ function fieldsOf(def, opts) {
60
+ const out = {};
61
+ for (const f of def.fields)
62
+ if (opts[f.name] !== undefined)
63
+ out[f.name] = opts[f.name];
64
+ if (opts.tags !== undefined)
65
+ out.tags = opts.tags;
66
+ if (opts.title !== undefined)
67
+ out.title = opts.title;
68
+ return out;
69
+ }
70
+ export function registerEntityCommands(program, def) {
71
+ const what = { contacts: "people and organisations", places: "addresses and points of interest", events: "dated one-off entries" }[def.kind];
72
+ const group = program.command(def.kind).description(`${def.kind[0].toUpperCase()}${def.kind.slice(1)}: ${what}, one Markdown file each — list, show, create, set, remove`);
73
+ const withFields = (cmd) => {
74
+ for (const f of def.fields)
75
+ cmd.option(`--${f.name} <value>`, `${f.hint}${f.required ? " (required)" : ""}`);
76
+ cmd.option("--tags <list>", "Tags, comma-separated");
77
+ return cmd;
78
+ };
79
+ const list = group
80
+ .command("list", { isDefault: true })
81
+ .description(`List the ${def.kind} under the ${def.kind} root`)
82
+ .option("--json", "Machine-readable output");
83
+ if (def.span)
84
+ list.option("--from <date>", "Only entries still running on or after this date (ISO)").option("--to <date>", "Only entries that have started by this date (ISO)");
85
+ list.action(async (opts) => {
86
+ await prepare();
87
+ const view = await listEntities(def, { from: opts.from, to: opts.to });
88
+ if (opts.json) {
89
+ console.log(JSON.stringify(view, null, 2));
90
+ return;
91
+ }
92
+ if (!view.enabled)
93
+ die(view.error ?? `${def.kind} are off`);
94
+ if (view.items.length === 0) {
95
+ console.log(chalk.dim(`No ${def.kind} under ${view.root} yet — \`kraftwerk ${def.kind} create "<title>"\` adds one.`));
96
+ return;
97
+ }
98
+ const table = new Table({ head: [def.singular, "details", "changed"].map((h) => chalk.bold(h)), wordWrap: true, colWidths: [30, 56, 14] });
99
+ for (const e of view.items) {
100
+ table.push([`${chalk.cyan(e.title)}\n${chalk.dim(e.slug)}`, e.configError ? chalk.red(e.configError) : def.detail(e) || chalk.dim("—"), chalk.dim(fmtAgo(e.updatedAt))]);
101
+ }
102
+ console.log(table.toString());
103
+ console.log(chalk.dim(view.root ?? ""));
104
+ });
105
+ group
106
+ .command("show")
107
+ .description(`Show one ${def.singular}: fields, records, links with their state, notes`)
108
+ .argument("<slug>", "File name under the root, without .md")
109
+ .option("--json", "Machine-readable output")
110
+ .action(async (slug, opts) => {
111
+ await prepare();
112
+ try {
113
+ const e = await getEntity(def, slug);
114
+ if (!e)
115
+ die(`no ${def.singular} "${slug}"`);
116
+ if (opts.json)
117
+ console.log(JSON.stringify(e, null, 2));
118
+ else
119
+ printDetail(def, e);
120
+ }
121
+ catch (err) {
122
+ die(err.message);
123
+ }
124
+ });
125
+ withFields(group
126
+ .command("create")
127
+ .description(`Create a ${def.singular} file from its title and fields`)
128
+ .argument("<title>", "Display title; the file name derives from it unless --slug is given")
129
+ .option("--slug <slug>", "File name under the root (lowercase, digits, dashes)")
130
+ .option("--notes <text>", "The Markdown body")
131
+ .option("--notes-file <path>", "Read the body from a file")
132
+ .option("--json", `Print the new ${def.singular} as JSON`)).action(async (title, opts) => {
133
+ await prepare();
134
+ try {
135
+ const notes = opts.notesFile ? await readFile(opts.notesFile, "utf8") : opts.notes;
136
+ const e = await createEntity(def, { ...fieldsOf(def, opts), title, slug: opts.slug, ...(notes !== undefined ? { notes } : {}) });
137
+ if (opts.json)
138
+ console.log(JSON.stringify(e, null, 2));
139
+ else {
140
+ const missing = Object.values(e.links).flat().filter((l) => !l.found);
141
+ console.log(`${chalk.green("✔")} ${chalk.cyan(`${def.singular}:${e.slug}`)} → ${e.path}${missing.length ? chalk.yellow(` (not found in this workspace yet: ${missing.map((l) => l.slug).join(", ")})`) : ""}`);
142
+ }
143
+ }
144
+ catch (err) {
145
+ die(err.message);
146
+ }
147
+ });
148
+ withFields(group
149
+ .command("set")
150
+ .description(`Change a ${def.singular}'s fields ("" clears one) or notes`)
151
+ .argument("<slug>", "File name under the root")
152
+ .option("--title <text>", "New title")
153
+ .option("--notes <text>", "Replace the Markdown body")
154
+ .option("--notes-file <path>", "Replace the body with a file's content")
155
+ .option("--json", `Print the ${def.singular} as JSON`)).action(async (slug, opts) => {
156
+ await prepare();
157
+ try {
158
+ const notes = opts.notesFile ? await readFile(opts.notesFile, "utf8") : opts.notes;
159
+ const input = { ...fieldsOf(def, opts), ...(notes !== undefined ? { notes } : {}) };
160
+ if (Object.keys(input).length === 0)
161
+ die(`nothing to set — pass --${def.fields.map((f) => f.name).join(", --")}, --tags, --title or --notes`);
162
+ const e = await saveEntity(def, slug, input);
163
+ if (opts.json)
164
+ console.log(JSON.stringify(e, null, 2));
165
+ else
166
+ console.log(`${chalk.green("✔")} ${chalk.cyan(`${def.singular}:${e.slug}`)} — ${def.describe(e)}`);
167
+ }
168
+ catch (err) {
169
+ die(err.message);
170
+ }
171
+ });
172
+ group
173
+ .command("remove")
174
+ .description(`Move a ${def.singular} file to the trash (\`kraftwerk trash\` puts it back)`)
175
+ .argument("<slug>", "File name under the root")
176
+ .action(async (slug) => {
177
+ await prepare();
178
+ try {
179
+ await deleteEntity(def, slug);
180
+ console.log(`${chalk.green("✔")} moved ${chalk.cyan(`${def.singular}:${slug}`)} to the trash`);
181
+ }
182
+ catch (err) {
183
+ die(err.message);
184
+ }
185
+ });
186
+ }
187
+ export const registerContactCommands = (program) => registerEntityCommands(program, CONTACTS);
188
+ export const registerPlaceCommands = (program) => registerEntityCommands(program, PLACES);
189
+ export const registerEventCommands = (program) => registerEntityCommands(program, EVENTS);
190
+ const refLine = (r) => `${mark(r.found)} ${r.token}${r.label ? chalk.dim(` ${r.label}`) : r.found ? "" : chalk.dim(" (not found)")}`;
191
+ /** `kraftwerk ref` — resolve one reference, or every reference in a file. */
192
+ export function registerRefCommand(program) {
193
+ program
194
+ .command("ref")
195
+ .description("Resolve a reference (contact:anna-meier, place:…, event:…, project:…, agent:…, workflow:…, repo:…, vibeable:…, knowledge:…) or every one in a file")
196
+ .argument("[ref]", "The reference, <kind>:<slug>")
197
+ .option("--file <path>", "Resolve every reference found in this file")
198
+ .option("--json", "Machine-readable output")
199
+ .action(async (ref, opts) => {
200
+ await prepare();
201
+ let refs;
202
+ if (opts.file)
203
+ refs = await resolveTokens(findRefs(await readFile(opts.file, "utf8").catch((e) => die(e.message))));
204
+ else if (ref) {
205
+ const r = await resolveRef(ref);
206
+ if (!r)
207
+ die(`not a reference: "${ref}" (expected <kind>:<slug>, e.g. contact:anna-meier)`);
208
+ refs = [r];
209
+ }
210
+ else
211
+ die("pass a reference like contact:anna-meier, or --file <path>");
212
+ if (opts.json)
213
+ console.log(JSON.stringify({ refs: refs }, null, 2));
214
+ else if (refs.length === 0)
215
+ console.log(chalk.dim("no references found"));
216
+ else
217
+ for (const r of refs)
218
+ console.log(refLine(r));
219
+ if (refs.some((r) => !r.found))
220
+ process.exit(1);
221
+ });
222
+ }
package/dist/cli/init.js CHANGED
@@ -31,6 +31,9 @@ skills: ${DATA_DIR}/skills # workspace skills (shared instruction packag
31
31
  # root: ${DATA_DIR}/repos # clones land here (git-ignored); a bare \`repos:\` uses repos/ instead
32
32
  # vibeables: # small apps built live in a chat with a preview pane (uncomment to enable; one folder per app under ${DATA_DIR}/vibeables, versioned with the workspace)
33
33
  # projects: # goal-scoped folders — brief, systems of record, links to knowledge/vibeables/repos/workflows/agents — that chats work in (uncomment to enable; one folder per project under ${DATA_DIR}/projects)
34
+ # contacts: # people and organisations, one Markdown file each under ${DATA_DIR}/contacts — referenced as contact:<slug> (uncomment to enable)
35
+ # places: # addresses and points of interest, one Markdown file each under ${DATA_DIR}/places — referenced as place:<slug> (uncomment to enable)
36
+ # events: # dated one-off entries with place, contacts and project, one Markdown file each under ${DATA_DIR}/events — referenced as event:<slug> (uncomment to enable)
34
37
  # cloud: # the daemon registers this MACHINE with the kraftwerk cloud and lists this workspace (\`kraftwerk login\` claims the machine)
35
38
  # enabled: false # uncomment both lines to leave this workspace out (\`kraftwerk cloud off\` turns the cloud off for the whole machine)
36
39
  `;
@@ -136,11 +139,12 @@ export async function runInit(cwd) {
136
139
  created.push(bundleRel);
137
140
  }
138
141
  // .gitignore: the output dir, the repos root (clones must never become
139
- // gitlinks of the workspace repo) and .env (secrets). A missing file gets
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
140
144
  // all in one write; an existing one only the entries it lacks.
141
145
  const gitignorePath = path.join(cwd, ".gitignore");
142
146
  const gitignore = (await readFile(gitignorePath, "utf8").catch(() => null)) ?? null;
143
- const entries = [`${DATA_DIR}/output/`, `${DATA_DIR}/repos/`, DOTENV_FILE];
147
+ const entries = [`${DATA_DIR}/output/`, `${DATA_DIR}/repos/`, `${DATA_DIR}/temp/`, DOTENV_FILE];
144
148
  if (gitignore === null) {
145
149
  await writeFile(gitignorePath, entries.map((e) => `${e}\n`).join(""));
146
150
  created.push(".gitignore");
@@ -13,10 +13,12 @@ import { runDoctor } from "./doctor.js";
13
13
  import { runInit } from "./init.js";
14
14
  import { registerKnowledgeCommands } from "./knowledge.js";
15
15
  import { registerProjectCommands } from "./projects.js";
16
+ import { registerContactCommands, registerEventCommands, registerPlaceCommands, registerRefCommand } from "./entities.js";
16
17
  import { registerWorkspaceCommands } from "./workspaces.js";
17
18
  import { registerRepoCommands } from "./repos.js";
18
19
  import { registerVibeableCommands } from "./vibeables.js";
19
20
  import { registerTrashCommands } from "./trash.js";
21
+ import { registerTempCommands } from "./temp.js";
20
22
  import { registerDevicesCommands } from "./devices.js";
21
23
  import { registerDaemonCommand } from "./daemon.js";
22
24
  import { registerRemoteCommand } from "./remote.js";
@@ -42,10 +44,13 @@ import { runUi } from "./ui.js";
42
44
  * kraftwerk ui start the inspector web UI (localhost:1981)
43
45
  * kraftwerk workspaces ... known workspaces on this machine (list/start/stop/forget)
44
46
  * kraftwerk projects ... goal-scoped project folders (list/create/show/link/log/remove)
47
+ * kraftwerk contacts|places|events who, where and when: one Markdown file each (list/show/create/set/remove)
48
+ * kraftwerk ref <kind:slug> resolve a reference (contact:anna-meier) or every one in a file
45
49
  * kraftwerk vibeables ... small apps built live in a chat (list/create/remove)
46
50
  * kraftwerk repos ... repositories the agents work on (list/add/update/remove)
47
51
  * kraftwerk journal <agent> [entry] an agent's memory across sessions: print it, add a line
48
52
  * kraftwerk trash ... what was deleted: list, restore, purge, empty
53
+ * kraftwerk temp [clean] the sessions' scratch folders (TMPDIR of every agent): list, clean
49
54
  * kraftwerk devices ... paired devices: list, pair one, revoke one
50
55
  * kraftwerk daemon the machine's kraftwerk: every open workspace at <slug>.localhost
51
56
  * kraftwerk cloud [on|off|status] this machine in the kraftwerk cloud (the daemon registers it)
@@ -299,9 +304,14 @@ registerKnowledgeCommands(program);
299
304
  registerRoutineCommands(program);
300
305
  registerWorkspaceCommands(program);
301
306
  registerProjectCommands(program);
307
+ registerContactCommands(program);
308
+ registerPlaceCommands(program);
309
+ registerEventCommands(program);
310
+ registerRefCommand(program);
302
311
  registerRepoCommands(program);
303
312
  registerVibeableCommands(program);
304
313
  registerTrashCommands(program);
314
+ registerTempCommands(program);
305
315
  registerDevicesCommands(program);
306
316
  registerDaemonCommand(program);
307
317
  registerRemoteCommand(program);
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerTempCommands(program: Command): void;
@@ -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/cli/ui.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
+ import { log } from "../core/log.js";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { fileURLToPath } from "node:url";
@@ -144,7 +145,7 @@ export async function supervise(argv, what) {
144
145
  if (ran > 120_000)
145
146
  backoff = 1000;
146
147
  const how = result.signal ? `signal ${result.signal}` : `exit ${result.code}`;
147
- console.error(chalk.yellow(`↻ ${new Date().toISOString()} ${what} stopped unexpectedly (${how}) — starting it again in ${backoff / 1000}s`));
148
+ log("warn", "supervise", `${what} stopped unexpectedly (${how}) — starting it again in ${backoff / 1000}s`);
148
149
  await new Promise((r) => setTimeout(r, backoff));
149
150
  backoff = Math.min(backoff * 2, 60_000);
150
151
  continue;
@@ -50,6 +50,11 @@ export declare const ROUTES: {
50
50
  readonly "cloud.status": readonly ["GET", "/api/cloud"];
51
51
  readonly "cloud.sync": readonly ["POST", "/api/cloud/sync"];
52
52
  readonly "cloud.unlink": readonly ["POST", "/api/cloud/unlink"];
53
+ readonly "contacts.create": readonly ["POST", "/api/contacts"];
54
+ readonly "contacts.delete": readonly ["DELETE", "/api/contacts/:slug"];
55
+ readonly "contacts.get": readonly ["GET", "/api/contacts/:slug"];
56
+ readonly "contacts.list": readonly ["GET", "/api/contacts"];
57
+ readonly "contacts.save": readonly ["PUT", "/api/contacts/:slug"];
53
58
  readonly "devices.code": readonly ["POST", "/api/devices/code"];
54
59
  readonly "devices.dropPush": readonly ["DELETE", "/api/devices/push"];
55
60
  readonly "devices.list": readonly ["GET", "/api/devices"];
@@ -59,6 +64,11 @@ export declare const ROUTES: {
59
64
  readonly "devices.self": readonly ["GET", "/api/devices/self"];
60
65
  readonly "devices.setPush": readonly ["POST", "/api/devices/push"];
61
66
  readonly "devices.setRelay": readonly ["POST", "/api/devices/relay"];
67
+ readonly "events.create": readonly ["POST", "/api/events"];
68
+ readonly "events.delete": readonly ["DELETE", "/api/events/:slug"];
69
+ readonly "events.get": readonly ["GET", "/api/events/:slug"];
70
+ readonly "events.list": readonly ["GET", "/api/events"];
71
+ readonly "events.save": readonly ["PUT", "/api/events/:slug"];
62
72
  readonly "files.delete": readonly ["DELETE", "/api/files"];
63
73
  readonly "files.list": readonly ["GET", "/api/files/list"];
64
74
  readonly "files.mkdir": readonly ["POST", "/api/files/folder"];
@@ -89,6 +99,11 @@ export declare const ROUTES: {
89
99
  readonly "notifications.diagnose": readonly ["POST", "/api/notifications/:id/diagnose"];
90
100
  readonly "notifications.list": readonly ["GET", "/api/notifications"];
91
101
  readonly "notifications.read": readonly ["POST", "/api/notifications/read"];
102
+ readonly "places.create": readonly ["POST", "/api/places"];
103
+ readonly "places.delete": readonly ["DELETE", "/api/places/:slug"];
104
+ readonly "places.get": readonly ["GET", "/api/places/:slug"];
105
+ readonly "places.list": readonly ["GET", "/api/places"];
106
+ readonly "places.save": readonly ["PUT", "/api/places/:slug"];
92
107
  readonly "projects.create": readonly ["POST", "/api/projects"];
93
108
  readonly "projects.delete": readonly ["DELETE", "/api/projects/:slug"];
94
109
  readonly "projects.get": readonly ["GET", "/api/projects/:slug"];
@@ -98,6 +113,7 @@ export declare const ROUTES: {
98
113
  readonly "projects.save": readonly ["PUT", "/api/projects/:slug"];
99
114
  readonly "projects.saveLayout": readonly ["PUT", "/api/projects"];
100
115
  readonly "protocol.get": readonly ["GET", "/api/protocol"];
116
+ readonly "refs.resolve": readonly ["POST", "/api/refs/resolve"];
101
117
  readonly "repos.add": readonly ["POST", "/api/repos"];
102
118
  readonly "repos.commit": readonly ["GET", "/api/repos/:slug/commits/:hash"];
103
119
  readonly "repos.diff": readonly ["GET", "/api/repos/:slug/diff"];
@@ -120,6 +136,8 @@ export declare const ROUTES: {
120
136
  readonly "settings.save": readonly ["PUT", "/api/settings"];
121
137
  readonly "skills.get": readonly ["GET", "/api/skills/:name"];
122
138
  readonly "skills.list": readonly ["GET", "/api/skills"];
139
+ readonly "temp.clean": readonly ["DELETE", "/api/temp"];
140
+ readonly "temp.list": readonly ["GET", "/api/temp"];
123
141
  readonly "trash.empty": readonly ["DELETE", "/api/trash"];
124
142
  readonly "trash.list": readonly ["GET", "/api/trash"];
125
143
  readonly "trash.purge": readonly ["POST", "/api/trash/purge"];