@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.
- package/README.md +166 -5
- package/dist/cli/doctor.js +38 -0
- package/dist/cli/knowledge.d.ts +2 -0
- package/dist/cli/knowledge.js +213 -0
- package/dist/cli/kraftwerk.js +5 -2
- package/dist/cli/ui.js +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.js +4 -2
- package/dist/gates.d.ts +7 -0
- package/dist/gates.js +27 -0
- package/dist/inspector/chat/acp.d.ts +2 -0
- package/dist/inspector/chat/acp.js +155 -0
- package/dist/inspector/chat/backend.d.ts +36 -0
- package/dist/inspector/chat/backend.js +1 -0
- package/dist/inspector/chat/pi.d.ts +2 -0
- package/dist/inspector/chat/pi.js +105 -0
- package/dist/inspector/chat/sessions.d.ts +29 -0
- package/dist/inspector/chat/sessions.js +316 -0
- package/dist/inspector/chat/store.d.ts +8 -0
- package/dist/inspector/chat/store.js +69 -0
- package/dist/inspector/chat/types.d.ts +77 -0
- package/dist/inspector/chat/types.js +8 -0
- package/dist/inspector/knowledge.d.ts +24 -0
- package/dist/inspector/knowledge.js +41 -0
- package/dist/inspector/routines.d.ts +56 -0
- package/dist/inspector/routines.js +266 -0
- package/dist/inspector/server.js +272 -0
- package/dist/inspector/team.d.ts +50 -0
- package/dist/inspector/team.js +119 -0
- package/dist/okf.d.ts +118 -0
- package/dist/okf.js +398 -0
- package/dist/yaml.js +41 -8
- package/inspector/dist/assets/{index-BCSWkslF.css → index-BtcPa8nx.css} +1 -1
- package/inspector/dist/assets/index-CImcmBVs.js +10 -0
- package/inspector/dist/index.html +2 -2
- package/package.json +4 -1
- package/schema/workflow.schema.json +166 -31
- package/inspector/dist/assets/index-D6WuziFQ.js +0 -9
package/dist/inspector/server.js
CHANGED
|
@@ -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[]>;
|