@netnodeag/kraftwerk 0.78.1 → 0.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/dist/cli/daemon.js +10 -9
- package/dist/cli/doctor.js +1 -1
- package/dist/cli/entities.d.ts +8 -0
- package/dist/cli/entities.js +222 -0
- package/dist/cli/init.js +3 -0
- package/dist/cli/kraftwerk.js +7 -0
- package/dist/cli/ui.js +2 -1
- package/dist/client/routes.d.ts +16 -0
- package/dist/client/routes.js +16 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +17 -6
- package/dist/core/chat/context.js +5 -2
- package/dist/core/entities.d.ts +171 -0
- package/dist/core/entities.js +441 -0
- package/dist/core/git.js +3 -1
- package/dist/core/journal.d.ts +12 -2
- package/dist/core/journal.js +50 -5
- package/dist/core/log.d.ts +18 -0
- package/dist/core/log.js +23 -0
- package/dist/core/projects.d.ts +22 -33
- package/dist/core/projects.js +40 -134
- package/dist/core/records.d.ts +28 -0
- package/dist/core/records.js +71 -0
- package/dist/core/refs.d.ts +74 -0
- package/dist/core/refs.js +143 -0
- package/dist/core/search.d.ts +10 -0
- package/dist/core/search.js +12 -0
- package/dist/core/settings.d.ts +4 -0
- package/dist/core/settings.js +24 -3
- package/dist/core/slug.d.ts +9 -0
- package/dist/core/slug.js +18 -0
- package/dist/core/trash.d.ts +1 -1
- package/dist/server/api/devices.js +2 -1
- package/dist/server/api/entities.d.ts +74 -0
- package/dist/server/api/entities.js +60 -0
- package/dist/server/api/index.d.ts +66 -4
- package/dist/server/api/index.js +5 -0
- package/dist/server/api/projects.d.ts +3 -0
- package/dist/server/api/projects.js +3 -0
- package/dist/server/api/workspace.d.ts +15 -0
- package/dist/server/api/workspace.js +12 -4
- package/dist/server/cloud.js +3 -2
- package/dist/server/relay.d.ts +3 -2
- package/dist/server/relay.js +7 -7
- package/dist/server/server.js +3 -2
- package/package.json +1 -1
- package/schema/kraftwerk.schema.json +54 -0
- package/web/dist/assets/{dist-Crnb8VVO.js → dist-1PajaYPg.js} +1 -1
- package/web/dist/assets/{dist-B-yVL5lk.js → dist-B4dqwo7_.js} +1 -1
- package/web/dist/assets/dist-Bp1iMuQ7.js +1 -0
- package/web/dist/assets/{dist-C90kUzps.js → dist-CK2u1OXb.js} +1 -1
- package/web/dist/assets/{dist-DQbifCX0.js → dist-D-f0nszo.js} +1 -1
- package/web/dist/assets/{dist-BTc878j7.js → dist-DKduisGW.js} +1 -1
- package/web/dist/assets/{dist-D0rjnN8S.js → dist-DgKgeYFJ.js} +1 -1
- package/web/dist/assets/{dist-D4SDXoGw.js → dist-DlZPeneT.js} +1 -1
- package/web/dist/assets/{dist-DHRhdODt.js → dist-K_3Y5Nbl.js} +1 -1
- package/web/dist/assets/{dist-B9EMB1Ie.js → dist-NX4_yvho.js} +1 -1
- package/web/dist/assets/{dist-CpLdAqFQ.js → dist-u_oAKIhZ.js} +1 -1
- package/web/dist/assets/{editor-DHAdW1g_.js → editor-CE9TlKkJ.js} +3 -3
- package/web/dist/assets/index-Dvt-vE1h.css +2 -0
- package/web/dist/assets/index-IAuwD22j.js +231 -0
- package/web/dist/index.html +2 -2
- package/web/dist-remote/assets/index-D6Wznb5o.js +27 -0
- package/web/dist-remote/index.html +2 -2
- package/web/dist-remote/sw.js +1 -1
- package/web/dist/assets/dist-lSJ-O7kT.js +0 -1
- package/web/dist/assets/index-BKDJocDd.css +0 -2
- package/web/dist/assets/index-D7JVAjll.js +0 -231
- package/web/dist-remote/assets/index-2nAI1HTV.js +0 -13
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
|
|
@@ -626,6 +631,84 @@ kraftwerk projects remove relaunch-netnode-ch
|
|
|
626
631
|
The registry of workspaces on this machine, which answered to
|
|
627
632
|
`kraftwerk projects` until 0.48, is `kraftwerk workspaces` now.
|
|
628
633
|
|
|
634
|
+
## Contacts, places and events
|
|
635
|
+
|
|
636
|
+
Who, where and when, kept by the workspace itself so people and agents can
|
|
637
|
+
point at them reliably. Three features with one shape — turn each on in
|
|
638
|
+
`kraftwerk.yml` (or its checkbox in settings):
|
|
639
|
+
|
|
640
|
+
```yaml
|
|
641
|
+
contacts: # people and organisations
|
|
642
|
+
places: # addresses and points of interest
|
|
643
|
+
events: # dated one-off entries
|
|
644
|
+
root: kraftwerk-data/events # default kraftwerk-data/<kind>; part of the workspace, synced like agents and knowledge
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
One Markdown file per entry: the typed fields in YAML frontmatter, free
|
|
648
|
+
notes in the body. Readable and editable by hand, by agents and by the UI;
|
|
649
|
+
versioned with the workspace, deleted into the trash.
|
|
650
|
+
|
|
651
|
+
```
|
|
652
|
+
kraftwerk-data/contacts/anna-meier.md # title, role, org, email, phone, place, url, tags, records + notes
|
|
653
|
+
kraftwerk-data/places/netnode-office.md # title, address, lat, lng, url, tags, records + notes
|
|
654
|
+
kraftwerk-data/events/2026-11-03-kickoff.md # title, start, end, place, contacts, project, url, tags, records + notes
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
```markdown
|
|
658
|
+
---
|
|
659
|
+
title: Kickoff
|
|
660
|
+
start: 2026-11-03T14:00 # ISO date or date-time; events are one-off (recurrence lives in the calendar entry)
|
|
661
|
+
end: 2026-11-03T16:00
|
|
662
|
+
place: netnode-office # a place's slug
|
|
663
|
+
contacts: [anna-meier] # contact slugs
|
|
664
|
+
project: relaunch # a project's slug
|
|
665
|
+
records:
|
|
666
|
+
- kind: calendar
|
|
667
|
+
url: https://calendar.google.com/event?eid=…
|
|
668
|
+
---
|
|
669
|
+
Agenda: scope, timeline, who does what.
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
**References.** Fields that point at other entries hold slugs, and anything
|
|
673
|
+
in the workspace is referenced in text as `kind:slug` — `contact:anna-meier`,
|
|
674
|
+
`place:netnode-office`, `event:2026-11-03-kickoff`, and likewise `project:`,
|
|
675
|
+
`agent:`, `workflow:`, `repo:`, `vibeable:`, `knowledge:`. The same resolver
|
|
676
|
+
serves a project's links, an event's place and contacts, a token in notes or
|
|
677
|
+
in a chat reply, the ⌘K palette and the context an agent starts with: a
|
|
678
|
+
target that does not exist is reported ("not found"), never an error, and
|
|
679
|
+
only these tokens match, so `https:` never does. Projects link to contacts,
|
|
680
|
+
places and events like to anything else (`kraftwerk projects link <slug>
|
|
681
|
+
contacts anna-meier`), and a project chat starts with them as one-liners.
|
|
682
|
+
|
|
683
|
+
**Systems of record.** As on a project: where the truth lives outside
|
|
684
|
+
kraftwerk — a contact's CRM record and newsletter subscription (`crm`,
|
|
685
|
+
`newsletter`), an event's calendar entry (`calendar`), a place on Google
|
|
686
|
+
Maps (`google-maps`). A place without a maps record gets a Google Maps link
|
|
687
|
+
derived from its coordinates or its address.
|
|
688
|
+
|
|
689
|
+
**Agents** get a "Contacts, places and events" block in every chat: what
|
|
690
|
+
exists, the next events, the token syntax and the CLI — they read and write
|
|
691
|
+
the files through it, never by hand:
|
|
692
|
+
|
|
693
|
+
```bash
|
|
694
|
+
kraftwerk contacts # list (--json); places, events the same
|
|
695
|
+
kraftwerk contacts create "Anna Meier" --org ACME --email anna@acme.ch --place netnode-office
|
|
696
|
+
kraftwerk contacts show anna-meier # fields, records, references with their state, notes
|
|
697
|
+
kraftwerk contacts set anna-meier --role CTO --email "" # "" clears a field; --notes-file replaces the notes
|
|
698
|
+
kraftwerk places create "NETNODE Office" --address "Zentralstrasse 1, 6003 Luzern" --lat 47.05 --lng 8.3
|
|
699
|
+
kraftwerk places show netnode-office # prints the Google Maps link
|
|
700
|
+
kraftwerk events create "Kickoff" --start 2026-11-03T14:00 --place netnode-office --contacts anna-meier --project relaunch
|
|
701
|
+
kraftwerk events --from 2026-11-01 --to 2026-11-30 # the month's events
|
|
702
|
+
kraftwerk ref contact:anna-meier # ✔ with its label, or ? not found (exit 1); --file notes.md checks a whole file
|
|
703
|
+
kraftwerk contacts remove anna-meier # to the trash
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
Over the API each kind is its own route family — `contacts.list|get|create|save|delete`,
|
|
707
|
+
`places.*`, `events.*` (`GET /api/events?from=&to=`) — and `refs.resolve`
|
|
708
|
+
resolves tokens or every reference found in a text. In the inspector each
|
|
709
|
+
kind is a page of the workspace (expert mode) with the list beside the open
|
|
710
|
+
entry; a project's context column shows its contacts, places and events.
|
|
711
|
+
|
|
629
712
|
## Files
|
|
630
713
|
|
|
631
714
|
Files are the material of the work, in any format: briefings, contracts,
|
package/dist/cli/daemon.js
CHANGED
|
@@ -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
|
-
|
|
48
|
+
log("warn", "workspace", `not reopened: ${tildify(root)} — ${err.message}`);
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
|
-
|
|
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
|
-
|
|
53
|
+
log("info", "workspace", `open: ${o.url} (${o.name})${o.aliasPort ? ` · also :${o.aliasPort}` : ""}`);
|
|
53
54
|
if (!hub.list().length)
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/cli/doctor.js
CHANGED
|
@@ -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
|
`;
|
package/dist/cli/kraftwerk.js
CHANGED
|
@@ -13,6 +13,7 @@ import { runDoctor } from "./doctor.js";
|
|
|
13
13
|
import { runInit } from "./init.js";
|
|
14
14
|
import { registerKnowledgeCommands } from "./knowledge.js";
|
|
15
15
|
import { registerProjectCommands } from "./projects.js";
|
|
16
|
+
import { 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";
|
|
@@ -42,6 +43,8 @@ import { runUi } from "./ui.js";
|
|
|
42
43
|
* kraftwerk ui start the inspector web UI (localhost:1981)
|
|
43
44
|
* kraftwerk workspaces ... known workspaces on this machine (list/start/stop/forget)
|
|
44
45
|
* kraftwerk projects ... goal-scoped project folders (list/create/show/link/log/remove)
|
|
46
|
+
* kraftwerk contacts|places|events who, where and when: one Markdown file each (list/show/create/set/remove)
|
|
47
|
+
* kraftwerk ref <kind:slug> resolve a reference (contact:anna-meier) or every one in a file
|
|
45
48
|
* kraftwerk vibeables ... small apps built live in a chat (list/create/remove)
|
|
46
49
|
* kraftwerk repos ... repositories the agents work on (list/add/update/remove)
|
|
47
50
|
* kraftwerk journal <agent> [entry] an agent's memory across sessions: print it, add a line
|
|
@@ -299,6 +302,10 @@ registerKnowledgeCommands(program);
|
|
|
299
302
|
registerRoutineCommands(program);
|
|
300
303
|
registerWorkspaceCommands(program);
|
|
301
304
|
registerProjectCommands(program);
|
|
305
|
+
registerContactCommands(program);
|
|
306
|
+
registerPlaceCommands(program);
|
|
307
|
+
registerEventCommands(program);
|
|
308
|
+
registerRefCommand(program);
|
|
302
309
|
registerRepoCommands(program);
|
|
303
310
|
registerVibeableCommands(program);
|
|
304
311
|
registerTrashCommands(program);
|
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
|
-
|
|
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;
|
package/dist/client/routes.d.ts
CHANGED
|
@@ -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"];
|
package/dist/client/routes.js
CHANGED
|
@@ -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"],
|
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,
|
|
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 `
|
|
407
|
-
* must never be the project itself or
|
|
408
|
-
* remove/delete operate under, and what
|
|
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);
|