@netnodeag/kraftwerk 0.4.0 → 0.6.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 (38) hide show
  1. package/README.md +166 -5
  2. package/dist/cli/doctor.js +38 -0
  3. package/dist/cli/knowledge.d.ts +2 -0
  4. package/dist/cli/knowledge.js +213 -0
  5. package/dist/cli/kraftwerk.js +5 -2
  6. package/dist/cli/ui.js +1 -1
  7. package/dist/config.d.ts +6 -0
  8. package/dist/config.js +4 -2
  9. package/dist/gates.d.ts +7 -0
  10. package/dist/gates.js +27 -0
  11. package/dist/inspector/chat/acp.d.ts +2 -0
  12. package/dist/inspector/chat/acp.js +155 -0
  13. package/dist/inspector/chat/backend.d.ts +36 -0
  14. package/dist/inspector/chat/backend.js +1 -0
  15. package/dist/inspector/chat/pi.d.ts +2 -0
  16. package/dist/inspector/chat/pi.js +105 -0
  17. package/dist/inspector/chat/sessions.d.ts +29 -0
  18. package/dist/inspector/chat/sessions.js +316 -0
  19. package/dist/inspector/chat/store.d.ts +8 -0
  20. package/dist/inspector/chat/store.js +69 -0
  21. package/dist/inspector/chat/types.d.ts +77 -0
  22. package/dist/inspector/chat/types.js +8 -0
  23. package/dist/inspector/knowledge.d.ts +24 -0
  24. package/dist/inspector/knowledge.js +41 -0
  25. package/dist/inspector/routines.d.ts +56 -0
  26. package/dist/inspector/routines.js +266 -0
  27. package/dist/inspector/server.js +272 -0
  28. package/dist/inspector/team.d.ts +50 -0
  29. package/dist/inspector/team.js +119 -0
  30. package/dist/okf.d.ts +118 -0
  31. package/dist/okf.js +398 -0
  32. package/dist/yaml.js +41 -8
  33. package/inspector/dist/assets/{index-BCSWkslF.css → index-BtcPa8nx.css} +1 -1
  34. package/inspector/dist/assets/index-CImcmBVs.js +10 -0
  35. package/inspector/dist/index.html +2 -2
  36. package/package.json +4 -1
  37. package/schema/workflow.schema.json +166 -31
  38. package/inspector/dist/assets/index-D6WuziFQ.js +0 -9
@@ -5,6 +5,10 @@ import { setOutputDir, getOutputDir } from "./context.js";
5
5
  import { listRuns, getRun, readRunFile } from "./runs.js";
6
6
  import { listWorkflows, getWorkflow } from "./workflows.js";
7
7
  import { dockerStatus, triggerRun, stopRun } from "./runner.js";
8
+ import { cancelChat, createChat, getChat, listChats, postMessage, resolvePermission, subscribeChat, } from "./chat/sessions.js";
9
+ import { bundleDetail, conceptDetail, createBundle, knowledgeIndex, putConcept, verifyFromUi, } from "./knowledge.js";
10
+ import { deleteMember, getMember, listMembers, saveMember, teamRoot, } from "./team.js";
11
+ import { deleteRoutine, routineStatuses, runRoutineNow, saveRoutine, startRoutineScheduler, } from "./routines.js";
8
12
  /**
9
13
  * The inspector server: a plain node:http server with no dependencies.
10
14
  * Serves the prebuilt SPA (inspector/dist) plus the JSON/file API the
@@ -31,6 +35,9 @@ const MIME = {
31
35
  /** Text preview payloads are capped; the tail matters most for logs. */
32
36
  const MAX_TEXT = 400_000;
33
37
  function json(res, body, status = 200) {
38
+ // A late error on an SSE response must not try to write headers again.
39
+ if (res.headersSent)
40
+ return void res.end();
34
41
  res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
35
42
  res.end(JSON.stringify(body));
36
43
  }
@@ -140,6 +147,270 @@ async function handleApi(req, res, url) {
140
147
  }
141
148
  }
142
149
  }
150
+ // GET/POST /api/knowledge — bundle index / create a bundle
151
+ if (seg.length === 2 && seg[1] === "knowledge") {
152
+ if (method === "GET")
153
+ return json(res, await knowledgeIndex());
154
+ if (method === "POST") {
155
+ let body;
156
+ try {
157
+ body = JSON.parse(await readBody(req));
158
+ }
159
+ catch {
160
+ return json(res, { error: "invalid JSON body" }, 400);
161
+ }
162
+ try {
163
+ return json(res, await createBundle(String(body.name ?? "").trim()));
164
+ }
165
+ catch (err) {
166
+ return json(res, { error: err.message }, 400);
167
+ }
168
+ }
169
+ }
170
+ // GET /api/knowledge/:bundle — concepts + log
171
+ if (seg.length === 3 && seg[1] === "knowledge" && method === "GET") {
172
+ try {
173
+ const detail = await bundleDetail(decodeURIComponent(seg[2]));
174
+ return detail ? json(res, detail) : json(res, { error: "not found" }, 404);
175
+ }
176
+ catch {
177
+ return json(res, { error: "invalid bundle name" }, 400);
178
+ }
179
+ }
180
+ // GET/POST /api/knowledge/:bundle/concept?id=... — read / write one concept
181
+ if (seg.length === 4 && seg[1] === "knowledge" && seg[3] === "concept") {
182
+ const bundle = decodeURIComponent(seg[2]);
183
+ if (method === "GET") {
184
+ const id = url.searchParams.get("id") ?? "";
185
+ try {
186
+ const concept = await conceptDetail(bundle, id);
187
+ return concept ? json(res, concept) : json(res, { error: "not found" }, 404);
188
+ }
189
+ catch (err) {
190
+ return json(res, { error: err.message }, 400);
191
+ }
192
+ }
193
+ if (method === "POST") {
194
+ let body;
195
+ try {
196
+ body = JSON.parse(await readBody(req));
197
+ }
198
+ catch {
199
+ return json(res, { error: "invalid JSON body" }, 400);
200
+ }
201
+ if (!body.id || !body.content?.trim()) {
202
+ return json(res, { error: "id and content are required" }, 400);
203
+ }
204
+ try {
205
+ return json(res, await putConcept(bundle, body.id, body.content));
206
+ }
207
+ catch (err) {
208
+ return json(res, { error: err.message }, 400);
209
+ }
210
+ }
211
+ }
212
+ // POST /api/knowledge/:bundle/verify — human verification from the UI
213
+ if (seg.length === 4 && seg[1] === "knowledge" && seg[3] === "verify" && method === "POST") {
214
+ let body;
215
+ try {
216
+ body = JSON.parse(await readBody(req));
217
+ }
218
+ catch {
219
+ return json(res, { error: "invalid JSON body" }, 400);
220
+ }
221
+ if (!body.id)
222
+ return json(res, { error: "id is required" }, 400);
223
+ try {
224
+ return json(res, await verifyFromUi(decodeURIComponent(seg[2]), body.id));
225
+ }
226
+ catch (err) {
227
+ return json(res, { error: err.message }, 400);
228
+ }
229
+ }
230
+ // GET/POST /api/team — member list / create a member
231
+ if (seg.length === 2 && seg[1] === "team") {
232
+ if (method === "GET") {
233
+ return json(res, { root: await teamRoot(), members: await listMembers() });
234
+ }
235
+ if (method === "POST") {
236
+ try {
237
+ const body = JSON.parse(await readBody(req));
238
+ return json(res, await saveMember({ ...body, slug: undefined }));
239
+ }
240
+ catch (err) {
241
+ return json(res, { error: err.message }, 400);
242
+ }
243
+ }
244
+ }
245
+ // GET/POST /api/team/:slug/routines — list (with run state) / upsert
246
+ if (seg.length === 4 && seg[1] === "team" && seg[3] === "routines") {
247
+ const slug = decodeURIComponent(seg[2]);
248
+ try {
249
+ if (method === "GET")
250
+ return json(res, { routines: await routineStatuses(slug) });
251
+ if (method === "POST") {
252
+ const body = JSON.parse(await readBody(req));
253
+ return json(res, await saveRoutine(slug, body));
254
+ }
255
+ }
256
+ catch (err) {
257
+ return json(res, { error: err.message }, 400);
258
+ }
259
+ }
260
+ // DELETE /api/team/:slug/routines/:id | POST /api/team/:slug/routines/:id/run
261
+ if (seg.length >= 5 && seg[1] === "team" && seg[3] === "routines") {
262
+ const slug = decodeURIComponent(seg[2]);
263
+ const id = decodeURIComponent(seg[4]);
264
+ try {
265
+ if (seg.length === 5 && method === "DELETE") {
266
+ await deleteRoutine(slug, id);
267
+ return json(res, { ok: true });
268
+ }
269
+ if (seg.length === 6 && seg[5] === "run" && method === "POST") {
270
+ return json(res, await runRoutineNow(slug, id));
271
+ }
272
+ }
273
+ catch (err) {
274
+ return json(res, { error: err.message }, 400);
275
+ }
276
+ }
277
+ // GET/PUT/DELETE /api/team/:slug
278
+ if (seg.length === 3 && seg[1] === "team") {
279
+ const slug = decodeURIComponent(seg[2]);
280
+ try {
281
+ if (method === "GET") {
282
+ const member = await getMember(slug);
283
+ return member ? json(res, member) : json(res, { error: "not found" }, 404);
284
+ }
285
+ if (method === "PUT") {
286
+ const body = JSON.parse(await readBody(req));
287
+ return json(res, await saveMember({ ...body, slug }));
288
+ }
289
+ if (method === "DELETE") {
290
+ await deleteMember(slug);
291
+ return json(res, { ok: true });
292
+ }
293
+ }
294
+ catch (err) {
295
+ return json(res, { error: err.message }, 400);
296
+ }
297
+ }
298
+ // GET/POST /api/chats
299
+ if (seg.length === 2 && seg[1] === "chats") {
300
+ if (method === "GET")
301
+ return json(res, { chats: await listChats() });
302
+ if (method === "POST") {
303
+ let body;
304
+ try {
305
+ body = JSON.parse(await readBody(req));
306
+ }
307
+ catch {
308
+ return json(res, { error: "invalid JSON body" }, 400);
309
+ }
310
+ let agent = body.agent;
311
+ let scope;
312
+ if (body.scope?.kind === "team" && body.scope.member) {
313
+ // Team sessions run on the member's configured harness.
314
+ const member = await getMember(body.scope.member).catch(() => null);
315
+ if (!member)
316
+ return json(res, { error: "team agent not found" }, 404);
317
+ scope = { kind: "team", member: member.slug };
318
+ agent = member.harness;
319
+ }
320
+ else if (body.scope?.kind === "run" && body.scope.runId) {
321
+ scope = { kind: "run", runId: body.scope.runId };
322
+ }
323
+ else if (body.scope?.kind === "kraftwerk") {
324
+ scope = { kind: "kraftwerk" };
325
+ }
326
+ else if (body.scope?.kind === "knowledge") {
327
+ scope = { kind: "knowledge", ...(body.scope.bundle ? { bundle: body.scope.bundle } : {}) };
328
+ }
329
+ else {
330
+ scope = { kind: "general" };
331
+ }
332
+ if (!["claude", "codex", "pi"].includes(agent)) {
333
+ return json(res, { error: "agent must be claude, codex, or pi" }, 400);
334
+ }
335
+ try {
336
+ return json(res, await createChat({ agent, scope }));
337
+ }
338
+ catch (err) {
339
+ return json(res, { error: err.message }, 400);
340
+ }
341
+ }
342
+ }
343
+ // GET /api/chats/:id
344
+ if (seg.length === 3 && seg[1] === "chats" && method === "GET") {
345
+ try {
346
+ const chat = await getChat(seg[2]);
347
+ return chat ? json(res, chat) : json(res, { error: "not found" }, 404);
348
+ }
349
+ catch {
350
+ return json(res, { error: "invalid chat id" }, 400);
351
+ }
352
+ }
353
+ // GET /api/chats/:id/events?after=N — SSE stream of thread events.
354
+ if (seg.length === 4 && seg[1] === "chats" && seg[3] === "events" && method === "GET") {
355
+ const afterSeq = Number(url.searchParams.get("after") ?? "0") || 0;
356
+ const send = (ev) => {
357
+ res.write(`id: ${ev.seq}\ndata: ${JSON.stringify(ev)}\n\n`);
358
+ };
359
+ let unsubscribe = null;
360
+ try {
361
+ res.writeHead(200, {
362
+ "content-type": "text/event-stream",
363
+ "cache-control": "no-store",
364
+ connection: "keep-alive",
365
+ });
366
+ unsubscribe = await subscribeChat(seg[2], afterSeq, send);
367
+ }
368
+ catch {
369
+ return void res.end();
370
+ }
371
+ if (!unsubscribe)
372
+ return void res.end();
373
+ const heartbeat = setInterval(() => res.write(":hb\n\n"), 25_000);
374
+ req.on("close", () => {
375
+ clearInterval(heartbeat);
376
+ unsubscribe?.();
377
+ });
378
+ return;
379
+ }
380
+ // POST /api/chats/:id/{message,permission,cancel}
381
+ if (seg.length === 4 && seg[1] === "chats" && method === "POST") {
382
+ let body = {};
383
+ try {
384
+ const raw = await readBody(req);
385
+ if (raw)
386
+ body = JSON.parse(raw);
387
+ }
388
+ catch {
389
+ return json(res, { error: "invalid JSON body" }, 400);
390
+ }
391
+ try {
392
+ let result;
393
+ if (seg[3] === "message") {
394
+ const text = String(body.text ?? "").trim();
395
+ if (!text)
396
+ return json(res, { error: "text is required" }, 400);
397
+ result = await postMessage(seg[2], text);
398
+ }
399
+ else if (seg[3] === "permission") {
400
+ result = await resolvePermission(seg[2], String(body.requestId ?? ""), body.optionId == null ? null : String(body.optionId));
401
+ }
402
+ else if (seg[3] === "cancel") {
403
+ result = await cancelChat(seg[2]);
404
+ }
405
+ else {
406
+ return json(res, { error: "not found" }, 404);
407
+ }
408
+ return result.error ? json(res, result, 409) : json(res, { ok: true });
409
+ }
410
+ catch (err) {
411
+ return json(res, { error: err.message }, 400);
412
+ }
413
+ }
143
414
  json(res, { error: "not found" }, 404);
144
415
  }
145
416
  async function serveStatic(res, staticDir, pathname) {
@@ -167,6 +438,7 @@ async function serveStatic(res, staticDir, pathname) {
167
438
  /** Start the server; resolves once it listens. Runs until the process ends. */
168
439
  export function startInspector(opts) {
169
440
  setOutputDir(opts.outputDir);
441
+ startRoutineScheduler();
170
442
  const server = http.createServer(async (req, res) => {
171
443
  try {
172
444
  const url = new URL(req.url ?? "/", "http://localhost");
@@ -0,0 +1,50 @@
1
+ import type { ChatAgentId } from "./chat/types.js";
2
+ /**
3
+ * Team: persistent agent teammates, defined on the filesystem. Each member
4
+ * is one folder under the project's agents/ root:
5
+ *
6
+ * agents/<slug>/agent.yml # name, emoji, harness, model, effort, workflows
7
+ * agents/<slug>/system.md # the member's system prompt / role description
8
+ *
9
+ * Definitions are git-tracked project config (like workflows/), not run
10
+ * state — sessions with a member are ordinary chats scoped to it.
11
+ */
12
+ export declare const HARNESSES: ChatAgentId[];
13
+ export declare const EFFORTS: readonly ["low", "medium", "high", "xhigh", "max"];
14
+ export interface TeamMember {
15
+ slug: string;
16
+ name: string;
17
+ emoji: string;
18
+ description?: string;
19
+ harness: ChatAgentId;
20
+ model?: string;
21
+ effort?: string;
22
+ /** Workflow slugs this member knows about and may run. */
23
+ workflows: string[];
24
+ /** Knowledge bundles (OKF) this member works with. */
25
+ knowledge: string[];
26
+ }
27
+ export interface TeamMemberDetail extends TeamMember {
28
+ /** Contents of system.md — the member's system prompt. */
29
+ system: string;
30
+ }
31
+ export declare function teamRoot(): Promise<string>;
32
+ export declare function safeMemberSlug(slug: string): string;
33
+ /** Derive a slug from a display name: "Max Müller" -> "max-mueller"-ish. */
34
+ export declare function slugFromName(name: string): string;
35
+ export declare function listMembers(): Promise<TeamMember[]>;
36
+ export declare function getMember(slug: string): Promise<TeamMemberDetail | null>;
37
+ export interface SaveMemberInput {
38
+ slug?: string;
39
+ name: string;
40
+ emoji?: string;
41
+ description?: string;
42
+ harness: string;
43
+ model?: string;
44
+ effort?: string;
45
+ workflows?: string[];
46
+ knowledge?: string[];
47
+ system?: string;
48
+ }
49
+ export declare function saveMember(input: SaveMemberInput): Promise<TeamMemberDetail>;
50
+ export declare function deleteMember(slug: string): Promise<void>;
@@ -0,0 +1,119 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { parse, stringify } from "yaml";
4
+ import { resolveProject } from "../config.js";
5
+ import { getProjectRoot } from "./context.js";
6
+ /**
7
+ * Team: persistent agent teammates, defined on the filesystem. Each member
8
+ * is one folder under the project's agents/ root:
9
+ *
10
+ * agents/<slug>/agent.yml # name, emoji, harness, model, effort, workflows
11
+ * agents/<slug>/system.md # the member's system prompt / role description
12
+ *
13
+ * Definitions are git-tracked project config (like workflows/), not run
14
+ * state — sessions with a member are ordinary chats scoped to it.
15
+ */
16
+ export const HARNESSES = ["claude", "codex", "pi"];
17
+ export const EFFORTS = ["low", "medium", "high", "xhigh", "max"];
18
+ export async function teamRoot() {
19
+ const project = await resolveProject(getProjectRoot());
20
+ return path.resolve(project.root, project.config.agents ?? "agents");
21
+ }
22
+ const SLUG_RE = /^[a-z0-9][a-z0-9-]{0,48}$/;
23
+ export function safeMemberSlug(slug) {
24
+ if (!SLUG_RE.test(slug))
25
+ throw new Error(`invalid agent slug "${slug}"`);
26
+ return slug;
27
+ }
28
+ /** Derive a slug from a display name: "Max Müller" -> "max-mueller"-ish. */
29
+ export function slugFromName(name) {
30
+ const slug = name
31
+ .toLowerCase()
32
+ .normalize("NFKD")
33
+ .replace(/[\u0300-\u036f]/g, "")
34
+ .replace(/[^a-z0-9]+/g, "-")
35
+ .replace(/^-+|-+$/g, "")
36
+ .slice(0, 48);
37
+ if (!SLUG_RE.test(slug))
38
+ throw new Error("name must contain at least one letter or digit");
39
+ return slug;
40
+ }
41
+ function normalize(slug, raw) {
42
+ const harness = String(raw.harness ?? "claude");
43
+ return {
44
+ slug,
45
+ name: String(raw.name ?? slug),
46
+ emoji: String(raw.emoji ?? "🤖"),
47
+ ...(raw.description ? { description: String(raw.description) } : {}),
48
+ harness: HARNESSES.includes(harness) ? harness : "claude",
49
+ ...(raw.model ? { model: String(raw.model) } : {}),
50
+ ...(raw.effort ? { effort: String(raw.effort) } : {}),
51
+ workflows: Array.isArray(raw.workflows) ? raw.workflows.map(String) : [],
52
+ knowledge: Array.isArray(raw.knowledge) ? raw.knowledge.map(String) : [],
53
+ };
54
+ }
55
+ export async function listMembers() {
56
+ const root = await teamRoot();
57
+ let entries;
58
+ try {
59
+ entries = (await fs.readdir(root, { withFileTypes: true }))
60
+ .filter((e) => e.isDirectory() && SLUG_RE.test(e.name))
61
+ .map((e) => e.name);
62
+ }
63
+ catch {
64
+ return [];
65
+ }
66
+ const members = await Promise.all(entries.map(async (slug) => {
67
+ try {
68
+ const raw = parse(await fs.readFile(path.join(root, slug, "agent.yml"), "utf8"));
69
+ return normalize(slug, (raw ?? {}));
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }));
75
+ return members.filter(Boolean).sort((a, b) => a.name.localeCompare(b.name));
76
+ }
77
+ export async function getMember(slug) {
78
+ const dir = path.join(await teamRoot(), safeMemberSlug(slug));
79
+ let raw;
80
+ try {
81
+ raw = parse(await fs.readFile(path.join(dir, "agent.yml"), "utf8"));
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ const system = await fs.readFile(path.join(dir, "system.md"), "utf8").catch(() => "");
87
+ return { ...normalize(slug, (raw ?? {})), system: system.trim() };
88
+ }
89
+ export async function saveMember(input) {
90
+ const name = input.name?.trim();
91
+ if (!name)
92
+ throw new Error("name is required");
93
+ if (!HARNESSES.includes(input.harness)) {
94
+ throw new Error(`harness must be one of: ${HARNESSES.join(", ")}`);
95
+ }
96
+ if (input.effort && !EFFORTS.includes(input.effort)) {
97
+ throw new Error(`effort must be one of: ${EFFORTS.join(", ")}`);
98
+ }
99
+ const slug = input.slug ? safeMemberSlug(input.slug) : slugFromName(name);
100
+ const dir = path.join(await teamRoot(), slug);
101
+ await fs.mkdir(dir, { recursive: true });
102
+ const yml = {
103
+ name,
104
+ emoji: input.emoji?.trim() || "🤖",
105
+ ...(input.description?.trim() ? { description: input.description.trim() } : {}),
106
+ harness: input.harness,
107
+ ...(input.model?.trim() ? { model: input.model.trim() } : {}),
108
+ ...(input.effort ? { effort: input.effort } : {}),
109
+ workflows: (input.workflows ?? []).map(String),
110
+ knowledge: (input.knowledge ?? []).map(String),
111
+ };
112
+ await fs.writeFile(path.join(dir, "agent.yml"), stringify(yml));
113
+ await fs.writeFile(path.join(dir, "system.md"), (input.system ?? "").trim() + "\n");
114
+ return (await getMember(slug));
115
+ }
116
+ export async function deleteMember(slug) {
117
+ const dir = path.join(await teamRoot(), safeMemberSlug(slug));
118
+ await fs.rm(dir, { recursive: true, force: true });
119
+ }
package/dist/okf.d.ts ADDED
@@ -0,0 +1,118 @@
1
+ /**
2
+ * OKF (Open Knowledge Format) core — kraftwerk's "Context & Knowledge"
3
+ * feature. Implements OKF v0.2 (github.com/GoogleCloudPlatform/knowledge-catalog):
4
+ * a knowledge bundle is a directory of markdown files with YAML frontmatter,
5
+ * one concept per file, plus the reserved index.md / log.md.
6
+ *
7
+ * Layout: <project>/knowledge/<bundle>/**\/*.md — each direct subdirectory of
8
+ * the knowledge root is one bundle (the unit of distribution). The root is
9
+ * configurable via `knowledge:` in kraftwerk.yml.
10
+ *
11
+ * Design decisions:
12
+ * - Writes go through writeConcept()/verifyConcept() so provenance is
13
+ * enforced: every put stamps `generated: { by, at }`, appends to the
14
+ * bundle's log.md, and regenerates index.md. Frontmatter edits use the
15
+ * yaml Document API, so unknown keys and comments survive round-trips
16
+ * (spec §4.1 requires preserving unknown keys).
17
+ * - index.md is fully derived from concept frontmatter — regeneration is
18
+ * idempotent, so concurrent writers and out-of-band edits self-heal on
19
+ * the next regen (or via fsck --fix).
20
+ * - Consumers are permissive per spec §11: unknown types, missing optional
21
+ * families, and broken links never reject a concept.
22
+ */
23
+ export declare const DEFAULT_KNOWLEDGE_DIR = "knowledge";
24
+ export declare const OKF_VERSION = "0.2";
25
+ export type TrustTier = "unverified" | "machine-confirmed" | "human-reviewed";
26
+ export type ConceptStatus = "draft" | "stable" | "deprecated";
27
+ export interface VerifiedEntry {
28
+ by?: string;
29
+ at?: string;
30
+ }
31
+ export interface SourceEntry {
32
+ id?: string;
33
+ resource?: string;
34
+ title?: string;
35
+ author?: string;
36
+ usage_count?: number;
37
+ last_modified?: string;
38
+ }
39
+ export interface ConceptInfo {
40
+ /** Concept ID: path within the bundle, `.md` stripped (spec §2). */
41
+ id: string;
42
+ bundle: string;
43
+ type?: string;
44
+ title: string;
45
+ description?: string;
46
+ tags: string[];
47
+ status: ConceptStatus;
48
+ trustTier: TrustTier;
49
+ staleAfter?: string;
50
+ stale: boolean;
51
+ generated?: {
52
+ by?: string;
53
+ at?: string;
54
+ };
55
+ verified: VerifiedEntry[];
56
+ /** Frontmatter problem (missing block, bad YAML, missing type). */
57
+ error?: string;
58
+ }
59
+ export interface ConceptDetail extends ConceptInfo {
60
+ frontmatter: Record<string, unknown> | null;
61
+ body: string;
62
+ raw: string;
63
+ sources: SourceEntry[];
64
+ }
65
+ export interface BundleInfo {
66
+ name: string;
67
+ concepts: number;
68
+ /** Latest generated.at / verified.at across concepts, for sorting. */
69
+ updatedAt?: string;
70
+ okfVersion?: string;
71
+ }
72
+ export interface ValidationIssue {
73
+ bundle: string;
74
+ file: string;
75
+ level: "error" | "warning";
76
+ message: string;
77
+ }
78
+ export declare function knowledgeRoot(projectRoot: string, configured?: string): string;
79
+ export declare function safeBundleName(name: string): string;
80
+ /** Normalize a concept id: strip .md, forbid escapes and absolute paths. */
81
+ export declare function safeConceptId(id: string): string;
82
+ export declare function conceptFile(root: string, bundle: string, id: string): string;
83
+ export declare function trustTier(verified: VerifiedEntry[]): TrustTier;
84
+ export declare function listBundles(root: string): Promise<BundleInfo[]>;
85
+ export declare function listConcepts(root: string, bundle: string): Promise<ConceptInfo[]>;
86
+ export declare function getConcept(root: string, bundle: string, id: string): Promise<ConceptDetail | null>;
87
+ export interface WriteResult {
88
+ file: string;
89
+ created: boolean;
90
+ concept: ConceptDetail;
91
+ }
92
+ /**
93
+ * The enforced write path: validates, stamps provenance, appends the
94
+ * bundle log, and regenerates index.md.
95
+ */
96
+ export declare function writeConcept(root: string, bundle: string, id: string, content: string, actor: string): Promise<WriteResult>;
97
+ /** Append a verification event (`verified: [{ by, at }]`) to a concept. */
98
+ export declare function verifyConcept(root: string, bundle: string, id: string, actor: string): Promise<ConceptDetail>;
99
+ /**
100
+ * Derived, idempotent bundle-root index.md (§8): one section per top-level
101
+ * subdirectory plus a root section, entries pulled from frontmatter.
102
+ */
103
+ export declare function regenerateIndex(root: string, bundle: string): Promise<void>;
104
+ /** Prepend an entry under today's date heading in the bundle's log.md (§9). */
105
+ export declare function appendLog(root: string, bundle: string, entry: string): Promise<void>;
106
+ export declare function initBundle(root: string, bundle: string): Promise<string>;
107
+ /** Conformance per §11 plus advisory warnings (missing provenance, stale). */
108
+ export declare function validateBundle(root: string, bundle: string): Promise<ValidationIssue[]>;
109
+ export interface SearchHit {
110
+ bundle: string;
111
+ id: string;
112
+ title: string;
113
+ type?: string;
114
+ snippet: string;
115
+ }
116
+ export declare function searchKnowledge(root: string, query: string, bundle?: string): Promise<SearchHit[]>;
117
+ /** Re-derive every bundle index; returns all validation issues found. */
118
+ export declare function fsck(root: string, fix: boolean): Promise<ValidationIssue[]>;