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