@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/cli.js
CHANGED
|
@@ -522,7 +522,7 @@ function errText(err) {
|
|
|
522
522
|
}
|
|
523
523
|
|
|
524
524
|
// src/constants.ts
|
|
525
|
-
var VERSION = "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
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
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
|
|
2508
|
+
if (options.taskJson !== void 0) {
|
|
2424
2509
|
try {
|
|
2425
|
-
body.task = JSON.parse(options
|
|
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
|
|
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/
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
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
|
-
|
|
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
|
|
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", "--
|
|
2472
|
-
value: ["--
|
|
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,
|
|
2767
|
+
write(ctx.io.stdout, ORG_CHART_HELP);
|
|
2476
2768
|
return 0;
|
|
2477
2769
|
}
|
|
2478
|
-
const
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
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
|
-
|
|
2486
|
-
const
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
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
|
-
|
|
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/
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
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
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
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
|
|
2521
|
-
const
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
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
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
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
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
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
|
|
5689
|
-
agents info Show one agent
|
|
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
|