@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.
- package/README.md +25 -0
- package/dist/cli.js +1315 -73
- package/dist/openwop.js +1315 -73
- 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.
|
|
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
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
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
|
|
2509
|
+
if (options.taskJson !== void 0) {
|
|
2425
2510
|
try {
|
|
2426
|
-
body.task = JSON.parse(options
|
|
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
|
|
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/
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
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
|
-
|
|
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
|
|
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", "--
|
|
2473
|
-
value: ["--
|
|
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,
|
|
2768
|
+
write(ctx.io.stdout, ORG_CHART_HELP);
|
|
2477
2769
|
return 0;
|
|
2478
2770
|
}
|
|
2479
|
-
const
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
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
|
-
|
|
2487
|
-
const
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
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
|
-
|
|
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/
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
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
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
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
|
|
2522
|
-
const
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
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
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
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
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
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
|
|
5690
|
-
agents info Show one agent
|
|
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
|