@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
@@ -1,5 +1,7 @@
1
1
  import { type ResolvedWorkspace } from "../config.js";
2
2
  import type { ChatAgentId } from "./chat/types.js";
3
+ import { RECORD_KINDS, recordLine, type SystemOfRecord } from "./records.js";
4
+ import { type LinkState } from "./refs.js";
3
5
  /**
4
6
  * Projects: a goal with everything the agents need to reach it gathered in
5
7
  * one folder — the brief, the systems of record where the truth is managed
@@ -18,33 +20,20 @@ import type { ChatAgentId } from "./chat/types.js";
18
20
  * projects/<slug>/state.md # current state, rewritten at the end of a session
19
21
  * projects/<slug>/log.md # append-only: decisions, milestones (newest first, stamped)
20
22
  *
21
- * Links are one-directional lists of slugs; a target that does not exist
22
- * is reported ("configured but not found"), never an error — the same
23
- * rule agent knowledge follows. A system of record is context, not a
23
+ * Links are one-directional lists of slugs, resolved through the reference
24
+ * registry (refs.ts); a target that does not exist is reported
25
+ * ("configured but not found"), never an error — the same rule agent
26
+ * knowledge follows. A system of record (records.ts) is context, not a
24
27
  * credential and not a grant: it says where the truth lives and how it is
25
28
  * usually reached, and the harness decides what the agent may call.
26
29
  */
27
30
  export declare const PROJECT_FILE = "project.yml";
28
31
  export declare const PROJECT_STATUSES: readonly ["active", "paused", "done", "archived"];
29
32
  export type ProjectStatus = (typeof PROJECT_STATUSES)[number];
30
- /** Where truth is managed outside kraftwerk: a tool, a folder, a board. */
31
- export interface SystemOfRecord {
32
- /** my-netnode | google-drive | github | bitbucket | notion | slack | url | anything else (rendered by name). */
33
- kind: string;
34
- /** Display name ("Figma board", "Contracts folder"). */
35
- title?: string;
36
- url?: string;
37
- /** For tools with workspaces or boards: the id the agent needs (a my.netnode.ch workspace, a Slack channel). */
38
- workspace?: string;
39
- /** What lives there and how it is usually reached ("tickets and roadmap, `my` CLI"). */
40
- note?: string;
41
- }
42
- /** What kraftwerk knows about a record kind: a label and what such a place usually holds. */
43
- export declare const RECORD_KINDS: Record<string, {
44
- label: string;
45
- hint: string;
46
- }>;
47
- export declare const LINK_KINDS: readonly ["knowledge", "vibeables", "repos", "workflows", "agents"];
33
+ export { RECORD_KINDS, recordLine, type SystemOfRecord };
34
+ export type { LinkState };
35
+ /** What a project links to, in the order project.yml and the UI list them; each is a kind of the reference registry. */
36
+ export declare const LINK_KINDS: readonly ["knowledge", "vibeables", "repos", "workflows", "agents", "contacts", "places", "events"];
48
37
  export type LinkKind = (typeof LINK_KINDS)[number];
49
38
  export interface ProjectDef {
50
39
  slug: string;
@@ -69,6 +58,12 @@ export interface ProjectDef {
69
58
  workflows: string[];
70
59
  /** Agent slugs. */
71
60
  agents: string[];
61
+ /** Contact slugs (contact:<slug>). */
62
+ contacts: string[];
63
+ /** Place slugs. */
64
+ places: string[];
65
+ /** Event slugs. */
66
+ events: string[];
72
67
  }
73
68
  /** What the ⌘K palette needs of a project: also the shape kept in the workspace registry. */
74
69
  export interface ProjectHit {
@@ -86,13 +81,6 @@ export interface ProjectSummary extends ProjectDef {
86
81
  /** Set when project.yml exists but is unusable; the rest shows defaults. */
87
82
  configError?: string;
88
83
  }
89
- /** One linked slug and whether its target exists right now. */
90
- export interface LinkState {
91
- slug: string;
92
- found: boolean;
93
- /** Display name of the target when found (an agent's name, a workflow's description). */
94
- label?: string;
95
- }
96
84
  export type ProjectLinks = Record<LinkKind, LinkState[]>;
97
85
  export interface ProjectDetail extends ProjectSummary {
98
86
  brief: string;
@@ -136,12 +124,15 @@ export interface SaveProjectInput {
136
124
  repos?: unknown;
137
125
  workflows?: unknown;
138
126
  agents?: unknown;
127
+ contacts?: unknown;
128
+ places?: unknown;
129
+ events?: unknown;
139
130
  brief?: string;
140
131
  state?: string;
141
132
  }
142
133
  export declare function safeProjectSlug(slug: string): string;
143
134
  /** Derive a slug from a title: "Relaunch netnode.ch" -> "relaunch-netnode-ch". */
144
- export declare function projectSlugFromTitle(title: string): string;
135
+ export declare const projectSlugFromTitle: (title: string) => string;
145
136
  export declare function openProjects(): Promise<{
146
137
  project?: ResolvedWorkspace;
147
138
  root?: string;
@@ -154,8 +145,8 @@ export declare function ensureProjectsRoot(): Promise<void>;
154
145
  export declare function listProjects(): Promise<ProjectsView>;
155
146
  /** Save the sidebar's order (PUT /api/projects); section names must be unique and non-empty. */
156
147
  export declare function saveLayout(input: unknown): Promise<ProjectLayout>;
157
- /** Which of the linked slugs exist right now, with a display label for the ones that do. */
158
- export declare function resolveLinks(def: ProjectDef): Promise<ProjectLinks>;
148
+ /** Which of the linked slugs exist right now, with a display label for the ones that do (the reference registry decides). */
149
+ export declare const resolveLinks: (def: ProjectDef) => Promise<ProjectLinks>;
159
150
  /** One project with its files and link states; null when the folder is not a project. */
160
151
  export declare function getProject(slug: string): Promise<ProjectDetail | null>;
161
152
  /** The files a fresh project starts with. */
@@ -180,8 +171,6 @@ export declare function linkProject(slug: string, kind: string, target: string,
180
171
  export declare function appendProjectLog(slug: string, entry: string, actor: string): Promise<string>;
181
172
  /** Move the folder to the trash. */
182
173
  export declare function deleteProject(slug: string): Promise<void>;
183
- /** One line per system of record: what it is, where, and what usually lives there. */
184
- export declare function recordLine(r: SystemOfRecord): string;
185
174
  /**
186
175
  * The "## …" blocks a project chat starts with: brief, state, records,
187
176
  * every link with how to reach it, and how to keep the project current.
@@ -2,15 +2,16 @@ import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { parse, stringify } from "yaml";
4
4
  import { projectsRootFor, resolveWorkspace } from "../config.js";
5
- import { EFFORTS, HARNESSES, listAgents } from "./agents.js";
5
+ import { EFFORTS, HARNESSES } from "./agents.js";
6
6
  import { workspaceRoot } from "./context.js";
7
+ import { CONTACTS, entityLine, EVENTS, listEntities, PLACES } from "./entities.js";
7
8
  import { syncWorkspaceProjects } from "./instances.js";
8
- import { knowledgeIndex } from "./knowledge.js";
9
9
  import { newestMtime } from "./mtime.js";
10
+ import { cleanRecords, RECORD_KINDS, recordLine } from "./records.js";
11
+ import { resolveRefs } from "./refs.js";
10
12
  import { listRepos } from "./repos.js";
13
+ import { SLUG_RE, slugify } from "./slug.js";
11
14
  import { moveToTrash } from "./trash.js";
12
- import { listVibeables } from "./vibeables.js";
13
- import { listWorkflows } from "./workflows.js";
14
15
  import { filesContext } from "./files.js";
15
16
  /**
16
17
  * Projects: a goal with everything the agents need to reach it gathered in
@@ -30,48 +31,31 @@ import { filesContext } from "./files.js";
30
31
  * projects/<slug>/state.md # current state, rewritten at the end of a session
31
32
  * projects/<slug>/log.md # append-only: decisions, milestones (newest first, stamped)
32
33
  *
33
- * Links are one-directional lists of slugs; a target that does not exist
34
- * is reported ("configured but not found"), never an error — the same
35
- * rule agent knowledge follows. A system of record is context, not a
34
+ * Links are one-directional lists of slugs, resolved through the reference
35
+ * registry (refs.ts); a target that does not exist is reported
36
+ * ("configured but not found"), never an error — the same rule agent
37
+ * knowledge follows. A system of record (records.ts) is context, not a
36
38
  * credential and not a grant: it says where the truth lives and how it is
37
39
  * usually reached, and the harness decides what the agent may call.
38
40
  */
39
41
  export const PROJECT_FILE = "project.yml";
40
42
  export const PROJECT_STATUSES = ["active", "paused", "done", "archived"];
41
- /** What kraftwerk knows about a record kind: a label and what such a place usually holds. */
42
- export const RECORD_KINDS = {
43
- "my-netnode": { label: "my.netnode.ch", hint: "tickets, roadmap items, meetings and workspace memory" },
44
- "google-drive": { label: "Google Drive", hint: "documents and folders" },
45
- github: { label: "GitHub", hint: "issues, pull requests and discussions" },
46
- bitbucket: { label: "Bitbucket", hint: "pull requests and pipelines" },
47
- notion: { label: "Notion", hint: "pages and databases" },
48
- slack: { label: "Slack", hint: "conversations and decisions" },
49
- url: { label: "Link", hint: "" },
50
- };
51
- export const LINK_KINDS = ["knowledge", "vibeables", "repos", "workflows", "agents"];
43
+ export { RECORD_KINDS, recordLine };
44
+ /** What a project links to, in the order project.yml and the UI list them; each is a kind of the reference registry. */
45
+ export const LINK_KINDS = ["knowledge", "vibeables", "repos", "workflows", "agents", "contacts", "places", "events"];
52
46
  export const toProjectHit = (p) => ({
53
47
  slug: p.slug,
54
48
  title: p.title,
55
49
  status: p.status,
56
50
  ...(p.goal ? { goal: p.goal } : {}),
57
51
  });
58
- const SLUG_RE = /^[a-z0-9][a-z0-9-]{0,48}$/;
59
52
  export function safeProjectSlug(slug) {
60
53
  if (!SLUG_RE.test(slug))
61
54
  throw new Error(`invalid project name "${slug}"`);
62
55
  return slug;
63
56
  }
64
57
  /** Derive a slug from a title: "Relaunch netnode.ch" -> "relaunch-netnode-ch". */
65
- export function projectSlugFromTitle(title) {
66
- return title
67
- .normalize("NFKD")
68
- .replace(/[̀-ͯ]/g, "")
69
- .toLowerCase()
70
- .replace(/[^a-z0-9]+/g, "-")
71
- .replace(/^-+|-+$/g, "")
72
- .slice(0, 48)
73
- .replace(/-+$/, "");
74
- }
58
+ export const projectSlugFromTitle = (title) => slugify(title);
75
59
  /* ---------- root ---------- */
76
60
  export async function openProjects() {
77
61
  let project;
@@ -106,46 +90,6 @@ const strList = (v, field) => {
106
90
  throw new Error(`${field} must be a list of names`);
107
91
  return [...new Set(v.map((x) => x.trim()).filter(Boolean))];
108
92
  };
109
- const cleanRecords = (v) => {
110
- if (v === undefined || v === null)
111
- return [];
112
- if (!Array.isArray(v))
113
- throw new Error("records must be a list");
114
- return v.map((r, i) => {
115
- if (typeof r !== "object" || r === null || Array.isArray(r))
116
- throw new Error(`records[${i}] must be a mapping (kind, title, url, workspace, note)`);
117
- const o = r;
118
- const kind = typeof o.kind === "string" ? o.kind.trim().toLowerCase() : "";
119
- if (!kind)
120
- throw new Error(`records[${i}]: kind is required (e.g. my-netnode, google-drive, url)`);
121
- const str = (k) => {
122
- const x = o[k];
123
- if (x === undefined || x === null || x === "")
124
- return undefined;
125
- if (typeof x === "number")
126
- return String(x);
127
- if (typeof x !== "string")
128
- throw new Error(`records[${i}].${k} must be a string`);
129
- return x.trim() || undefined;
130
- };
131
- const url = str("url");
132
- if (url && !/^https?:\/\//.test(url))
133
- throw new Error(`records[${i}].url must start with http:// or https://`);
134
- const out = { kind };
135
- const title = str("title");
136
- const workspace = str("workspace");
137
- const note = str("note");
138
- if (title)
139
- out.title = title;
140
- if (url)
141
- out.url = url;
142
- if (workspace)
143
- out.workspace = workspace;
144
- if (note)
145
- out.note = note;
146
- return out;
147
- });
148
- };
149
93
  const cleanHarness = (v) => {
150
94
  if (v === undefined || v === null || v === "")
151
95
  return "claude";
@@ -196,13 +140,11 @@ function parseDef(slug, raw) {
196
140
  ...(model ? { model } : {}),
197
141
  ...(effort ? { effort } : {}),
198
142
  records: cleanRecords(d.records),
199
- knowledge: strList(d.knowledge, "knowledge"),
200
- vibeables: strList(d.vibeables, "vibeables"),
201
- repos: strList(d.repos, "repos"),
202
- workflows: strList(d.workflows, "workflows"),
203
- agents: strList(d.agents, "agents"),
143
+ ...linkLists((kind) => strList(d[kind], kind)),
204
144
  };
205
145
  }
146
+ /** The link lists of a definition, one per LINK_KIND, from a function that gives each. */
147
+ const linkLists = (of) => Object.fromEntries(LINK_KINDS.map((kind) => [kind, of(kind)]));
206
148
  const emptyDef = (slug) => ({
207
149
  slug,
208
150
  title: slug,
@@ -210,11 +152,7 @@ const emptyDef = (slug) => ({
210
152
  goal: "",
211
153
  harness: "claude",
212
154
  records: [],
213
- knowledge: [],
214
- vibeables: [],
215
- repos: [],
216
- workflows: [],
217
- agents: [],
155
+ ...linkLists(() => []),
218
156
  });
219
157
  /** project.yml as written: no slug (the folder is the slug), empty lists left out. */
220
158
  function defToYaml(def) {
@@ -315,42 +253,8 @@ export async function saveLayout(input) {
315
253
  await fs.writeFile(path.join(root, ORDER_FILE), `# The order of the projects in the sidebar, with its sections. Drag them there.\n${stringify(doc)}`);
316
254
  return layout;
317
255
  }
318
- /** Which of the linked slugs exist right now, with a display label for the ones that do. */
319
- export async function resolveLinks(def) {
320
- const [knowledge, vibeables, repos, workflows, agents] = await Promise.all([
321
- knowledgeIndex().catch(() => ({ bundles: [] })),
322
- listVibeables().catch(() => ({ vibeables: [] })),
323
- listRepos().catch(() => ({ repos: [] })),
324
- listWorkflows().catch(() => ({ workflows: [] })),
325
- listAgents().catch(() => []),
326
- ]);
327
- const state = (slugs, lookup) => slugs.map((slug) => {
328
- const label = lookup(slug);
329
- return label === undefined ? { slug, found: false } : { slug, found: true, ...(label ? { label } : {}) };
330
- });
331
- return {
332
- knowledge: state(def.knowledge, (s) => {
333
- const b = knowledge.bundles.find((x) => x.name === s);
334
- return b ? `${b.concepts} concepts` : undefined;
335
- }),
336
- vibeables: state(def.vibeables, (s) => {
337
- const v = vibeables.vibeables.find((x) => x.slug === s);
338
- return v ? (v.dev ? "dev" : "static") : undefined;
339
- }),
340
- repos: state(def.repos, (s) => {
341
- const r = repos.repos.find((x) => x.slug === s);
342
- return r ? r.branch ?? "" : undefined;
343
- }),
344
- workflows: state(def.workflows, (s) => {
345
- const w = workflows.workflows.find((x) => x.slug === s);
346
- return w ? w.description ?? w.name ?? "" : undefined;
347
- }),
348
- agents: state(def.agents, (s) => {
349
- const a = agents.find((x) => x.slug === s && !x.archived);
350
- return a ? `${a.emoji} ${a.name}` : undefined;
351
- }),
352
- };
353
- }
256
+ /** Which of the linked slugs exist right now, with a display label for the ones that do (the reference registry decides). */
257
+ export const resolveLinks = (def) => resolveRefs(linkLists((kind) => def[kind]));
354
258
  /** One project with its files and link states; null when the folder is not a project. */
355
259
  export async function getProject(slug) {
356
260
  const root = await requireRoot();
@@ -413,11 +317,7 @@ export async function saveProject(slug, input) {
413
317
  goal: input.goal !== undefined ? String(input.goal).trim() : current.goal,
414
318
  harness: input.harness !== undefined ? cleanHarness(input.harness) : current.harness,
415
319
  records: input.records !== undefined ? cleanRecords(input.records) : current.records,
416
- knowledge: input.knowledge !== undefined ? strList(input.knowledge, "knowledge") : current.knowledge,
417
- vibeables: input.vibeables !== undefined ? strList(input.vibeables, "vibeables") : current.vibeables,
418
- repos: input.repos !== undefined ? strList(input.repos, "repos") : current.repos,
419
- workflows: input.workflows !== undefined ? strList(input.workflows, "workflows") : current.workflows,
420
- agents: input.agents !== undefined ? strList(input.agents, "agents") : current.agents,
320
+ ...linkLists((kind) => (input[kind] !== undefined ? strList(input[kind], kind) : current[kind])),
421
321
  };
422
322
  const model = input.model !== undefined ? cleanModel(input.model) : current.model;
423
323
  const effort = input.effort !== undefined ? cleanEffort(input.effort) : current.effort;
@@ -488,19 +388,6 @@ export async function deleteProject(slug) {
488
388
  await moveToTrash("projects", slug, dir);
489
389
  }
490
390
  /* ---------- chat context ---------- */
491
- /** One line per system of record: what it is, where, and what usually lives there. */
492
- export function recordLine(r) {
493
- const known = RECORD_KINDS[r.kind];
494
- const label = known?.label ?? r.kind;
495
- const head = r.title ? `${label}: ${r.title}` : label;
496
- const parts = [
497
- r.workspace ? `workspace ${r.workspace}` : "",
498
- r.url ?? "",
499
- known?.hint ?? "",
500
- r.note ?? "",
501
- ].filter(Boolean);
502
- return `- ${head}${parts.length ? ` — ${parts.join("; ")}` : ""}`;
503
- }
504
391
  /**
505
392
  * The "## …" blocks a project chat starts with: brief, state, records,
506
393
  * every link with how to reach it, and how to keep the project current.
@@ -517,6 +404,22 @@ export async function projectContext(slug, actor, opts = {}) {
517
404
  const repos = await listRepos().catch(() => null);
518
405
  const reposRoot = repos?.enabled && repos.root ? repos.root : undefined;
519
406
  const vibeables = p.vibeables.length > 0;
407
+ // Who, where and when: the linked entries as one-liners, each with its reference token.
408
+ const entityBlock = async (def, slugs, heading, intro) => {
409
+ if (slugs.length === 0)
410
+ return "";
411
+ const view = await listEntities(def).catch(() => ({ items: [] }));
412
+ const lines = slugs.map((slug) => {
413
+ const item = view.items.find((x) => x.slug === slug);
414
+ return item ? entityLine(def, item) : `- ${def.singular}:${slug} (configured but not found in this workspace — tell the user if you need it)`;
415
+ });
416
+ return `## ${heading}\n${intro}\n${lines.join("\n")}\n\`npx kraftwerk ${def.kind} show <slug> --json\` has the full entry and notes.`;
417
+ };
418
+ const [contactsBlock, placesBlock, eventsBlock] = await Promise.all([
419
+ entityBlock(CONTACTS, p.contacts, "Contacts", "People and organisations of this project (reference one as contact:<slug>):"),
420
+ entityBlock(PLACES, p.places, "Places", "Where this project happens (reference one as place:<slug>):"),
421
+ entityBlock(EVENTS, p.events, "Events", "Dates of this project (reference one as event:<slug>):"),
422
+ ]);
520
423
  const sections = [
521
424
  (opts.member
522
425
  ? `## Project: ${p.title}\nThis channel works in the project "${p.title}" (status ${p.status}) of this kraftwerk workspace. ` +
@@ -538,7 +441,7 @@ export async function projectContext(slug, actor, opts = {}) {
538
441
  ? `## Knowledge\nOKF bundles linked to this project (markdown + YAML frontmatter under the knowledge root):\n` +
539
442
  linkLines("knowledge", (l) => `- ${l.slug}${l.label ? ` (${l.label})` : ""}`) +
540
443
  `\nRead with \`npx kraftwerk knowledge list <bundle>\`, \`get <bundle>/<path>\`, \`search <text>\`. ` +
541
- `Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file <tmp.md> --actor ${actor}\` ` +
444
+ `Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file $KRAFTWERK_TEMP/<name>.md --actor ${actor}\` ` +
542
445
  `(stamps provenance, maintains index.md/log.md — never hand-edit those).`
543
446
  : "",
544
447
  p.repos.length > 0
@@ -569,6 +472,9 @@ export async function projectContext(slug, actor, opts = {}) {
569
472
  linkLines("agents", (l) => `- ${l.slug}${l.label ? ` — ${l.label}` : ""}`) +
570
473
  `\nThe user talks to them on the Agents screen; you cannot message them yourself. When a request clearly belongs to one of them, say so.`
571
474
  : "",
475
+ contactsBlock,
476
+ placesBlock,
477
+ eventsBlock,
572
478
  `## Keeping the project current\n` +
573
479
  `- brief.md is the user's: propose changes and write them only when they agree.\n` +
574
480
  `- state.md is the project's memory between sessions. At the end of a session that changed something, rewrite it so the next session ` +
@@ -576,7 +482,7 @@ export async function projectContext(slug, actor, opts = {}) {
576
482
  `- log.md is append-only. Record decisions and milestones with\n` +
577
483
  ` npx kraftwerk projects log ${p.slug} "<one line>" --actor ${actor}\n` +
578
484
  ` (stamps the date and you as the author; never edit log.md by hand).\n` +
579
- `- Links (knowledge, vibeables, repositories, workflows, agents) live in project.yml: \`npx kraftwerk projects link ${p.slug} <kind> <name>\` adds one, \`unlink\` removes it.`,
485
+ `- Links (knowledge, vibeables, repositories, workflows, agents, contacts, places, events) live in project.yml: \`npx kraftwerk projects link ${p.slug} <kind> <name>\` adds one, \`unlink\` removes it.`,
580
486
  ];
581
487
  return sections.filter(Boolean).join("\n\n");
582
488
  }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Systems of record: where the truth of something is managed outside
3
+ * kraftwerk — a tool, a folder, a board. A project points at its ticket
4
+ * system, a contact at the CRM and the newsletter tool, an event at its
5
+ * calendar entry, a place at Google Maps. An entry is context, not a
6
+ * credential and not a grant: it says where the truth lives and how it is
7
+ * usually reached, and the harness decides what the agent may call.
8
+ */
9
+ export interface SystemOfRecord {
10
+ /** my-netnode | google-drive | github | bitbucket | notion | slack | crm | newsletter | calendar | google-maps | url | anything else (rendered by name). */
11
+ kind: string;
12
+ /** Display name ("Figma board", "Contracts folder"). */
13
+ title?: string;
14
+ url?: string;
15
+ /** For tools with workspaces or boards: the id the agent needs (a my.netnode.ch workspace, a Slack channel). */
16
+ workspace?: string;
17
+ /** What lives there and how it is usually reached ("tickets and roadmap, `my` CLI"). */
18
+ note?: string;
19
+ }
20
+ /** What kraftwerk knows about a record kind: a label and what such a place usually holds. */
21
+ export declare const RECORD_KINDS: Record<string, {
22
+ label: string;
23
+ hint: string;
24
+ }>;
25
+ /** Any input -> a clean list of records; every problem is one error the caller can show. */
26
+ export declare const cleanRecords: (v: unknown) => SystemOfRecord[];
27
+ /** One line per system of record: what it is, where, and what usually lives there. */
28
+ export declare function recordLine(r: SystemOfRecord): string;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Systems of record: where the truth of something is managed outside
3
+ * kraftwerk — a tool, a folder, a board. A project points at its ticket
4
+ * system, a contact at the CRM and the newsletter tool, an event at its
5
+ * calendar entry, a place at Google Maps. An entry is context, not a
6
+ * credential and not a grant: it says where the truth lives and how it is
7
+ * usually reached, and the harness decides what the agent may call.
8
+ */
9
+ /** What kraftwerk knows about a record kind: a label and what such a place usually holds. */
10
+ export const RECORD_KINDS = {
11
+ "my-netnode": { label: "my.netnode.ch", hint: "tickets, roadmap items, meetings and workspace memory" },
12
+ "google-drive": { label: "Google Drive", hint: "documents and folders" },
13
+ github: { label: "GitHub", hint: "issues, pull requests and discussions" },
14
+ bitbucket: { label: "Bitbucket", hint: "pull requests and pipelines" },
15
+ notion: { label: "Notion", hint: "pages and databases" },
16
+ slack: { label: "Slack", hint: "conversations and decisions" },
17
+ crm: { label: "CRM", hint: "the contact's record, deals and history" },
18
+ newsletter: { label: "Newsletter tool", hint: "subscription and mailing lists" },
19
+ calendar: { label: "Calendar", hint: "the calendar entry, invitees and reminders" },
20
+ "google-maps": { label: "Google Maps", hint: "the place on the map, opening hours and directions" },
21
+ url: { label: "Link", hint: "" },
22
+ };
23
+ /** Any input -> a clean list of records; every problem is one error the caller can show. */
24
+ export const cleanRecords = (v) => {
25
+ if (v === undefined || v === null)
26
+ return [];
27
+ if (!Array.isArray(v))
28
+ throw new Error("records must be a list");
29
+ return v.map((r, i) => {
30
+ if (typeof r !== "object" || r === null || Array.isArray(r))
31
+ throw new Error(`records[${i}] must be a mapping (kind, title, url, workspace, note)`);
32
+ const o = r;
33
+ const kind = typeof o.kind === "string" ? o.kind.trim().toLowerCase() : "";
34
+ if (!kind)
35
+ throw new Error(`records[${i}]: kind is required (e.g. my-netnode, google-drive, url)`);
36
+ const str = (k) => {
37
+ const x = o[k];
38
+ if (x === undefined || x === null || x === "")
39
+ return undefined;
40
+ if (typeof x === "number")
41
+ return String(x);
42
+ if (typeof x !== "string")
43
+ throw new Error(`records[${i}].${k} must be a string`);
44
+ return x.trim() || undefined;
45
+ };
46
+ const url = str("url");
47
+ if (url && !/^https?:\/\//.test(url))
48
+ throw new Error(`records[${i}].url must start with http:// or https://`);
49
+ const out = { kind };
50
+ const title = str("title");
51
+ const workspace = str("workspace");
52
+ const note = str("note");
53
+ if (title)
54
+ out.title = title;
55
+ if (url)
56
+ out.url = url;
57
+ if (workspace)
58
+ out.workspace = workspace;
59
+ if (note)
60
+ out.note = note;
61
+ return out;
62
+ });
63
+ };
64
+ /** One line per system of record: what it is, where, and what usually lives there. */
65
+ export function recordLine(r) {
66
+ const known = RECORD_KINDS[r.kind];
67
+ const label = known?.label ?? r.kind;
68
+ const head = r.title ? `${label}: ${r.title}` : label;
69
+ const parts = [r.workspace ? `workspace ${r.workspace}` : "", r.url ?? "", known?.hint ?? "", r.note ?? ""].filter(Boolean);
70
+ return `- ${head}${parts.length ? ` — ${parts.join("; ")}` : ""}`;
71
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The reference registry: everything in a workspace that can be pointed
3
+ * at by name — knowledge bundles, vibeables, repositories, workflows,
4
+ * agents, projects, contacts, places and events. Each kind registers only
5
+ * a lookup (its storage and routes stay where they are); one resolver then
6
+ * serves every place a reference appears: a project's link lists, an
7
+ * event's place and contacts, a `contact:anna-meier` token in text, the
8
+ * ⌘K palette and the context an agent starts with.
9
+ *
10
+ * A reference is `<token>:<slug>` — the kind's singular token (contact,
11
+ * place, event, project, agent, workflow, repo, vibeable, knowledge) and
12
+ * the target's slug. Only registered tokens match, so `https:` and the
13
+ * like never do. A target that does not exist is reported (found: false),
14
+ * never an error — the same rule project links have always followed.
15
+ */
16
+ export declare const REF_KIND_NAMES: readonly ["knowledge", "vibeables", "repos", "workflows", "agents", "projects", "contacts", "places", "events"];
17
+ /** The kind's plural: the link-list name in project.yml and the folder/route name. */
18
+ export type RefKind = (typeof REF_KIND_NAMES)[number];
19
+ /** One existing target. */
20
+ export interface RefTarget {
21
+ slug: string;
22
+ /** Display text next to the slug (an agent's name, a bundle's size); "" when there is nothing to add. */
23
+ label: string;
24
+ /** Where the web UI opens it (a hash route). */
25
+ href: string;
26
+ }
27
+ export interface RefKindDef {
28
+ kind: RefKind;
29
+ /** The token in text: contact in contact:anna-meier. */
30
+ token: string;
31
+ /** How the kind reads in a sentence ("knowledge bundle", "contact"). */
32
+ noun: string;
33
+ /** Every target that exists right now; [] when the feature is off. */
34
+ list(): Promise<RefTarget[]>;
35
+ }
36
+ /** One referenced slug and whether its target exists right now. */
37
+ export interface LinkState {
38
+ slug: string;
39
+ found: boolean;
40
+ /** Display name of the target when found (an agent's name, a workflow's description). */
41
+ label?: string;
42
+ }
43
+ /** A `kind:slug` token, resolved. */
44
+ export interface ResolvedRef extends LinkState {
45
+ kind: RefKind;
46
+ token: string;
47
+ href: string;
48
+ }
49
+ export declare const REF_KINDS: readonly RefKindDef[];
50
+ /** The kind registered under a plural name or a token; undefined for anything else. */
51
+ export declare const refKind: (name: string) => RefKindDef | undefined;
52
+ /** Which of the slugs exist right now, per kind. Kinds without slugs are not listed at all. */
53
+ export declare function resolveRefs<K extends RefKind>(byKind: Record<K, string[]>): Promise<Record<K, LinkState[]>>;
54
+ /** A `token:slug` reference in text: a registered token, a colon, a slug — not inside a word, a path or a URL. */
55
+ export declare const REF_TOKEN_RE: RegExp;
56
+ /** Every distinct reference in a text, in order of appearance. */
57
+ export declare function findRefs(text: string): Array<{
58
+ token: string;
59
+ kind: RefKind;
60
+ slug: string;
61
+ }>;
62
+ /** Parse one reference ("contact:anna-meier"); null when it is not one. */
63
+ export declare function parseRef(text: string): {
64
+ token: string;
65
+ kind: RefKind;
66
+ slug: string;
67
+ } | null;
68
+ /** Resolve references: each with whether its target exists and where it opens. */
69
+ export declare function resolveTokens(refs: Array<{
70
+ kind: RefKind;
71
+ slug: string;
72
+ }>): Promise<ResolvedRef[]>;
73
+ /** Resolve one reference written as text; null when the text is not a reference. */
74
+ export declare function resolveRef(text: string): Promise<ResolvedRef | null>;