@theagilemonkeys/facility 0.3.1

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 (75) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +68 -0
  3. package/bin/facility.mjs +10 -0
  4. package/modules/README.md +35 -0
  5. package/modules/ai-queryability/agents/queryability-reviewer.md +35 -0
  6. package/modules/ai-queryability/module.json +9 -0
  7. package/modules/ai-queryability/standard-section.md +22 -0
  8. package/modules/analytics/agents/analytics-reviewer.md +32 -0
  9. package/modules/analytics/commands/add-telemetry.md +23 -0
  10. package/modules/analytics/module.json +10 -0
  11. package/modules/analytics/standard-section.md +23 -0
  12. package/modules/database/agents/data-security-reviewer.md +38 -0
  13. package/modules/database/commands/new-migration.md +24 -0
  14. package/modules/database/guards/migration-versions.mjs +41 -0
  15. package/modules/database/guards/migrations-immutable.mjs +57 -0
  16. package/modules/database/hooks/protect-migrations.fragment.mjs +10 -0
  17. package/modules/database/module.json +25 -0
  18. package/modules/database/standard-section.md +20 -0
  19. package/modules/design-system/agents/design-reviewer.md +37 -0
  20. package/modules/design-system/module.json +9 -0
  21. package/modules/design-system/standard-section.md +15 -0
  22. package/package.json +42 -0
  23. package/src/add.mjs +77 -0
  24. package/src/cli.mjs +352 -0
  25. package/src/detect.mjs +127 -0
  26. package/src/doctor.mjs +582 -0
  27. package/src/init.mjs +572 -0
  28. package/src/instance.mjs +114 -0
  29. package/src/platform-admin.mjs +1542 -0
  30. package/src/platform-config.mjs +39 -0
  31. package/src/platform.mjs +1759 -0
  32. package/src/prompts.mjs +64 -0
  33. package/src/render.mjs +66 -0
  34. package/src/ui.mjs +30 -0
  35. package/templates/claude/agents/security-reviewer.md +41 -0
  36. package/templates/claude/agents/standards-reviewer.md +31 -0
  37. package/templates/claude/commands/open-pr.md +21 -0
  38. package/templates/claude/commands/verify.md +16 -0
  39. package/templates/claude/hooks/protect-branch.mjs +58 -0
  40. package/templates/claude/hooks/protect-files.mjs +35 -0
  41. package/templates/claude/settings.json +71 -0
  42. package/templates/claude/skills/maintainable-software/SKILL.md +67 -0
  43. package/templates/claude/skills/reviewing-to-standard/SKILL.md +49 -0
  44. package/templates/claude/skills/working-to-standard/SKILL.md +45 -0
  45. package/templates/delivery/verify.mjs +157 -0
  46. package/templates/doctor/resolve.mjs +144 -0
  47. package/templates/guards/README.md +30 -0
  48. package/templates/guards/_kit.mjs +81 -0
  49. package/templates/guards/actions-pinned.mjs +38 -0
  50. package/templates/guards/run.mjs +111 -0
  51. package/templates/guards/watchtower-locked.mjs +66 -0
  52. package/templates/prompts/address-review.md +14 -0
  53. package/templates/prompts/architect.md +62 -0
  54. package/templates/prompts/builder.md +71 -0
  55. package/templates/prompts/doctor.md +64 -0
  56. package/templates/prompts/review.md +14 -0
  57. package/templates/prompts/sweep.md +75 -0
  58. package/templates/receipts/collect.mjs +289 -0
  59. package/templates/review/finalize.mjs +38 -0
  60. package/templates/scripts/move-board-status.sh +155 -0
  61. package/templates/security/sync-findings.mjs +226 -0
  62. package/templates/standard/STANDARD.md +141 -0
  63. package/templates/standard/agents-block.md +25 -0
  64. package/templates/watchtower/budgets.json +12 -0
  65. package/templates/watchtower/canary.mjs +216 -0
  66. package/templates/watchtower/health.mjs +148 -0
  67. package/templates/watchtower/outcomes.mjs +188 -0
  68. package/templates/workflows/facility-address-review.yml +153 -0
  69. package/templates/workflows/facility-canary.yml +61 -0
  70. package/templates/workflows/facility-codex.yml +326 -0
  71. package/templates/workflows/facility-crew.yml +350 -0
  72. package/templates/workflows/facility-doctor.yml +155 -0
  73. package/templates/workflows/facility-review.yml +134 -0
  74. package/templates/workflows/facility-security-sweep.yml +204 -0
  75. package/templates/workflows/facility-watchtower.yml +87 -0
@@ -0,0 +1,1542 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { green, yellow } from "./ui.mjs";
3
+
4
+ export const ADMIN_GROUPS = new Set([
5
+ "org",
6
+ "members",
7
+ "roles",
8
+ "repos",
9
+ "agents",
10
+ "conversations",
11
+ "github",
12
+ "providers",
13
+ "budgets",
14
+ "registry",
15
+ "sandboxes",
16
+ "tasks",
17
+ "virtual-keys",
18
+ "kb",
19
+ "analytics",
20
+ "audit",
21
+ "integrations",
22
+ "spend",
23
+ "proposals",
24
+ "action-types",
25
+ "health",
26
+ "outcomes",
27
+ "catalog",
28
+ ]);
29
+
30
+ export async function runAdminCommand(group, args, ctx, flags) {
31
+ validateAdminFlags(group, flags);
32
+ switch (group) {
33
+ case "org":
34
+ return org(args, ctx, flags);
35
+ case "members":
36
+ return members(args, ctx, flags);
37
+ case "roles":
38
+ return roles(args, ctx, flags);
39
+ case "repos":
40
+ return repos(args, ctx, flags);
41
+ case "agents":
42
+ return agents(args, ctx, flags);
43
+ case "conversations":
44
+ return conversations(args, ctx, flags);
45
+ case "github":
46
+ return github(args, ctx, flags);
47
+ case "providers":
48
+ return providers(args, ctx, flags);
49
+ case "budgets":
50
+ return budgets(args, ctx, flags);
51
+ case "registry":
52
+ return registry(args, ctx, flags);
53
+ case "sandboxes":
54
+ return sandboxes(args, ctx, flags);
55
+ case "tasks":
56
+ return tasks(args, ctx, flags);
57
+ case "virtual-keys":
58
+ return virtualKeys(args, ctx, flags);
59
+ case "kb":
60
+ return kb(args, ctx, flags);
61
+ case "analytics":
62
+ return analytics(args, ctx, flags);
63
+ case "audit":
64
+ return audit(args, ctx, flags);
65
+ case "integrations":
66
+ return integrations(args, ctx, flags);
67
+ case "spend":
68
+ return spend(args, ctx, flags);
69
+ case "proposals":
70
+ return proposals(args, ctx, flags);
71
+ case "action-types":
72
+ return actionTypes(args, ctx, flags);
73
+ case "health":
74
+ return projectHealth(args, ctx);
75
+ case "outcomes":
76
+ return outcomes(args, ctx, flags);
77
+ case "catalog":
78
+ return catalog(args, ctx);
79
+ default:
80
+ throw cliError(`Unknown command group: ${group}`);
81
+ }
82
+ }
83
+
84
+ const GLOBAL_FLAGS = ["profile", "json", "timeout", "help", "yes"];
85
+ const PAGE_FLAGS = ["limit", "offset"];
86
+ const WRITE_FLAGS = ["idempotency-key"];
87
+ const ADMIN_FLAGS = {
88
+ org: ["name", "settings"],
89
+ members: ["email", "role", ...PAGE_FLAGS, ...WRITE_FLAGS],
90
+ roles: ["name", "description", "permissions", ...PAGE_FLAGS, ...WRITE_FLAGS],
91
+ repos: ["repo", "branch", "private", "description", "auto-init", ...PAGE_FLAGS, ...WRITE_FLAGS],
92
+ agents: [
93
+ "name",
94
+ "engine",
95
+ "contract",
96
+ "model",
97
+ "triggers",
98
+ "schedule",
99
+ "timezone",
100
+ "harness",
101
+ "sandbox",
102
+ "enabled",
103
+ ...PAGE_FLAGS,
104
+ ...WRITE_FLAGS,
105
+ ],
106
+ conversations: ["agent", "title", ...WRITE_FLAGS],
107
+ github: ["query", "state", "cursor", "agent", "limit", ...WRITE_FLAGS],
108
+ providers: ["provider", "name", "secret", "base-url", ...PAGE_FLAGS, ...WRITE_FLAGS],
109
+ budgets: ["id", "scope", "project", "agent", "period", "limit-cents", "mode", "enabled", ...PAGE_FLAGS, ...WRITE_FLAGS],
110
+ registry: [
111
+ "kind",
112
+ "scope",
113
+ "project",
114
+ "name",
115
+ "description",
116
+ "content",
117
+ "content-file",
118
+ "changelog",
119
+ ...PAGE_FLAGS,
120
+ ...WRITE_FLAGS,
121
+ ],
122
+ sandboxes: ["name", "driver", "image", "setup", "resources", "network", ...PAGE_FLAGS, ...WRITE_FLAGS],
123
+ tasks: ["title", "body", "body-file", "status", "kb", "wsjf", "gh", ...PAGE_FLAGS, ...WRITE_FLAGS],
124
+ "virtual-keys": ["name", "models", "expires", ...PAGE_FLAGS, ...WRITE_FLAGS],
125
+ kb: [
126
+ "charter",
127
+ "charter-file",
128
+ "active",
129
+ "active-file",
130
+ "config",
131
+ "type",
132
+ "slug",
133
+ "body",
134
+ "body-file",
135
+ "frontmatter",
136
+ "status",
137
+ "links",
138
+ "dry",
139
+ "number",
140
+ "supersedes",
141
+ ...PAGE_FLAGS,
142
+ ...WRITE_FLAGS,
143
+ ],
144
+ analytics: ["project", "from", "to", "group-by", ...PAGE_FLAGS],
145
+ audit: ["actor", "action", "from", "to", "cursor", "limit"],
146
+ integrations: [
147
+ "project",
148
+ "kind",
149
+ "enabled",
150
+ "limit",
151
+ "offset",
152
+ "name",
153
+ "config",
154
+ "secret",
155
+ "status",
156
+ ...WRITE_FLAGS,
157
+ ],
158
+ spend: ["project", "from", "to", "group-by", ...PAGE_FLAGS],
159
+ proposals: ["project", "run", "action", "payload", "context", "expires", ...WRITE_FLAGS],
160
+ "action-types": [...PAGE_FLAGS],
161
+ health: [],
162
+ outcomes: ["project", "state", "limit"],
163
+ catalog: [],
164
+ };
165
+
166
+ export function validateAdminFlags(group, flags) {
167
+ const allowed = new Set([...GLOBAL_FLAGS, ...(ADMIN_FLAGS[group] ?? [])]);
168
+ const unknown = Object.keys(flags).filter((flag) => !allowed.has(flag));
169
+ if (unknown.length) {
170
+ throw cliError(
171
+ `Unknown flag${unknown.length === 1 ? "" : "s"}: ${unknown.map((flag) => `--${flag}`).join(", ")}`,
172
+ "unknown_flag",
173
+ );
174
+ }
175
+ const booleanFlags = new Set(["json", "yes", "help", "private", "auto-init", "enabled", "dry"]);
176
+ for (const [name, value] of Object.entries(flags)) {
177
+ if (value === true && !booleanFlags.has(name)) {
178
+ throw cliError(`--${name} requires a value`, "invalid_flag");
179
+ }
180
+ }
181
+ }
182
+
183
+ const ADMIN_SUBCOMMAND_FLAGS = {
184
+ org: { __default: [], get: [], show: [], update: ["name", "settings"] },
185
+ members: {
186
+ __default: [...PAGE_FLAGS],
187
+ list: [...PAGE_FLAGS],
188
+ add: ["email", "role", ...WRITE_FLAGS],
189
+ update: ["role"],
190
+ remove: ["yes"],
191
+ delete: ["yes"],
192
+ },
193
+ roles: {
194
+ __default: [...PAGE_FLAGS],
195
+ list: [...PAGE_FLAGS],
196
+ create: ["name", "description", "permissions", ...WRITE_FLAGS],
197
+ update: ["description", "permissions"],
198
+ delete: ["yes"],
199
+ },
200
+ repos: {
201
+ __default: [...PAGE_FLAGS],
202
+ list: [...PAGE_FLAGS],
203
+ connect: ["repo", "branch", "private", "description", "auto-init", ...WRITE_FLAGS],
204
+ create: ["repo", "branch", "private", "description", "auto-init", ...WRITE_FLAGS],
205
+ disconnect: ["yes"],
206
+ delete: ["yes"],
207
+ verify: [...WRITE_FLAGS],
208
+ adopt: [...WRITE_FLAGS],
209
+ },
210
+ agents: {
211
+ __default: [...PAGE_FLAGS],
212
+ list: [...PAGE_FLAGS],
213
+ status: [],
214
+ create: ["name", "engine", "contract", "model", "triggers", "schedule", "timezone", "harness", "sandbox", "enabled", ...WRITE_FLAGS],
215
+ update: ["name", "engine", "contract", "model", "triggers", "schedule", "timezone", "harness", "sandbox", "enabled"],
216
+ delete: ["yes"],
217
+ },
218
+ conversations: {
219
+ __default: [],
220
+ list: [],
221
+ get: [],
222
+ start: ["agent", "title", ...WRITE_FLAGS],
223
+ send: [...WRITE_FLAGS],
224
+ },
225
+ github: {
226
+ installations: [],
227
+ repos: ["query"],
228
+ issues: ["state", "query", "cursor", "limit"],
229
+ issue: [],
230
+ sync: [...WRITE_FLAGS],
231
+ trigger: ["agent", ...WRITE_FLAGS],
232
+ },
233
+ providers: {
234
+ __default: [...PAGE_FLAGS],
235
+ list: [...PAGE_FLAGS],
236
+ create: ["provider", "name", "secret", "base-url", ...WRITE_FLAGS],
237
+ delete: ["yes"],
238
+ },
239
+ budgets: {
240
+ __default: [...PAGE_FLAGS],
241
+ list: [...PAGE_FLAGS],
242
+ get: [],
243
+ set: ["id", "scope", "project", "agent", "period", "limit-cents", "mode", "enabled", ...WRITE_FLAGS],
244
+ create: ["id", "scope", "project", "agent", "period", "limit-cents", "mode", "enabled", ...WRITE_FLAGS],
245
+ delete: ["yes"],
246
+ },
247
+ registry: {
248
+ __default: ["kind", "scope", "project", ...PAGE_FLAGS],
249
+ list: ["kind", "scope", "project", ...PAGE_FLAGS],
250
+ get: [],
251
+ create: ["scope", "project", "kind", "name", "description", "content", "content-file", ...WRITE_FLAGS],
252
+ version: ["content", "content-file", "changelog", ...WRITE_FLAGS],
253
+ publish: [...WRITE_FLAGS],
254
+ deprecate: [...WRITE_FLAGS],
255
+ },
256
+ sandboxes: {
257
+ __default: [...PAGE_FLAGS],
258
+ list: [...PAGE_FLAGS],
259
+ create: ["name", "driver", "image", "setup", "resources", "network", ...WRITE_FLAGS],
260
+ update: ["name", "driver", "image", "setup", "resources", "network"],
261
+ delete: ["yes"],
262
+ },
263
+ tasks: {
264
+ __default: [...PAGE_FLAGS],
265
+ list: [...PAGE_FLAGS],
266
+ create: ["title", "body", "body-file", "status", "kb", "wsjf", ...WRITE_FLAGS],
267
+ update: ["title", "body", "body-file", "status", "kb", "wsjf", "gh"],
268
+ delete: ["yes"],
269
+ transition: ["status", ...WRITE_FLAGS],
270
+ propose: [...WRITE_FLAGS],
271
+ },
272
+ "virtual-keys": {
273
+ __default: [...PAGE_FLAGS],
274
+ list: [...PAGE_FLAGS],
275
+ issue: ["name", "models", "expires", ...WRITE_FLAGS],
276
+ revoke: ["yes"],
277
+ },
278
+ kb: {},
279
+ analytics: {
280
+ __default: ["project", "from", "to", ...PAGE_FLAGS],
281
+ overview: ["project", "from", "to", ...PAGE_FLAGS],
282
+ timeseries: ["project", "from", "to", "group-by", ...PAGE_FLAGS],
283
+ },
284
+ audit: {
285
+ __default: ["actor", "action", "from", "to", "cursor", "limit"],
286
+ list: ["actor", "action", "from", "to", "cursor", "limit"],
287
+ tail: ["actor", "action", "from", "to", "cursor", "limit"],
288
+ verify: [],
289
+ },
290
+ integrations: {
291
+ __default: ["project", "kind", "enabled", "limit", "offset"],
292
+ list: ["project", "kind", "enabled", "limit", "offset"],
293
+ get: [],
294
+ create: ["project", "kind", "enabled", "name", "config", "secret", ...WRITE_FLAGS],
295
+ update: ["enabled", "name", "config"],
296
+ "rotate-secret": ["secret", ...WRITE_FLAGS],
297
+ events: ["limit", "offset"],
298
+ deliveries: ["status", "limit", "offset"],
299
+ retry: [...WRITE_FLAGS],
300
+ delete: ["yes"],
301
+ },
302
+ spend: { __default: ["project", "from", "to", "group-by", ...PAGE_FLAGS] },
303
+ proposals: {
304
+ __default: [],
305
+ get: [],
306
+ create: ["project", "run", "action", "payload", "context", "expires", ...WRITE_FLAGS],
307
+ execute: [...WRITE_FLAGS],
308
+ },
309
+ "action-types": { __default: [...PAGE_FLAGS], list: [...PAGE_FLAGS], get: [] },
310
+ health: { __default: [] },
311
+ outcomes: { __default: ["project", "state", "limit"], list: ["project", "state", "limit"] },
312
+ catalog: { __default: [], show: [] },
313
+ };
314
+
315
+ export function validateAdminSubcommandFlags(group, args, flags) {
316
+ if (flags.help) return;
317
+ let spec = ADMIN_SUBCOMMAND_FLAGS[group];
318
+ if (!spec) return;
319
+ let sub = args[0] || "__default";
320
+ if (group === "health") {
321
+ // `facility health <project>` has a positional resource, not a subcommand.
322
+ sub = "__default";
323
+ } else if (group === "kb") {
324
+ const area = args[0];
325
+ sub = area === "validate" ? "validate" : `${area || ""}:${args[1] || (area === "space" ? "get" : "list")}`;
326
+ spec = {
327
+ "space:get": [],
328
+ "space:set": ["charter", "charter-file", "active", "active-file", "config"],
329
+ "entries:list": ["type", ...PAGE_FLAGS],
330
+ "entries:get": [],
331
+ "entries:create": ["type", "slug", "body", "body-file", "frontmatter", "status", "links", "dry", ...WRITE_FLAGS],
332
+ "entries:update": ["type", "number", "slug", "body", "body-file", "frontmatter", "status", "supersedes"],
333
+ validate: [...WRITE_FLAGS],
334
+ };
335
+ }
336
+ const allowed = spec[sub];
337
+ if (!allowed) {
338
+ throw cliError(
339
+ `Unknown subcommand ${JSON.stringify(args.join(" ") || sub)} for ${group}. Run facility ${group} --help.`,
340
+ "usage",
341
+ );
342
+ }
343
+ const permitted = new Set(["profile", "json", "timeout", ...allowed]);
344
+ const irrelevant = Object.keys(flags).filter((flag) => !permitted.has(flag));
345
+ if (irrelevant.length) {
346
+ throw cliError(
347
+ `Flag${irrelevant.length === 1 ? "" : "s"} ${irrelevant.map((flag) => `--${flag}`).join(", ")} ${irrelevant.length === 1 ? "is" : "are"} not valid for ${group} ${sub === "__default" ? "" : sub}`.trim(),
348
+ "invalid_flag",
349
+ );
350
+ }
351
+ // Validate pagination while the command is still in the local, pre-auth
352
+ // phase. Malformed input must not trigger project resolution or a request.
353
+ if (flags.limit !== undefined || flags.offset !== undefined) pageQuery(flags);
354
+ }
355
+
356
+ async function org(args, ctx, flags) {
357
+ const sub = args[0] || "get";
358
+ if (sub === "get" || sub === "show") {
359
+ const result = await ctx.api("GET", "/v1/org");
360
+ return details(ctx, result);
361
+ }
362
+ if (sub === "update") {
363
+ const body = compact({
364
+ name: stringFlag(flags.name),
365
+ settings: flags.settings ? jsonObject(flags.settings, "--settings") : undefined,
366
+ });
367
+ if (!Object.keys(body).length) throw usage("facility org update --name|--settings");
368
+ const result = await ctx.api("PATCH", "/v1/org", { body });
369
+ return changed(ctx, result, "updated organization");
370
+ }
371
+ throw usage("facility org get|update");
372
+ }
373
+
374
+ async function members(args, ctx, flags) {
375
+ const sub = args[0] || "list";
376
+ if (sub === "list") {
377
+ const result = await ctx.api("GET", "/v1/members", { query: pageQuery(flags) });
378
+ return list(ctx, result, ["user", "email", "name", "role"], (row) => [
379
+ row.user?.id ?? row.userId,
380
+ row.user?.email ?? row.email,
381
+ row.user?.name ?? row.name,
382
+ row.role?.name ?? row.roleName ?? row.member?.roleId,
383
+ ]);
384
+ }
385
+ if (sub === "add") {
386
+ const email = requiredStringFlag(flags.email, "--email");
387
+ const roleId = requiredStringFlag(flags.role, "--role");
388
+ return changed(
389
+ ctx,
390
+ await ctx.api("POST", "/v1/members", {
391
+ body: { email, roleId },
392
+ }),
393
+ `added ${email}`,
394
+ );
395
+ }
396
+ if (sub === "update") {
397
+ if (!args[1]) throw usage("facility members update <userId> --role <roleId>");
398
+ const roleId = requiredStringFlag(flags.role, "--role");
399
+ return changed(
400
+ ctx,
401
+ await ctx.api("PATCH", `/v1/members/${args[1]}`, { body: { roleId } }),
402
+ `updated member ${args[1]}`,
403
+ );
404
+ }
405
+ if (sub === "remove" || sub === "delete") {
406
+ if (!args[1]) throw usage("facility members remove <userId> --yes");
407
+ confirmed(flags, `Removing member ${args[1]}`);
408
+ return changed(ctx, await ctx.api("DELETE", `/v1/members/${args[1]}`), `removed member ${args[1]}`);
409
+ }
410
+ throw usage("facility members list|add|update|remove");
411
+ }
412
+
413
+ async function roles(args, ctx, flags) {
414
+ const sub = args[0] || "list";
415
+ if (sub === "list") {
416
+ const result = await ctx.api("GET", "/v1/roles", { query: pageQuery(flags) });
417
+ return list(ctx, result, ["id", "name", "permissions", "description"], (row) => [
418
+ row.id,
419
+ row.name,
420
+ arrayValue(row.permissions).join(", "),
421
+ row.description,
422
+ ]);
423
+ }
424
+ if (sub === "create") {
425
+ const name = requiredStringFlag(flags.name, "--name");
426
+ if (flags.permissions === undefined) {
427
+ throw usage("facility roles create --name <name> --permissions <a,b>");
428
+ }
429
+ return changed(
430
+ ctx,
431
+ await ctx.api("POST", "/v1/roles", {
432
+ body: {
433
+ name,
434
+ description: stringFlag(flags.description),
435
+ permissions: stringList(flags.permissions, "--permissions"),
436
+ },
437
+ }),
438
+ `created role ${name}`,
439
+ );
440
+ }
441
+ if (sub === "update") {
442
+ if (!args[1]) throw usage("facility roles update <roleId> --description|--permissions");
443
+ const body = compact({
444
+ description: stringFlag(flags.description),
445
+ permissions: flags.permissions ? stringList(flags.permissions, "--permissions") : undefined,
446
+ });
447
+ if (!Object.keys(body).length) throw usage("facility roles update <roleId> --description|--permissions");
448
+ return changed(ctx, await ctx.api("PATCH", `/v1/roles/${args[1]}`, { body }), `updated role ${args[1]}`);
449
+ }
450
+ if (sub === "delete") {
451
+ if (!args[1]) throw usage("facility roles delete <roleId> --yes");
452
+ confirmed(flags, `Deleting role ${args[1]}`);
453
+ return changed(ctx, await ctx.api("DELETE", `/v1/roles/${args[1]}`), `deleted role ${args[1]}`);
454
+ }
455
+ throw usage("facility roles list|create|update|delete");
456
+ }
457
+
458
+ async function repos(args, ctx, flags) {
459
+ const sub = args[0] || "list";
460
+ if (sub === "list") {
461
+ const project = await ctx.resolveProject(args[1]);
462
+ const result = await ctx.api("GET", `/v1/projects/${project.id}/repos`, {
463
+ query: pageQuery(flags),
464
+ });
465
+ return list(ctx, result, ["id", "repository", "branch", "fingerprint"], (row) => [
466
+ row.id,
467
+ `${row.owner}/${row.name}`,
468
+ row.defaultBranch,
469
+ row.fingerprintStatus,
470
+ ]);
471
+ }
472
+ if (sub === "connect" || sub === "create") {
473
+ const project = await ctx.resolveProject(args[1]);
474
+ const repo = requiredStringFlag(flags.repo, "--repo");
475
+ const [owner, name] = repo.split("/");
476
+ if (!owner || !name) throw cliError("--repo must use owner/name", "invalid_flag");
477
+ const result = await ctx.api("POST", `/v1/projects/${project.id}/repos`, {
478
+ body: {
479
+ owner,
480
+ name,
481
+ mode: sub,
482
+ defaultBranch: stringFlag(flags.branch) ?? "main",
483
+ private: booleanFlag(flags.private, true),
484
+ description: stringFlag(flags.description),
485
+ autoInit: booleanFlag(flags["auto-init"], true),
486
+ },
487
+ });
488
+ return changed(ctx, result, `${sub === "create" ? "created" : "connected"} ${owner}/${name}`);
489
+ }
490
+ if (sub === "disconnect" || sub === "delete") {
491
+ const project = await ctx.resolveProject(args[1]);
492
+ if (!args[2]) throw usage("facility repos disconnect <project> <repoId> --yes");
493
+ confirmed(flags, `Disconnecting repo ${args[2]}`);
494
+ return changed(
495
+ ctx,
496
+ await ctx.api("DELETE", `/v1/projects/${project.id}/repos/${args[2]}`),
497
+ `disconnected repo ${args[2]}`,
498
+ );
499
+ }
500
+ if (sub === "verify" || sub === "adopt") {
501
+ if (!args[1]) throw usage(`facility repos ${sub} <repoId>`);
502
+ return changed(
503
+ ctx,
504
+ await ctx.api("POST", `/v1/repos/${args[1]}/fingerprints/${sub}`),
505
+ `${sub} fingerprints for ${args[1]}`,
506
+ );
507
+ }
508
+ throw usage("facility repos list|connect|create|disconnect|verify|adopt");
509
+ }
510
+
511
+ async function agents(args, ctx, flags) {
512
+ const sub = args[0] || "list";
513
+ const project = await ctx.resolveProject(args[1]);
514
+ const base = `/v1/projects/${project.id}/agents`;
515
+ if (sub === "list") {
516
+ return list(ctx, await ctx.api("GET", base, { query: pageQuery(flags) }), ["id", "name", "engine", "enabled", "contract"], (row) => [
517
+ row.id,
518
+ row.name,
519
+ row.engine,
520
+ row.enabled ? "yes" : "no",
521
+ row.contractItemId,
522
+ ]);
523
+ }
524
+ if (sub === "status") {
525
+ return list(
526
+ ctx,
527
+ await ctx.api("GET", `${base}/status`),
528
+ ["agent", "engine", "enabled", "live", "last", "next", "14d"],
529
+ (row) => [
530
+ row.name,
531
+ row.engine,
532
+ row.enabled ? "yes" : "no",
533
+ row.liveRun?.status,
534
+ row.lastRun?.status,
535
+ row.nextRunAt,
536
+ `${row.counts14d?.succeeded ?? 0}/${row.counts14d?.total ?? 0}`,
537
+ ],
538
+ );
539
+ }
540
+ if (sub === "create") {
541
+ const name = requiredStringFlag(flags.name, "--name");
542
+ const engine = requiredStringFlag(flags.engine, "--engine");
543
+ const contractItemId = requiredStringFlag(flags.contract, "--contract");
544
+ const body = {
545
+ name,
546
+ engine,
547
+ contractItemId,
548
+ model: flags.model ? jsonObject(flags.model, "--model") : {},
549
+ triggers: agentTriggers(flags, []),
550
+ harnessItemId: stringFlag(flags.harness),
551
+ sandboxProfileId: stringFlag(flags.sandbox),
552
+ enabled: booleanFlag(flags.enabled, true),
553
+ };
554
+ return changed(ctx, await ctx.api("POST", base, { body }), `created agent ${name}`);
555
+ }
556
+ if (sub === "update") {
557
+ if (!args[2]) throw usage("facility agents update <project> <agentId> [flags]");
558
+ const body = compact({
559
+ name: stringFlag(flags.name),
560
+ engine: stringFlag(flags.engine),
561
+ contractItemId: stringFlag(flags.contract),
562
+ harnessItemId: stringFlag(flags.harness),
563
+ sandboxProfileId: stringFlag(flags.sandbox),
564
+ model: flags.model ? jsonObject(flags.model, "--model") : undefined,
565
+ triggers:
566
+ flags.triggers !== undefined || flags.schedule !== undefined
567
+ ? agentTriggers(flags, [])
568
+ : undefined,
569
+ enabled: flags.enabled === undefined ? undefined : booleanFlag(flags.enabled),
570
+ });
571
+ if (!Object.keys(body).length) throw usage("facility agents update <project> <agentId> [flags]");
572
+ return changed(ctx, await ctx.api("PATCH", `${base}/${args[2]}`, { body }), `updated agent ${args[2]}`);
573
+ }
574
+ if (sub === "delete") {
575
+ if (!args[2]) throw usage("facility agents delete <project> <agentId> --yes");
576
+ confirmed(flags, `Deleting agent ${args[2]}`);
577
+ return changed(ctx, await ctx.api("DELETE", `${base}/${args[2]}`), `deleted agent ${args[2]}`);
578
+ }
579
+ throw usage("facility agents list|status|create|update|delete <project>");
580
+ }
581
+
582
+ async function conversations(args, ctx, flags) {
583
+ const sub = args[0] || "list";
584
+ if (sub === "list") {
585
+ const project = await ctx.resolveProject(args[1]);
586
+ return list(
587
+ ctx,
588
+ await ctx.api("GET", `/v1/projects/${project.id}/conversations`),
589
+ ["id", "title", "status", "agent", "updated", "last message"],
590
+ (row) => [
591
+ row.id,
592
+ row.title,
593
+ row.status,
594
+ row.agentDefId,
595
+ row.updatedAt,
596
+ row.lastMessage?.body,
597
+ ],
598
+ );
599
+ }
600
+ if (sub === "get") {
601
+ if (!args[1]) throw usage("facility conversations get <conversationId>");
602
+ const result = await ctx.api("GET", `/v1/conversations/${args[1]}`);
603
+ if (ctx.json) ctx.writeJson(result);
604
+ else {
605
+ ctx.table(
606
+ ["field", "value"],
607
+ Object.entries(result.conversation ?? {}).map(([key, value]) => [key, display(value)]),
608
+ );
609
+ ctx.table(
610
+ ["#", "role", "message", "session", "time"],
611
+ arrayValue(result.messages).map((row) => [
612
+ row.seq,
613
+ row.role,
614
+ row.body,
615
+ row.runId,
616
+ row.createdAt,
617
+ ]),
618
+ );
619
+ }
620
+ return 0;
621
+ }
622
+ if (sub === "start") {
623
+ const project = await ctx.resolveProject(args[1]);
624
+ const result = await ctx.api("POST", `/v1/projects/${project.id}/conversations`, {
625
+ body: compact({
626
+ agentDefId: stringFlag(flags.agent),
627
+ title: stringFlag(flags.title),
628
+ }),
629
+ });
630
+ return changed(ctx, result, `started conversation ${result.id}`);
631
+ }
632
+ if (sub === "send") {
633
+ if (!args[1] || args.length < 3) {
634
+ throw usage("facility conversations send <conversationId> <message>");
635
+ }
636
+ const result = await ctx.api("POST", `/v1/conversations/${args[1]}/messages`, {
637
+ body: { body: args.slice(2).join(" ") },
638
+ });
639
+ return changed(ctx, result, `queued session ${result.runId}`);
640
+ }
641
+ throw usage("facility conversations list|get|start|send");
642
+ }
643
+
644
+ async function github(args, ctx, flags) {
645
+ const sub = args[0];
646
+ if (sub === "installations") {
647
+ return list(
648
+ ctx,
649
+ await ctx.api("GET", "/v1/github/installations"),
650
+ ["installation", "account", "target", "status"],
651
+ (row) => [
652
+ row.installationId,
653
+ row.accountLogin,
654
+ row.targetType,
655
+ row.suspendedAt ? "suspended" : "active",
656
+ ],
657
+ );
658
+ }
659
+ if (sub === "repos") {
660
+ const installationId = integerArgument(args[1], "installationId");
661
+ const result = await ctx.api("GET", `/v1/github/installations/${installationId}/repos`, {
662
+ query: compact({ query: stringFlag(flags.query) }),
663
+ });
664
+ return list(ctx, result, ["repository", "visibility", "branch", "URL"], (row) => [
665
+ row.fullName,
666
+ row.private ? "private" : "public",
667
+ row.defaultBranch,
668
+ row.htmlUrl,
669
+ ]);
670
+ }
671
+ if (sub === "issues") {
672
+ const project = await ctx.resolveProject(args[1]);
673
+ const result = await ctx.api("GET", `/v1/projects/${project.id}/issues`, {
674
+ query: compact({
675
+ state: stringFlag(flags.state) ?? "open",
676
+ q: stringFlag(flags.query),
677
+ cursor: stringFlag(flags.cursor),
678
+ limit: numberFlag(flags.limit, "--limit"),
679
+ }),
680
+ });
681
+ return list(ctx, result, ["#", "state", "title", "author", "updated", "sessions"], (row) => [
682
+ row.number,
683
+ row.state,
684
+ row.title,
685
+ row.author,
686
+ row.ghUpdatedAt,
687
+ row.linkedRuns?.length ?? 0,
688
+ ]);
689
+ }
690
+ if (sub === "issue") {
691
+ const project = await ctx.resolveProject(args[1]);
692
+ const number = integerArgument(args[2], "issue number");
693
+ return details(ctx, await ctx.api("GET", `/v1/projects/${project.id}/issues/${number}`));
694
+ }
695
+ if (sub === "sync") {
696
+ const project = await ctx.resolveProject(args[1]);
697
+ const result = await ctx.api("POST", `/v1/projects/${project.id}/issues/sync`);
698
+ return changed(ctx, result, `queued issue sync for ${result.queued ?? 0} repositories`);
699
+ }
700
+ if (sub === "trigger") {
701
+ const project = await ctx.resolveProject(args[1]);
702
+ const number = integerArgument(args[2], "issue number");
703
+ const agent = requiredStringFlag(flags.agent, "--agent");
704
+ const result = await ctx.api(
705
+ "POST",
706
+ `/v1/projects/${project.id}/issues/${number}/trigger`,
707
+ { body: { agent } },
708
+ );
709
+ return changed(ctx, result, `triggered session ${result.id} from issue #${number}`);
710
+ }
711
+ throw usage("facility github installations|repos|issues|issue|sync|trigger");
712
+ }
713
+
714
+ async function providers(args, ctx, flags) {
715
+ const sub = args[0] || "list";
716
+ if (sub === "list") {
717
+ return list(ctx, await ctx.api("GET", "/v1/providers", { query: pageQuery(flags) }), ["id", "provider", "name", "base URL"], (row) => [
718
+ row.id,
719
+ row.provider,
720
+ row.name,
721
+ row.baseUrl,
722
+ ]);
723
+ }
724
+ if (sub === "create") {
725
+ const provider = requiredStringFlag(flags.provider, "--provider");
726
+ const name = requiredStringFlag(flags.name, "--name");
727
+ const secret = requiredStringFlag(flags.secret, "--secret");
728
+ return changed(
729
+ ctx,
730
+ await ctx.api("POST", "/v1/providers", {
731
+ body: {
732
+ provider,
733
+ name,
734
+ secret,
735
+ baseUrl: stringFlag(flags["base-url"]),
736
+ },
737
+ }),
738
+ `created ${provider} provider ${name}`,
739
+ );
740
+ }
741
+ if (sub === "delete") {
742
+ if (!args[1]) throw usage("facility providers delete <providerId> --yes");
743
+ confirmed(flags, `Deleting provider ${args[1]}`);
744
+ return changed(ctx, await ctx.api("DELETE", `/v1/providers/${args[1]}`), `deleted provider ${args[1]}`);
745
+ }
746
+ throw usage("facility providers list|create|delete");
747
+ }
748
+
749
+ async function budgets(args, ctx, flags) {
750
+ const sub = args[0] || "list";
751
+ if (sub === "list") {
752
+ const result = await ctx.api("GET", "/v1/budgets", { query: pageQuery(flags) });
753
+ return list(ctx, result, ["id", "scope", "period", "limit", "mode", "enabled"], (row) => [
754
+ row.id,
755
+ row.scope,
756
+ row.period,
757
+ money(row.limitCents),
758
+ row.mode,
759
+ row.enabled ? "yes" : "no",
760
+ ]);
761
+ }
762
+ if (sub === "get") {
763
+ if (!args[1]) throw usage("facility budgets get <budgetId>");
764
+ return details(ctx, await ctx.api("GET", `/v1/budgets/${args[1]}`));
765
+ }
766
+ if (sub === "set" || sub === "create") {
767
+ const budgetId = args[1] ?? stringFlag(flags.id);
768
+ const body = compact({
769
+ scope: stringFlag(flags.scope),
770
+ projectId: stringFlag(flags.project),
771
+ agentDefId: stringFlag(flags.agent),
772
+ period: stringFlag(flags.period),
773
+ limitCents: numberFlag(flags["limit-cents"], "--limit-cents"),
774
+ mode: stringFlag(flags.mode),
775
+ enabled: flags.enabled === undefined ? undefined : booleanFlag(flags.enabled),
776
+ });
777
+ if (!budgetId && (!body.scope || !body.period || body.limitCents === undefined || !body.mode)) {
778
+ throw usage("facility budgets set --scope --period --limit-cents --mode [--project|--agent]");
779
+ }
780
+ const result = budgetId
781
+ ? await ctx.api("PATCH", `/v1/budgets/${budgetId}`, { body })
782
+ : await ctx.api("POST", "/v1/budgets", { body });
783
+ return changed(ctx, result, `${budgetId ? "updated" : "created"} budget ${result.id ?? budgetId}`);
784
+ }
785
+ if (sub === "delete") {
786
+ if (!args[1]) throw usage("facility budgets delete <budgetId> --yes");
787
+ confirmed(flags, `Deleting budget ${args[1]}`);
788
+ return changed(ctx, await ctx.api("DELETE", `/v1/budgets/${args[1]}`), `deleted budget ${args[1]}`);
789
+ }
790
+ throw usage("facility budgets list|get|set|delete");
791
+ }
792
+
793
+ async function registry(args, ctx, flags) {
794
+ const sub = args[0] || "list";
795
+ if (sub === "list") {
796
+ const result = await ctx.api("GET", "/v1/registry/items", {
797
+ query: compact({ kind: stringFlag(flags.kind), scope: stringFlag(flags.scope), projectId: stringFlag(flags.project), ...pageQuery(flags) }),
798
+ });
799
+ return list(ctx, result, ["id", "scope", "kind", "name", "latest"], (row) => [
800
+ row.id,
801
+ row.scope,
802
+ row.kind,
803
+ row.name,
804
+ row.latestVersion,
805
+ ]);
806
+ }
807
+ if (sub === "get") {
808
+ if (!args[1]) throw usage("facility registry get <itemId>");
809
+ return details(ctx, await ctx.api("GET", `/v1/registry/items/${args[1]}`));
810
+ }
811
+ if (sub === "create") {
812
+ const scope = requiredStringFlag(flags.scope, "--scope");
813
+ const kind = requiredStringFlag(flags.kind, "--kind");
814
+ const name = requiredStringFlag(flags.name, "--name");
815
+ const content = contentFlag(flags, "content");
816
+ return changed(
817
+ ctx,
818
+ await ctx.api("POST", "/v1/registry/items", {
819
+ body: {
820
+ scope,
821
+ projectId: stringFlag(flags.project),
822
+ kind,
823
+ name,
824
+ description: stringFlag(flags.description),
825
+ content,
826
+ },
827
+ }),
828
+ `created registry item ${name}`,
829
+ );
830
+ }
831
+ if (sub === "version") {
832
+ if (!args[1]) throw usage("facility registry version <itemId> --content|--content-file");
833
+ return changed(
834
+ ctx,
835
+ await ctx.api("POST", `/v1/registry/items/${args[1]}/versions`, {
836
+ body: { content: contentFlag(flags, "content"), changelog: stringFlag(flags.changelog) },
837
+ }),
838
+ `created version for ${args[1]}`,
839
+ );
840
+ }
841
+ if (sub === "publish" || sub === "deprecate") {
842
+ if (!args[1]) throw usage(`facility registry ${sub} <versionId>`);
843
+ return changed(
844
+ ctx,
845
+ await ctx.api("POST", `/v1/registry/versions/${args[1]}/${sub}`),
846
+ `${sub}d registry version ${args[1]}`,
847
+ );
848
+ }
849
+ throw usage("facility registry list|get|create|version|publish|deprecate");
850
+ }
851
+
852
+ async function sandboxes(args, ctx, flags) {
853
+ const sub = args[0] || "list";
854
+ if (sub === "list") {
855
+ return list(ctx, await ctx.api("GET", "/v1/sandbox-profiles", { query: pageQuery(flags) }), ["id", "name", "driver", "image", "project"], (row) => [
856
+ row.id,
857
+ row.name,
858
+ row.driver,
859
+ row.image,
860
+ row.projectId,
861
+ ]);
862
+ }
863
+ if (sub === "create") {
864
+ const name = requiredStringFlag(flags.name, "--name");
865
+ const driver = requiredStringFlag(flags.driver, "--driver");
866
+ const image = requiredStringFlag(flags.image, "--image");
867
+ const body = {
868
+ name,
869
+ driver,
870
+ image,
871
+ setup: flags.setup ? jsonObject(flags.setup, "--setup") : {},
872
+ resources: flags.resources ? jsonObject(flags.resources, "--resources") : {},
873
+ network: flags.network ? jsonObject(flags.network, "--network") : {},
874
+ };
875
+ return changed(ctx, await ctx.api("POST", "/v1/sandbox-profiles", { body }), `created sandbox ${name}`);
876
+ }
877
+ if (sub === "update") {
878
+ if (!args[1]) throw usage("facility sandboxes update <id> [flags]");
879
+ const body = compact({
880
+ name: stringFlag(flags.name),
881
+ driver: stringFlag(flags.driver),
882
+ image: stringFlag(flags.image),
883
+ setup: flags.setup ? jsonObject(flags.setup, "--setup") : undefined,
884
+ resources: flags.resources ? jsonObject(flags.resources, "--resources") : undefined,
885
+ network: flags.network ? jsonObject(flags.network, "--network") : undefined,
886
+ });
887
+ return changed(ctx, await ctx.api("PATCH", `/v1/sandbox-profiles/${args[1]}`, { body }), `updated sandbox ${args[1]}`);
888
+ }
889
+ if (sub === "delete") {
890
+ if (!args[1]) throw usage("facility sandboxes delete <id> --yes");
891
+ confirmed(flags, `Deleting sandbox ${args[1]}`);
892
+ return changed(ctx, await ctx.api("DELETE", `/v1/sandbox-profiles/${args[1]}`), `deleted sandbox ${args[1]}`);
893
+ }
894
+ throw usage("facility sandboxes list|create|update|delete");
895
+ }
896
+
897
+ async function tasks(args, ctx, flags) {
898
+ const sub = args[0] || "list";
899
+ if (["list", "create", "update", "delete"].includes(sub)) {
900
+ const project = await ctx.resolveProject(args[1]);
901
+ const base = `/v1/projects/${project.id}/tasks`;
902
+ if (sub === "list") {
903
+ return list(ctx, await ctx.api("GET", base, { query: pageQuery(flags) }), ["id", "status", "title", "KB entry"], (row) => [
904
+ row.id,
905
+ row.status,
906
+ row.title,
907
+ row.kbEntryId,
908
+ ]);
909
+ }
910
+ if (sub === "create") {
911
+ const title = requiredStringFlag(flags.title, "--title");
912
+ return changed(
913
+ ctx,
914
+ await ctx.api("POST", base, {
915
+ body: {
916
+ title,
917
+ bodyMd: contentFlag(flags, "body"),
918
+ status: stringFlag(flags.status) ?? "draft",
919
+ kbEntryId: stringFlag(flags.kb),
920
+ wsjf: flags.wsjf ? jsonObject(flags.wsjf, "--wsjf") : {},
921
+ },
922
+ }),
923
+ `created task ${title}`,
924
+ );
925
+ }
926
+ if (!args[2]) throw usage(`facility tasks ${sub} <project> <taskId>`);
927
+ if (sub === "delete") {
928
+ confirmed(flags, `Deleting task ${args[2]}`);
929
+ return changed(ctx, await ctx.api("DELETE", `${base}/${args[2]}`), `deleted task ${args[2]}`);
930
+ }
931
+ const body = compact({
932
+ title: stringFlag(flags.title),
933
+ bodyMd: optionalContentFlag(flags, "body"),
934
+ status: stringFlag(flags.status),
935
+ kbEntryId: stringFlag(flags.kb),
936
+ wsjf: flags.wsjf ? jsonObject(flags.wsjf, "--wsjf") : undefined,
937
+ gh: flags.gh ? jsonObject(flags.gh, "--gh") : undefined,
938
+ });
939
+ return changed(ctx, await ctx.api("PATCH", `${base}/${args[2]}`, { body }), `updated task ${args[2]}`);
940
+ }
941
+ if (sub === "transition") {
942
+ if (!args[1]) throw usage("facility tasks transition <taskId> --status <status>");
943
+ const status = requiredStringFlag(flags.status, "--status");
944
+ return changed(ctx, await ctx.api("POST", `/v1/tasks/${args[1]}/transition`, { body: { status } }), `transitioned task ${args[1]}`);
945
+ }
946
+ if (sub === "propose") {
947
+ if (!args[1]) throw usage("facility tasks propose <taskId>");
948
+ return changed(ctx, await ctx.api("POST", `/v1/tasks/${args[1]}/propose`), `proposed task ${args[1]}`);
949
+ }
950
+ throw usage("facility tasks list|create|update|delete|transition|propose");
951
+ }
952
+
953
+ async function virtualKeys(args, ctx, flags) {
954
+ const sub = args[0] || "list";
955
+ const project = await ctx.resolveProject(args[1]);
956
+ const base = `/v1/projects/${project.id}/virtual-keys`;
957
+ if (sub === "list") {
958
+ return list(ctx, await ctx.api("GET", base, { query: pageQuery(flags) }), ["id", "name", "last4", "expires", "revoked"], (row) => [
959
+ row.id,
960
+ row.name,
961
+ row.last4,
962
+ row.expiresAt,
963
+ row.revokedAt ? "yes" : "no",
964
+ ]);
965
+ }
966
+ if (sub === "issue") {
967
+ const name = requiredStringFlag(flags.name, "--name");
968
+ const result = await ctx.api("POST", base, {
969
+ body: {
970
+ name,
971
+ allowedModels: flags.models ? stringList(flags.models, "--models") : undefined,
972
+ expiresAt: stringFlag(flags.expires),
973
+ },
974
+ });
975
+ return secretResult(ctx, result, `issued virtual key ${result.id}`);
976
+ }
977
+ if (sub === "revoke") {
978
+ if (!args[2]) throw usage("facility virtual-keys revoke <project> <keyId> --yes");
979
+ confirmed(flags, `Revoking virtual key ${args[2]}`);
980
+ return changed(ctx, await ctx.api("DELETE", `${base}/${args[2]}`), `revoked virtual key ${args[2]}`);
981
+ }
982
+ throw usage("facility virtual-keys list|issue|revoke <project>");
983
+ }
984
+
985
+ async function kb(args, ctx, flags) {
986
+ const area = args[0];
987
+ if (area === "space") {
988
+ const sub = args[1] || "get";
989
+ const project = await ctx.resolveProject(args[2]);
990
+ const path = `/v1/projects/${project.id}/kb/space`;
991
+ if (sub === "get") return details(ctx, await ctx.api("GET", path));
992
+ if (sub === "set") {
993
+ const charterMd = optionalContentFlag(flags, "charter");
994
+ const activeMd = optionalContentFlag(flags, "active");
995
+ const config = flags.config ? jsonObject(flags.config, "--config") : undefined;
996
+ if (charterMd === undefined && activeMd === undefined && config === undefined) {
997
+ throw usage("facility kb space set <project> --charter|--charter-file|--active|--active-file|--config <value>");
998
+ }
999
+ const current = await ctx.api("GET", path);
1000
+ return changed(
1001
+ ctx,
1002
+ await ctx.api("PUT", path, {
1003
+ body: {
1004
+ charterMd: charterMd ?? current?.charterMd ?? "",
1005
+ activeMd: activeMd ?? current?.activeMd ?? "",
1006
+ config: config ?? current?.config ?? {},
1007
+ },
1008
+ }),
1009
+ `updated KB space for ${project.slug}`,
1010
+ );
1011
+ }
1012
+ }
1013
+ if (area === "entries") {
1014
+ const sub = args[1] || "list";
1015
+ if (sub === "get") {
1016
+ if (!args[2]) throw usage("facility kb entries get <entryId>");
1017
+ return details(ctx, await ctx.api("GET", `/v1/kb/entries/${args[2]}`));
1018
+ }
1019
+ const project = await ctx.resolveProject(args[2]);
1020
+ const base = `/v1/projects/${project.id}/kb/entries`;
1021
+ if (sub === "list") {
1022
+ return list(ctx, await ctx.api("GET", base, { query: { type: stringFlag(flags.type), ...pageQuery(flags) } }), ["id", "type", "number", "slug", "status"], (row) => [
1023
+ row.id,
1024
+ row.type,
1025
+ row.number,
1026
+ row.slug,
1027
+ row.status,
1028
+ ]);
1029
+ }
1030
+ if (sub === "create") {
1031
+ const type = requiredStringFlag(flags.type, "--type");
1032
+ const slug = requiredStringFlag(flags.slug, "--slug");
1033
+ return changed(
1034
+ ctx,
1035
+ await ctx.api("POST", base, {
1036
+ query: flags.dry ? { dry: 1 } : undefined,
1037
+ body: {
1038
+ type,
1039
+ slug,
1040
+ frontmatter: flags.frontmatter ? jsonObject(flags.frontmatter, "--frontmatter") : {},
1041
+ bodyMd: contentFlag(flags, "body"),
1042
+ status: stringFlag(flags.status),
1043
+ links: flags.links ? stringList(flags.links, "--links") : [],
1044
+ },
1045
+ }),
1046
+ `${flags.dry ? "validated" : "created"} KB entry ${slug}`,
1047
+ );
1048
+ }
1049
+ if (sub === "update") {
1050
+ if (!args[3]) throw usage("facility kb entries update <project> <entryId> [flags]");
1051
+ const body = compact({
1052
+ type: stringFlag(flags.type),
1053
+ number: numberFlag(flags.number, "--number"),
1054
+ slug: stringFlag(flags.slug),
1055
+ frontmatter: flags.frontmatter ? jsonObject(flags.frontmatter, "--frontmatter") : undefined,
1056
+ bodyMd: optionalContentFlag(flags, "body"),
1057
+ status: stringFlag(flags.status),
1058
+ supersedes: stringFlag(flags.supersedes),
1059
+ });
1060
+ return changed(ctx, await ctx.api("PATCH", `/v1/kb/entries/${args[3]}`, { body }), `updated KB entry ${args[3]}`);
1061
+ }
1062
+ }
1063
+ if (area === "validate") {
1064
+ const project = await ctx.resolveProject(args[1]);
1065
+ return verificationDetails(
1066
+ ctx,
1067
+ await ctx.api("POST", `/v1/projects/${project.id}/kb/validate`),
1068
+ );
1069
+ }
1070
+ throw usage("facility kb space get|set | entries list|get|create|update | validate");
1071
+ }
1072
+
1073
+ async function analytics(args, ctx, flags) {
1074
+ const sub = args[0] || "overview";
1075
+ if (!["overview", "timeseries"].includes(sub)) {
1076
+ throw usage("facility analytics overview|timeseries [--project <id>] [--from <date>] [--to <date>] [--group-by <day|agent|model>]");
1077
+ }
1078
+ const path = sub === "overview" ? "/v1/analytics/overview" : "/v1/analytics";
1079
+ const groupBy = stringFlag(flags["group-by"]);
1080
+ if (groupBy && !["day", "agent", "model"].includes(groupBy)) {
1081
+ throw cliError("--group-by must be day, agent, or model", "invalid_flag");
1082
+ }
1083
+ if (sub === "overview" && groupBy) {
1084
+ throw cliError("--group-by is only available with `facility analytics timeseries`", "invalid_flag");
1085
+ }
1086
+ const result = await ctx.api("GET", path, {
1087
+ query: compact({
1088
+ projectId: stringFlag(flags.project),
1089
+ from: stringFlag(flags.from),
1090
+ to: stringFlag(flags.to),
1091
+ groupBy,
1092
+ ...pageQuery(flags),
1093
+ }),
1094
+ });
1095
+ if (ctx.json || !Array.isArray(result)) return details(ctx, result);
1096
+ return list(ctx, result, ["date", "project", "runs", "success", "cost"], (row) => [
1097
+ row.day ?? row.date ?? row.bucket,
1098
+ row.projectId,
1099
+ row.runsStarted,
1100
+ row.runsSucceeded,
1101
+ money(row.costCents),
1102
+ ]);
1103
+ }
1104
+
1105
+ async function audit(args, ctx, flags) {
1106
+ const sub = args[0] || "list";
1107
+ if (sub === "verify") {
1108
+ return verificationDetails(ctx, await ctx.api("GET", "/v1/audit/verify"));
1109
+ }
1110
+ if (sub === "list" || sub === "tail") {
1111
+ const result = await ctx.api("GET", "/v1/audit", {
1112
+ query: compact({
1113
+ actor: stringFlag(flags.actor),
1114
+ action: stringFlag(flags.action),
1115
+ from: numberFlag(flags.from, "--from"),
1116
+ to: numberFlag(flags.to, "--to"),
1117
+ cursor: numberFlag(flags.cursor, "--cursor"),
1118
+ limit: numberFlag(flags.limit, "--limit"),
1119
+ }),
1120
+ });
1121
+ if (ctx.json) return details(ctx, result);
1122
+ return list(ctx, result.items ?? result, ["seq", "time", "actor", "action", "target"], (row) => [
1123
+ row.seq,
1124
+ row.createdAt,
1125
+ `${row.actor?.type ?? ""}:${row.actor?.id ?? ""}`,
1126
+ row.action,
1127
+ row.target?.id,
1128
+ ]);
1129
+ }
1130
+ throw usage("facility audit list|verify");
1131
+ }
1132
+
1133
+ async function integrations(args, ctx, flags) {
1134
+ const sub = args[0] || "list";
1135
+ if (sub === "list") {
1136
+ const result = await ctx.api("GET", "/v1/integrations", {
1137
+ query: compact({
1138
+ projectId: stringFlag(flags.project),
1139
+ kind: stringFlag(flags.kind),
1140
+ enabled: flags.enabled === undefined ? undefined : String(booleanFlag(flags.enabled)),
1141
+ ...pageQuery(flags),
1142
+ }),
1143
+ });
1144
+ return list(ctx, result, ["id", "kind", "name", "project", "enabled", "webhook URL"], (row) => [
1145
+ row.id,
1146
+ row.kind,
1147
+ row.name,
1148
+ row.projectId,
1149
+ row.enabled ? "yes" : "no",
1150
+ row.webhookUrl,
1151
+ ]);
1152
+ }
1153
+ if (sub === "get") {
1154
+ if (!args[1]) throw usage("facility integrations get <id>");
1155
+ return details(ctx, await ctx.api("GET", `/v1/integrations/${args[1]}`));
1156
+ }
1157
+ if (sub === "create") {
1158
+ const kind = requiredStringFlag(flags.kind, "--kind");
1159
+ const name = requiredStringFlag(flags.name, "--name");
1160
+ const result = await ctx.api("POST", "/v1/integrations", {
1161
+ body: {
1162
+ projectId: stringFlag(flags.project),
1163
+ kind,
1164
+ name,
1165
+ config: flags.config ? jsonObject(flags.config, "--config") : {},
1166
+ secret: stringFlag(flags.secret),
1167
+ enabled: booleanFlag(flags.enabled, true),
1168
+ },
1169
+ });
1170
+ return secretResult(ctx, result, `created integration ${result.id}`);
1171
+ }
1172
+ if (sub === "update") {
1173
+ if (!args[1]) throw usage("facility integrations update <id> [--name|--config|--enabled]");
1174
+ const body = compact({
1175
+ name: stringFlag(flags.name),
1176
+ config: flags.config ? jsonObject(flags.config, "--config") : undefined,
1177
+ enabled: flags.enabled === undefined ? undefined : booleanFlag(flags.enabled),
1178
+ });
1179
+ return changed(ctx, await ctx.api("PATCH", `/v1/integrations/${args[1]}`, { body }), `updated integration ${args[1]}`);
1180
+ }
1181
+ if (sub === "rotate-secret") {
1182
+ if (!args[1]) throw usage("facility integrations rotate-secret <id> [--secret]");
1183
+ return secretResult(
1184
+ ctx,
1185
+ await ctx.api("POST", `/v1/integrations/${args[1]}/rotate-secret`, { body: { secret: stringFlag(flags.secret) } }),
1186
+ `rotated secret for integration ${args[1]}`,
1187
+ );
1188
+ }
1189
+ if (sub === "deliveries") {
1190
+ if (!args[1]) throw usage("facility integrations deliveries <id>");
1191
+ return list(
1192
+ ctx,
1193
+ await ctx.api("GET", `/v1/integrations/${args[1]}/deliveries`, {
1194
+ query: compact({
1195
+ status: stringFlag(flags.status),
1196
+ ...pageQuery(flags),
1197
+ }),
1198
+ }),
1199
+ ["id", "event", "status", "attempts", "HTTP", "error"],
1200
+ (row) => [row.id, row.eventType, row.status, row.attempts, row.responseStatus, row.error],
1201
+ );
1202
+ }
1203
+ if (sub === "events") {
1204
+ if (!args[1]) throw usage("facility integrations events <id>");
1205
+ return list(
1206
+ ctx,
1207
+ await ctx.api("GET", `/v1/integrations/${args[1]}/events`, {
1208
+ query: pageQuery(flags),
1209
+ }),
1210
+ ["id", "received", "type", "verified", "processed", "error"],
1211
+ (row) => [
1212
+ row.id,
1213
+ row.receivedAt,
1214
+ row.eventType,
1215
+ row.verified ? "yes" : "no",
1216
+ row.processedAt,
1217
+ row.error,
1218
+ ],
1219
+ );
1220
+ }
1221
+ if (sub === "retry") {
1222
+ if (!args[1]) throw usage("facility integrations retry <deliveryId>");
1223
+ return changed(ctx, await ctx.api("POST", `/v1/webhook-deliveries/${args[1]}/retry`), `queued delivery ${args[1]} for retry`);
1224
+ }
1225
+ if (sub === "delete") {
1226
+ if (!args[1]) throw usage("facility integrations delete <id> --yes");
1227
+ confirmed(flags, `Disabling integration ${args[1]}`);
1228
+ return changed(ctx, await ctx.api("DELETE", `/v1/integrations/${args[1]}`), `disabled integration ${args[1]}`);
1229
+ }
1230
+ throw usage("facility integrations list|get|create|update|rotate-secret|events|deliveries|retry|delete");
1231
+ }
1232
+
1233
+ async function spend(_args, ctx, flags) {
1234
+ const groupBy = stringFlag(flags["group-by"]);
1235
+ if (groupBy && !["day", "model", "agent", "task"].includes(groupBy)) {
1236
+ throw cliError("--group-by must be day, model, agent, or task", "invalid_flag");
1237
+ }
1238
+ const result = await ctx.api("GET", "/v1/spend", {
1239
+ query: compact({
1240
+ projectId: stringFlag(flags.project),
1241
+ from: stringFlag(flags.from),
1242
+ to: stringFlag(flags.to),
1243
+ groupBy,
1244
+ ...pageQuery(flags),
1245
+ }),
1246
+ });
1247
+ return list(ctx, result, ["bucket", "cost"], (row) => [
1248
+ row.bucket,
1249
+ money(row.costCents ?? row.cost_cents),
1250
+ ]);
1251
+ }
1252
+
1253
+ async function proposals(args, ctx, flags) {
1254
+ const sub = args[0] || "get";
1255
+ if (sub === "get") {
1256
+ if (!args[1]) throw usage("facility proposals get <proposalId>");
1257
+ return details(ctx, await ctx.api("GET", `/v1/proposals/${args[1]}`));
1258
+ }
1259
+ if (sub === "create") {
1260
+ const actionTypeId = requiredStringFlag(flags.action, "--action");
1261
+ const contextMd = requiredStringFlag(flags.context, "--context");
1262
+ return changed(
1263
+ ctx,
1264
+ await ctx.api("POST", "/v1/proposals", {
1265
+ body: {
1266
+ projectId: stringFlag(flags.project),
1267
+ runId: stringFlag(flags.run),
1268
+ actionTypeId,
1269
+ payload: flags.payload ? jsonObject(flags.payload, "--payload") : {},
1270
+ contextMd,
1271
+ expiresAt: stringFlag(flags.expires),
1272
+ },
1273
+ idempotencyKey: stringFlag(flags["idempotency-key"]),
1274
+ }),
1275
+ "created proposal",
1276
+ );
1277
+ }
1278
+ if (sub === "execute") {
1279
+ if (!args[1]) throw usage("facility proposals execute <proposalId>");
1280
+ return changed(ctx, await ctx.api("POST", `/v1/proposals/${args[1]}/execute`), `executed proposal ${args[1]}`);
1281
+ }
1282
+ throw usage("facility proposals get|create|execute");
1283
+ }
1284
+
1285
+ async function actionTypes(args, ctx, flags) {
1286
+ const sub = args[0] || "list";
1287
+ if (sub === "list") {
1288
+ return list(
1289
+ ctx,
1290
+ await ctx.api("GET", "/v1/action-types", { query: pageQuery(flags) }),
1291
+ ["id", "name", "TTL", "payload schema"],
1292
+ (row) => [row.id, row.name, `${row.defaultTtlHours}h`, display(row.payloadSchema)],
1293
+ );
1294
+ }
1295
+ if (sub === "get") {
1296
+ if (!args[1]) throw usage("facility action-types get <actionTypeId>");
1297
+ return details(ctx, await ctx.api("GET", `/v1/action-types/${args[1]}`));
1298
+ }
1299
+ throw usage("facility action-types list|get");
1300
+ }
1301
+
1302
+ async function projectHealth(args, ctx) {
1303
+ const project = await ctx.resolveProject(args[0]);
1304
+ return details(ctx, await ctx.api("GET", `/v1/projects/${project.id}/health`));
1305
+ }
1306
+
1307
+ async function outcomes(_args, ctx, flags) {
1308
+ const result = await ctx.api("GET", "/v1/outcomes", {
1309
+ query: compact({
1310
+ projectId: stringFlag(flags.project),
1311
+ state: stringFlag(flags.state) ?? "open",
1312
+ limit: numberFlag(flags.limit, "--limit"),
1313
+ }),
1314
+ });
1315
+ return list(ctx, result, ["repository", "PR", "lane", "fate", "opened", "terminal", "session"], (row) => [
1316
+ row.repo,
1317
+ `#${row.prNumber}`,
1318
+ row.agentLane,
1319
+ row.fate ?? "open",
1320
+ row.openedAt,
1321
+ row.terminalAt,
1322
+ row.runId,
1323
+ ]);
1324
+ }
1325
+
1326
+ async function catalog(_args, ctx) {
1327
+ const result = await ctx.api("GET", "/v1/catalog");
1328
+ if (ctx.json) ctx.writeJson(result);
1329
+ else {
1330
+ ctx.table(
1331
+ ["engine", "label", "note"],
1332
+ arrayValue(result.engines).map((row) => [row.id, row.label, row.note]),
1333
+ );
1334
+ ctx.table(
1335
+ ["model", "provider", "input / 1M", "output / 1M"],
1336
+ arrayValue(result.models).map((row) => [
1337
+ row.id,
1338
+ row.provider,
1339
+ `$${row.inputPer1M}`,
1340
+ `$${row.outputPer1M}`,
1341
+ ]),
1342
+ );
1343
+ ctx.table(
1344
+ ["trigger", "label", "note"],
1345
+ arrayValue(result.triggerTypes).map((row) => [row.type, row.label, row.note]),
1346
+ );
1347
+ ctx.table(
1348
+ ["permission"],
1349
+ arrayValue(result.permissions?.all).map((permission) => [permission]),
1350
+ );
1351
+ }
1352
+ return 0;
1353
+ }
1354
+
1355
+ function list(ctx, value, headers, row) {
1356
+ if (ctx.json) ctx.writeJson(value);
1357
+ else ctx.table(headers, arrayValue(value).map(row));
1358
+ return 0;
1359
+ }
1360
+
1361
+ function details(ctx, value) {
1362
+ if (ctx.json) ctx.writeJson(value);
1363
+ else if (value && typeof value === "object" && !Array.isArray(value)) {
1364
+ ctx.table(["field", "value"], Object.entries(value).map(([key, item]) => [key, display(item)]));
1365
+ } else if (value === null || value === undefined) ctx.stdout.write(" No data.\n");
1366
+ else ctx.writeJson(value);
1367
+ return 0;
1368
+ }
1369
+
1370
+ function verificationDetails(ctx, value) {
1371
+ details(ctx, value);
1372
+ return value?.ok === false ? 1 : 0;
1373
+ }
1374
+
1375
+ function changed(ctx, value, message) {
1376
+ if (ctx.json) ctx.writeJson(value);
1377
+ else ctx.stdout.write(` ${green("✓")} ${message}\n`);
1378
+ return 0;
1379
+ }
1380
+
1381
+ function secretResult(ctx, value, message) {
1382
+ if (ctx.json) ctx.writeJson(value);
1383
+ else {
1384
+ ctx.stdout.write(` ${green("✓")} ${message}\n`);
1385
+ if (value?.secret) {
1386
+ ctx.stdout.write(` ${value.secret}\n`);
1387
+ ctx.stdout.write(
1388
+ ` ${yellow("!")} copy this secret now — it is shown once and cannot be retrieved later\n`,
1389
+ );
1390
+ }
1391
+ }
1392
+ return 0;
1393
+ }
1394
+
1395
+ function contentFlag(flags, name) {
1396
+ const value = optionalContentFlag(flags, name);
1397
+ if (value === undefined) throw cliError(`--${name} or --${name}-file is required`, "invalid_flag");
1398
+ return value;
1399
+ }
1400
+
1401
+ function optionalContentFlag(flags, name) {
1402
+ if (flags[name] !== undefined) return requiredStringFlag(flags[name], `--${name}`);
1403
+ const path = flags[`${name}-file`];
1404
+ return path === undefined
1405
+ ? undefined
1406
+ : readFileSync(requiredStringFlag(path, `--${name}-file`), "utf8");
1407
+ }
1408
+
1409
+ function jsonObject(value, flag) {
1410
+ const parsed = parseJson(value, flag);
1411
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw cliError(`${flag} must be a JSON object`, "invalid_flag");
1412
+ return parsed;
1413
+ }
1414
+
1415
+ function jsonArray(value, flag) {
1416
+ const parsed = parseJson(value, flag);
1417
+ if (!Array.isArray(parsed)) throw cliError(`${flag} must be a JSON array`, "invalid_flag");
1418
+ return parsed;
1419
+ }
1420
+
1421
+ function parseJson(value, flag) {
1422
+ try {
1423
+ return JSON.parse(String(value));
1424
+ } catch {
1425
+ throw cliError(`${flag} contains invalid JSON`, "invalid_flag");
1426
+ }
1427
+ }
1428
+
1429
+ function stringList(value, flag) {
1430
+ const text = requiredStringFlag(value, flag);
1431
+ if (text.trim().startsWith("[")) {
1432
+ const parsed = jsonArray(value, flag);
1433
+ if (!parsed.every((item) => typeof item === "string")) throw cliError(`${flag} must contain only strings`, "invalid_flag");
1434
+ return parsed;
1435
+ }
1436
+ return text.split(",").map((item) => item.trim()).filter(Boolean);
1437
+ }
1438
+
1439
+ function agentTriggers(flags, fallback) {
1440
+ if (flags.triggers !== undefined) return jsonArray(flags.triggers, "--triggers");
1441
+ if (typeof flags.schedule === "string") {
1442
+ return [
1443
+ {
1444
+ type: "schedule",
1445
+ config: {
1446
+ cron: flags.schedule,
1447
+ timezone: stringFlag(flags.timezone) ?? "UTC",
1448
+ },
1449
+ },
1450
+ ];
1451
+ }
1452
+ if (flags.schedule !== undefined) throw cliError("--schedule requires a cron expression", "invalid_flag");
1453
+ return fallback;
1454
+ }
1455
+
1456
+ function booleanFlag(value, fallback) {
1457
+ if (value === undefined) return fallback;
1458
+ if (value === true || value === "true") return true;
1459
+ if (value === false || value === "false") return false;
1460
+ throw cliError(`Expected true or false, received ${String(value)}`, "invalid_flag");
1461
+ }
1462
+
1463
+ function numberFlag(value, flag = "Flag") {
1464
+ if (value === undefined) return undefined;
1465
+ if (value === true || value === "") {
1466
+ throw cliError(`${flag} requires a numeric value`, "invalid_flag");
1467
+ }
1468
+ const number = Number(value);
1469
+ if (!Number.isFinite(number) || number < 0)
1470
+ throw cliError(`${flag} must be a non-negative number; received ${String(value)}`, "invalid_flag");
1471
+ return number;
1472
+ }
1473
+
1474
+ function integerArgument(value, name) {
1475
+ const number = Number(value);
1476
+ if (!Number.isSafeInteger(number) || number < 0) {
1477
+ throw cliError(`${name} must be a non-negative integer`, "invalid_argument");
1478
+ }
1479
+ return number;
1480
+ }
1481
+
1482
+ function pageQuery(flags) {
1483
+ const limit = flags.limit === undefined ? undefined : Number(flags.limit);
1484
+ const offset = numberFlag(flags.offset, "--offset");
1485
+ if (
1486
+ flags.limit === true ||
1487
+ flags.limit === "" ||
1488
+ (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > 200))
1489
+ ) {
1490
+ throw cliError("--limit must be an integer from 1 to 200", "invalid_flag");
1491
+ }
1492
+ if (offset !== undefined && !Number.isInteger(offset)) {
1493
+ throw cliError("--offset must be a non-negative integer", "invalid_flag");
1494
+ }
1495
+ return { limit, offset };
1496
+ }
1497
+
1498
+ function stringFlag(value) {
1499
+ if (value === undefined) return undefined;
1500
+ if (typeof value === "string" && value.length > 0) return value;
1501
+ throw cliError("Flag requires a value", "invalid_flag");
1502
+ }
1503
+
1504
+ function requiredStringFlag(value, flag) {
1505
+ const parsed = stringFlag(value);
1506
+ if (parsed === undefined) throw cliError(`${flag} is required`, "invalid_flag");
1507
+ return parsed;
1508
+ }
1509
+
1510
+ function compact(object) {
1511
+ return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== undefined));
1512
+ }
1513
+
1514
+ function arrayValue(value) {
1515
+ if (Array.isArray(value)) return value;
1516
+ if (Array.isArray(value?.items)) return value.items;
1517
+ return [];
1518
+ }
1519
+
1520
+ function display(value) {
1521
+ if (value === null || value === undefined) return "—";
1522
+ return typeof value === "object" ? JSON.stringify(value) : String(value);
1523
+ }
1524
+
1525
+ function money(value) {
1526
+ return `$${(Number(value ?? 0) / 100).toFixed(2)}`;
1527
+ }
1528
+
1529
+ function confirmed(flags, action) {
1530
+ if (!flags.yes) throw cliError(`${action} requires --yes`, "confirmation_required");
1531
+ }
1532
+
1533
+ function usage(command) {
1534
+ return cliError(`Usage: ${command}`, "usage");
1535
+ }
1536
+
1537
+ function cliError(message, code = "cli_error") {
1538
+ const error = new Error(message);
1539
+ error.code = code;
1540
+ error.exitCode = 1;
1541
+ return error;
1542
+ }