@netnodeag/kraftwerk 0.4.1 → 0.7.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 (81) hide show
  1. package/README.md +192 -6
  2. package/dist/cli/doctor.js +77 -2
  3. package/dist/cli/init.js +65 -15
  4. package/dist/cli/knowledge.d.ts +2 -0
  5. package/dist/cli/knowledge.js +213 -0
  6. package/dist/cli/kraftwerk.js +12 -10
  7. package/dist/cli/runs.js +13 -6
  8. package/dist/cli/ui.js +3 -4
  9. package/dist/config.d.ts +9 -0
  10. package/dist/config.js +5 -2
  11. package/dist/gates.d.ts +7 -0
  12. package/dist/gates.js +27 -0
  13. package/dist/harnesses/claude.js +0 -1
  14. package/dist/inspector/chat/acp.d.ts +2 -2
  15. package/dist/inspector/chat/acp.js +44 -3
  16. package/dist/inspector/chat/backend.d.ts +14 -0
  17. package/dist/inspector/chat/pi.d.ts +2 -0
  18. package/dist/inspector/chat/pi.js +106 -0
  19. package/dist/inspector/chat/sessions.d.ts +33 -0
  20. package/dist/inspector/chat/sessions.js +407 -0
  21. package/dist/inspector/chat/store.d.ts +8 -0
  22. package/dist/inspector/chat/store.js +69 -0
  23. package/dist/inspector/chat/types.d.ts +7 -0
  24. package/dist/inspector/context.d.ts +7 -2
  25. package/dist/inspector/context.js +11 -3
  26. package/dist/inspector/knowledge.d.ts +24 -0
  27. package/dist/inspector/knowledge.js +41 -0
  28. package/dist/inspector/routines.d.ts +56 -0
  29. package/dist/inspector/routines.js +266 -0
  30. package/dist/inspector/runner.d.ts +10 -0
  31. package/dist/inspector/runner.js +6 -8
  32. package/dist/inspector/runs.d.ts +2 -0
  33. package/dist/inspector/runs.js +17 -12
  34. package/dist/inspector/server.d.ts +2 -0
  35. package/dist/inspector/server.js +311 -1
  36. package/dist/inspector/skills.d.ts +26 -0
  37. package/dist/inspector/skills.js +65 -0
  38. package/dist/inspector/team.d.ts +57 -0
  39. package/dist/inspector/team.js +121 -0
  40. package/dist/okf.d.ts +118 -0
  41. package/dist/okf.js +398 -0
  42. package/dist/runner/docker.d.ts +3 -1
  43. package/dist/runner/docker.js +5 -5
  44. package/dist/workflow.d.ts +7 -0
  45. package/dist/workflow.js +19 -0
  46. package/dist/yaml.js +43 -10
  47. package/inspector/dist/assets/index-C8AKqc_i.css +1 -0
  48. package/inspector/dist/assets/index-G3eFm1X4.js +13 -0
  49. package/inspector/dist/assets/roboto-latin-400-normal-BqEyEoaF.woff2 +0 -0
  50. package/inspector/dist/assets/roboto-latin-400-normal-D27F5sTu.woff +0 -0
  51. package/inspector/dist/assets/roboto-latin-500-normal-7RbcRiD8.woff2 +0 -0
  52. package/inspector/dist/assets/roboto-latin-500-normal-fsTFPL3E.woff +0 -0
  53. package/inspector/dist/assets/roboto-latin-700-normal-BZpUvMxY.woff2 +0 -0
  54. package/inspector/dist/assets/roboto-latin-700-normal-YuyVweIx.woff +0 -0
  55. package/inspector/dist/assets/roboto-mono-latin-400-normal-DBZPkcnn.woff +0 -0
  56. package/inspector/dist/assets/roboto-mono-latin-400-normal-GekRknry.woff2 +0 -0
  57. package/inspector/dist/assets/roboto-mono-latin-500-normal-D6j6AMzd.woff +0 -0
  58. package/inspector/dist/assets/roboto-mono-latin-500-normal-pxIOXZmA.woff2 +0 -0
  59. package/inspector/dist/assets/roboto-mono-latin-700-normal-CHYrj1DZ.woff +0 -0
  60. package/inspector/dist/assets/roboto-mono-latin-700-normal-foxDnrFi.woff2 +0 -0
  61. package/inspector/dist/index.html +2 -2
  62. package/package.json +1 -1
  63. package/schema/workflow.schema.json +166 -31
  64. package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  65. package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  66. package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  67. package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  68. package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  69. package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  70. package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-7sUh57Bg.woff2 +0 -0
  71. package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-CNHXzs6v.woff +0 -0
  72. package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  73. package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  74. package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  75. package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  76. package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  77. package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  78. package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bth3BMcD.woff +0 -0
  79. package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bxkt5Cjx.woff2 +0 -0
  80. package/inspector/dist/assets/index-BCSWkslF.css +0 -1
  81. package/inspector/dist/assets/index-D6WuziFQ.js +0 -9
@@ -1,10 +1,16 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import http from "node:http";
3
3
  import path from "node:path";
4
- import { setOutputDir, getOutputDir } from "./context.js";
4
+ import { setOutputDir, setProjectRoot, getOutputDir, getProjectRoot } from "./context.js";
5
+ import { resolveProject } from "../config.js";
5
6
  import { listRuns, getRun, readRunFile } from "./runs.js";
6
7
  import { listWorkflows, getWorkflow } from "./workflows.js";
7
8
  import { dockerStatus, triggerRun, stopRun } from "./runner.js";
9
+ import { cancelChat, createChat, deleteChat, getChat, listChats, postMessage, resolvePermission, subscribeChat, } from "./chat/sessions.js";
10
+ import { bundleDetail, conceptDetail, createBundle, knowledgeIndex, putConcept, verifyFromUi, } from "./knowledge.js";
11
+ import { deleteMember, getMember, listMembers, saveMember, teamRoot, } from "./team.js";
12
+ import { listSkills } from "./skills.js";
13
+ import { deleteRoutine, routineStatuses, runRoutineNow, saveRoutine, startRoutineScheduler, } from "./routines.js";
8
14
  /**
9
15
  * The inspector server: a plain node:http server with no dependencies.
10
16
  * Serves the prebuilt SPA (inspector/dist) plus the JSON/file API the
@@ -31,6 +37,9 @@ const MIME = {
31
37
  /** Text preview payloads are capped; the tail matters most for logs. */
32
38
  const MAX_TEXT = 400_000;
33
39
  function json(res, body, status = 200) {
40
+ // A late error on an SSE response must not try to write headers again.
41
+ if (res.headersSent)
42
+ return void res.end();
34
43
  res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
35
44
  res.end(JSON.stringify(body));
36
45
  }
@@ -46,9 +55,29 @@ function readBody(req) {
46
55
  req.on("error", reject);
47
56
  });
48
57
  }
58
+ /** Package version, read once. Works from src/ (tsx) and dist/ alike. */
59
+ let pkgVersion;
60
+ async function getPkgVersion() {
61
+ if (pkgVersion === undefined) {
62
+ try {
63
+ const raw = await fs.readFile(new URL("../../package.json", import.meta.url), "utf8");
64
+ pkgVersion = JSON.parse(raw).version ?? "";
65
+ }
66
+ catch {
67
+ pkgVersion = "";
68
+ }
69
+ }
70
+ return pkgVersion;
71
+ }
49
72
  async function handleApi(req, res, url) {
50
73
  const seg = url.pathname.split("/").filter(Boolean); // ["api", ...]
51
74
  const method = req.method ?? "GET";
75
+ // GET /api/meta — package version + project name ("environment") for the UI
76
+ if (seg.length === 2 && seg[1] === "meta" && method === "GET") {
77
+ const project = await resolveProject(getProjectRoot()).catch(() => null);
78
+ const projectName = project?.config.name ?? (project ? path.basename(project.root) : "");
79
+ return json(res, { version: await getPkgVersion(), projectName });
80
+ }
52
81
  // GET /api/runs
53
82
  if (seg.length === 2 && seg[1] === "runs" && method === "GET") {
54
83
  return json(res, { outputDir: getOutputDir(), runs: await listRuns() });
@@ -140,6 +169,284 @@ async function handleApi(req, res, url) {
140
169
  }
141
170
  }
142
171
  }
172
+ // GET/POST /api/knowledge — bundle index / create a bundle
173
+ if (seg.length === 2 && seg[1] === "knowledge") {
174
+ if (method === "GET")
175
+ return json(res, await knowledgeIndex());
176
+ if (method === "POST") {
177
+ let body;
178
+ try {
179
+ body = JSON.parse(await readBody(req));
180
+ }
181
+ catch {
182
+ return json(res, { error: "invalid JSON body" }, 400);
183
+ }
184
+ try {
185
+ return json(res, await createBundle(String(body.name ?? "").trim()));
186
+ }
187
+ catch (err) {
188
+ return json(res, { error: err.message }, 400);
189
+ }
190
+ }
191
+ }
192
+ // GET /api/knowledge/:bundle — concepts + log
193
+ if (seg.length === 3 && seg[1] === "knowledge" && method === "GET") {
194
+ try {
195
+ const detail = await bundleDetail(decodeURIComponent(seg[2]));
196
+ return detail ? json(res, detail) : json(res, { error: "not found" }, 404);
197
+ }
198
+ catch {
199
+ return json(res, { error: "invalid bundle name" }, 400);
200
+ }
201
+ }
202
+ // GET/POST /api/knowledge/:bundle/concept?id=... — read / write one concept
203
+ if (seg.length === 4 && seg[1] === "knowledge" && seg[3] === "concept") {
204
+ const bundle = decodeURIComponent(seg[2]);
205
+ if (method === "GET") {
206
+ const id = url.searchParams.get("id") ?? "";
207
+ try {
208
+ const concept = await conceptDetail(bundle, id);
209
+ return concept ? json(res, concept) : json(res, { error: "not found" }, 404);
210
+ }
211
+ catch (err) {
212
+ return json(res, { error: err.message }, 400);
213
+ }
214
+ }
215
+ if (method === "POST") {
216
+ let body;
217
+ try {
218
+ body = JSON.parse(await readBody(req));
219
+ }
220
+ catch {
221
+ return json(res, { error: "invalid JSON body" }, 400);
222
+ }
223
+ if (!body.id || !body.content?.trim()) {
224
+ return json(res, { error: "id and content are required" }, 400);
225
+ }
226
+ try {
227
+ return json(res, await putConcept(bundle, body.id, body.content));
228
+ }
229
+ catch (err) {
230
+ return json(res, { error: err.message }, 400);
231
+ }
232
+ }
233
+ }
234
+ // POST /api/knowledge/:bundle/verify — human verification from the UI
235
+ if (seg.length === 4 && seg[1] === "knowledge" && seg[3] === "verify" && method === "POST") {
236
+ let body;
237
+ try {
238
+ body = JSON.parse(await readBody(req));
239
+ }
240
+ catch {
241
+ return json(res, { error: "invalid JSON body" }, 400);
242
+ }
243
+ if (!body.id)
244
+ return json(res, { error: "id is required" }, 400);
245
+ try {
246
+ return json(res, await verifyFromUi(decodeURIComponent(seg[2]), body.id));
247
+ }
248
+ catch (err) {
249
+ return json(res, { error: err.message }, 400);
250
+ }
251
+ }
252
+ // GET /api/skills — discovered skills (project + user .claude/skills)
253
+ if (seg.length === 2 && seg[1] === "skills" && method === "GET") {
254
+ return json(res, { skills: await listSkills() });
255
+ }
256
+ // GET/POST /api/team — member list / create a member
257
+ if (seg.length === 2 && seg[1] === "team") {
258
+ if (method === "GET") {
259
+ return json(res, { root: await teamRoot(), members: await listMembers() });
260
+ }
261
+ if (method === "POST") {
262
+ try {
263
+ const body = JSON.parse(await readBody(req));
264
+ return json(res, await saveMember({ ...body, slug: undefined }));
265
+ }
266
+ catch (err) {
267
+ return json(res, { error: err.message }, 400);
268
+ }
269
+ }
270
+ }
271
+ // GET/POST /api/team/:slug/routines — list (with run state) / upsert
272
+ if (seg.length === 4 && seg[1] === "team" && seg[3] === "routines") {
273
+ const slug = decodeURIComponent(seg[2]);
274
+ try {
275
+ if (method === "GET")
276
+ return json(res, { routines: await routineStatuses(slug) });
277
+ if (method === "POST") {
278
+ const body = JSON.parse(await readBody(req));
279
+ return json(res, await saveRoutine(slug, body));
280
+ }
281
+ }
282
+ catch (err) {
283
+ return json(res, { error: err.message }, 400);
284
+ }
285
+ }
286
+ // DELETE /api/team/:slug/routines/:id | POST /api/team/:slug/routines/:id/run
287
+ if (seg.length >= 5 && seg[1] === "team" && seg[3] === "routines") {
288
+ const slug = decodeURIComponent(seg[2]);
289
+ const id = decodeURIComponent(seg[4]);
290
+ try {
291
+ if (seg.length === 5 && method === "DELETE") {
292
+ await deleteRoutine(slug, id);
293
+ return json(res, { ok: true });
294
+ }
295
+ if (seg.length === 6 && seg[5] === "run" && method === "POST") {
296
+ return json(res, await runRoutineNow(slug, id));
297
+ }
298
+ }
299
+ catch (err) {
300
+ return json(res, { error: err.message }, 400);
301
+ }
302
+ }
303
+ // GET/PUT/DELETE /api/team/:slug
304
+ if (seg.length === 3 && seg[1] === "team") {
305
+ const slug = decodeURIComponent(seg[2]);
306
+ try {
307
+ if (method === "GET") {
308
+ const member = await getMember(slug);
309
+ return member ? json(res, member) : json(res, { error: "not found" }, 404);
310
+ }
311
+ if (method === "PUT") {
312
+ const body = JSON.parse(await readBody(req));
313
+ return json(res, await saveMember({ ...body, slug }));
314
+ }
315
+ if (method === "DELETE") {
316
+ await deleteMember(slug);
317
+ return json(res, { ok: true });
318
+ }
319
+ }
320
+ catch (err) {
321
+ return json(res, { error: err.message }, 400);
322
+ }
323
+ }
324
+ // GET/POST /api/chats
325
+ if (seg.length === 2 && seg[1] === "chats") {
326
+ if (method === "GET")
327
+ return json(res, { chats: await listChats() });
328
+ if (method === "POST") {
329
+ let body;
330
+ try {
331
+ body = JSON.parse(await readBody(req));
332
+ }
333
+ catch {
334
+ return json(res, { error: "invalid JSON body" }, 400);
335
+ }
336
+ let agent = body.agent;
337
+ let scope;
338
+ if (body.scope?.kind === "team" && body.scope.member) {
339
+ // Team sessions run on the member's configured harness.
340
+ const member = await getMember(body.scope.member).catch(() => null);
341
+ if (!member)
342
+ return json(res, { error: "team agent not found" }, 404);
343
+ scope = { kind: "team", member: member.slug };
344
+ agent = member.harness;
345
+ }
346
+ else if (body.scope?.kind === "run" && body.scope.runId) {
347
+ scope = { kind: "run", runId: body.scope.runId };
348
+ }
349
+ else if (body.scope?.kind === "kraftwerk") {
350
+ scope = { kind: "kraftwerk" };
351
+ }
352
+ else if (body.scope?.kind === "knowledge") {
353
+ scope = { kind: "knowledge", ...(body.scope.bundle ? { bundle: body.scope.bundle } : {}) };
354
+ }
355
+ else {
356
+ scope = { kind: "general" };
357
+ }
358
+ if (!["claude", "codex", "pi"].includes(agent)) {
359
+ return json(res, { error: "agent must be claude, codex, or pi" }, 400);
360
+ }
361
+ try {
362
+ return json(res, await createChat({ agent, scope }));
363
+ }
364
+ catch (err) {
365
+ return json(res, { error: err.message }, 400);
366
+ }
367
+ }
368
+ }
369
+ // DELETE /api/chats/:id
370
+ if (seg.length === 3 && seg[1] === "chats" && method === "DELETE") {
371
+ try {
372
+ const result = await deleteChat(seg[2]);
373
+ return json(res, result, result.error ? 404 : 200);
374
+ }
375
+ catch {
376
+ return json(res, { error: "invalid chat id" }, 400);
377
+ }
378
+ }
379
+ // GET /api/chats/:id
380
+ if (seg.length === 3 && seg[1] === "chats" && method === "GET") {
381
+ try {
382
+ const chat = await getChat(seg[2]);
383
+ return chat ? json(res, chat) : json(res, { error: "not found" }, 404);
384
+ }
385
+ catch {
386
+ return json(res, { error: "invalid chat id" }, 400);
387
+ }
388
+ }
389
+ // GET /api/chats/:id/events?after=N — SSE stream of thread events.
390
+ if (seg.length === 4 && seg[1] === "chats" && seg[3] === "events" && method === "GET") {
391
+ const afterSeq = Number(url.searchParams.get("after") ?? "0") || 0;
392
+ const send = (ev) => {
393
+ res.write(`id: ${ev.seq}\ndata: ${JSON.stringify(ev)}\n\n`);
394
+ };
395
+ let unsubscribe = null;
396
+ try {
397
+ res.writeHead(200, {
398
+ "content-type": "text/event-stream",
399
+ "cache-control": "no-store",
400
+ connection: "keep-alive",
401
+ });
402
+ unsubscribe = await subscribeChat(seg[2], afterSeq, send);
403
+ }
404
+ catch {
405
+ return void res.end();
406
+ }
407
+ if (!unsubscribe)
408
+ return void res.end();
409
+ const heartbeat = setInterval(() => res.write(":hb\n\n"), 25_000);
410
+ req.on("close", () => {
411
+ clearInterval(heartbeat);
412
+ unsubscribe?.();
413
+ });
414
+ return;
415
+ }
416
+ // POST /api/chats/:id/{message,permission,cancel}
417
+ if (seg.length === 4 && seg[1] === "chats" && method === "POST") {
418
+ let body = {};
419
+ try {
420
+ const raw = await readBody(req);
421
+ if (raw)
422
+ body = JSON.parse(raw);
423
+ }
424
+ catch {
425
+ return json(res, { error: "invalid JSON body" }, 400);
426
+ }
427
+ try {
428
+ let result;
429
+ if (seg[3] === "message") {
430
+ const text = String(body.text ?? "").trim();
431
+ if (!text)
432
+ return json(res, { error: "text is required" }, 400);
433
+ result = await postMessage(seg[2], text);
434
+ }
435
+ else if (seg[3] === "permission") {
436
+ result = await resolvePermission(seg[2], String(body.requestId ?? ""), body.optionId == null ? null : String(body.optionId));
437
+ }
438
+ else if (seg[3] === "cancel") {
439
+ result = await cancelChat(seg[2]);
440
+ }
441
+ else {
442
+ return json(res, { error: "not found" }, 404);
443
+ }
444
+ return result.error ? json(res, result, 409) : json(res, { ok: true });
445
+ }
446
+ catch (err) {
447
+ return json(res, { error: err.message }, 400);
448
+ }
449
+ }
143
450
  json(res, { error: "not found" }, 404);
144
451
  }
145
452
  async function serveStatic(res, staticDir, pathname) {
@@ -167,6 +474,9 @@ async function serveStatic(res, staticDir, pathname) {
167
474
  /** Start the server; resolves once it listens. Runs until the process ends. */
168
475
  export function startInspector(opts) {
169
476
  setOutputDir(opts.outputDir);
477
+ if (opts.projectRoot)
478
+ setProjectRoot(opts.projectRoot);
479
+ startRoutineScheduler();
170
480
  const server = http.createServer(async (req, res) => {
171
481
  try {
172
482
  const url = new URL(req.url ?? "/", "http://localhost");
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Skills: reusable instruction packages for chats, discovered from the
3
+ * Claude-style skill layout — one folder per skill holding a SKILL.md with
4
+ * YAML frontmatter (name, description). Two roots are scanned:
5
+ *
6
+ * <projectRoot>/.claude/skills/<name>/SKILL.md (git-tracked, per project)
7
+ * ~/.claude/skills/<name>/SKILL.md (personal, per user)
8
+ *
9
+ * A project skill shadows a user skill with the same name. Skills are
10
+ * surfaced to every chat as context (and expanded on /<name> invocation),
11
+ * so they work identically on claude, codex, and pi.
12
+ */
13
+ export interface SkillInfo {
14
+ /** Frontmatter `name`, falling back to the folder name. */
15
+ name: string;
16
+ description: string;
17
+ /** Absolute path to the SKILL.md file. */
18
+ path: string;
19
+ /** Absolute path to the skill folder (for harnesses that take a dir). */
20
+ dir: string;
21
+ source: "project" | "user";
22
+ }
23
+ /** All discovered skills; a project skill shadows a same-named user skill. */
24
+ export declare function listSkills(): Promise<SkillInfo[]>;
25
+ /** Read a skill's SKILL.md contents (empty string when unreadable). */
26
+ export declare function readSkill(skill: SkillInfo): Promise<string>;
@@ -0,0 +1,65 @@
1
+ import { promises as fs } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { parse } from "yaml";
5
+ import { getProjectRoot } from "./context.js";
6
+ const NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
7
+ function frontmatter(md) {
8
+ const m = /^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/.exec(md);
9
+ if (!m)
10
+ return {};
11
+ try {
12
+ const parsed = parse(m[1]);
13
+ return parsed && typeof parsed === "object" ? parsed : {};
14
+ }
15
+ catch {
16
+ return {};
17
+ }
18
+ }
19
+ async function scanRoot(root, source) {
20
+ let entries;
21
+ try {
22
+ entries = (await fs.readdir(root, { withFileTypes: true }))
23
+ .filter((e) => e.isDirectory())
24
+ .map((e) => e.name);
25
+ }
26
+ catch {
27
+ return [];
28
+ }
29
+ const skills = await Promise.all(entries.map(async (dirName) => {
30
+ const dir = path.join(root, dirName);
31
+ const file = path.join(dir, "SKILL.md");
32
+ let md;
33
+ try {
34
+ md = await fs.readFile(file, "utf8");
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ const fm = frontmatter(md);
40
+ const name = String(fm.name ?? dirName).trim() || dirName;
41
+ if (!NAME_RE.test(name))
42
+ return null;
43
+ return {
44
+ name,
45
+ description: String(fm.description ?? "").trim(),
46
+ path: file,
47
+ dir,
48
+ source,
49
+ };
50
+ }));
51
+ return skills.filter(Boolean).sort((a, b) => a.name.localeCompare(b.name));
52
+ }
53
+ /** All discovered skills; a project skill shadows a same-named user skill. */
54
+ export async function listSkills() {
55
+ const [project, user] = await Promise.all([
56
+ scanRoot(path.join(getProjectRoot(), ".claude", "skills"), "project"),
57
+ scanRoot(path.join(os.homedir(), ".claude", "skills"), "user"),
58
+ ]);
59
+ const seen = new Set(project.map((s) => s.name.toLowerCase()));
60
+ return [...project, ...user.filter((s) => !seen.has(s.name.toLowerCase()))];
61
+ }
62
+ /** Read a skill's SKILL.md contents (empty string when unreadable). */
63
+ export function readSkill(skill) {
64
+ return fs.readFile(skill.path, "utf8").catch(() => "");
65
+ }
@@ -0,0 +1,57 @@
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
+ * Skill allowlist (names from .claude/skills, project or user level).
28
+ * Absent = all discovered skills; empty list = no skills.
29
+ */
30
+ skills?: string[];
31
+ }
32
+ export interface TeamMemberDetail extends TeamMember {
33
+ /** Contents of system.md — the member's system prompt. */
34
+ system: string;
35
+ }
36
+ export declare function teamRoot(): Promise<string>;
37
+ export declare function safeMemberSlug(slug: string): string;
38
+ /** Derive a slug from a display name: "Max Müller" -> "max-mueller"-ish. */
39
+ export declare function slugFromName(name: string): string;
40
+ export declare function listMembers(): Promise<TeamMember[]>;
41
+ export declare function getMember(slug: string): Promise<TeamMemberDetail | null>;
42
+ export interface SaveMemberInput {
43
+ slug?: string;
44
+ name: string;
45
+ emoji?: string;
46
+ description?: string;
47
+ harness: string;
48
+ model?: string;
49
+ effort?: string;
50
+ workflows?: string[];
51
+ knowledge?: string[];
52
+ /** Omit for "all skills"; a list (possibly empty) restricts to those names. */
53
+ skills?: string[];
54
+ system?: string;
55
+ }
56
+ export declare function saveMember(input: SaveMemberInput): Promise<TeamMemberDetail>;
57
+ export declare function deleteMember(slug: string): Promise<void>;
@@ -0,0 +1,121 @@
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
+ ...(Array.isArray(raw.skills) ? { skills: raw.skills.map(String) } : {}),
54
+ };
55
+ }
56
+ export async function listMembers() {
57
+ const root = await teamRoot();
58
+ let entries;
59
+ try {
60
+ entries = (await fs.readdir(root, { withFileTypes: true }))
61
+ .filter((e) => e.isDirectory() && SLUG_RE.test(e.name))
62
+ .map((e) => e.name);
63
+ }
64
+ catch {
65
+ return [];
66
+ }
67
+ const members = await Promise.all(entries.map(async (slug) => {
68
+ try {
69
+ const raw = parse(await fs.readFile(path.join(root, slug, "agent.yml"), "utf8"));
70
+ return normalize(slug, (raw ?? {}));
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }));
76
+ return members.filter(Boolean).sort((a, b) => a.name.localeCompare(b.name));
77
+ }
78
+ export async function getMember(slug) {
79
+ const dir = path.join(await teamRoot(), safeMemberSlug(slug));
80
+ let raw;
81
+ try {
82
+ raw = parse(await fs.readFile(path.join(dir, "agent.yml"), "utf8"));
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ const system = await fs.readFile(path.join(dir, "system.md"), "utf8").catch(() => "");
88
+ return { ...normalize(slug, (raw ?? {})), system: system.trim() };
89
+ }
90
+ export async function saveMember(input) {
91
+ const name = input.name?.trim();
92
+ if (!name)
93
+ throw new Error("name is required");
94
+ if (!HARNESSES.includes(input.harness)) {
95
+ throw new Error(`harness must be one of: ${HARNESSES.join(", ")}`);
96
+ }
97
+ if (input.effort && !EFFORTS.includes(input.effort)) {
98
+ throw new Error(`effort must be one of: ${EFFORTS.join(", ")}`);
99
+ }
100
+ const slug = input.slug ? safeMemberSlug(input.slug) : slugFromName(name);
101
+ const dir = path.join(await teamRoot(), slug);
102
+ await fs.mkdir(dir, { recursive: true });
103
+ const yml = {
104
+ name,
105
+ emoji: input.emoji?.trim() || "🤖",
106
+ ...(input.description?.trim() ? { description: input.description.trim() } : {}),
107
+ harness: input.harness,
108
+ ...(input.model?.trim() ? { model: input.model.trim() } : {}),
109
+ ...(input.effort ? { effort: input.effort } : {}),
110
+ workflows: (input.workflows ?? []).map(String),
111
+ knowledge: (input.knowledge ?? []).map(String),
112
+ ...(input.skills ? { skills: input.skills.map(String) } : {}),
113
+ };
114
+ await fs.writeFile(path.join(dir, "agent.yml"), stringify(yml));
115
+ await fs.writeFile(path.join(dir, "system.md"), (input.system ?? "").trim() + "\n");
116
+ return (await getMember(slug));
117
+ }
118
+ export async function deleteMember(slug) {
119
+ const dir = path.join(await teamRoot(), safeMemberSlug(slug));
120
+ await fs.rm(dir, { recursive: true, force: true });
121
+ }