@openwop/cli 0.1.0 → 0.2.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 (4) hide show
  1. package/README.md +25 -0
  2. package/dist/cli.js +1315 -73
  3. package/dist/openwop.js +1315 -73
  4. package/package.json +17 -4
package/dist/openwop.js CHANGED
@@ -523,7 +523,7 @@ function errText(err) {
523
523
  }
524
524
 
525
525
  // src/constants.ts
526
- var VERSION = "0.1.0";
526
+ var VERSION = "0.2.0";
527
527
  var DEFAULT_BASE_URL = "http://localhost:8080";
528
528
  var DEFAULT_REGISTRY_URL = "https://packs.openwop.dev";
529
529
  var DEFAULT_API_KEY = "sample-token";
@@ -2318,6 +2318,9 @@ var AGENTS_HELP = `Usage:
2318
2318
  openwop agents list [--json]
2319
2319
  openwop agents info <agentId> [--json]
2320
2320
  openwop agents run <agentId> [--task-json '{...}'] [--tool <id>]... [--threshold <n>] [--no-validate] [--json]
2321
+ openwop agents create --persona <name> [--label <t>] [--description <t>] [--model-class <c>] [--system-prompt <t>] [--tool <id>]... [--threshold <n>] [--json]
2322
+ openwop agents update <agentId> [--persona <n>] [--label <t>] [--description <t>] [--model-class <c>] [--system-prompt <t>] [--tool <id>]... [--threshold <n>] [--json]
2323
+ openwop agents delete <agentId> [--yes]
2321
2324
 
2322
2325
  Manifest agents (RFC 0070). The host loads pack agents[] (RFC 0003) into an
2323
2326
  AgentRegistry and advertises capabilities.agents.manifestRuntime. 'list'/'info'
@@ -2327,10 +2330,17 @@ the agent's toolAllowlist (RFC 0002 \xA7A14), task/return payloads are validated
2327
2330
  against the agent's handoff schemas (RFC 0003 \xA7D, unless --no-validate), and a
2328
2331
  sub-threshold decision escalates rather than proceeding (RFC 0002 \xA7F).
2329
2332
 
2330
- --task-json J Inbound task payload (validated against handoff.taskSchemaRef).
2331
- --tool <id> A tool the host offers this turn (repeatable); kept only if allowlisted.
2332
- --threshold <n> Per-run confidence threshold override (default: the agent's).
2333
- --no-validate Dispatch with opaque payloads (skip handoff schema validation).
2333
+ 'create'/'update'/'delete' manage tenant-scoped user-defined agents on the demo
2334
+ host (POST/PATCH/DELETE /v1/host/sample/agents) \u2014 distinct from the pack-loaded
2335
+ manifest agents that 'list'/'run' operate on.
2336
+
2337
+ --task-json J Inbound task payload (validated against handoff.taskSchemaRef).
2338
+ --tool <id> (run) A tool the host offers this turn (repeatable); kept only if allowlisted.
2339
+ (create/update) An entry in the agent's toolAllowlist (repeatable).
2340
+ --threshold <n> Confidence threshold (run: per-run override; create/update: the agent's default).
2341
+ --no-validate (run) Dispatch with opaque payloads (skip handoff schema validation).
2342
+ --persona <name> (create/update) The agent's persona (required on create).
2343
+ --label / --description / --model-class / --system-prompt (create/update) Agent metadata.
2334
2344
 
2335
2345
  'run' exits 0 (completed), 3 (escalated), or 1 (failed) so scripts can branch.
2336
2346
 
@@ -2338,10 +2348,12 @@ Examples:
2338
2348
  openwop agents list
2339
2349
  openwop agents info core.openwop.agents.supervisor.default --json
2340
2350
  openwop agents run core.openwop.agents.code-reviewer.default --task-json '{"diff":"..."}' --tool openwop:fs.read
2351
+ openwop agents create --persona "Triage bot" --model-class fast --tool openwop:fs.read
2352
+ openwop agents delete agent_123 --yes
2341
2353
  `;
2342
2354
  async function runAgents(ctx, argv) {
2343
2355
  const sub = argv[0] ?? "list";
2344
- const args = argv.slice(["list", "info", "run"].includes(sub) ? 1 : 0);
2356
+ const args = argv.slice(["list", "info", "run", "create", "update", "delete"].includes(sub) ? 1 : 0);
2345
2357
  if (sub === "--help" || sub === "-h") {
2346
2358
  write(ctx.io.stdout, AGENTS_HELP);
2347
2359
  return 0;
@@ -2353,11 +2365,84 @@ async function runAgents(ctx, argv) {
2353
2365
  return await runAgentsInfo(ctx, args);
2354
2366
  case "run":
2355
2367
  return await runAgentsRun(ctx, args);
2368
+ case "create":
2369
+ return await runAgentsCreate(ctx, args);
2370
+ case "update":
2371
+ return await runAgentsUpdate(ctx, args);
2372
+ case "delete":
2373
+ return await runAgentsDelete(ctx, args);
2356
2374
  default:
2357
2375
  throw new CliError(`Unknown agents command: ${sub}
2358
2376
  Run \`openwop agents --help\` for usage.`);
2359
2377
  }
2360
2378
  }
2379
+ function userAgentBody(options) {
2380
+ const body = {};
2381
+ if (options.persona) body.persona = options.persona;
2382
+ if (options.label) body.label = options.label;
2383
+ if (options.description) body.description = options.description;
2384
+ if (options.modelClass) body.modelClass = options.modelClass;
2385
+ if (options.systemPrompt) body.systemPrompt = options.systemPrompt;
2386
+ if (Array.isArray(options.tool) && options.tool.length) body.toolAllowlist = options.tool;
2387
+ if (options.threshold !== void 0) {
2388
+ const t = Number(options.threshold);
2389
+ if (!Number.isFinite(t) || t < 0 || t > 1) throw new CliError("--threshold must be a number between 0 and 1", 2);
2390
+ body.confidenceThreshold = t;
2391
+ }
2392
+ return body;
2393
+ }
2394
+ async function runAgentsCreate(ctx, argv) {
2395
+ const { options } = parseOptions(argv, {
2396
+ bool: ["--help"],
2397
+ value: ["--persona", "--label", "--description", "--model-class", "--system-prompt", "--threshold"],
2398
+ multi: ["--tool"]
2399
+ });
2400
+ if (options.help || !options.persona) {
2401
+ write(ctx.io.stdout, "Usage: openwop agents create --persona <name> [--label <t>] [--description <t>] [--model-class <c>] [--system-prompt <t>] [--tool <id>]... [--threshold <n>] [--json]\n");
2402
+ return options.help ? 0 : 2;
2403
+ }
2404
+ const res = await requestJson(ctx, "/v1/host/sample/agents", { method: "POST", body: userAgentBody(options) });
2405
+ if (ctx.json) {
2406
+ writeJson(ctx.io.stdout, res.body);
2407
+ return 0;
2408
+ }
2409
+ writeLine(ctx.io.stdout, `Created agent ${res.body?.agentId ?? ""} (${res.body?.persona ?? options.persona}).`);
2410
+ return 0;
2411
+ }
2412
+ async function runAgentsUpdate(ctx, argv) {
2413
+ const { options, positionals } = parseOptions(argv, {
2414
+ bool: ["--help"],
2415
+ value: ["--persona", "--label", "--description", "--model-class", "--system-prompt", "--threshold"],
2416
+ multi: ["--tool"]
2417
+ });
2418
+ if (options.help || positionals.length !== 1) {
2419
+ write(ctx.io.stdout, "Usage: openwop agents update <agentId> [--persona <n>] [--label <t>] [--description <t>] [--model-class <c>] [--system-prompt <t>] [--tool <id>]... [--threshold <n>] [--json]\n");
2420
+ return options.help ? 0 : 2;
2421
+ }
2422
+ const body = userAgentBody(options);
2423
+ if (Object.keys(body).length === 0) throw new CliError("Nothing to update \u2014 pass at least one field.", 2);
2424
+ const res = await requestJson(ctx, `/v1/host/sample/agents/${encodeURIComponent(positionals[0])}`, { method: "PATCH", body });
2425
+ if (ctx.json) {
2426
+ writeJson(ctx.io.stdout, res.body);
2427
+ return 0;
2428
+ }
2429
+ writeLine(ctx.io.stdout, `Updated agent ${positionals[0]}.`);
2430
+ return 0;
2431
+ }
2432
+ async function runAgentsDelete(ctx, argv) {
2433
+ const { options, positionals } = parseOptions(argv, { bool: ["--help", "--yes"] });
2434
+ if (options.help || positionals.length !== 1) {
2435
+ write(ctx.io.stdout, "Usage: openwop agents delete <agentId> [--yes]\n");
2436
+ return options.help ? 0 : 2;
2437
+ }
2438
+ if (!options.yes) {
2439
+ writeLine(ctx.io.stderr, `Refusing to delete agent ${positionals[0]} without --yes.`);
2440
+ return 2;
2441
+ }
2442
+ await requestJson(ctx, `/v1/host/sample/agents/${encodeURIComponent(positionals[0])}`, { method: "DELETE" });
2443
+ writeLine(ctx.io.stdout, `Deleted agent ${positionals[0]}.`);
2444
+ return 0;
2445
+ }
2361
2446
  async function runAgentsList(ctx, argv) {
2362
2447
  const { options } = parseOptions(argv, { bool: ["--help"] });
2363
2448
  if (options.help) {
@@ -2421,9 +2506,9 @@ async function runAgentsRun(ctx, argv) {
2421
2506
  }
2422
2507
  const agentId = positionals[0];
2423
2508
  const body = {};
2424
- if (options["task-json"] !== void 0) {
2509
+ if (options.taskJson !== void 0) {
2425
2510
  try {
2426
- body.task = JSON.parse(options["task-json"]);
2511
+ body.task = JSON.parse(options.taskJson);
2427
2512
  } catch {
2428
2513
  throw new CliError("--task-json must be valid JSON", 2);
2429
2514
  }
@@ -2436,7 +2521,7 @@ async function runAgentsRun(ctx, argv) {
2436
2521
  }
2437
2522
  body.confidenceThreshold = t;
2438
2523
  }
2439
- if (options["no-validate"]) body.validateHandoff = false;
2524
+ if (options.noValidate) body.validateHandoff = false;
2440
2525
  const res = await requestJson(ctx, `/v1/host/sample/agents/${encodeURIComponent(agentId)}/dispatch`, {
2441
2526
  method: "POST",
2442
2527
  body
@@ -2460,85 +2545,1064 @@ async function runAgentsRun(ctx, argv) {
2460
2545
  return r.status === "completed" ? 0 : r.status === "escalated" ? 3 : 1;
2461
2546
  }
2462
2547
 
2463
- // src/cli/conformance.ts
2464
- import { spawnSync as spawnSync2 } from "node:child_process";
2465
- import { join as join4 } from "node:path";
2466
- var CONFORMANCE_HELP = `Usage: openwop conformance [--offline] [--filter pattern]
2548
+ // src/cli/roster.ts
2549
+ var ROSTER_HELP = `Usage:
2550
+ openwop roster list [--json]
2551
+ openwop roster get <rosterId> [--json]
2552
+ openwop roster create --persona <name> --agent-ref <agentId> [--agent-version <v> | --agent-channel <c>] [--workflow <id>]... [--label <text>] [--description <text>] [--disabled] [--avatar-url <url>] [--json]
2553
+ openwop roster update <rosterId> [--persona <name>] [--workflow <id>]... [--label <text>] [--description <text>] [--enabled|--disabled] [--avatar-url <url>] [--json]
2554
+ openwop roster delete <rosterId> [--yes]
2467
2555
 
2468
- Runs the in-repo @openwop/openwop-conformance CLI. Without --offline it targets the configured --base-url.
2556
+ Agent roster (RFC 0086). A roster entry is a named, tenant-scoped standing
2557
+ agent that owns a portfolio of workflows[]; its triggers compose RFC 0052
2558
+ (schedules) + RFC 0083 (the durable trigger bridge), and a run it initiates
2559
+ emits the content-free roster.run.initiated attribution event. Drives the
2560
+ host-extension surface POST/GET/PATCH/DELETE /v1/host/sample/roster.
2561
+
2562
+ --persona <name> The agent's display persona (required on create).
2563
+ --agent-ref <id> The agentId this entry binds to (RFC 0002 AgentRef; required on create).
2564
+ --agent-version <v> | --agent-channel <c> Pin the AgentRef to a version XOR a channel.
2565
+ --workflow <id> A workflow in the entry's portfolio (repeatable).
2566
+ --label <text> Short human label.
2567
+ --description <t> Longer description.
2568
+ --enabled (update) Re-enable the entry's triggers + heartbeat.
2569
+ --disabled Create/leave the entry paused (triggers inert).
2570
+ --avatar-url <url> Photo URL for the entry.
2571
+
2572
+ Examples:
2573
+ openwop roster list
2574
+ openwop roster create --persona "Sally" --workflow sample.demo.triage --label "Support lead"
2575
+ openwop roster update r_123 --disabled
2576
+ openwop roster delete r_123 --yes
2469
2577
  `;
2470
- async function runConformance(ctx, argv) {
2578
+ async function runRoster(ctx, argv) {
2579
+ const sub = argv[0] ?? "list";
2580
+ if (sub === "--help" || sub === "-h") {
2581
+ write(ctx.io.stdout, ROSTER_HELP);
2582
+ return 0;
2583
+ }
2584
+ const args = argv.slice(["list", "get", "create", "update", "delete"].includes(sub) ? 1 : 0);
2585
+ switch (sub) {
2586
+ case "list":
2587
+ return await rosterList(ctx, args);
2588
+ case "get":
2589
+ return await rosterGet(ctx, args);
2590
+ case "create":
2591
+ return await rosterCreate(ctx, args);
2592
+ case "update":
2593
+ return await rosterUpdate(ctx, args);
2594
+ case "delete":
2595
+ return await rosterDelete(ctx, args);
2596
+ default:
2597
+ throw new CliError(`Unknown roster command: ${sub}
2598
+ Run \`openwop roster --help\` for usage.`);
2599
+ }
2600
+ }
2601
+ async function rosterList(ctx, argv) {
2602
+ const { options } = parseOptions(argv, { bool: ["--help"] });
2603
+ if (options.help) {
2604
+ write(ctx.io.stdout, ROSTER_HELP);
2605
+ return 0;
2606
+ }
2607
+ const res = await requestJson(ctx, "/v1/host/sample/roster");
2608
+ if (ctx.json) {
2609
+ writeJson(ctx.io.stdout, res.body);
2610
+ return 0;
2611
+ }
2612
+ const roster = Array.isArray(res.body?.roster) ? res.body.roster : [];
2613
+ if (roster.length === 0) {
2614
+ writeLine(ctx.io.stdout, "No roster entries on this host. Create one with `openwop roster create --persona <name>`.");
2615
+ return 0;
2616
+ }
2617
+ const rows = roster.map((e) => ({
2618
+ rosterId: e.rosterId,
2619
+ persona: e.persona,
2620
+ label: e.label ?? "",
2621
+ workflows: Array.isArray(e.workflows) ? String(e.workflows.length) : "0",
2622
+ enabled: e.enabled === false ? "no" : "yes"
2623
+ }));
2624
+ writeLine(ctx.io.stdout, formatTable(rows, ["rosterId", "persona", "label", "workflows", "enabled"]));
2625
+ return 0;
2626
+ }
2627
+ async function rosterGet(ctx, argv) {
2628
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
2629
+ if (options.help || positionals.length !== 1) {
2630
+ write(ctx.io.stdout, "Usage: openwop roster get <rosterId> [--json]\n");
2631
+ return options.help ? 0 : 2;
2632
+ }
2633
+ const res = await requestJson(ctx, `/v1/host/sample/roster/${encodeURIComponent(positionals[0])}`);
2634
+ if (ctx.json) {
2635
+ writeJson(ctx.io.stdout, res.body);
2636
+ return 0;
2637
+ }
2638
+ const e = res.body ?? {};
2639
+ writeLine(ctx.io.stdout, `rosterId: ${e.rosterId ?? positionals[0]}`);
2640
+ writeLine(ctx.io.stdout, `persona: ${e.persona ?? ""}`);
2641
+ if (e.label) writeLine(ctx.io.stdout, `label: ${e.label}`);
2642
+ if (e.agentRef) writeLine(ctx.io.stdout, `agentRef: ${typeof e.agentRef === "string" ? e.agentRef : JSON.stringify(e.agentRef)}`);
2643
+ writeLine(ctx.io.stdout, `enabled: ${e.enabled === false ? "no" : "yes"}`);
2644
+ writeLine(ctx.io.stdout, `workflows: ${Array.isArray(e.workflows) && e.workflows.length ? e.workflows.join(", ") : "(none)"}`);
2645
+ if (e.description) writeLine(ctx.io.stdout, `description: ${e.description}`);
2646
+ return 0;
2647
+ }
2648
+ async function rosterCreate(ctx, argv) {
2471
2649
  const { options } = parseOptions(argv, {
2472
- bool: ["--help", "--offline"],
2473
- value: ["--filter"]
2650
+ bool: ["--help", "--disabled"],
2651
+ value: ["--persona", "--agent-ref", "--agent-version", "--agent-channel", "--label", "--description", "--avatar-url"],
2652
+ multi: ["--workflow"]
2653
+ });
2654
+ if (options.help || !options.persona || !options.agentRef) {
2655
+ write(ctx.io.stdout, "Usage: openwop roster create --persona <name> --agent-ref <agentId> [--agent-version <v> | --agent-channel <c>] [--workflow <id>]... [--label <t>] [--description <t>] [--disabled] [--avatar-url <url>] [--json]\n");
2656
+ return options.help ? 0 : 2;
2657
+ }
2658
+ if (options.agentVersion && options.agentChannel) {
2659
+ throw new CliError("--agent-version and --agent-channel are mutually exclusive (RFC 0082 \xA7A).", 2);
2660
+ }
2661
+ const agentRef = { agentId: options.agentRef };
2662
+ if (options.agentVersion) agentRef.version = options.agentVersion;
2663
+ if (options.agentChannel) agentRef.channel = options.agentChannel;
2664
+ const body = { persona: options.persona, agentRef };
2665
+ if (Array.isArray(options.workflow) && options.workflow.length) body.workflows = options.workflow;
2666
+ if (options.label) body.label = options.label;
2667
+ if (options.description) body.description = options.description;
2668
+ if (options.disabled) body.enabled = false;
2669
+ if (options.avatarUrl) body.avatarUrl = options.avatarUrl;
2670
+ const res = await requestJson(ctx, "/v1/host/sample/roster", { method: "POST", body });
2671
+ if (ctx.json) {
2672
+ writeJson(ctx.io.stdout, res.body);
2673
+ return 0;
2674
+ }
2675
+ writeLine(ctx.io.stdout, `Created roster entry ${res.body?.rosterId} (${res.body?.persona}).`);
2676
+ return 0;
2677
+ }
2678
+ async function rosterUpdate(ctx, argv) {
2679
+ const { options, positionals } = parseOptions(argv, {
2680
+ bool: ["--help", "--enabled", "--disabled"],
2681
+ value: ["--persona", "--label", "--description", "--avatar-url"],
2682
+ multi: ["--workflow"]
2474
2683
  });
2684
+ if (options.help || positionals.length !== 1) {
2685
+ write(ctx.io.stdout, "Usage: openwop roster update <rosterId> [--persona <n>] [--workflow <id>]... [--label <t>] [--description <t>] [--enabled|--disabled] [--avatar-url <url>] [--json]\n");
2686
+ return options.help ? 0 : 2;
2687
+ }
2688
+ if (options.enabled && options.disabled) throw new CliError("Pass only one of --enabled / --disabled", 2);
2689
+ const body = {};
2690
+ if (options.persona) body.persona = options.persona;
2691
+ if (Array.isArray(options.workflow) && options.workflow.length) body.workflows = options.workflow;
2692
+ if (options.label) body.label = options.label;
2693
+ if (options.description) body.description = options.description;
2694
+ if (options.enabled) body.enabled = true;
2695
+ if (options.disabled) body.enabled = false;
2696
+ if (options.avatarUrl) body.avatarUrl = options.avatarUrl;
2697
+ const res = await requestJson(ctx, `/v1/host/sample/roster/${encodeURIComponent(positionals[0])}`, { method: "PATCH", body });
2698
+ if (ctx.json) {
2699
+ writeJson(ctx.io.stdout, res.body);
2700
+ return 0;
2701
+ }
2702
+ writeLine(ctx.io.stdout, `Updated roster entry ${res.body?.rosterId ?? positionals[0]}.`);
2703
+ return 0;
2704
+ }
2705
+ async function rosterDelete(ctx, argv) {
2706
+ const { options, positionals } = parseOptions(argv, { bool: ["--help", "--yes"] });
2707
+ if (options.help || positionals.length !== 1) {
2708
+ write(ctx.io.stdout, "Usage: openwop roster delete <rosterId> [--yes]\n");
2709
+ return options.help ? 0 : 2;
2710
+ }
2711
+ if (!options.yes) {
2712
+ writeLine(ctx.io.stderr, `Refusing to delete ${positionals[0]} without --yes (this removes the roster entry and its triggers).`);
2713
+ return 2;
2714
+ }
2715
+ await requestJson(ctx, `/v1/host/sample/roster/${encodeURIComponent(positionals[0])}`, { method: "DELETE" });
2716
+ writeLine(ctx.io.stdout, `Deleted roster entry ${positionals[0]}.`);
2717
+ return 0;
2718
+ }
2719
+
2720
+ // src/cli/orgChart.ts
2721
+ import { readFile as readFile2 } from "node:fs/promises";
2722
+ var ORG_CHART_HELP = `Usage:
2723
+ openwop org-chart get [--json]
2724
+ openwop org-chart dept <departmentId> [--no-recursive] [--json]
2725
+ openwop org-chart set --file <chart.json> [--json]
2726
+ openwop org-chart clear [--yes]
2727
+
2728
+ Org chart (RFC 0087). A purely DESCRIPTIVE map of departments, roles, and
2729
+ reportsTo edges \u2014 it confers no authority (the org-position-no-authority-
2730
+ escalation invariant: the schema carries no permissions/scopes/canDispatch
2731
+ field). Drives the host-extension surface GET/PUT/DELETE
2732
+ /v1/host/sample/org-chart. 'set' replaces the whole chart from a JSON file
2733
+ with { "departments": [...], "members": [...] }.
2734
+
2735
+ --no-recursive (dept) Show only the named department, not its sub-tree.
2736
+ --file <path> (set) JSON document with departments[] + members[] arrays.
2737
+
2738
+ Examples:
2739
+ openwop org-chart get
2740
+ openwop org-chart dept engineering
2741
+ openwop org-chart set --file ./org.json
2742
+ openwop org-chart clear --yes
2743
+ `;
2744
+ async function runOrgChart(ctx, argv) {
2745
+ const sub = argv[0] ?? "get";
2746
+ if (sub === "--help" || sub === "-h") {
2747
+ write(ctx.io.stdout, ORG_CHART_HELP);
2748
+ return 0;
2749
+ }
2750
+ const args = argv.slice(["get", "dept", "set", "clear"].includes(sub) ? 1 : 0);
2751
+ switch (sub) {
2752
+ case "get":
2753
+ return await orgChartGet(ctx, args);
2754
+ case "dept":
2755
+ return await orgChartDept(ctx, args);
2756
+ case "set":
2757
+ return await orgChartSet(ctx, args);
2758
+ case "clear":
2759
+ return await orgChartClear(ctx, args);
2760
+ default:
2761
+ throw new CliError(`Unknown org-chart command: ${sub}
2762
+ Run \`openwop org-chart --help\` for usage.`);
2763
+ }
2764
+ }
2765
+ async function orgChartGet(ctx, argv) {
2766
+ const { options } = parseOptions(argv, { bool: ["--help"] });
2475
2767
  if (options.help) {
2476
- write(ctx.io.stdout, CONFORMANCE_HELP);
2768
+ write(ctx.io.stdout, ORG_CHART_HELP);
2477
2769
  return 0;
2478
2770
  }
2479
- const root = requireRepoRoot(ctx);
2480
- const args = ["run", "cli", "--"];
2481
- if (options.offline) {
2482
- args.push("--offline");
2483
- } else {
2484
- args.push("--base-url", ctx.baseUrl, "--api-key", ctx.apiKey ?? DEFAULT_API_KEY);
2771
+ const res = await requestJson(ctx, "/v1/host/sample/org-chart");
2772
+ if (ctx.json) {
2773
+ writeJson(ctx.io.stdout, res.body);
2774
+ return 0;
2485
2775
  }
2486
- if (options.filter) args.push("--filter", options.filter);
2487
- const result = spawnSync2(npmCommand(), args, {
2488
- cwd: join4(root, "conformance"),
2489
- stdio: "inherit",
2490
- env: ctx.env
2776
+ const chart = res.body ?? {};
2777
+ const departments = Array.isArray(chart.departments) ? chart.departments : [];
2778
+ const members = Array.isArray(chart.members) ? chart.members : [];
2779
+ if (departments.length === 0 && members.length === 0) {
2780
+ writeLine(ctx.io.stdout, "Org chart is empty. Populate it with `openwop org-chart set --file <chart.json>`.");
2781
+ return 0;
2782
+ }
2783
+ writeLine(ctx.io.stdout, `Departments (${departments.length}):`);
2784
+ if (departments.length) {
2785
+ writeLine(ctx.io.stdout, formatTable(departments.map((d) => ({
2786
+ departmentId: d.departmentId ?? d.id,
2787
+ name: d.name ?? "",
2788
+ reportsTo: d.reportsTo ?? d.parentId ?? ""
2789
+ })), ["departmentId", "name", "reportsTo"]));
2790
+ }
2791
+ writeLine(ctx.io.stdout, `
2792
+ Members (${members.length}):`);
2793
+ if (members.length) {
2794
+ writeLine(ctx.io.stdout, formatTable(members.map((m) => ({
2795
+ memberId: m.memberId ?? m.id,
2796
+ name: m.name ?? m.displayName ?? "",
2797
+ role: m.role ?? "",
2798
+ department: m.departmentId ?? ""
2799
+ })), ["memberId", "name", "role", "department"]));
2800
+ }
2801
+ return 0;
2802
+ }
2803
+ async function orgChartDept(ctx, argv) {
2804
+ const { options, positionals } = parseOptions(argv, { bool: ["--help", "--no-recursive"] });
2805
+ if (options.help || positionals.length !== 1) {
2806
+ write(ctx.io.stdout, "Usage: openwop org-chart dept <departmentId> [--no-recursive] [--json]\n");
2807
+ return options.help ? 0 : 2;
2808
+ }
2809
+ const recursive = options.noRecursive ? "false" : "true";
2810
+ const res = await requestJson(ctx, `/v1/host/sample/org-chart/${encodeURIComponent(positionals[0])}?recursive=${recursive}`);
2811
+ if (ctx.json) {
2812
+ writeJson(ctx.io.stdout, res.body);
2813
+ return 0;
2814
+ }
2815
+ writeJson(ctx.io.stdout, res.body);
2816
+ return 0;
2817
+ }
2818
+ async function orgChartSet(ctx, argv) {
2819
+ const { options } = parseOptions(argv, { bool: ["--help"], value: ["--file"] });
2820
+ if (options.help || !options.file) {
2821
+ write(ctx.io.stdout, "Usage: openwop org-chart set --file <chart.json> [--json]\n");
2822
+ return options.help ? 0 : 2;
2823
+ }
2824
+ let parsed;
2825
+ try {
2826
+ parsed = JSON.parse(await readFile2(options.file, "utf8"));
2827
+ } catch (err) {
2828
+ throw new CliError(`Could not read/parse ${options.file}: ${err instanceof Error ? err.message : String(err)}`, 2);
2829
+ }
2830
+ if (!Array.isArray(parsed?.departments) || !Array.isArray(parsed?.members)) {
2831
+ throw new CliError("The chart file MUST be a JSON object with `departments` and `members` arrays.", 2);
2832
+ }
2833
+ const res = await requestJson(ctx, "/v1/host/sample/org-chart", {
2834
+ method: "PUT",
2835
+ body: { departments: parsed.departments, members: parsed.members }
2491
2836
  });
2492
- return result.status ?? 1;
2837
+ if (ctx.json) {
2838
+ writeJson(ctx.io.stdout, res.body);
2839
+ return 0;
2840
+ }
2841
+ const chart = res.body ?? {};
2842
+ writeLine(ctx.io.stdout, `Set org chart: ${Array.isArray(chart.departments) ? chart.departments.length : 0} departments, ${Array.isArray(chart.members) ? chart.members.length : 0} members.`);
2843
+ return 0;
2844
+ }
2845
+ async function orgChartClear(ctx, argv) {
2846
+ const { options } = parseOptions(argv, { bool: ["--help", "--yes"] });
2847
+ if (options.help) {
2848
+ write(ctx.io.stdout, "Usage: openwop org-chart clear [--yes]\n");
2849
+ return 0;
2850
+ }
2851
+ if (!options.yes) {
2852
+ writeLine(ctx.io.stderr, "Refusing to clear the org chart without --yes.");
2853
+ return 2;
2854
+ }
2855
+ await requestJson(ctx, "/v1/host/sample/org-chart", { method: "DELETE" });
2856
+ writeLine(ctx.io.stdout, "Cleared the org chart.");
2857
+ return 0;
2493
2858
  }
2494
2859
 
2495
- // src/prompt.ts
2496
- import { createInterface as createInterface3 } from "node:readline";
2497
- async function promptChoice(ctx, label, choices) {
2498
- writeLine(ctx.io.stdout, label);
2499
- choices.forEach((c, i) => {
2500
- const tag = c.recommended ? " (recommended)" : "";
2501
- writeLine(ctx.io.stdout, ` ${i + 1}) ${c.label}${tag}`);
2502
- });
2503
- const defaultIdx = Math.max(0, choices.findIndex((c) => c.recommended));
2504
- const answer = await promptText(ctx, `Choice [${defaultIdx + 1}]: `, "");
2505
- const idx = answer.trim() === "" ? defaultIdx : Number(answer.trim()) - 1;
2506
- if (!Number.isInteger(idx) || idx < 0 || idx >= choices.length) {
2507
- throw new CliError(`Invalid choice: ${answer}`);
2860
+ // src/cli/kanban.ts
2861
+ var KANBAN_HELP = `Usage:
2862
+ openwop kanban boards [--json]
2863
+ openwop kanban board <boardId> [--json]
2864
+ openwop kanban board-create --name <name> [--column <id:Label>]... [--trigger-workflow <id>] [--roster <rosterId>] [--json]
2865
+ openwop kanban board-delete <boardId> [--yes]
2866
+ openwop kanban card-add <boardId> --title <text> [--column <columnId>] [--description <text>] [--workflow <id>] [--priority <p>] [--json]
2867
+ openwop kanban card-move <cardId> --column <columnId> [--json]
2868
+ openwop kanban card-update <cardId> [--title <text>] [--description <text>] [--column <columnId>] [--workflow <id>] [--json]
2869
+ openwop kanban card-delete <cardId> [--yes]
2870
+ openwop kanban watch <boardId>
2871
+
2872
+ Kanban boards (host-extension under /v1/host/sample/kanban). A board is a lane
2873
+ of cards an agent works; a board MAY bind to an RFC 0086 roster entry so its
2874
+ To Do column fires the named agent's first portfolio workflow. 'watch' streams
2875
+ the board's server-sent card events until interrupted.
2876
+
2877
+ --column <id:Label> (board-create) A column, repeatable; default lanes if omitted.
2878
+ --trigger-workflow (board-create) Workflow the To Do column fires.
2879
+ --roster <rosterId> (board-create) Bind the board to a roster entry (RFC 0086).
2880
+ --priority <p> (card-add) low | normal | high | urgent.
2881
+
2882
+ Examples:
2883
+ openwop kanban boards
2884
+ openwop kanban board-create --name "Sally's work" --roster r_123
2885
+ openwop kanban card-add b_1 --title "Triage inbox" --column todo
2886
+ openwop kanban card-move c_9 --column working
2887
+ openwop kanban watch b_1
2888
+ `;
2889
+ async function runKanban(ctx, argv) {
2890
+ const sub = argv[0] ?? "boards";
2891
+ if (sub === "--help" || sub === "-h") {
2892
+ write(ctx.io.stdout, KANBAN_HELP);
2893
+ return 0;
2894
+ }
2895
+ const known = ["boards", "board", "board-create", "board-delete", "card-add", "card-move", "card-update", "card-delete", "watch"];
2896
+ const args = argv.slice(known.includes(sub) ? 1 : 0);
2897
+ switch (sub) {
2898
+ case "boards":
2899
+ return await boardsList(ctx, args);
2900
+ case "board":
2901
+ return await boardGet(ctx, args);
2902
+ case "board-create":
2903
+ return await boardCreate(ctx, args);
2904
+ case "board-delete":
2905
+ return await boardDelete(ctx, args);
2906
+ case "card-add":
2907
+ return await cardAdd(ctx, args);
2908
+ case "card-move":
2909
+ return await cardPatch(ctx, args, true);
2910
+ case "card-update":
2911
+ return await cardPatch(ctx, args, false);
2912
+ case "card-delete":
2913
+ return await cardDelete(ctx, args);
2914
+ case "watch":
2915
+ return await boardWatch(ctx, args);
2916
+ default:
2917
+ throw new CliError(`Unknown kanban command: ${sub}
2918
+ Run \`openwop kanban --help\` for usage.`);
2508
2919
  }
2509
- return choices[idx].key;
2510
2920
  }
2511
- async function promptText(ctx, prompt, defaultValue = "") {
2512
- ctx.io.stdout.write(prompt);
2513
- const rl = createInterface3({ input: process.stdin, output: process.stdout, terminal: false });
2514
- return new Promise((resolve) => {
2515
- rl.once("line", (line) => {
2516
- rl.close();
2517
- resolve(line.length > 0 ? line : defaultValue);
2518
- });
2519
- });
2921
+ async function boardsList(ctx, argv) {
2922
+ const { options } = parseOptions(argv, { bool: ["--help"] });
2923
+ if (options.help) {
2924
+ write(ctx.io.stdout, KANBAN_HELP);
2925
+ return 0;
2926
+ }
2927
+ const res = await requestJson(ctx, "/v1/host/sample/kanban/boards");
2928
+ if (ctx.json) {
2929
+ writeJson(ctx.io.stdout, res.body);
2930
+ return 0;
2931
+ }
2932
+ const boards = Array.isArray(res.body?.boards) ? res.body.boards : Array.isArray(res.body) ? res.body : [];
2933
+ if (boards.length === 0) {
2934
+ writeLine(ctx.io.stdout, "No boards on this host. Create one with `openwop kanban board-create --name <name>`.");
2935
+ return 0;
2936
+ }
2937
+ writeLine(ctx.io.stdout, formatTable(boards.map((b) => ({
2938
+ boardId: b.id ?? b.boardId,
2939
+ name: b.name ?? "",
2940
+ columns: Array.isArray(b.columns) ? String(b.columns.length) : "0",
2941
+ roster: b.rosterId ?? ""
2942
+ })), ["boardId", "name", "columns", "roster"]));
2943
+ return 0;
2520
2944
  }
2521
- async function promptYesNo(ctx, label, defaultYes = true) {
2522
- const hint = defaultYes ? "[Y/n]" : "[y/N]";
2523
- const answer = (await promptText(ctx, `${label} ${hint} `, "")).trim().toLowerCase();
2524
- if (answer === "") return defaultYes;
2525
- return answer === "y" || answer === "yes";
2945
+ async function boardGet(ctx, argv) {
2946
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
2947
+ if (options.help || positionals.length !== 1) {
2948
+ write(ctx.io.stdout, "Usage: openwop kanban board <boardId> [--json]\n");
2949
+ return options.help ? 0 : 2;
2950
+ }
2951
+ const res = await requestJson(ctx, `/v1/host/sample/kanban/boards/${encodeURIComponent(positionals[0])}`);
2952
+ if (ctx.json) {
2953
+ writeJson(ctx.io.stdout, res.body);
2954
+ return 0;
2955
+ }
2956
+ const b = res.body ?? {};
2957
+ writeLine(ctx.io.stdout, `board: ${b.id ?? positionals[0]} \u2014 ${b.name ?? ""}`);
2958
+ if (b.rosterId) writeLine(ctx.io.stdout, `roster: ${b.rosterId}`);
2959
+ const columns = Array.isArray(b.columns) ? b.columns : [];
2960
+ const cards = Array.isArray(b.cards) ? b.cards : [];
2961
+ for (const col of columns) {
2962
+ const inCol = cards.filter((c) => c.columnId === col.id);
2963
+ writeLine(ctx.io.stdout, `
2964
+ [${col.name ?? col.id}] (${inCol.length})`);
2965
+ for (const c of inCol) writeLine(ctx.io.stdout, ` \xB7 ${c.id} ${c.title}`);
2966
+ }
2967
+ return 0;
2526
2968
  }
2527
- async function readSecret(ctx, prompt) {
2528
- ctx.io.stdout.write(prompt);
2529
- const stdin = process.stdin;
2530
- if (!stdin.isTTY) {
2531
- const rl = createInterface3({ input: stdin, output: process.stdout, terminal: false });
2532
- return new Promise((resolve) => {
2533
- rl.once("line", (line) => {
2534
- rl.close();
2535
- resolve(line);
2536
- });
2969
+ async function boardCreate(ctx, argv) {
2970
+ const { options } = parseOptions(argv, {
2971
+ bool: ["--help"],
2972
+ value: ["--name", "--trigger-workflow", "--roster"],
2973
+ multi: ["--column"]
2974
+ });
2975
+ if (options.help || !options.name) {
2976
+ write(ctx.io.stdout, "Usage: openwop kanban board-create --name <name> [--column <id:Label>]... [--trigger-workflow <id>] [--roster <rosterId>] [--json]\n");
2977
+ return options.help ? 0 : 2;
2978
+ }
2979
+ const body = { name: options.name };
2980
+ if (Array.isArray(options.column) && options.column.length) {
2981
+ body.columns = options.column.map((spec) => {
2982
+ const [id, ...rest] = spec.split(":");
2983
+ return { id, name: rest.length ? rest.join(":") : id };
2537
2984
  });
2538
2985
  }
2539
- return new Promise((resolve, reject) => {
2540
- stdin.setRawMode(true);
2541
- stdin.resume();
2986
+ if (options.triggerWorkflow) body.triggerWorkflowId = options.triggerWorkflow;
2987
+ if (options.roster) body.rosterId = options.roster;
2988
+ const res = await requestJson(ctx, "/v1/host/sample/kanban/boards", { method: "POST", body });
2989
+ if (ctx.json) {
2990
+ writeJson(ctx.io.stdout, res.body);
2991
+ return 0;
2992
+ }
2993
+ writeLine(ctx.io.stdout, `Created board ${res.body?.id ?? res.body?.boardId} (${res.body?.name}).`);
2994
+ return 0;
2995
+ }
2996
+ async function boardDelete(ctx, argv) {
2997
+ const { options, positionals } = parseOptions(argv, { bool: ["--help", "--yes"] });
2998
+ if (options.help || positionals.length !== 1) {
2999
+ write(ctx.io.stdout, "Usage: openwop kanban board-delete <boardId> [--yes]\n");
3000
+ return options.help ? 0 : 2;
3001
+ }
3002
+ if (!options.yes) {
3003
+ writeLine(ctx.io.stderr, `Refusing to delete board ${positionals[0]} without --yes (this removes the board and its cards).`);
3004
+ return 2;
3005
+ }
3006
+ await requestJson(ctx, `/v1/host/sample/kanban/boards/${encodeURIComponent(positionals[0])}`, { method: "DELETE" });
3007
+ writeLine(ctx.io.stdout, `Deleted board ${positionals[0]}.`);
3008
+ return 0;
3009
+ }
3010
+ async function cardAdd(ctx, argv) {
3011
+ const { options, positionals } = parseOptions(argv, {
3012
+ bool: ["--help"],
3013
+ value: ["--title", "--column", "--description", "--workflow", "--priority"]
3014
+ });
3015
+ if (options.help || positionals.length !== 1 || !options.title) {
3016
+ write(ctx.io.stdout, "Usage: openwop kanban card-add <boardId> --title <text> [--column <columnId>] [--description <text>] [--workflow <id>] [--priority <p>] [--json]\n");
3017
+ return options.help ? 0 : 2;
3018
+ }
3019
+ const body = { title: options.title };
3020
+ if (options.column) body.columnId = options.column;
3021
+ if (options.description) body.description = options.description;
3022
+ if (options.workflow) body.workflowId = options.workflow;
3023
+ if (options.priority) body.priority = options.priority;
3024
+ const res = await requestJson(ctx, `/v1/host/sample/kanban/boards/${encodeURIComponent(positionals[0])}/cards`, { method: "POST", body });
3025
+ if (ctx.json) {
3026
+ writeJson(ctx.io.stdout, res.body);
3027
+ return 0;
3028
+ }
3029
+ writeLine(ctx.io.stdout, `Added card ${res.body?.id ?? ""} to board ${positionals[0]}.`);
3030
+ return 0;
3031
+ }
3032
+ async function cardPatch(ctx, argv, moveOnly) {
3033
+ const { options, positionals } = parseOptions(argv, {
3034
+ bool: ["--help"],
3035
+ value: ["--title", "--description", "--column", "--workflow"]
3036
+ });
3037
+ const usage = moveOnly ? "Usage: openwop kanban card-move <cardId> --column <columnId> [--json]\n" : "Usage: openwop kanban card-update <cardId> [--title <t>] [--description <t>] [--column <columnId>] [--workflow <id>] [--json]\n";
3038
+ if (options.help || positionals.length !== 1 || moveOnly && !options.column) {
3039
+ write(ctx.io.stdout, usage);
3040
+ return options.help ? 0 : 2;
3041
+ }
3042
+ const body = {};
3043
+ if (options.column) body.columnId = options.column;
3044
+ if (!moveOnly) {
3045
+ if (options.title) body.title = options.title;
3046
+ if (options.description) body.description = options.description;
3047
+ if (options.workflow) body.workflowId = options.workflow;
3048
+ }
3049
+ if (Object.keys(body).length === 0) throw new CliError("Nothing to update \u2014 pass at least one field.", 2);
3050
+ const res = await requestJson(ctx, `/v1/host/sample/kanban/cards/${encodeURIComponent(positionals[0])}`, { method: "PATCH", body });
3051
+ if (ctx.json) {
3052
+ writeJson(ctx.io.stdout, res.body);
3053
+ return 0;
3054
+ }
3055
+ writeLine(ctx.io.stdout, `Updated card ${positionals[0]}.`);
3056
+ return 0;
3057
+ }
3058
+ async function cardDelete(ctx, argv) {
3059
+ const { options, positionals } = parseOptions(argv, { bool: ["--help", "--yes"] });
3060
+ if (options.help || positionals.length !== 1) {
3061
+ write(ctx.io.stdout, "Usage: openwop kanban card-delete <cardId> [--yes]\n");
3062
+ return options.help ? 0 : 2;
3063
+ }
3064
+ if (!options.yes) {
3065
+ writeLine(ctx.io.stderr, `Refusing to delete card ${positionals[0]} without --yes.`);
3066
+ return 2;
3067
+ }
3068
+ await requestJson(ctx, `/v1/host/sample/kanban/cards/${encodeURIComponent(positionals[0])}`, { method: "DELETE" });
3069
+ writeLine(ctx.io.stdout, `Deleted card ${positionals[0]}.`);
3070
+ return 0;
3071
+ }
3072
+ async function boardWatch(ctx, argv) {
3073
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
3074
+ if (options.help || positionals.length !== 1) {
3075
+ write(ctx.io.stdout, "Usage: openwop kanban watch <boardId>\n");
3076
+ return options.help ? 0 : 2;
3077
+ }
3078
+ const url = new URL(`v1/host/sample/kanban/boards/${encodeURIComponent(positionals[0])}/events`, ctx.baseUrl.endsWith("/") ? ctx.baseUrl : `${ctx.baseUrl}/`);
3079
+ const headers = { accept: "text/event-stream" };
3080
+ if (ctx.apiKey) headers.authorization = `Bearer ${ctx.apiKey}`;
3081
+ const res = await ctx.fetchImpl(url, { headers });
3082
+ if (!res.ok || !res.body) {
3083
+ throw new CliError(`Could not open the board event stream (HTTP ${res.status}).`, 1);
3084
+ }
3085
+ writeLine(ctx.io.stdout, `Watching board ${positionals[0]} (Ctrl-C to stop)\u2026`);
3086
+ await consumeSse(res.body, (frame) => {
3087
+ if (ctx.json) {
3088
+ writeLine(ctx.io.stdout, JSON.stringify(frame));
3089
+ return;
3090
+ }
3091
+ const type = frame.event ?? "message";
3092
+ writeLine(ctx.io.stdout, `\xB7 ${type}${frame.data ? `: ${frame.data}` : ""}`);
3093
+ });
3094
+ return 0;
3095
+ }
3096
+
3097
+ // src/cli/accessControl.ts
3098
+ var ORGS_HELP = `Usage:
3099
+ openwop orgs list [--json]
3100
+ openwop orgs get <orgId> [--json]
3101
+ openwop orgs create --name <name> [--description <text>] [--json]
3102
+ openwop orgs update <orgId> [--name <name>] [--description <text>] [--json]
3103
+ openwop orgs delete <orgId> [--yes]
3104
+
3105
+ openwop orgs teams <orgId> list|create|update|delete [...]
3106
+ openwop orgs groups <orgId> list|create|update|delete [...]
3107
+ openwop orgs roles <orgId> list|create|update|delete [...]
3108
+ openwop orgs members <orgId> list|create|update|delete [...]
3109
+
3110
+ openwop orgs role-catalog [--json] # the host's global role catalog (GET /roles)
3111
+ openwop orgs effective [--subject <id>] [--json] # effective access for a subject (GET /access/effective)
3112
+
3113
+ Organizations + RBAC (RFC 0049, authorization fail-closed). Orgs own teams,
3114
+ groups, roles, and members; an effective-access query resolves a subject's
3115
+ granted scopes. Drives the host-extension surface under /v1/host/sample/orgs,
3116
+ /v1/host/sample/roles, and /v1/host/sample/access/effective.
3117
+
3118
+ Nested-entity flags:
3119
+ teams create/update --name <n> [--description <t>] [--color <c>]
3120
+ groups create/update --name <n> [--description <t>] [--role <id>]... [--member <id>]...
3121
+ roles create/update --name <n> [--description <t>] [--scope <s>]...
3122
+ members create --subject <id> --display-name <n> [--email <e>] [--role <id>]... [--team <id>]...
3123
+ members update [--display-name <n>] [--email <e>] [--role <id>]... [--team <id>]...
3124
+
3125
+ Examples:
3126
+ openwop orgs create --name "Acme"
3127
+ openwop orgs teams o_1 create --name "Support" --color blue
3128
+ openwop orgs roles o_1 create --name "Reviewer" --scope runs:read --scope runs:annotate
3129
+ openwop orgs members o_1 create --subject user:jo --display-name "Jo" --role role_reviewer --team t_support
3130
+ openwop orgs effective --subject user:jo
3131
+ `;
3132
+ var ENTITIES = ["teams", "groups", "roles", "members"];
3133
+ async function runOrgs(ctx, argv) {
3134
+ const sub = argv[0] ?? "list";
3135
+ if (sub === "--help" || sub === "-h") {
3136
+ write(ctx.io.stdout, ORGS_HELP);
3137
+ return 0;
3138
+ }
3139
+ if (ENTITIES.includes(sub)) {
3140
+ return await runEntity(ctx, sub, argv.slice(1));
3141
+ }
3142
+ const args = ["list", "get", "create", "update", "delete", "role-catalog", "effective"].includes(sub) ? argv.slice(1) : argv;
3143
+ switch (sub) {
3144
+ case "list":
3145
+ return await orgsList(ctx, args);
3146
+ case "get":
3147
+ return await orgsGet(ctx, args);
3148
+ case "create":
3149
+ return await orgsCreate(ctx, args);
3150
+ case "update":
3151
+ return await orgsUpdate(ctx, args);
3152
+ case "delete":
3153
+ return await orgsDelete(ctx, args);
3154
+ case "role-catalog":
3155
+ return await roleCatalog(ctx, args);
3156
+ case "effective":
3157
+ return await effectiveAccess(ctx, args);
3158
+ default:
3159
+ throw new CliError(`Unknown orgs command: ${sub}
3160
+ Run \`openwop orgs --help\` for usage.`);
3161
+ }
3162
+ }
3163
+ async function orgsList(ctx, argv) {
3164
+ const { options } = parseOptions(argv, { bool: ["--help"] });
3165
+ if (options.help) {
3166
+ write(ctx.io.stdout, ORGS_HELP);
3167
+ return 0;
3168
+ }
3169
+ const res = await requestJson(ctx, "/v1/host/sample/orgs");
3170
+ if (ctx.json) {
3171
+ writeJson(ctx.io.stdout, res.body);
3172
+ return 0;
3173
+ }
3174
+ const orgs = arrayOf(res.body, "orgs");
3175
+ if (orgs.length === 0) {
3176
+ writeLine(ctx.io.stdout, "No organizations. Create one with `openwop orgs create --name <name>`.");
3177
+ return 0;
3178
+ }
3179
+ writeLine(ctx.io.stdout, formatTable(orgs.map((o) => ({
3180
+ orgId: o.orgId ?? o.id,
3181
+ name: o.name ?? "",
3182
+ description: o.description ?? ""
3183
+ })), ["orgId", "name", "description"]));
3184
+ return 0;
3185
+ }
3186
+ async function orgsGet(ctx, argv) {
3187
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
3188
+ if (options.help || positionals.length !== 1) {
3189
+ write(ctx.io.stdout, "Usage: openwop orgs get <orgId> [--json]\n");
3190
+ return options.help ? 0 : 2;
3191
+ }
3192
+ const res = await requestJson(ctx, `/v1/host/sample/orgs/${encodeURIComponent(positionals[0])}`);
3193
+ writeJson(ctx.io.stdout, res.body);
3194
+ return 0;
3195
+ }
3196
+ async function orgsCreate(ctx, argv) {
3197
+ const { options } = parseOptions(argv, { bool: ["--help"], value: ["--name", "--description"] });
3198
+ if (options.help || !options.name) {
3199
+ write(ctx.io.stdout, "Usage: openwop orgs create --name <name> [--description <text>] [--json]\n");
3200
+ return options.help ? 0 : 2;
3201
+ }
3202
+ const body = { name: options.name };
3203
+ if (options.description) body.description = options.description;
3204
+ const res = await requestJson(ctx, "/v1/host/sample/orgs", { method: "POST", body });
3205
+ if (ctx.json) {
3206
+ writeJson(ctx.io.stdout, res.body);
3207
+ return 0;
3208
+ }
3209
+ writeLine(ctx.io.stdout, `Created org ${res.body?.orgId ?? res.body?.id} (${res.body?.name}).`);
3210
+ return 0;
3211
+ }
3212
+ async function orgsUpdate(ctx, argv) {
3213
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"], value: ["--name", "--description"] });
3214
+ if (options.help || positionals.length !== 1) {
3215
+ write(ctx.io.stdout, "Usage: openwop orgs update <orgId> [--name <name>] [--description <text>] [--json]\n");
3216
+ return options.help ? 0 : 2;
3217
+ }
3218
+ const body = {};
3219
+ if (options.name) body.name = options.name;
3220
+ if (options.description) body.description = options.description;
3221
+ if (Object.keys(body).length === 0) throw new CliError("Nothing to update \u2014 pass --name and/or --description.", 2);
3222
+ const res = await requestJson(ctx, `/v1/host/sample/orgs/${encodeURIComponent(positionals[0])}`, { method: "PATCH", body });
3223
+ if (ctx.json) {
3224
+ writeJson(ctx.io.stdout, res.body);
3225
+ return 0;
3226
+ }
3227
+ writeLine(ctx.io.stdout, `Updated org ${positionals[0]}.`);
3228
+ return 0;
3229
+ }
3230
+ async function orgsDelete(ctx, argv) {
3231
+ const { options, positionals } = parseOptions(argv, { bool: ["--help", "--yes"] });
3232
+ if (options.help || positionals.length !== 1) {
3233
+ write(ctx.io.stdout, "Usage: openwop orgs delete <orgId> [--yes]\n");
3234
+ return options.help ? 0 : 2;
3235
+ }
3236
+ if (!options.yes) {
3237
+ writeLine(ctx.io.stderr, `Refusing to delete org ${positionals[0]} without --yes (removes its teams, roles, and memberships).`);
3238
+ return 2;
3239
+ }
3240
+ await requestJson(ctx, `/v1/host/sample/orgs/${encodeURIComponent(positionals[0])}`, { method: "DELETE" });
3241
+ writeLine(ctx.io.stdout, `Deleted org ${positionals[0]}.`);
3242
+ return 0;
3243
+ }
3244
+ async function roleCatalog(ctx, argv) {
3245
+ const { options } = parseOptions(argv, { bool: ["--help"] });
3246
+ if (options.help) {
3247
+ write(ctx.io.stdout, "Usage: openwop orgs role-catalog [--json]\n");
3248
+ return 0;
3249
+ }
3250
+ const res = await requestJson(ctx, "/v1/host/sample/roles");
3251
+ if (ctx.json) {
3252
+ writeJson(ctx.io.stdout, res.body);
3253
+ return 0;
3254
+ }
3255
+ const roles = arrayOf(res.body, "roles");
3256
+ writeLine(ctx.io.stdout, formatTable(roles.map((r) => ({
3257
+ roleId: r.roleId ?? r.id,
3258
+ name: r.name ?? "",
3259
+ scopes: Array.isArray(r.scopes) ? r.scopes.join(",") : ""
3260
+ })), ["roleId", "name", "scopes"]));
3261
+ return 0;
3262
+ }
3263
+ async function effectiveAccess(ctx, argv) {
3264
+ const { options } = parseOptions(argv, { bool: ["--help"], value: ["--subject"] });
3265
+ if (options.help) {
3266
+ write(ctx.io.stdout, "Usage: openwop orgs effective [--subject <id>] [--json]\n");
3267
+ return 0;
3268
+ }
3269
+ const q = options.subject ? `?subject=${encodeURIComponent(options.subject)}` : "";
3270
+ const res = await requestJson(ctx, `/v1/host/sample/access/effective${q}`);
3271
+ writeJson(ctx.io.stdout, res.body);
3272
+ return 0;
3273
+ }
3274
+ async function runEntity(ctx, entity, argv) {
3275
+ const orgId = argv[0];
3276
+ const verb = argv[1] ?? "list";
3277
+ if (!orgId || orgId === "--help" || orgId === "-h") {
3278
+ write(ctx.io.stdout, `Usage: openwop orgs ${entity} <orgId> list|create|update|delete [...]
3279
+ `);
3280
+ return orgId ? 0 : 2;
3281
+ }
3282
+ const rest = argv.slice(2);
3283
+ const base = `/v1/host/sample/orgs/${encodeURIComponent(orgId)}/${entity}`;
3284
+ switch (verb) {
3285
+ case "list": {
3286
+ const res = await requestJson(ctx, base);
3287
+ if (ctx.json) {
3288
+ writeJson(ctx.io.stdout, res.body);
3289
+ return 0;
3290
+ }
3291
+ const items = arrayOf(res.body, entity);
3292
+ if (items.length === 0) {
3293
+ writeLine(ctx.io.stdout, `No ${entity} in org ${orgId}.`);
3294
+ return 0;
3295
+ }
3296
+ writeJson(ctx.io.stdout, items);
3297
+ return 0;
3298
+ }
3299
+ case "create": {
3300
+ const { body, ok: ok3 } = entityBody(ctx, entity, rest, true);
3301
+ if (!ok3) return 2;
3302
+ const res = await requestJson(ctx, base, { method: "POST", body });
3303
+ if (ctx.json) {
3304
+ writeJson(ctx.io.stdout, res.body);
3305
+ return 0;
3306
+ }
3307
+ writeLine(ctx.io.stdout, `Created ${singular(entity)} in org ${orgId}.`);
3308
+ return 0;
3309
+ }
3310
+ case "update": {
3311
+ const { positionals } = parseOptions(rest, { bool: ["--help"], value: passThroughValues(entity), multi: passThroughMulti(entity) });
3312
+ if (positionals.length !== 1) {
3313
+ writeLine(ctx.io.stderr, `Usage: openwop orgs ${entity} <orgId> update <${singular(entity)}Id> [...]`);
3314
+ return 2;
3315
+ }
3316
+ const { body, ok: ok3 } = entityBody(ctx, entity, rest, false);
3317
+ if (!ok3) return 2;
3318
+ const res = await requestJson(ctx, `${base}/${encodeURIComponent(positionals[0])}`, { method: "PATCH", body });
3319
+ if (ctx.json) {
3320
+ writeJson(ctx.io.stdout, res.body);
3321
+ return 0;
3322
+ }
3323
+ writeLine(ctx.io.stdout, `Updated ${singular(entity)} ${positionals[0]}.`);
3324
+ return 0;
3325
+ }
3326
+ case "delete": {
3327
+ const { options, positionals } = parseOptions(rest, { bool: ["--help", "--yes"] });
3328
+ if (options.help || positionals.length !== 1) {
3329
+ writeLine(ctx.io.stderr, `Usage: openwop orgs ${entity} <orgId> delete <${singular(entity)}Id> [--yes]`);
3330
+ return options.help ? 0 : 2;
3331
+ }
3332
+ if (!options.yes) {
3333
+ writeLine(ctx.io.stderr, `Refusing to delete ${singular(entity)} ${positionals[0]} without --yes.`);
3334
+ return 2;
3335
+ }
3336
+ await requestJson(ctx, `${base}/${encodeURIComponent(positionals[0])}`, { method: "DELETE" });
3337
+ writeLine(ctx.io.stdout, `Deleted ${singular(entity)} ${positionals[0]}.`);
3338
+ return 0;
3339
+ }
3340
+ default:
3341
+ throw new CliError(`Unknown ${entity} verb: ${verb}`);
3342
+ }
3343
+ }
3344
+ function passThroughValues(entity) {
3345
+ switch (entity) {
3346
+ case "teams":
3347
+ return ["--name", "--description", "--color"];
3348
+ case "groups":
3349
+ return ["--name", "--description"];
3350
+ case "roles":
3351
+ return ["--name", "--description"];
3352
+ case "members":
3353
+ return ["--subject", "--display-name", "--email"];
3354
+ }
3355
+ }
3356
+ function passThroughMulti(entity) {
3357
+ switch (entity) {
3358
+ case "groups":
3359
+ return ["--role", "--member"];
3360
+ case "roles":
3361
+ return ["--scope"];
3362
+ case "members":
3363
+ return ["--role", "--team"];
3364
+ default:
3365
+ return [];
3366
+ }
3367
+ }
3368
+ function entityBody(ctx, entity, argv, requireName) {
3369
+ const { options } = parseOptions(argv, { bool: ["--help", "--yes"], value: passThroughValues(entity), multi: passThroughMulti(entity) });
3370
+ const body = {};
3371
+ const setIf = (k, v) => {
3372
+ if (v !== void 0) body[k] = v;
3373
+ };
3374
+ switch (entity) {
3375
+ case "teams":
3376
+ setIf("name", options.name);
3377
+ setIf("description", options.description);
3378
+ setIf("color", options.color);
3379
+ break;
3380
+ case "roles":
3381
+ setIf("name", options.name);
3382
+ setIf("description", options.description);
3383
+ if (Array.isArray(options.scope) && options.scope.length) body.scopes = options.scope;
3384
+ break;
3385
+ case "groups":
3386
+ setIf("name", options.name);
3387
+ setIf("description", options.description);
3388
+ if (Array.isArray(options.role) && options.role.length) body.roles = options.role;
3389
+ if (Array.isArray(options.member) && options.member.length) body.memberIds = options.member;
3390
+ break;
3391
+ case "members":
3392
+ setIf("subject", options.subject);
3393
+ setIf("displayName", options.displayName);
3394
+ setIf("email", options.email);
3395
+ if (Array.isArray(options.role) && options.role.length) body.roles = options.role;
3396
+ if (Array.isArray(options.team) && options.team.length) body.teamIds = options.team;
3397
+ break;
3398
+ }
3399
+ if (requireName) {
3400
+ if (entity === "members") {
3401
+ if (!body.subject || !body.displayName) {
3402
+ writeLine(ctx.io.stderr, "members create requires --subject and --display-name.");
3403
+ return { body, ok: false };
3404
+ }
3405
+ } else if (!body.name) {
3406
+ writeLine(ctx.io.stderr, `${entity} create requires --name.`);
3407
+ return { body, ok: false };
3408
+ }
3409
+ } else if (Object.keys(body).length === 0) {
3410
+ writeLine(ctx.io.stderr, `Nothing to update \u2014 pass at least one field for ${entity}.`);
3411
+ return { body, ok: false };
3412
+ }
3413
+ return { body, ok: true };
3414
+ }
3415
+ function singular(entity) {
3416
+ return entity === "teams" ? "team" : entity === "groups" ? "group" : entity === "roles" ? "role" : "member";
3417
+ }
3418
+ function arrayOf(body, key) {
3419
+ if (Array.isArray(body)) return body;
3420
+ if (body && Array.isArray(body[key])) return body[key];
3421
+ return [];
3422
+ }
3423
+
3424
+ // src/cli/workspace.ts
3425
+ import { readFile as readFile3 } from "node:fs/promises";
3426
+ var WORKSPACE_HELP = `Usage:
3427
+ openwop workspace list [--prefix <p>] [--json]
3428
+ openwop workspace get <path> [--json]
3429
+ openwop workspace put <path> (--content <text> | --file <localPath>) [--content-type <ct>] [--if-match <etag>] [--json]
3430
+ openwop workspace delete <path> [--yes]
3431
+
3432
+ Agent workspace (RFC 0059 \xA7C). A tenant-scoped file area an agent reads and
3433
+ writes during a run, with optimistic concurrency via ETag / If-Match (WCT-1
3434
+ cross-owner isolation is enforced host-side). Drives the real CRUD surface
3435
+ GET/PUT/DELETE /v1/host/workspace/files. (The /v1/host/sample/workspace/op
3436
+ cross-owner seam is a conformance-only test seam and is intentionally not
3437
+ exposed here.)
3438
+
3439
+ --prefix <p> (list) Only files whose path starts with this prefix.
3440
+ --content <text> (put) Inline file content.
3441
+ --file <path> (put) Read content from a local file instead of --content.
3442
+ --content-type (put) MIME type to store.
3443
+ --if-match <etag> (put) Only write if the current ETag matches (optimistic lock).
3444
+
3445
+ Examples:
3446
+ openwop workspace list
3447
+ openwop workspace put notes/todo.md --content "- ship it"
3448
+ openwop workspace get notes/todo.md
3449
+ openwop workspace delete notes/todo.md --yes
3450
+ `;
3451
+ async function runWorkspace(ctx, argv) {
3452
+ const sub = argv[0] ?? "list";
3453
+ if (sub === "--help" || sub === "-h") {
3454
+ write(ctx.io.stdout, WORKSPACE_HELP);
3455
+ return 0;
3456
+ }
3457
+ const args = argv.slice(["list", "get", "put", "delete"].includes(sub) ? 1 : 0);
3458
+ switch (sub) {
3459
+ case "list":
3460
+ return await wsList(ctx, args);
3461
+ case "get":
3462
+ return await wsGet(ctx, args);
3463
+ case "put":
3464
+ return await wsPut(ctx, args);
3465
+ case "delete":
3466
+ return await wsDelete(ctx, args);
3467
+ default:
3468
+ throw new CliError(`Unknown workspace command: ${sub}
3469
+ Run \`openwop workspace --help\` for usage.`);
3470
+ }
3471
+ }
3472
+ async function wsList(ctx, argv) {
3473
+ const { options } = parseOptions(argv, { bool: ["--help"], value: ["--prefix"] });
3474
+ if (options.help) {
3475
+ write(ctx.io.stdout, WORKSPACE_HELP);
3476
+ return 0;
3477
+ }
3478
+ const q = options.prefix ? `?prefix=${encodeURIComponent(options.prefix)}` : "";
3479
+ const res = await requestJson(ctx, `/v1/host/workspace/files${q}`);
3480
+ if (ctx.json) {
3481
+ writeJson(ctx.io.stdout, res.body);
3482
+ return 0;
3483
+ }
3484
+ const files = Array.isArray(res.body?.files) ? res.body.files : [];
3485
+ if (files.length === 0) {
3486
+ writeLine(ctx.io.stdout, "No files in the workspace.");
3487
+ return 0;
3488
+ }
3489
+ writeLine(ctx.io.stdout, formatTable(files.map((f) => ({
3490
+ path: f.path ?? f.name ?? "",
3491
+ size: f.size ?? (typeof f.content === "string" ? String(f.content.length) : ""),
3492
+ contentType: f.contentType ?? "",
3493
+ etag: f.etag ?? ""
3494
+ })), ["path", "size", "contentType", "etag"]));
3495
+ return 0;
3496
+ }
3497
+ async function wsGet(ctx, argv) {
3498
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
3499
+ if (options.help || positionals.length !== 1) {
3500
+ write(ctx.io.stdout, "Usage: openwop workspace get <path> [--json]\n");
3501
+ return options.help ? 0 : 2;
3502
+ }
3503
+ const res = await requestJson(ctx, `/v1/host/workspace/files/${encodeURIComponent(positionals[0])}`);
3504
+ if (ctx.json) {
3505
+ writeJson(ctx.io.stdout, res.body);
3506
+ return 0;
3507
+ }
3508
+ const f = res.body ?? {};
3509
+ if (f.etag) writeLine(ctx.io.stderr, `# etag: ${f.etag}${f.contentType ? ` contentType: ${f.contentType}` : ""}`);
3510
+ write(ctx.io.stdout, typeof f.content === "string" ? f.content : JSON.stringify(f, null, 2));
3511
+ if (typeof f.content === "string" && !f.content.endsWith("\n")) write(ctx.io.stdout, "\n");
3512
+ return 0;
3513
+ }
3514
+ async function wsPut(ctx, argv) {
3515
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"], value: ["--content", "--file", "--content-type", "--if-match"] });
3516
+ if (options.help || positionals.length !== 1) {
3517
+ write(ctx.io.stdout, "Usage: openwop workspace put <path> (--content <text> | --file <localPath>) [--content-type <ct>] [--if-match <etag>] [--json]\n");
3518
+ return options.help ? 0 : 2;
3519
+ }
3520
+ let content;
3521
+ if (options.file !== void 0) {
3522
+ try {
3523
+ content = await readFile3(options.file, "utf8");
3524
+ } catch (err) {
3525
+ throw new CliError(`Could not read ${options.file}: ${err instanceof Error ? err.message : String(err)}`, 2);
3526
+ }
3527
+ } else if (options.content !== void 0) {
3528
+ content = options.content;
3529
+ } else {
3530
+ throw new CliError("Pass either --content <text> or --file <localPath>.", 2);
3531
+ }
3532
+ const body = { content };
3533
+ if (options.contentType) body.contentType = options.contentType;
3534
+ const headers = {};
3535
+ if (options.ifMatch) headers["if-match"] = options.ifMatch;
3536
+ const res = await requestJson(ctx, `/v1/host/workspace/files/${encodeURIComponent(positionals[0])}`, { method: "PUT", body, headers });
3537
+ if (ctx.json) {
3538
+ writeJson(ctx.io.stdout, res.body);
3539
+ return 0;
3540
+ }
3541
+ writeLine(ctx.io.stdout, `Wrote ${positionals[0]}${res.body?.etag ? ` (etag ${res.body.etag})` : ""}.`);
3542
+ return 0;
3543
+ }
3544
+ async function wsDelete(ctx, argv) {
3545
+ const { options, positionals } = parseOptions(argv, { bool: ["--help", "--yes"] });
3546
+ if (options.help || positionals.length !== 1) {
3547
+ write(ctx.io.stdout, "Usage: openwop workspace delete <path> [--yes]\n");
3548
+ return options.help ? 0 : 2;
3549
+ }
3550
+ if (!options.yes) {
3551
+ writeLine(ctx.io.stderr, `Refusing to delete ${positionals[0]} without --yes.`);
3552
+ return 2;
3553
+ }
3554
+ await requestJson(ctx, `/v1/host/workspace/files/${encodeURIComponent(positionals[0])}`, { method: "DELETE" });
3555
+ writeLine(ctx.io.stdout, `Deleted ${positionals[0]}.`);
3556
+ return 0;
3557
+ }
3558
+
3559
+ // src/prompt.ts
3560
+ import { createInterface as createInterface3 } from "node:readline";
3561
+ async function promptChoice(ctx, label, choices) {
3562
+ writeLine(ctx.io.stdout, label);
3563
+ choices.forEach((c, i) => {
3564
+ const tag = c.recommended ? " (recommended)" : "";
3565
+ writeLine(ctx.io.stdout, ` ${i + 1}) ${c.label}${tag}`);
3566
+ });
3567
+ const defaultIdx = Math.max(0, choices.findIndex((c) => c.recommended));
3568
+ const answer = await promptText(ctx, `Choice [${defaultIdx + 1}]: `, "");
3569
+ const idx = answer.trim() === "" ? defaultIdx : Number(answer.trim()) - 1;
3570
+ if (!Number.isInteger(idx) || idx < 0 || idx >= choices.length) {
3571
+ throw new CliError(`Invalid choice: ${answer}`);
3572
+ }
3573
+ return choices[idx].key;
3574
+ }
3575
+ async function promptText(ctx, prompt, defaultValue = "") {
3576
+ ctx.io.stdout.write(prompt);
3577
+ const rl = createInterface3({ input: process.stdin, output: process.stdout, terminal: false });
3578
+ return new Promise((resolve) => {
3579
+ rl.once("line", (line) => {
3580
+ rl.close();
3581
+ resolve(line.length > 0 ? line : defaultValue);
3582
+ });
3583
+ });
3584
+ }
3585
+ async function promptYesNo(ctx, label, defaultYes = true) {
3586
+ const hint = defaultYes ? "[Y/n]" : "[y/N]";
3587
+ const answer = (await promptText(ctx, `${label} ${hint} `, "")).trim().toLowerCase();
3588
+ if (answer === "") return defaultYes;
3589
+ return answer === "y" || answer === "yes";
3590
+ }
3591
+ async function readSecret(ctx, prompt) {
3592
+ ctx.io.stdout.write(prompt);
3593
+ const stdin = process.stdin;
3594
+ if (!stdin.isTTY) {
3595
+ const rl = createInterface3({ input: stdin, output: process.stdout, terminal: false });
3596
+ return new Promise((resolve) => {
3597
+ rl.once("line", (line) => {
3598
+ rl.close();
3599
+ resolve(line);
3600
+ });
3601
+ });
3602
+ }
3603
+ return new Promise((resolve, reject) => {
3604
+ stdin.setRawMode(true);
3605
+ stdin.resume();
2542
3606
  stdin.setEncoding("utf8");
2543
3607
  let value = "";
2544
3608
  const cleanup = () => {
@@ -2571,6 +3635,137 @@ async function readSecret(ctx, prompt) {
2571
3635
  });
2572
3636
  }
2573
3637
 
3638
+ // src/cli/byok.ts
3639
+ var BYOK_HELP = `Usage:
3640
+ openwop byok list [--json]
3641
+ openwop byok set --ref <credentialRef> [--value <secret>] [--json]
3642
+ openwop byok delete <credentialRef> [--yes]
3643
+
3644
+ Bring-your-own-key secret store (host-side). The host holds the secret; the
3645
+ credential-payload-redaction invariant (RFC 0046) means a value is NEVER
3646
+ returned over the wire \u2014 'list' shows only refs, and 'set' echoes only a
3647
+ masked preview. Drives /v1/host/sample/byok/secrets. If --value is omitted,
3648
+ 'set' prompts for it without echoing to the terminal.
3649
+
3650
+ --ref <credentialRef> Opaque ref a workflow/pack uses to fetch the secret
3651
+ (matches [a-zA-Z0-9_.:-]{1,128}).
3652
+ --value <secret> The secret material. Prefer the interactive prompt
3653
+ (omit this flag) so the secret never lands in shell history.
3654
+
3655
+ Examples:
3656
+ openwop byok list
3657
+ openwop byok set --ref anthropic-prod # prompts for the value
3658
+ openwop byok delete anthropic-prod --yes
3659
+ `;
3660
+ async function runByok(ctx, argv) {
3661
+ const sub = argv[0] ?? "list";
3662
+ if (sub === "--help" || sub === "-h") {
3663
+ write(ctx.io.stdout, BYOK_HELP);
3664
+ return 0;
3665
+ }
3666
+ const args = argv.slice(["list", "set", "delete"].includes(sub) ? 1 : 0);
3667
+ switch (sub) {
3668
+ case "list":
3669
+ return await byokList(ctx, args);
3670
+ case "set":
3671
+ return await byokSet(ctx, args);
3672
+ case "delete":
3673
+ return await byokDelete(ctx, args);
3674
+ default:
3675
+ throw new CliError(`Unknown byok command: ${sub}
3676
+ Run \`openwop byok --help\` for usage.`);
3677
+ }
3678
+ }
3679
+ async function byokList(ctx, argv) {
3680
+ const { options } = parseOptions(argv, { bool: ["--help"] });
3681
+ if (options.help) {
3682
+ write(ctx.io.stdout, BYOK_HELP);
3683
+ return 0;
3684
+ }
3685
+ const res = await requestJson(ctx, "/v1/host/sample/byok/secrets");
3686
+ if (ctx.json) {
3687
+ writeJson(ctx.io.stdout, res.body);
3688
+ return 0;
3689
+ }
3690
+ const refs = Array.isArray(res.body?.credentialRefs) ? res.body.credentialRefs : [];
3691
+ if (refs.length === 0) {
3692
+ writeLine(ctx.io.stdout, "No BYOK secrets stored. Add one with `openwop byok set --ref <name>`.");
3693
+ return 0;
3694
+ }
3695
+ const rows = refs.map((r) => typeof r === "string" ? { credentialRef: r, masked: "", createdAt: "" } : { credentialRef: r.credentialRef ?? "", masked: r.masked ?? "", createdAt: r.createdAt ?? "" });
3696
+ writeLine(ctx.io.stdout, formatTable(rows, ["credentialRef", "masked", "createdAt"]));
3697
+ return 0;
3698
+ }
3699
+ async function byokSet(ctx, argv) {
3700
+ const { options } = parseOptions(argv, { bool: ["--help"], value: ["--ref", "--value"] });
3701
+ if (options.help || !options.ref) {
3702
+ write(ctx.io.stdout, "Usage: openwop byok set --ref <credentialRef> [--value <secret>] [--json]\n");
3703
+ return options.help ? 0 : 2;
3704
+ }
3705
+ let value = options.value;
3706
+ if (value === void 0) {
3707
+ const entered = await readSecret(ctx, `Secret value for "${options.ref}": `);
3708
+ value = typeof entered === "string" ? entered : String(entered ?? "");
3709
+ }
3710
+ if (!value) throw new CliError("A non-empty --value (or prompted secret) is required.", 2);
3711
+ const res = await requestJson(ctx, "/v1/host/sample/byok/secrets", {
3712
+ method: "POST",
3713
+ body: { credentialRef: options.ref, value }
3714
+ });
3715
+ if (ctx.json) {
3716
+ writeJson(ctx.io.stdout, res.body);
3717
+ return 0;
3718
+ }
3719
+ writeLine(ctx.io.stdout, `Stored ${res.body?.credentialRef ?? options.ref} (${res.body?.masked ?? "masked"}).`);
3720
+ return 0;
3721
+ }
3722
+ async function byokDelete(ctx, argv) {
3723
+ const { options, positionals } = parseOptions(argv, { bool: ["--help", "--yes"] });
3724
+ if (options.help || positionals.length !== 1) {
3725
+ write(ctx.io.stdout, "Usage: openwop byok delete <credentialRef> [--yes]\n");
3726
+ return options.help ? 0 : 2;
3727
+ }
3728
+ if (!options.yes) {
3729
+ writeLine(ctx.io.stderr, `Refusing to delete secret ${positionals[0]} without --yes.`);
3730
+ return 2;
3731
+ }
3732
+ await requestJson(ctx, `/v1/host/sample/byok/secrets/${encodeURIComponent(positionals[0])}`, { method: "DELETE" });
3733
+ writeLine(ctx.io.stdout, `Deleted secret ref ${positionals[0]}.`);
3734
+ return 0;
3735
+ }
3736
+
3737
+ // src/cli/conformance.ts
3738
+ import { spawnSync as spawnSync2 } from "node:child_process";
3739
+ import { join as join4 } from "node:path";
3740
+ var CONFORMANCE_HELP = `Usage: openwop conformance [--offline] [--filter pattern]
3741
+
3742
+ Runs the in-repo @openwop/openwop-conformance CLI. Without --offline it targets the configured --base-url.
3743
+ `;
3744
+ async function runConformance(ctx, argv) {
3745
+ const { options } = parseOptions(argv, {
3746
+ bool: ["--help", "--offline"],
3747
+ value: ["--filter"]
3748
+ });
3749
+ if (options.help) {
3750
+ write(ctx.io.stdout, CONFORMANCE_HELP);
3751
+ return 0;
3752
+ }
3753
+ const root = requireRepoRoot(ctx);
3754
+ const args = ["run", "cli", "--"];
3755
+ if (options.offline) {
3756
+ args.push("--offline");
3757
+ } else {
3758
+ args.push("--base-url", ctx.baseUrl, "--api-key", ctx.apiKey ?? DEFAULT_API_KEY);
3759
+ }
3760
+ if (options.filter) args.push("--filter", options.filter);
3761
+ const result = spawnSync2(npmCommand(), args, {
3762
+ cwd: join4(root, "conformance"),
3763
+ stdio: "inherit",
3764
+ env: ctx.env
3765
+ });
3766
+ return result.status ?? 1;
3767
+ }
3768
+
2574
3769
  // src/cli/account.ts
2575
3770
  var ACCOUNT_HELP = `Usage:
2576
3771
  openwop account delete [--confirm] [--json]
@@ -5545,6 +6740,21 @@ async function runCli(argv, options = {}) {
5545
6740
  case "agents":
5546
6741
  case "agent":
5547
6742
  return await runAgents(ctx, commandArgs);
6743
+ case "roster":
6744
+ return await runRoster(ctx, commandArgs);
6745
+ case "org-chart":
6746
+ case "orgchart":
6747
+ return await runOrgChart(ctx, commandArgs);
6748
+ case "kanban":
6749
+ case "boards":
6750
+ return await runKanban(ctx, commandArgs);
6751
+ case "orgs":
6752
+ case "org":
6753
+ return await runOrgs(ctx, commandArgs);
6754
+ case "workspace":
6755
+ return await runWorkspace(ctx, commandArgs);
6756
+ case "byok":
6757
+ return await runByok(ctx, commandArgs);
5548
6758
  case "config":
5549
6759
  return await runConfig(ctx, commandArgs);
5550
6760
  case "webhooks":
@@ -5607,6 +6817,15 @@ function showHelp(io, command) {
5607
6817
  provider: PROVIDERS_HELP,
5608
6818
  agents: AGENTS_HELP,
5609
6819
  agent: AGENTS_HELP,
6820
+ roster: ROSTER_HELP,
6821
+ "org-chart": ORG_CHART_HELP,
6822
+ orgchart: ORG_CHART_HELP,
6823
+ kanban: KANBAN_HELP,
6824
+ boards: KANBAN_HELP,
6825
+ orgs: ORGS_HELP,
6826
+ org: ORGS_HELP,
6827
+ workspace: WORKSPACE_HELP,
6828
+ byok: BYOK_HELP,
5610
6829
  config: CONFIG_HELP,
5611
6830
  doctor: DOCTOR_HELP,
5612
6831
  health: HEALTH_HELP,
@@ -5686,8 +6905,31 @@ Commands:
5686
6905
  memory get Show one memory entry
5687
6906
  memory delete Delete one memory entry
5688
6907
  runs ancestry Show a run's cross-host parent chain (RFC 0040)
5689
- agents list List agent-attributed node roles on the host
5690
- agents info Show one agent role's details
6908
+ agents list List manifest agents (RFC 0070) on the host
6909
+ agents info Show one agent's details
6910
+ agents create Create a tenant-scoped user-defined agent
6911
+ agents update Update a user-defined agent
6912
+ agents delete Delete a user-defined agent
6913
+ roster list List named standing agents + their workflow portfolio (RFC 0086)
6914
+ roster create Create a roster entry
6915
+ roster update Update / pause / re-enable a roster entry
6916
+ roster delete Delete a roster entry
6917
+ org-chart get Show the descriptive department/reporting structure (RFC 0087)
6918
+ org-chart set Replace the org chart from a JSON file
6919
+ kanban boards List agent task boards
6920
+ kanban board-create Create a board (optionally bound to a roster entry)
6921
+ kanban card-add Add a card to a board
6922
+ kanban card-move Move a card between columns
6923
+ kanban watch Stream a board's card events
6924
+ orgs list List organizations (RFC 0049 RBAC)
6925
+ orgs create Create an organization
6926
+ orgs teams|groups|roles|members Manage org teams/groups/roles/members
6927
+ orgs effective Resolve a subject's effective access
6928
+ workspace list List per-tenant agent workspace files (RFC 0059)
6929
+ workspace put|get Write / read a workspace file
6930
+ byok list List BYOK credential refs (never values)
6931
+ byok set Store a BYOK secret (prompts if --value omitted)
6932
+ byok delete Remove a BYOK secret ref
5691
6933
  webhooks list List webhook subscriptions
5692
6934
  webhooks add Register a webhook subscription
5693
6935
  webhooks remove Delete a webhook subscription