@openwop/cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +274 -15
  3. package/dist/cli.js +3996 -30
  4. package/dist/openwop.js +3996 -30
  5. package/package.json +4 -5
package/dist/cli.js CHANGED
@@ -311,12 +311,12 @@ async function getWaSocket(env) {
311
311
  const { state, saveCreds } = await useMultiFileAuthState(authDir);
312
312
  const sock = makeWASocket({ auth: state, printQRInTerminal: true });
313
313
  sock.ev.on("creds.update", saveCreds);
314
- await new Promise((resolve, reject) => {
314
+ await new Promise((resolve2, reject) => {
315
315
  const timer = setTimeout(() => reject(new Error("WhatsApp connection timed out (scan the QR within 90s, or check the linked session)")), 9e4);
316
316
  sock.ev.on("connection.update", (u) => {
317
317
  if (u?.connection === "open") {
318
318
  clearTimeout(timer);
319
- resolve();
319
+ resolve2();
320
320
  }
321
321
  });
322
322
  });
@@ -393,9 +393,9 @@ async function getDiscordClient(env) {
393
393
  GatewayIntentBits.GuildMessageReactions
394
394
  ]
395
395
  });
396
- const ready = new Promise((resolve) => {
397
- client.once("clientReady", resolve);
398
- client.once("ready", resolve);
396
+ const ready = new Promise((resolve2) => {
397
+ client.once("clientReady", resolve2);
398
+ client.once("ready", resolve2);
399
399
  });
400
400
  await client.login(token);
401
401
  await ready;
@@ -522,7 +522,7 @@ function errText(err) {
522
522
  }
523
523
 
524
524
  // src/constants.ts
525
- var VERSION = "0.2.0";
525
+ var VERSION = "0.3.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";
@@ -1010,8 +1010,8 @@ function parseJsonResponse(text) {
1010
1010
  }
1011
1011
 
1012
1012
  // src/util.ts
1013
- function sleep(ms) {
1014
- return new Promise((resolve) => setTimeout(resolve, ms));
1013
+ function sleep(ms2) {
1014
+ return new Promise((resolve2) => setTimeout(resolve2, ms2));
1015
1015
  }
1016
1016
 
1017
1017
  // src/sse.ts
@@ -1200,19 +1200,19 @@ function defaultReadTurn(ctx) {
1200
1200
  rl.on("close", () => {
1201
1201
  closed = true;
1202
1202
  });
1203
- return (prompt) => new Promise((resolve) => {
1203
+ return (prompt) => new Promise((resolve2) => {
1204
1204
  if (closed) {
1205
- resolve(null);
1205
+ resolve2(null);
1206
1206
  return;
1207
1207
  }
1208
1208
  if (!ctx.json) ctx.io.stdout.write(prompt);
1209
1209
  const onLine = (line) => {
1210
1210
  rl.removeListener("close", onClose);
1211
- resolve(line);
1211
+ resolve2(line);
1212
1212
  };
1213
1213
  const onClose = () => {
1214
1214
  rl.removeListener("line", onLine);
1215
- resolve(null);
1215
+ resolve2(null);
1216
1216
  };
1217
1217
  rl.once("line", onLine);
1218
1218
  rl.once("close", onClose);
@@ -1568,8 +1568,10 @@ async function runWebhooksTest(ctx, argv) {
1568
1568
 
1569
1569
  // src/cli/cron.ts
1570
1570
  var CRON_HELP = `Usage:
1571
- openwop cron list [--json]
1571
+ openwop cron list [--roster <rosterId>] [--json]
1572
1572
  openwop cron add "<cronExpr>" [--workflow <id>] [--job-id <id>] [--first-fire-at-ms <ms>] [--json]
1573
+ openwop cron enable <jobId> [--json]
1574
+ openwop cron disable <jobId> [--json]
1573
1575
  openwop cron remove <jobId> [--json]
1574
1576
  openwop cron trigger <jobId> [--json]
1575
1577
 
@@ -1577,16 +1579,21 @@ Manage scheduled (cron) jobs on the configured host via the RFC 0052 sample
1577
1579
  scheduler CRUD (/v1/host/sample/scheduler/jobs). This is a sample-extension
1578
1580
  surface \u2014 not part of the normative OpenWOP wire contract.
1579
1581
 
1582
+ list Lists jobs (tenant-scoped). --roster <id> filters to the schedules
1583
+ owned by one roster member (?rosterId=).
1580
1584
  add Registers a job. --job-id is optional (the host assigns a UUID when
1581
1585
  omitted). A --first-fire-at-ms beyond the host's maxFutureHorizon is
1582
1586
  rejected with schedule_horizon_exceeded (RFC 0052 \xA7B.3).
1587
+ enable Activates a job (PATCH {enabled:true}); its triggers fire again.
1588
+ disable Deactivates a job (PATCH {enabled:false}); the row stays but its
1589
+ triggers are inert (mirrors the roster 'enabled' posture).
1583
1590
  trigger Fires the job once now. Honors RFC 0052 \xA7B.2 fire-once-per-tick: a
1584
1591
  single trigger advances the scheduler clock one tick and produces
1585
1592
  exactly one run.
1586
1593
  `;
1587
1594
  async function runCron(ctx, argv) {
1588
1595
  const sub = argv[0] ?? "list";
1589
- const args = argv.slice(["list", "add", "remove", "rm", "trigger"].includes(sub) ? 1 : 0);
1596
+ const args = argv.slice(["list", "add", "enable", "disable", "remove", "rm", "trigger"].includes(sub) ? 1 : 0);
1590
1597
  if (sub === "--help" || sub === "-h") {
1591
1598
  write(ctx.io.stdout, CRON_HELP);
1592
1599
  return 0;
@@ -1596,6 +1603,10 @@ async function runCron(ctx, argv) {
1596
1603
  return await runCronList(ctx, args);
1597
1604
  case "add":
1598
1605
  return await runCronAdd(ctx, args);
1606
+ case "enable":
1607
+ return await runCronSetEnabled(ctx, args, true);
1608
+ case "disable":
1609
+ return await runCronSetEnabled(ctx, args, false);
1599
1610
  case "remove":
1600
1611
  case "rm":
1601
1612
  return await runCronRemove(ctx, args);
@@ -1607,28 +1618,50 @@ Run \`openwop cron --help\` for usage.`);
1607
1618
  }
1608
1619
  }
1609
1620
  async function runCronList(ctx, argv) {
1610
- const { options } = parseOptions(argv, { bool: ["--help"] });
1621
+ const { options } = parseOptions(argv, { bool: ["--help"], value: ["--roster"] });
1611
1622
  if (options.help) {
1612
1623
  write(ctx.io.stdout, CRON_HELP);
1613
1624
  return 0;
1614
1625
  }
1615
- const res = await requestJson(ctx, "/v1/host/sample/scheduler/jobs");
1626
+ const path = options.roster !== void 0 ? `/v1/host/sample/scheduler/jobs?rosterId=${encodeURIComponent(options.roster)}` : "/v1/host/sample/scheduler/jobs";
1627
+ const res = await requestJson(ctx, path);
1616
1628
  if (ctx.json) {
1617
1629
  writeJson(ctx.io.stdout, res.body);
1618
1630
  return 0;
1619
1631
  }
1620
1632
  const jobs = Array.isArray(res.body?.jobs) ? res.body.jobs : [];
1621
1633
  if (jobs.length === 0) {
1622
- writeLine(ctx.io.stdout, 'No scheduled jobs. Add one with `openwop cron add "<cronExpr>" --workflow <id>`.');
1634
+ writeLine(ctx.io.stdout, options.roster !== void 0 ? `No scheduled jobs for roster ${options.roster}.` : 'No scheduled jobs. Add one with `openwop cron add "<cronExpr>" --workflow <id>`.');
1623
1635
  return 0;
1624
1636
  }
1625
1637
  const rows = jobs.map((j) => ({
1626
1638
  jobId: j.jobId,
1627
1639
  cronExpr: j.cronExpr,
1628
1640
  workflowId: j.workflowId ?? "",
1641
+ enabled: j.enabled === false ? "no" : "yes",
1642
+ rosterId: j.rosterId ?? "",
1629
1643
  lastFiredTick: j.lastFiredTick ?? "-"
1630
1644
  }));
1631
- writeLine(ctx.io.stdout, formatTable(rows, ["jobId", "cronExpr", "workflowId", "lastFiredTick"]));
1645
+ writeLine(ctx.io.stdout, formatTable(rows, ["jobId", "cronExpr", "workflowId", "enabled", "rosterId", "lastFiredTick"]));
1646
+ return 0;
1647
+ }
1648
+ async function runCronSetEnabled(ctx, argv, enabled) {
1649
+ const verb = enabled ? "enable" : "disable";
1650
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
1651
+ if (options.help || positionals.length !== 1) {
1652
+ write(ctx.io.stdout, `Usage: openwop cron ${verb} <jobId> [--json]
1653
+ `);
1654
+ return options.help ? 0 : 2;
1655
+ }
1656
+ const res = await requestJson(ctx, `/v1/host/sample/scheduler/jobs/${encodeURIComponent(positionals[0])}`, {
1657
+ method: "PATCH",
1658
+ body: { enabled }
1659
+ });
1660
+ if (ctx.json) {
1661
+ writeJson(ctx.io.stdout, res.body);
1662
+ return 0;
1663
+ }
1664
+ writeLine(ctx.io.stdout, `\u2713 ${enabled ? "Enabled" : "Disabled"} job ${res.body?.jobId ?? positionals[0]} (enabled: ${res.body?.enabled === false ? "false" : enabled ? "true" : "false"})`);
1632
1665
  return 0;
1633
1666
  }
1634
1667
  async function runCronAdd(ctx, argv) {
@@ -3574,10 +3607,10 @@ async function promptChoice(ctx, label, choices) {
3574
3607
  async function promptText(ctx, prompt, defaultValue = "") {
3575
3608
  ctx.io.stdout.write(prompt);
3576
3609
  const rl = createInterface3({ input: process.stdin, output: process.stdout, terminal: false });
3577
- return new Promise((resolve) => {
3610
+ return new Promise((resolve2) => {
3578
3611
  rl.once("line", (line) => {
3579
3612
  rl.close();
3580
- resolve(line.length > 0 ? line : defaultValue);
3613
+ resolve2(line.length > 0 ? line : defaultValue);
3581
3614
  });
3582
3615
  });
3583
3616
  }
@@ -3592,14 +3625,14 @@ async function readSecret(ctx, prompt) {
3592
3625
  const stdin = process.stdin;
3593
3626
  if (!stdin.isTTY) {
3594
3627
  const rl = createInterface3({ input: stdin, output: process.stdout, terminal: false });
3595
- return new Promise((resolve) => {
3628
+ return new Promise((resolve2) => {
3596
3629
  rl.once("line", (line) => {
3597
3630
  rl.close();
3598
- resolve(line);
3631
+ resolve2(line);
3599
3632
  });
3600
3633
  });
3601
3634
  }
3602
- return new Promise((resolve, reject) => {
3635
+ return new Promise((resolve2, reject) => {
3603
3636
  stdin.setRawMode(true);
3604
3637
  stdin.resume();
3605
3638
  stdin.setEncoding("utf8");
@@ -3615,7 +3648,7 @@ async function readSecret(ctx, prompt) {
3615
3648
  const code = ch.charCodeAt(0);
3616
3649
  if (code === 13 || code === 10) {
3617
3650
  cleanup();
3618
- resolve(value);
3651
+ resolve2(value);
3619
3652
  return;
3620
3653
  }
3621
3654
  if (code === 3) {
@@ -6179,6 +6212,222 @@ async function runDoctor(ctx, argv) {
6179
6212
  return checks.some((c) => c.status === "fail") ? 1 : 0;
6180
6213
  }
6181
6214
 
6215
+ // src/cli/governance.ts
6216
+ var GOV_BASE = "/v1/host/sample/governance";
6217
+ var ACTION_KINDS = ["email.send", "calendar.invite", "calendar.reschedule", "nudge"];
6218
+ var POLICY_VALUES = ["disabled", "draft-only", "approval-required"];
6219
+ var GOVERNANCE_HELP = `Usage:
6220
+ openwop governance policy [get] [--json]
6221
+ openwop governance policy set [--provider-allowlist <a,b,...>] [--action <kind=policy>]... [--retention-graph-days <n>] [--retention-source-days <n>] [--json]
6222
+ openwop governance audit [--prefix <p>] [--limit <n>] [--since <iso>] [--json]
6223
+
6224
+ Tenant governance administration (ADR 0028, host extension \u2014 superadmin-gated).
6225
+ Endpoints under ${GOV_BASE}:
6226
+ GET /policy the tenant's stored policy + the host's declared defaults
6227
+ PUT /policy upsert provider allowlist / per-action policy / retention (itself audited)
6228
+ GET /audit the read view over the host audit log (assistant decisions, policy edits)
6229
+
6230
+ CAPABILITY HONESTY: the HOST is the authority for every policy decision \u2014 this command
6231
+ only RENDERS the host's resolved view (stored values + the host's declared defaults). It
6232
+ never evaluates or asserts a policy outcome locally. Governance is a NON-NORMATIVE host
6233
+ extension (not advertised in /.well-known/openwop); if the host does not expose the surface
6234
+ the command fails closed legibly (exit 2) rather than guessing.
6235
+
6236
+ Action kinds : ${ACTION_KINDS.join(", ")}
6237
+ Policy values: ${POLICY_VALUES.join(" | ")} (unset kinds fall back to the host's declared default)
6238
+
6239
+ --provider-allowlist L Comma-separated provider ids the tenant may connect/resolve.
6240
+ Empty string restricts ALL providers; omit to leave unchanged.
6241
+ --action <kind=policy> Set one action kind's policy (repeatable).
6242
+ --retention-graph-days N Assistant-graph retention window (days).
6243
+ --retention-source-days N Source-derived retention window (days).
6244
+ --prefix P (audit) action-id prefix filter (host default: assistant.).
6245
+ --limit N (audit) max rows (host default: 100).
6246
+ --since ISO (audit) only rows at/after this ISO timestamp.
6247
+
6248
+ Exit codes: 0 ok \xB7 2 usage error / surface not advertised / not authorized.
6249
+
6250
+ Examples:
6251
+ openwop governance policy
6252
+ openwop governance policy get --json
6253
+ openwop governance policy set --action email.send=approval-required --action nudge=disabled
6254
+ openwop governance policy set --provider-allowlist anthropic,openai --retention-graph-days 90
6255
+ openwop governance audit --prefix governance. --limit 20 --json
6256
+ `;
6257
+ async function govRequest(ctx, path, options) {
6258
+ try {
6259
+ return await requestJson(ctx, path, options);
6260
+ } catch (err) {
6261
+ if (err instanceof HttpError && err.status === 404) {
6262
+ throw new CliError(
6263
+ "Host does not advertise governance administration (ADR 0028 host extension). Surface unavailable \u2014 failing closed.",
6264
+ 2
6265
+ );
6266
+ }
6267
+ throw err;
6268
+ }
6269
+ }
6270
+ async function runGovernance(ctx, argv) {
6271
+ const sub = argv[0] ?? "policy";
6272
+ if (sub === "--help" || sub === "-h") {
6273
+ write(ctx.io.stdout, GOVERNANCE_HELP);
6274
+ return 0;
6275
+ }
6276
+ switch (sub) {
6277
+ case "policy":
6278
+ return await runGovernancePolicy(ctx, argv.slice(1));
6279
+ case "audit":
6280
+ return await runGovernanceAudit(ctx, argv.slice(1));
6281
+ // `get`/`set` at the top level are policy ops — lets the `policy` group alias
6282
+ // read naturally (`openwop policy set ...`) without a redundant `policy policy`.
6283
+ case "get":
6284
+ case "set":
6285
+ return await runGovernancePolicy(ctx, argv);
6286
+ default:
6287
+ throw new CliError(`Unknown governance command: ${sub}
6288
+ Run \`openwop governance --help\` for usage.`);
6289
+ }
6290
+ }
6291
+ async function runGovernancePolicy(ctx, argv) {
6292
+ const action = argv[0] === "set" ? "set" : "get";
6293
+ const rest = argv[0] === "get" || argv[0] === "set" ? argv.slice(1) : argv;
6294
+ return action === "set" ? await runPolicySet(ctx, rest) : await runPolicyGet(ctx, rest);
6295
+ }
6296
+ async function runPolicyGet(ctx, argv) {
6297
+ const { options } = parseOptions(argv, { bool: ["--help"] });
6298
+ if (options.help) {
6299
+ write(ctx.io.stdout, GOVERNANCE_HELP);
6300
+ return 0;
6301
+ }
6302
+ const res = await govRequest(ctx, `${GOV_BASE}/policy`);
6303
+ if (ctx.json) {
6304
+ writeJson(ctx.io.stdout, res.body);
6305
+ return 0;
6306
+ }
6307
+ const body = res.body ?? {};
6308
+ const policy = body.policy ?? {};
6309
+ const kinds = Array.isArray(body.actionKinds) ? body.actionKinds : [...ACTION_KINDS];
6310
+ const defaultPolicy = body.defaults?.actionPolicy;
6311
+ const stored = policy.actionPolicy ?? {};
6312
+ writeLine(ctx.io.stdout, `Governance policy \u2014 tenant ${policy.tenantId ?? "(default)"}`);
6313
+ writeLine(ctx.io.stdout, `Action policy (unset kinds default to '${defaultPolicy ?? "(host default)"}' \u2014 host-resolved):`);
6314
+ const rows = kinds.map((k) => ({
6315
+ kind: k,
6316
+ policy: stored[k] ?? `(default \u2192 ${defaultPolicy ?? "?"})`
6317
+ }));
6318
+ writeLine(ctx.io.stdout, formatTable(rows, ["kind", "policy"]));
6319
+ const allowlist = policy.providerAllowlist;
6320
+ const allowlistText = Array.isArray(allowlist) ? allowlist.length ? allowlist.join(", ") : "(empty \u2014 all providers blocked)" : "(unset \u2014 all providers allowed)";
6321
+ writeLine(ctx.io.stdout, `Provider allowlist: ${allowlistText}`);
6322
+ const retention = policy.retention;
6323
+ if (retention && (retention.assistantGraphDays !== void 0 || retention.sourceDerivedDays !== void 0)) {
6324
+ writeLine(ctx.io.stdout, `Retention: assistantGraphDays=${retention.assistantGraphDays ?? "\u2014"} sourceDerivedDays=${retention.sourceDerivedDays ?? "\u2014"}`);
6325
+ } else {
6326
+ writeLine(ctx.io.stdout, "Retention: (unset)");
6327
+ }
6328
+ if (policy.updatedAt) writeLine(ctx.io.stdout, `Updated: ${policy.updatedAt}${policy.updatedByUserId ? ` by ${policy.updatedByUserId}` : ""}`);
6329
+ return 0;
6330
+ }
6331
+ async function runPolicySet(ctx, argv) {
6332
+ const { options } = parseOptions(argv, {
6333
+ bool: ["--help"],
6334
+ value: ["--provider-allowlist", "--retention-graph-days", "--retention-source-days"],
6335
+ multi: ["--action"]
6336
+ });
6337
+ if (options.help) {
6338
+ write(ctx.io.stdout, GOVERNANCE_HELP);
6339
+ return 0;
6340
+ }
6341
+ const body = {};
6342
+ if (options.providerAllowlist !== void 0) {
6343
+ body.providerAllowlist = String(options.providerAllowlist).split(",").map((p) => p.trim()).filter((p) => p.length > 0);
6344
+ }
6345
+ if (Array.isArray(options.action) && options.action.length) {
6346
+ const actionPolicy = {};
6347
+ for (const entry of options.action) {
6348
+ const eq = String(entry).indexOf("=");
6349
+ if (eq < 0) throw new CliError(`--action must be <kind=policy>, got '${entry}'`, 2);
6350
+ const kind = entry.slice(0, eq).trim();
6351
+ const policy2 = entry.slice(eq + 1).trim();
6352
+ if (!ACTION_KINDS.includes(kind)) {
6353
+ throw new CliError(`Unknown action kind '${kind}'. Known: ${ACTION_KINDS.join(", ")}`, 2);
6354
+ }
6355
+ if (!POLICY_VALUES.includes(policy2)) {
6356
+ throw new CliError(`Policy for '${kind}' must be one of ${POLICY_VALUES.join(" | ")}`, 2);
6357
+ }
6358
+ actionPolicy[kind] = policy2;
6359
+ }
6360
+ body.actionPolicy = actionPolicy;
6361
+ }
6362
+ const retention = {};
6363
+ for (const [flag, key] of [
6364
+ ["retentionGraphDays", "assistantGraphDays"],
6365
+ ["retentionSourceDays", "sourceDerivedDays"]
6366
+ ]) {
6367
+ if (options[flag] !== void 0) {
6368
+ const n = Number(options[flag]);
6369
+ if (!Number.isFinite(n) || n < 0) throw new CliError(`--${flag.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)} must be a non-negative number`, 2);
6370
+ retention[key] = n;
6371
+ }
6372
+ }
6373
+ if (Object.keys(retention).length) body.retention = retention;
6374
+ if (Object.keys(body).length === 0) {
6375
+ throw new CliError("Nothing to set \u2014 pass --provider-allowlist, --action, and/or --retention-*.", 2);
6376
+ }
6377
+ const res = await govRequest(ctx, `${GOV_BASE}/policy`, { method: "PUT", body });
6378
+ if (ctx.json) {
6379
+ writeJson(ctx.io.stdout, res.body);
6380
+ return 0;
6381
+ }
6382
+ const policy = res.body?.policy ?? {};
6383
+ writeLine(ctx.io.stdout, `Updated governance policy for tenant ${policy.tenantId ?? "(default)"}.`);
6384
+ if (policy.actionPolicy) {
6385
+ for (const [k, v] of Object.entries(policy.actionPolicy)) writeLine(ctx.io.stdout, ` ${k} \u2192 ${v}`);
6386
+ }
6387
+ if (Array.isArray(policy.providerAllowlist)) {
6388
+ writeLine(ctx.io.stdout, ` providerAllowlist: ${policy.providerAllowlist.length ? policy.providerAllowlist.join(", ") : "(empty \u2014 all blocked)"}`);
6389
+ }
6390
+ return 0;
6391
+ }
6392
+ async function runGovernanceAudit(ctx, argv) {
6393
+ const { options } = parseOptions(argv, {
6394
+ bool: ["--help"],
6395
+ value: ["--prefix", "--limit", "--since"]
6396
+ });
6397
+ if (options.help) {
6398
+ write(ctx.io.stdout, GOVERNANCE_HELP);
6399
+ return 0;
6400
+ }
6401
+ const qs = new URLSearchParams();
6402
+ if (options.prefix !== void 0) qs.set("actionPrefix", String(options.prefix));
6403
+ if (options.limit !== void 0) {
6404
+ const n = Number(options.limit);
6405
+ if (!Number.isFinite(n) || n < 0) throw new CliError("--limit must be a non-negative number", 2);
6406
+ qs.set("limit", String(n));
6407
+ }
6408
+ if (options.since !== void 0) qs.set("since", String(options.since));
6409
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
6410
+ const res = await govRequest(ctx, `${GOV_BASE}/audit${suffix}`);
6411
+ if (ctx.json) {
6412
+ writeJson(ctx.io.stdout, res.body);
6413
+ return 0;
6414
+ }
6415
+ const items = Array.isArray(res.body?.items) ? res.body.items : [];
6416
+ if (items.length === 0) {
6417
+ writeLine(ctx.io.stdout, "No audit rows match (tenant-scoped; rows without a tenant stamp are withheld).");
6418
+ return 0;
6419
+ }
6420
+ const rows = items.map((r) => ({
6421
+ timestamp: r.timestamp ?? "",
6422
+ principal: r.principalId ?? "",
6423
+ action: r.action ?? "",
6424
+ resource: r.resource ?? "",
6425
+ outcome: r.outcome ?? ""
6426
+ }));
6427
+ writeLine(ctx.io.stdout, formatTable(rows, ["timestamp", "principal", "action", "resource", "outcome"]));
6428
+ return 0;
6429
+ }
6430
+
6182
6431
  // src/cli/demo.ts
6183
6432
  import { spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
6184
6433
  import { existsSync as existsSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync8, createReadStream, openSync as openSync2, statSync as statSync2, mkdirSync as mkdirSync6, rmSync as rmSync4, chmodSync as chmodSync4, watch } from "node:fs";
@@ -6459,7 +6708,7 @@ async function runDemoStart(ctx, argv) {
6459
6708
  };
6460
6709
  process.once("SIGINT", stop);
6461
6710
  process.once("SIGTERM", stop);
6462
- const exitCode = await new Promise((resolve) => {
6711
+ const exitCode = await new Promise((resolve2) => {
6463
6712
  let settled = false;
6464
6713
  for (const { label, child } of children) {
6465
6714
  child.on("exit", (code, signal) => {
@@ -6468,7 +6717,7 @@ async function runDemoStart(ctx, argv) {
6468
6717
  if (signal) writeLine(ctx.io.stderr, `${label} stopped by ${signal}`);
6469
6718
  else writeLine(ctx.io.stderr, `${label} exited with ${code ?? 1}`);
6470
6719
  stop();
6471
- resolve(code ?? 1);
6720
+ resolve2(code ?? 1);
6472
6721
  });
6473
6722
  }
6474
6723
  });
@@ -6555,7 +6804,7 @@ async function runDemoLogs(ctx, argv) {
6555
6804
  if (tail.length && !tail[tail.length - 1].endsWith("\n")) writeLine(ctx.io.stdout, "");
6556
6805
  if (!options.follow) return 0;
6557
6806
  let offset = Buffer.byteLength(existing, "utf8");
6558
- return await new Promise((resolve) => {
6807
+ return await new Promise((resolve2) => {
6559
6808
  const emit = () => {
6560
6809
  let size;
6561
6810
  try {
@@ -6574,7 +6823,7 @@ async function runDemoLogs(ctx, argv) {
6574
6823
  const watcher = watch(logPath, { persistent: true }, emit);
6575
6824
  const finish = () => {
6576
6825
  watcher.close();
6577
- resolve(0);
6826
+ resolve2(0);
6578
6827
  };
6579
6828
  process.once("SIGINT", finish);
6580
6829
  process.once("SIGTERM", finish);
@@ -6656,6 +6905,3635 @@ async function runDemoInstall(ctx, argv) {
6656
6905
  return 0;
6657
6906
  }
6658
6907
 
6908
+ // src/cli/approvals.ts
6909
+ var APPROVALS_HELP = `Usage:
6910
+ openwop approvals list [--status pending|approved|rejected] [--json]
6911
+ openwop approvals get <approvalId> [--json]
6912
+ openwop approvals claim <approvalId> [--note <text>] [--json]
6913
+ openwop approvals reject <approvalId> [--note <text>] [--json]
6914
+
6915
+ The approval inbox \u2014 the human side of "agents propose, humans dispose". A
6916
+ review-mode roster member's heartbeat queues a proposal instead of starting the
6917
+ run; these commands let a human resolve it. A CLAIM is the affirmative act: it
6918
+ starts the proposed run (replay/fork/observability inherited). A REJECT dismisses
6919
+ the proposal so the heartbeat won't re-propose it.
6920
+
6921
+ The host is the authority for every decision \u2014 the CLI renders the host's
6922
+ resolved queue and relays your claim/reject; it never decides locally. If the
6923
+ host does not advertise the approval surface (/.well-known/openwop), the group
6924
+ fails closed rather than guessing.
6925
+
6926
+ Endpoints:
6927
+ list GET /v1/host/sample/approvals[?status=...]
6928
+ get GET /v1/host/sample/approvals (filtered to <approvalId> client-side;
6929
+ the host exposes no single-approval GET)
6930
+ claim POST /v1/host/sample/approvals/{id}/claim
6931
+ reject POST /v1/host/sample/approvals/{id}/reject
6932
+
6933
+ --status <s> (list) Filter the queue: pending | approved | rejected (default: all).
6934
+ --note <text> (claim/reject) Optional human note recorded with the decision.
6935
+ --json Print the raw host response instead of the rendered view.
6936
+
6937
+ Boundary: this is NOT \`interrupts\`. Interrupts are run-level HITL tokens you
6938
+ resolve to resume a paused run; approvals are the policy-gated proposal queue.
6939
+
6940
+ Exit codes (get/claim/reject reflect the approval's resulting status, so scripts
6941
+ can gate on the verdict):
6942
+ 0 approved 3 pending 1 rejected (denied) or error
6943
+ \`list\` exits 0 on success.
6944
+
6945
+ Examples:
6946
+ openwop approvals list --status pending
6947
+ openwop approvals get appr_123 --json
6948
+ openwop approvals claim appr_123 --note "LGTM, ship it"
6949
+ openwop approvals reject appr_123 --note "out of policy"
6950
+ `;
6951
+ var SUBCOMMANDS = ["list", "get", "claim", "reject", "approve", "deny"];
6952
+ async function runApprovals(ctx, argv) {
6953
+ const sub = argv[0] ?? "list";
6954
+ if (sub === "--help" || sub === "-h") {
6955
+ write(ctx.io.stdout, APPROVALS_HELP);
6956
+ return 0;
6957
+ }
6958
+ const args = argv.slice(SUBCOMMANDS.includes(sub) ? 1 : 0);
6959
+ switch (sub) {
6960
+ case "list":
6961
+ return await runApprovalsList(ctx, args);
6962
+ case "get":
6963
+ return await runApprovalsGet(ctx, args);
6964
+ // `claim`/`reject` are the host's wire verbs; `approve`/`deny` are friendly aliases.
6965
+ case "claim":
6966
+ case "approve":
6967
+ return await runApprovalsResolve(ctx, args, "claim");
6968
+ case "reject":
6969
+ case "deny":
6970
+ return await runApprovalsResolve(ctx, args, "reject");
6971
+ default:
6972
+ throw new CliError(`Unknown approvals command: ${sub}
6973
+ Run \`openwop approvals --help\` for usage.`);
6974
+ }
6975
+ }
6976
+ function exitForStatus(status) {
6977
+ if (status === "approved") return 0;
6978
+ if (status === "pending") return 3;
6979
+ return 1;
6980
+ }
6981
+ async function ensureApprovalsAdvertised(ctx) {
6982
+ const wk = await safeRequest(ctx, "/.well-known/openwop", { auth: false });
6983
+ if (!wk.ok) return;
6984
+ const paths = wk.body && typeof wk.body === "object" ? wk.body.paths : void 0;
6985
+ const advertised = paths !== null && typeof paths === "object" && Object.keys(paths).some((p) => p.startsWith("/v1/host/sample/approvals"));
6986
+ if (!advertised) {
6987
+ throw new CliError(
6988
+ "approvals: this host does not advertise the approval-inbox surface (/v1/host/sample/approvals is absent from /.well-known/openwop). The host is the authority \u2014 refusing to guess.",
6989
+ 1
6990
+ );
6991
+ }
6992
+ }
6993
+ function gate404(err) {
6994
+ if (err instanceof HttpError && err.status === 404) {
6995
+ const detail = err.body && typeof err.body === "object" && typeof err.body.message === "string" ? err.body.message : "not found";
6996
+ throw new CliError(
6997
+ `approvals: ${detail} (the host must mount /v1/host/sample/approvals; the CLI renders the host's queue, it never decides).`,
6998
+ 1
6999
+ );
7000
+ }
7001
+ throw err;
7002
+ }
7003
+ async function fetchQueue(ctx, status) {
7004
+ const path = status ? `/v1/host/sample/approvals?status=${encodeURIComponent(status)}` : "/v1/host/sample/approvals";
7005
+ let res;
7006
+ try {
7007
+ res = await requestJson(ctx, path);
7008
+ } catch (err) {
7009
+ gate404(err);
7010
+ }
7011
+ return Array.isArray(res.body?.items) ? res.body.items : [];
7012
+ }
7013
+ async function runApprovalsList(ctx, argv) {
7014
+ const { options } = parseOptions(argv, { bool: ["--help"], value: ["--status"] });
7015
+ if (options.help) {
7016
+ write(ctx.io.stdout, APPROVALS_HELP);
7017
+ return 0;
7018
+ }
7019
+ if (options.status !== void 0 && !["pending", "approved", "rejected"].includes(options.status)) {
7020
+ throw new CliError("--status must be one of: pending, approved, rejected", 2);
7021
+ }
7022
+ await ensureApprovalsAdvertised(ctx);
7023
+ const items = await fetchQueue(ctx, options.status);
7024
+ if (ctx.json) {
7025
+ writeJson(ctx.io.stdout, { items });
7026
+ return 0;
7027
+ }
7028
+ if (items.length === 0) {
7029
+ writeLine(ctx.io.stdout, `No approvals${options.status ? ` with status ${options.status}` : ""} on this host.`);
7030
+ return 0;
7031
+ }
7032
+ const rows = items.map((a) => ({
7033
+ approvalId: a.approvalId,
7034
+ status: a.status,
7035
+ kind: a.kind ?? (a.actionId ? "assistant-action" : "run-proposal"),
7036
+ persona: a.persona ?? "",
7037
+ proposal: truncate2(a.cardTitle ?? a.proposal ?? "", 40),
7038
+ createdAt: a.createdAt ?? ""
7039
+ }));
7040
+ writeLine(ctx.io.stdout, formatTable(rows, ["approvalId", "status", "kind", "persona", "proposal", "createdAt"]));
7041
+ return 0;
7042
+ }
7043
+ async function runApprovalsGet(ctx, argv) {
7044
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
7045
+ if (options.help || positionals.length !== 1) {
7046
+ write(ctx.io.stdout, "Usage: openwop approvals get <approvalId> [--json]\n");
7047
+ return options.help ? 0 : 2;
7048
+ }
7049
+ await ensureApprovalsAdvertised(ctx);
7050
+ const items = await fetchQueue(ctx);
7051
+ const approval = items.find((a) => a.approvalId === positionals[0]);
7052
+ if (!approval) {
7053
+ throw new CliError(`approvals: no approval found with id ${positionals[0]}.`, 1);
7054
+ }
7055
+ if (ctx.json) {
7056
+ writeJson(ctx.io.stdout, approval);
7057
+ return exitForStatus(approval.status);
7058
+ }
7059
+ writeLine(ctx.io.stdout, `approvalId: ${approval.approvalId}`);
7060
+ writeLine(ctx.io.stdout, `status: ${approval.status}`);
7061
+ writeLine(ctx.io.stdout, `kind: ${approval.kind ?? (approval.actionId ? "assistant-action" : "run-proposal")}`);
7062
+ writeLine(ctx.io.stdout, `persona: ${approval.persona ?? ""}`);
7063
+ if (approval.workflowId) writeLine(ctx.io.stdout, `workflowId: ${approval.workflowId}`);
7064
+ if (approval.cardTitle) writeLine(ctx.io.stdout, `cardTitle: ${approval.cardTitle}`);
7065
+ if (approval.proposal) writeLine(ctx.io.stdout, `proposal: ${approval.proposal}`);
7066
+ writeLine(ctx.io.stdout, `createdAt: ${approval.createdAt ?? ""}`);
7067
+ if (approval.resolvedAt) writeLine(ctx.io.stdout, `resolvedAt: ${approval.resolvedAt}`);
7068
+ if (approval.runId) writeLine(ctx.io.stdout, `runId: ${approval.runId}`);
7069
+ if (approval.note) writeLine(ctx.io.stdout, `note: ${approval.note}`);
7070
+ return exitForStatus(approval.status);
7071
+ }
7072
+ async function runApprovalsResolve(ctx, argv, verb) {
7073
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"], value: ["--note"] });
7074
+ if (options.help || positionals.length !== 1) {
7075
+ write(ctx.io.stdout, `Usage: openwop approvals ${verb} <approvalId> [--note <text>] [--json]
7076
+ `);
7077
+ return options.help ? 0 : 2;
7078
+ }
7079
+ await ensureApprovalsAdvertised(ctx);
7080
+ const path = `/v1/host/sample/approvals/${encodeURIComponent(positionals[0])}/${verb}`;
7081
+ const body = options.note !== void 0 ? { note: options.note } : void 0;
7082
+ let res;
7083
+ try {
7084
+ res = await requestJson(ctx, path, { method: "POST", ...body !== void 0 ? { body } : {} });
7085
+ } catch (err) {
7086
+ if (err instanceof HttpError && err.status === 409) {
7087
+ const status = err.body?.status;
7088
+ throw new CliError(`approvals: ${positionals[0]} is already ${status ?? "resolved"} \u2014 the host rejected a second decision.`, 1);
7089
+ }
7090
+ if (err instanceof HttpError && err.status === 422) {
7091
+ const detail = err.body?.message ?? "proposal can no longer be acted on";
7092
+ throw new CliError(`approvals: ${detail}`, 1);
7093
+ }
7094
+ gate404(err);
7095
+ }
7096
+ const out = res.body ?? {};
7097
+ if (ctx.json) {
7098
+ writeJson(ctx.io.stdout, out);
7099
+ return exitForStatus(out.status);
7100
+ }
7101
+ if (verb === "claim") {
7102
+ writeLine(ctx.io.stdout, `\u2713 Claimed approval ${out.approvalId ?? positionals[0]} \u2192 ${out.status ?? "approved"}${out.runId ? ` (run ${out.runId})` : ""}${out.actionId ? ` (action ${out.actionId})` : ""}`);
7103
+ } else {
7104
+ writeLine(ctx.io.stdout, `\u2713 Rejected approval ${out.approvalId ?? positionals[0]} \u2192 ${out.status ?? "rejected"}`);
7105
+ }
7106
+ return exitForStatus(out.status ?? (verb === "claim" ? "approved" : "rejected"));
7107
+ }
7108
+ function truncate2(s, n) {
7109
+ return s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
7110
+ }
7111
+
7112
+ // src/cli/consent.ts
7113
+ var CONSENT_HELP = `Usage:
7114
+ openwop consent policy <orgId> [--json]
7115
+ openwop consent set-policy <orgId> [--default-mode opt-in|opt-out] [--regulated-region <r>]... [--json]
7116
+ openwop consent records <orgId> [--json]
7117
+ openwop consent get <orgId> <subjectKey> [--json]
7118
+ openwop consent erase <orgId> <subjectKey> [--yes] [--json]
7119
+ openwop consent public get <orgId> <subjectKey> [--json]
7120
+ openwop consent public record <orgId> <subjectKey> [--category <name=bool>]... [--region <r>] [--json]
7121
+
7122
+ Tenant-scoped consent (ADR 0020). The host is the authority: consent is stored
7123
+ per-tenant and the host resolves a subject's effective categories. This command
7124
+ RENDERS the host's resolved view \u2014 it never computes or asserts a consent outcome
7125
+ locally. The org\u2192tenant mapping and the per-tenant 'consent' feature toggle are
7126
+ enforced host-side; when the toggle is off the host returns a uniform 404 and this
7127
+ command fails closed with a legible message (it does not assume permissive).
7128
+
7129
+ Authed, org-scoped (RBAC) \u2014 hits /v1/host/sample/consent/orgs/<orgId>/*:
7130
+ policy GET .../policy Show the tenant consent policy (defaultMode + regulated regions).
7131
+ set-policy PUT .../policy Update defaultMode and/or regulatedRegions (workspace:write).
7132
+ records GET .../records List all consent records for the tenant.
7133
+ get GET .../subjects/<k> Show one subject's consent record (or none).
7134
+ erase DEL .../subjects/<k> GDPR erasure \u2014 purge the record + fan out to feature erasers (idempotent).
7135
+
7136
+ Public, unauthed \u2014 hits /v1/host/sample/public-consent/<orgId>:
7137
+ public get GET .../<k> Read a visitor's recorded categories, or the policy default if none.
7138
+ public record POST .../ Record a visitor's category choices (source=public).
7139
+
7140
+ --default-mode <m> (set-policy) opt-in | opt-out.
7141
+ --regulated-region <r> (set-policy) A regulated region code (repeatable; replaces the list).
7142
+ --category <name=bool> (public record) A category choice, e.g. analytics=true (repeatable).
7143
+ --region <r> (public record) The visitor's region.
7144
+ --yes (erase) Confirm the destructive GDPR erasure.
7145
+
7146
+ Exit codes: 0 success \xB7 1 host/HTTP error (incl. consent not enabled) \xB7 2 usage error.
7147
+
7148
+ Examples:
7149
+ openwop consent policy org_123
7150
+ openwop consent set-policy org_123 --default-mode opt-out --regulated-region EU --regulated-region UK
7151
+ openwop consent records org_123 --json
7152
+ openwop consent get org_123 visitor-abc
7153
+ openwop consent erase org_123 visitor-abc --yes
7154
+ openwop consent public get org_123 visitor-abc
7155
+ openwop consent public record org_123 visitor-abc --category analytics=true --category marketing=false
7156
+ `;
7157
+ var ORG_BASE = "/v1/host/sample/consent/orgs";
7158
+ var PUB_BASE = "/v1/host/sample/public-consent";
7159
+ async function consentRequest(ctx, path, options) {
7160
+ try {
7161
+ return await requestJson(ctx, path, options);
7162
+ } catch (err) {
7163
+ if (err instanceof HttpError && err.status === 404) {
7164
+ throw new CliError(
7165
+ "consent unavailable: the host returned 404. Consent is gated on the org-tenant's `consent` feature toggle (uniform 404 when off) \u2014 verify the org exists and the host advertises/enables consent.",
7166
+ 1
7167
+ );
7168
+ }
7169
+ throw err;
7170
+ }
7171
+ }
7172
+ async function runConsent(ctx, argv) {
7173
+ const sub = argv[0] ?? "";
7174
+ if (sub === "--help" || sub === "-h" || sub === "") {
7175
+ write(ctx.io.stdout, CONSENT_HELP);
7176
+ return 0;
7177
+ }
7178
+ if (sub === "public") {
7179
+ const psub = argv[1] ?? "";
7180
+ const rest = argv.slice(2);
7181
+ switch (psub) {
7182
+ case "get":
7183
+ return await runPublicGet(ctx, rest);
7184
+ case "record":
7185
+ return await runPublicRecord(ctx, rest);
7186
+ default:
7187
+ throw new CliError(`Unknown consent public command: ${psub || "(none)"}
7188
+ Run \`openwop consent --help\` for usage.`);
7189
+ }
7190
+ }
7191
+ const args = argv.slice(1);
7192
+ switch (sub) {
7193
+ case "policy":
7194
+ return await runPolicyGet2(ctx, args);
7195
+ case "set-policy":
7196
+ return await runSetPolicy(ctx, args);
7197
+ case "records":
7198
+ return await runRecords(ctx, args);
7199
+ case "get":
7200
+ return await runGet(ctx, args);
7201
+ case "erase":
7202
+ return await runErase(ctx, args);
7203
+ default:
7204
+ throw new CliError(`Unknown consent command: ${sub}
7205
+ Run \`openwop consent --help\` for usage.`);
7206
+ }
7207
+ }
7208
+ function renderCategories(c) {
7209
+ if (!c || typeof c !== "object") return "(none)";
7210
+ return ["necessary", "analytics", "marketing"].map((k) => `${k}=${c[k] === true ? "yes" : "no"}`).join(" ");
7211
+ }
7212
+ async function runPolicyGet2(ctx, argv) {
7213
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
7214
+ if (options.help || positionals.length !== 1) {
7215
+ write(ctx.io.stdout, "Usage: openwop consent policy <orgId> [--json]\n");
7216
+ return options.help ? 0 : 2;
7217
+ }
7218
+ const res = await consentRequest(ctx, `${ORG_BASE}/${encodeURIComponent(positionals[0])}/policy`);
7219
+ if (ctx.json) {
7220
+ writeJson(ctx.io.stdout, res.body);
7221
+ return 0;
7222
+ }
7223
+ const p = res.body?.policy ?? {};
7224
+ writeLine(ctx.io.stdout, `tenantId: ${p.tenantId ?? ""}`);
7225
+ writeLine(ctx.io.stdout, `defaultMode: ${p.defaultMode ?? "opt-in"}`);
7226
+ writeLine(ctx.io.stdout, `regulatedRegions: ${Array.isArray(p.regulatedRegions) && p.regulatedRegions.length ? p.regulatedRegions.join(", ") : "(none)"}`);
7227
+ return 0;
7228
+ }
7229
+ async function runSetPolicy(ctx, argv) {
7230
+ const { options, positionals } = parseOptions(argv, {
7231
+ bool: ["--help"],
7232
+ value: ["--default-mode"],
7233
+ multi: ["--regulated-region"]
7234
+ });
7235
+ if (options.help || positionals.length !== 1) {
7236
+ write(ctx.io.stdout, "Usage: openwop consent set-policy <orgId> [--default-mode opt-in|opt-out] [--regulated-region <r>]... [--json]\n");
7237
+ return options.help ? 0 : 2;
7238
+ }
7239
+ const body = {};
7240
+ if (options.defaultMode !== void 0) {
7241
+ if (options.defaultMode !== "opt-in" && options.defaultMode !== "opt-out") {
7242
+ throw new CliError("--default-mode must be opt-in or opt-out", 2);
7243
+ }
7244
+ body.defaultMode = options.defaultMode;
7245
+ }
7246
+ if (Array.isArray(options.regulatedRegion)) body.regulatedRegions = options.regulatedRegion;
7247
+ if (Object.keys(body).length === 0) throw new CliError("Nothing to set \u2014 pass --default-mode and/or --regulated-region.", 2);
7248
+ const res = await consentRequest(ctx, `${ORG_BASE}/${encodeURIComponent(positionals[0])}/policy`, { method: "PUT", body });
7249
+ if (ctx.json) {
7250
+ writeJson(ctx.io.stdout, res.body);
7251
+ return 0;
7252
+ }
7253
+ const p = res.body?.policy ?? {};
7254
+ writeLine(ctx.io.stdout, `Updated consent policy: defaultMode=${p.defaultMode ?? ""} regulatedRegions=${Array.isArray(p.regulatedRegions) && p.regulatedRegions.length ? p.regulatedRegions.join(", ") : "(none)"}`);
7255
+ return 0;
7256
+ }
7257
+ async function runRecords(ctx, argv) {
7258
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
7259
+ if (options.help || positionals.length !== 1) {
7260
+ write(ctx.io.stdout, "Usage: openwop consent records <orgId> [--json]\n");
7261
+ return options.help ? 0 : 2;
7262
+ }
7263
+ const res = await consentRequest(ctx, `${ORG_BASE}/${encodeURIComponent(positionals[0])}/records`);
7264
+ if (ctx.json) {
7265
+ writeJson(ctx.io.stdout, res.body);
7266
+ return 0;
7267
+ }
7268
+ const records = Array.isArray(res.body?.records) ? res.body.records : [];
7269
+ if (records.length === 0) {
7270
+ writeLine(ctx.io.stdout, "No consent records for this tenant.");
7271
+ return 0;
7272
+ }
7273
+ const rows = records.map((r) => ({
7274
+ subjectKey: r.subjectKey,
7275
+ region: r.region ?? "",
7276
+ categories: renderCategories(r.categories),
7277
+ source: r.source ?? "",
7278
+ ts: r.ts ?? ""
7279
+ }));
7280
+ writeLine(ctx.io.stdout, formatTable(rows, ["subjectKey", "region", "categories", "source", "ts"]));
7281
+ return 0;
7282
+ }
7283
+ async function runGet(ctx, argv) {
7284
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
7285
+ if (options.help || positionals.length !== 2) {
7286
+ write(ctx.io.stdout, "Usage: openwop consent get <orgId> <subjectKey> [--json]\n");
7287
+ return options.help ? 0 : 2;
7288
+ }
7289
+ const [orgId, subjectKey] = positionals;
7290
+ const res = await consentRequest(ctx, `${ORG_BASE}/${encodeURIComponent(orgId)}/subjects/${encodeURIComponent(subjectKey)}`);
7291
+ if (ctx.json) {
7292
+ writeJson(ctx.io.stdout, res.body);
7293
+ return 0;
7294
+ }
7295
+ const r = res.body?.record;
7296
+ if (!r) {
7297
+ writeLine(ctx.io.stdout, `No consent record for subject ${subjectKey}.`);
7298
+ return 0;
7299
+ }
7300
+ writeLine(ctx.io.stdout, `subjectKey: ${r.subjectKey}`);
7301
+ if (r.region) writeLine(ctx.io.stdout, `region: ${r.region}`);
7302
+ writeLine(ctx.io.stdout, `categories: ${renderCategories(r.categories)}`);
7303
+ writeLine(ctx.io.stdout, `source: ${r.source ?? ""}`);
7304
+ writeLine(ctx.io.stdout, `ts: ${r.ts ?? ""}`);
7305
+ if (r.expiresAt) writeLine(ctx.io.stdout, `expiresAt: ${r.expiresAt}`);
7306
+ return 0;
7307
+ }
7308
+ async function runErase(ctx, argv) {
7309
+ const { options, positionals } = parseOptions(argv, { bool: ["--help", "--yes"] });
7310
+ if (options.help || positionals.length !== 2) {
7311
+ write(ctx.io.stdout, "Usage: openwop consent erase <orgId> <subjectKey> [--yes] [--json]\n");
7312
+ return options.help ? 0 : 2;
7313
+ }
7314
+ const [orgId, subjectKey] = positionals;
7315
+ if (!options.yes) {
7316
+ writeLine(ctx.io.stderr, `Refusing to erase subject ${subjectKey} (GDPR erasure is irreversible) without --yes.`);
7317
+ return 2;
7318
+ }
7319
+ const res = await consentRequest(ctx, `${ORG_BASE}/${encodeURIComponent(orgId)}/subjects/${encodeURIComponent(subjectKey)}`, { method: "DELETE" });
7320
+ if (ctx.json) {
7321
+ writeJson(ctx.io.stdout, res.body);
7322
+ return 0;
7323
+ }
7324
+ const purged = res.body?.consentRecord === true;
7325
+ writeLine(ctx.io.stdout, `Erased subject ${subjectKey} (consent record ${purged ? "purged" : "absent"}; downstream feature data erased).`);
7326
+ return 0;
7327
+ }
7328
+ async function runPublicGet(ctx, argv) {
7329
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
7330
+ if (options.help || positionals.length !== 2) {
7331
+ write(ctx.io.stdout, "Usage: openwop consent public get <orgId> <subjectKey> [--json]\n");
7332
+ return options.help ? 0 : 2;
7333
+ }
7334
+ const [orgId, subjectKey] = positionals;
7335
+ const res = await consentRequest(ctx, `${PUB_BASE}/${encodeURIComponent(orgId)}/${encodeURIComponent(subjectKey)}`, { auth: false });
7336
+ if (ctx.json) {
7337
+ writeJson(ctx.io.stdout, res.body);
7338
+ return 0;
7339
+ }
7340
+ const b = res.body ?? {};
7341
+ writeLine(ctx.io.stdout, `recorded: ${b.recorded === true ? "yes" : "no"}`);
7342
+ if (b.recorded !== true && b.defaultMode) writeLine(ctx.io.stdout, `defaultMode: ${b.defaultMode}`);
7343
+ writeLine(ctx.io.stdout, `categories: ${renderCategories(b.categories)}`);
7344
+ if (b.region) writeLine(ctx.io.stdout, `region: ${b.region}`);
7345
+ return 0;
7346
+ }
7347
+ async function runPublicRecord(ctx, argv) {
7348
+ const { options, positionals } = parseOptions(argv, {
7349
+ bool: ["--help"],
7350
+ value: ["--region"],
7351
+ multi: ["--category"]
7352
+ });
7353
+ if (options.help || positionals.length !== 2) {
7354
+ write(ctx.io.stdout, "Usage: openwop consent public record <orgId> <subjectKey> [--category <name=bool>]... [--region <r>] [--json]\n");
7355
+ return options.help ? 0 : 2;
7356
+ }
7357
+ const [orgId, subjectKey] = positionals;
7358
+ const categories = {};
7359
+ for (const entry of Array.isArray(options.category) ? options.category : []) {
7360
+ const eq = entry.indexOf("=");
7361
+ if (eq < 0) throw new CliError(`--category must be name=bool (got '${entry}')`, 2);
7362
+ const name = entry.slice(0, eq);
7363
+ const val = entry.slice(eq + 1).toLowerCase();
7364
+ if (val !== "true" && val !== "false") throw new CliError(`--category value must be true or false (got '${entry}')`, 2);
7365
+ categories[name] = val === "true";
7366
+ }
7367
+ const body = { subjectKey, categories };
7368
+ if (options.region !== void 0) body.region = options.region;
7369
+ const res = await consentRequest(ctx, `${PUB_BASE}/${encodeURIComponent(orgId)}`, { method: "POST", body, auth: false });
7370
+ if (ctx.json) {
7371
+ writeJson(ctx.io.stdout, res.body);
7372
+ return 0;
7373
+ }
7374
+ writeLine(ctx.io.stdout, `Recorded consent for ${subjectKey}: ${renderCategories(res.body?.categories)}`);
7375
+ return 0;
7376
+ }
7377
+
7378
+ // src/cli/mcp.ts
7379
+ var MCP_ENDPOINT = "/v1/host/sample/mcp";
7380
+ var rpcId = 0;
7381
+ var MCP_HELP = `Usage:
7382
+ openwop mcp ping
7383
+ openwop mcp info [--json]
7384
+ openwop mcp tools [list] [--json]
7385
+ openwop mcp tools call <name> [--args '{...}'] [--json]
7386
+ openwop mcp resources [list] [--json]
7387
+ openwop mcp resources templates [--json]
7388
+ openwop mcp resources read <uri> [--json]
7389
+ openwop mcp prompts [list] [--json]
7390
+ openwop mcp prompts get <name> [--args '{...}'] [--json]
7391
+
7392
+ MCP client for the host's JSON-RPC server mount (RFC 0020): a single JSON-RPC 2.0
7393
+ endpoint at POST ${MCP_ENDPOINT} speaking modelcontextprotocol.io 2025-06-18.
7394
+ This is a SAMPLE host extension (the /v1/host/sample/* prefix) \u2014 NOT part of the openwop
7395
+ wire contract and NOT advertised in /.well-known/openwop. The mount is HOST-CONTROLLED:
7396
+ env-gated (OPENWOP_MCP_SERVER_ENABLED) and OFF by default; the CLI cannot toggle it. When
7397
+ the mount is not exposed the endpoint 404s and these commands FAIL CLOSED legibly (exit 2)
7398
+ rather than guessing a surface.
7399
+
7400
+ info 'initialize' \u2014 server name/version, protocol version, advertised capabilities.
7401
+ ping 'ping' \u2014 liveness probe of the mount (exit 0 if reachable).
7402
+ tools list 'tools/list' \u2014 workflows exposed as MCP tools (name + inputSchema).
7403
+ tools call 'tools/call' \u2014 invoke one exposed tool; exits 1 if the tool result isError.
7404
+ resources list 'resources/list' \u2014 exposed resources (uri/name/mimeType).
7405
+ resources templates 'resources/templates/list' \u2014 exposed URI templates.
7406
+ resources read 'resources/read' \u2014 read one resource's contents by uri.
7407
+ prompts list 'prompts/list' \u2014 exposed prompt templates.
7408
+ prompts get 'prompts/get' \u2014 fetch one prompt's rendered messages.
7409
+
7410
+ --args J (tools call / prompts get) JSON object passed as the call arguments.
7411
+ --json Emit the raw JSON-RPC result for any read.
7412
+
7413
+ Exit codes: 0 ok \xB7 1 host/tool error \xB7 2 usage error / mount not available / invalid params.
7414
+
7415
+ Examples:
7416
+ openwop mcp ping
7417
+ openwop mcp info --json
7418
+ openwop mcp tools list
7419
+ openwop mcp tools call sample.demo.uppercase --args '{"text":"hi"}'
7420
+ openwop mcp resources read mcp://sample/readme
7421
+ openwop mcp prompts get greet --args '{"name":"Ada"}' --json
7422
+ `;
7423
+ function rpcExit(code) {
7424
+ return code === -32600 || code === -32601 || code === -32602 || code === -32700 ? 2 : 1;
7425
+ }
7426
+ async function mcpRpc(ctx, method, params) {
7427
+ const body = { jsonrpc: "2.0", id: ++rpcId, method };
7428
+ if (params !== void 0) body.params = params;
7429
+ let res;
7430
+ try {
7431
+ res = await requestJson(ctx, MCP_ENDPOINT, { method: "POST", body });
7432
+ } catch (err) {
7433
+ if (err instanceof HttpError && err.status === 404) {
7434
+ throw new CliError(
7435
+ `Host doesn't serve the MCP endpoint at ${MCP_ENDPOINT} (RFC 0020 sample mount, env-gated OPENWOP_MCP_SERVER_ENABLED \u2014 OFF by default). Failing closed.`,
7436
+ 2
7437
+ );
7438
+ }
7439
+ throw err;
7440
+ }
7441
+ const rpc = res.body ?? {};
7442
+ if (rpc && typeof rpc === "object" && rpc.error) {
7443
+ const { code, message } = rpc.error;
7444
+ throw new CliError(`MCP error ${code}: ${message ?? "unknown"}`, rpcExit(Number(code)));
7445
+ }
7446
+ return rpc.result ?? {};
7447
+ }
7448
+ async function runMcp(ctx, argv) {
7449
+ const sub = argv[0] ?? "info";
7450
+ if (sub === "--help" || sub === "-h") {
7451
+ write(ctx.io.stdout, MCP_HELP);
7452
+ return 0;
7453
+ }
7454
+ switch (sub) {
7455
+ case "ping":
7456
+ return await runPing(ctx, argv.slice(1));
7457
+ case "info":
7458
+ case "initialize":
7459
+ return await runInfo(ctx, argv.slice(1));
7460
+ case "tools":
7461
+ return await runTools(ctx, argv.slice(1));
7462
+ case "resources":
7463
+ return await runResources(ctx, argv.slice(1));
7464
+ case "prompts":
7465
+ return await runPrompts2(ctx, argv.slice(1));
7466
+ default:
7467
+ throw new CliError(`Unknown mcp command: ${sub}
7468
+ Run \`openwop mcp --help\` for usage.`);
7469
+ }
7470
+ }
7471
+ async function runPing(ctx, argv) {
7472
+ const { options } = parseOptions(argv, { bool: ["--help"] });
7473
+ if (options.help) {
7474
+ write(ctx.io.stdout, MCP_HELP);
7475
+ return 0;
7476
+ }
7477
+ await mcpRpc(ctx, "ping");
7478
+ if (ctx.json) {
7479
+ writeJson(ctx.io.stdout, { ok: true });
7480
+ return 0;
7481
+ }
7482
+ writeLine(ctx.io.stdout, "MCP mount reachable (ping ok).");
7483
+ return 0;
7484
+ }
7485
+ async function runInfo(ctx, argv) {
7486
+ const { options } = parseOptions(argv, { bool: ["--help"] });
7487
+ if (options.help) {
7488
+ write(ctx.io.stdout, MCP_HELP);
7489
+ return 0;
7490
+ }
7491
+ const result = await mcpRpc(ctx, "initialize", {
7492
+ protocolVersion: "2025-06-18",
7493
+ clientInfo: { name: "openwop-cli", version: VERSION },
7494
+ capabilities: {}
7495
+ });
7496
+ if (ctx.json) {
7497
+ writeJson(ctx.io.stdout, result);
7498
+ return 0;
7499
+ }
7500
+ writeLine(ctx.io.stdout, `server: ${result.serverInfo?.name ?? "(unknown)"} ${result.serverInfo?.version ?? ""}`.trimEnd());
7501
+ writeLine(ctx.io.stdout, `protocolVersion: ${result.protocolVersion ?? "(unknown)"}`);
7502
+ const caps = result.capabilities && typeof result.capabilities === "object" ? Object.keys(result.capabilities) : [];
7503
+ writeLine(ctx.io.stdout, `capabilities: ${caps.length ? caps.join(", ") : "(none)"}`);
7504
+ return 0;
7505
+ }
7506
+ async function runTools(ctx, argv) {
7507
+ const action = argv[0] === "call" ? "call" : "list";
7508
+ const rest = argv[0] === "list" || argv[0] === "call" ? argv.slice(1) : argv;
7509
+ if (action === "call") return await runToolsCall(ctx, rest);
7510
+ const { options } = parseOptions(rest, { bool: ["--help"] });
7511
+ if (options.help) {
7512
+ write(ctx.io.stdout, MCP_HELP);
7513
+ return 0;
7514
+ }
7515
+ const result = await mcpRpc(ctx, "tools/list");
7516
+ if (ctx.json) {
7517
+ writeJson(ctx.io.stdout, result);
7518
+ return 0;
7519
+ }
7520
+ const tools = Array.isArray(result.tools) ? result.tools : [];
7521
+ if (tools.length === 0) {
7522
+ writeLine(ctx.io.stdout, "No tools exposed by the MCP mount.");
7523
+ return 0;
7524
+ }
7525
+ const rows = tools.map((t) => ({ name: t.name, description: t.description ?? "" }));
7526
+ writeLine(ctx.io.stdout, formatTable(rows, ["name", "description"]));
7527
+ return 0;
7528
+ }
7529
+ async function runToolsCall(ctx, argv) {
7530
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"], value: ["--args"] });
7531
+ if (options.help || positionals.length !== 1) {
7532
+ write(ctx.io.stdout, "Usage: openwop mcp tools call <name> [--args '{...}'] [--json]\n");
7533
+ return options.help ? 0 : 2;
7534
+ }
7535
+ const params = { name: positionals[0] };
7536
+ if (options.args !== void 0) {
7537
+ try {
7538
+ params.arguments = JSON.parse(options.args);
7539
+ } catch {
7540
+ throw new CliError("--args must be valid JSON", 2);
7541
+ }
7542
+ }
7543
+ const result = await mcpRpc(ctx, "tools/call", params);
7544
+ if (ctx.json) {
7545
+ writeJson(ctx.io.stdout, result);
7546
+ return result.isError ? 1 : 0;
7547
+ }
7548
+ const content = Array.isArray(result.content) ? result.content : [];
7549
+ for (const c of content) {
7550
+ if (c && c.type === "text") writeLine(ctx.io.stdout, String(c.text ?? ""));
7551
+ }
7552
+ return result.isError ? 1 : 0;
7553
+ }
7554
+ async function runResources(ctx, argv) {
7555
+ const action = argv[0] === "templates" ? "templates" : argv[0] === "read" ? "read" : "list";
7556
+ const rest = ["list", "templates", "read"].includes(argv[0]) ? argv.slice(1) : argv;
7557
+ if (action === "read") return await runResourcesRead(ctx, rest);
7558
+ const { options } = parseOptions(rest, { bool: ["--help"] });
7559
+ if (options.help) {
7560
+ write(ctx.io.stdout, MCP_HELP);
7561
+ return 0;
7562
+ }
7563
+ const method = action === "templates" ? "resources/templates/list" : "resources/list";
7564
+ const result = await mcpRpc(ctx, method);
7565
+ if (ctx.json) {
7566
+ writeJson(ctx.io.stdout, result);
7567
+ return 0;
7568
+ }
7569
+ if (action === "templates") {
7570
+ const templates = Array.isArray(result.resourceTemplates) ? result.resourceTemplates : [];
7571
+ if (templates.length === 0) {
7572
+ writeLine(ctx.io.stdout, "No resource templates exposed by the MCP mount.");
7573
+ return 0;
7574
+ }
7575
+ const rows2 = templates.map((r) => ({ uriTemplate: r.uriTemplate, name: r.name ?? "", mimeType: r.mimeType ?? "" }));
7576
+ writeLine(ctx.io.stdout, formatTable(rows2, ["uriTemplate", "name", "mimeType"]));
7577
+ return 0;
7578
+ }
7579
+ const resources = Array.isArray(result.resources) ? result.resources : [];
7580
+ if (resources.length === 0) {
7581
+ writeLine(ctx.io.stdout, "No resources exposed by the MCP mount.");
7582
+ return 0;
7583
+ }
7584
+ const rows = resources.map((r) => ({ uri: r.uri, name: r.name ?? "", mimeType: r.mimeType ?? "" }));
7585
+ writeLine(ctx.io.stdout, formatTable(rows, ["uri", "name", "mimeType"]));
7586
+ return 0;
7587
+ }
7588
+ async function runResourcesRead(ctx, argv) {
7589
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
7590
+ if (options.help || positionals.length !== 1) {
7591
+ write(ctx.io.stdout, "Usage: openwop mcp resources read <uri> [--json]\n");
7592
+ return options.help ? 0 : 2;
7593
+ }
7594
+ const result = await mcpRpc(ctx, "resources/read", { uri: positionals[0] });
7595
+ if (ctx.json) {
7596
+ writeJson(ctx.io.stdout, result);
7597
+ return 0;
7598
+ }
7599
+ const contents = Array.isArray(result.contents) ? result.contents : [];
7600
+ for (const c of contents) {
7601
+ if (c && typeof c.text === "string") writeLine(ctx.io.stdout, c.text);
7602
+ }
7603
+ return 0;
7604
+ }
7605
+ async function runPrompts2(ctx, argv) {
7606
+ const action = argv[0] === "get" ? "get" : "list";
7607
+ const rest = argv[0] === "list" || argv[0] === "get" ? argv.slice(1) : argv;
7608
+ if (action === "get") return await runPromptsGet(ctx, rest);
7609
+ const { options } = parseOptions(rest, { bool: ["--help"] });
7610
+ if (options.help) {
7611
+ write(ctx.io.stdout, MCP_HELP);
7612
+ return 0;
7613
+ }
7614
+ const result = await mcpRpc(ctx, "prompts/list");
7615
+ if (ctx.json) {
7616
+ writeJson(ctx.io.stdout, result);
7617
+ return 0;
7618
+ }
7619
+ const prompts = Array.isArray(result.prompts) ? result.prompts : [];
7620
+ if (prompts.length === 0) {
7621
+ writeLine(ctx.io.stdout, "No prompts exposed by the MCP mount.");
7622
+ return 0;
7623
+ }
7624
+ const rows = prompts.map((p) => ({
7625
+ name: p.name,
7626
+ description: p.description ?? "",
7627
+ args: Array.isArray(p.arguments) ? String(p.arguments.length) : "0"
7628
+ }));
7629
+ writeLine(ctx.io.stdout, formatTable(rows, ["name", "description", "args"]));
7630
+ return 0;
7631
+ }
7632
+ async function runPromptsGet(ctx, argv) {
7633
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"], value: ["--args"] });
7634
+ if (options.help || positionals.length !== 1) {
7635
+ write(ctx.io.stdout, "Usage: openwop mcp prompts get <name> [--args '{...}'] [--json]\n");
7636
+ return options.help ? 0 : 2;
7637
+ }
7638
+ const params = { name: positionals[0] };
7639
+ if (options.args !== void 0) {
7640
+ try {
7641
+ params.arguments = JSON.parse(options.args);
7642
+ } catch {
7643
+ throw new CliError("--args must be valid JSON", 2);
7644
+ }
7645
+ }
7646
+ const result = await mcpRpc(ctx, "prompts/get", params);
7647
+ if (ctx.json) {
7648
+ writeJson(ctx.io.stdout, result);
7649
+ return 0;
7650
+ }
7651
+ if (result.description) writeLine(ctx.io.stdout, `description: ${result.description}`);
7652
+ const messages = Array.isArray(result.messages) ? result.messages : [];
7653
+ for (const m of messages) {
7654
+ const text = m?.content?.type === "text" ? m.content.text : typeof m?.content === "string" ? m.content : JSON.stringify(m?.content);
7655
+ writeLine(ctx.io.stdout, `[${m?.role ?? "?"}] ${text ?? ""}`);
7656
+ }
7657
+ return 0;
7658
+ }
7659
+
7660
+ // src/cli/connections.ts
7661
+ var CONNECTIONS_HELP = `Usage:
7662
+ openwop connections list [--json]
7663
+ openwop connections get <connectionId> [--json]
7664
+ openwop connections test <connectionId> [--json]
7665
+ openwop connections authorize <provider> [--scope <s>]... [--write] [--return-to <url>] [--json]
7666
+ openwop connections oauth-clients list [--json]
7667
+ openwop connections oauth-clients get <provider> [--json]
7668
+
7669
+ Inspect the host's third-party connections and their OAuth client configuration
7670
+ (ADR 0024). \`list\`/\`get\` render connection metadata + status; \`test\` health-probes
7671
+ one credential (the host refreshes OAuth on the way); \`authorize\` mints a consent
7672
+ URL for a provider \u2014 it returns the URL only, the CLI never completes the OAuth
7673
+ exchange. \`oauth-clients\` shows the host-configured OAuth apps (superadmin).
7674
+
7675
+ SECRET BOUNDARY: client secrets and tokens are host-side and are NEVER returned,
7676
+ printed, or logged \u2014 only refs and status. The CLI additionally redacts any
7677
+ secret-looking field defensively before printing.
7678
+
7679
+ The host is the authority \u2014 the CLI renders its resolved view and fails closed if
7680
+ the connections surface isn't advertised (/.well-known/openwop).
7681
+
7682
+ Endpoints:
7683
+ list GET /v1/host/sample/connections
7684
+ get GET /v1/host/sample/connections (filtered to <id> client-side;
7685
+ the host exposes no single-connection GET)
7686
+ test POST /v1/host/sample/connections/{id}/test
7687
+ authorize POST /v1/host/sample/connections/{provider}/authorize
7688
+ oauth-clients GET /v1/host/sample/connections-oauth-clients[/{provider} filtered]
7689
+
7690
+ --scope <s> (authorize) An OAuth scope to request (repeatable).
7691
+ --write (authorize) Request write scopes (re-consent for write access).
7692
+ --return-to <u> (authorize) Where the host should bounce the browser back to.
7693
+ --json Print the (redacted) raw host response instead of the table.
7694
+
7695
+ Exit codes:
7696
+ list/get/authorize/oauth-clients 0 ok \xB7 2 usage \xB7 1 not-found/error
7697
+ test 0 connection healthy \xB7 1 unhealthy/error
7698
+
7699
+ Examples:
7700
+ openwop connections list
7701
+ openwop connections get conn:abc --json
7702
+ openwop connections test conn:abc
7703
+ openwop connections authorize google --scope https://www.googleapis.com/auth/calendar --write
7704
+ openwop connections oauth-clients list
7705
+ `;
7706
+ var SECRET_KEY = /secret|token|password|private[-_]?key|client[-_]?secret|api[-_]?key|credential/i;
7707
+ function redact(value) {
7708
+ if (Array.isArray(value)) return value.map(redact);
7709
+ if (value && typeof value === "object") {
7710
+ const out = {};
7711
+ for (const [k, v] of Object.entries(value)) {
7712
+ out[k] = SECRET_KEY.test(k) ? "[redacted]" : redact(v);
7713
+ }
7714
+ return out;
7715
+ }
7716
+ return value;
7717
+ }
7718
+ var TOP = ["list", "get", "test", "authorize", "oauth-clients", "oauth-client"];
7719
+ async function runConnections(ctx, argv) {
7720
+ const sub = argv[0] ?? "list";
7721
+ if (sub === "--help" || sub === "-h") {
7722
+ write(ctx.io.stdout, CONNECTIONS_HELP);
7723
+ return 0;
7724
+ }
7725
+ const args = argv.slice(TOP.includes(sub) ? 1 : 0);
7726
+ switch (sub) {
7727
+ case "list":
7728
+ return await runList(ctx, args);
7729
+ case "get":
7730
+ return await runGet2(ctx, args);
7731
+ case "test":
7732
+ return await runTest(ctx, args);
7733
+ case "authorize":
7734
+ return await runAuthorize(ctx, args);
7735
+ case "oauth-clients":
7736
+ case "oauth-client":
7737
+ return await runOAuthClients(ctx, args);
7738
+ default:
7739
+ throw new CliError(`Unknown connections command: ${sub}
7740
+ Run \`openwop connections --help\` for usage.`);
7741
+ }
7742
+ }
7743
+ async function ensureAdvertised(ctx) {
7744
+ const wk = await safeRequest(ctx, "/.well-known/openwop", { auth: false });
7745
+ if (!wk.ok) return;
7746
+ const paths = wk.body && typeof wk.body === "object" ? wk.body.paths : void 0;
7747
+ const advertised = paths !== null && typeof paths === "object" && Object.keys(paths).some((p) => p.startsWith("/v1/host/sample/connections"));
7748
+ if (!advertised) {
7749
+ throw new CliError(
7750
+ "connections: this host does not advertise the connections surface (/v1/host/sample/connections is absent from /.well-known/openwop). The host is the authority \u2014 refusing to guess.",
7751
+ 1
7752
+ );
7753
+ }
7754
+ }
7755
+ function gate4042(err) {
7756
+ if (err instanceof HttpError && err.status === 404) {
7757
+ const detail = err.body && typeof err.body === "object" && typeof err.body.message === "string" ? err.body.message : "not found";
7758
+ throw new CliError(
7759
+ `connections: ${detail} (the host must mount /v1/host/sample/connections; the CLI renders the host's view, it never holds credentials).`,
7760
+ 1
7761
+ );
7762
+ }
7763
+ throw err;
7764
+ }
7765
+ function scopeAxis(c) {
7766
+ if (c.orgId) return "org";
7767
+ if (c.userId) return "user";
7768
+ return "workspace";
7769
+ }
7770
+ async function fetchConnections(ctx) {
7771
+ let res;
7772
+ try {
7773
+ res = await requestJson(ctx, "/v1/host/sample/connections");
7774
+ } catch (err) {
7775
+ gate4042(err);
7776
+ }
7777
+ return Array.isArray(res.body?.connections) ? res.body.connections : [];
7778
+ }
7779
+ async function runList(ctx, argv) {
7780
+ const { options } = parseOptions(argv, { bool: ["--help"] });
7781
+ if (options.help) {
7782
+ write(ctx.io.stdout, CONNECTIONS_HELP);
7783
+ return 0;
7784
+ }
7785
+ await ensureAdvertised(ctx);
7786
+ const connections = (await fetchConnections(ctx)).map(redact);
7787
+ if (ctx.json) {
7788
+ writeJson(ctx.io.stdout, { connections });
7789
+ return 0;
7790
+ }
7791
+ if (connections.length === 0) {
7792
+ writeLine(ctx.io.stdout, "No connections on this host.");
7793
+ return 0;
7794
+ }
7795
+ const rows = connections.map((c) => ({
7796
+ connectionId: c.connectionId,
7797
+ provider: c.provider,
7798
+ kind: c.kind,
7799
+ scope: scopeAxis(c),
7800
+ status: c.status,
7801
+ displayName: c.displayName ?? "",
7802
+ expiresAt: c.expiresAt ?? ""
7803
+ }));
7804
+ writeLine(ctx.io.stdout, formatTable(rows, ["connectionId", "provider", "kind", "scope", "status", "displayName", "expiresAt"]));
7805
+ return 0;
7806
+ }
7807
+ async function runGet2(ctx, argv) {
7808
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
7809
+ if (options.help || positionals.length !== 1) {
7810
+ write(ctx.io.stdout, "Usage: openwop connections get <connectionId> [--json]\n");
7811
+ return options.help ? 0 : 2;
7812
+ }
7813
+ await ensureAdvertised(ctx);
7814
+ const c = (await fetchConnections(ctx)).find((x) => x.connectionId === positionals[0]);
7815
+ if (!c) {
7816
+ throw new CliError(`connections: no connection found with id ${positionals[0]}.`, 1);
7817
+ }
7818
+ const safe = redact(c);
7819
+ if (ctx.json) {
7820
+ writeJson(ctx.io.stdout, safe);
7821
+ return 0;
7822
+ }
7823
+ writeLine(ctx.io.stdout, `connectionId: ${safe.connectionId}`);
7824
+ writeLine(ctx.io.stdout, `provider: ${safe.provider}`);
7825
+ writeLine(ctx.io.stdout, `kind: ${safe.kind}`);
7826
+ writeLine(ctx.io.stdout, `scope: ${scopeAxis(safe)}`);
7827
+ writeLine(ctx.io.stdout, `status: ${safe.status}`);
7828
+ if (safe.displayName) writeLine(ctx.io.stdout, `displayName: ${safe.displayName}`);
7829
+ if (Array.isArray(safe.scopes)) writeLine(ctx.io.stdout, `scopes: ${safe.scopes.length ? safe.scopes.join(", ") : "(none)"}`);
7830
+ if (safe.createdAt) writeLine(ctx.io.stdout, `createdAt: ${safe.createdAt}`);
7831
+ if (safe.expiresAt) writeLine(ctx.io.stdout, `expiresAt: ${safe.expiresAt}`);
7832
+ return 0;
7833
+ }
7834
+ async function runTest(ctx, argv) {
7835
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
7836
+ if (options.help || positionals.length !== 1) {
7837
+ write(ctx.io.stdout, "Usage: openwop connections test <connectionId> [--json]\n");
7838
+ return options.help ? 0 : 2;
7839
+ }
7840
+ await ensureAdvertised(ctx);
7841
+ let res;
7842
+ try {
7843
+ res = await requestJson(ctx, `/v1/host/sample/connections/${encodeURIComponent(positionals[0])}/test`, { method: "POST" });
7844
+ } catch (err) {
7845
+ gate4042(err);
7846
+ }
7847
+ const result = redact(res.body ?? {});
7848
+ if (ctx.json) {
7849
+ writeJson(ctx.io.stdout, result);
7850
+ return result.ok === true ? 0 : 1;
7851
+ }
7852
+ writeLine(ctx.io.stdout, `connection: ${positionals[0]}`);
7853
+ writeLine(ctx.io.stdout, `ok: ${result.ok === true ? "yes" : "no"}`);
7854
+ writeLine(ctx.io.stdout, `status: ${result.status ?? "unknown"}`);
7855
+ if (result.expiresAt) writeLine(ctx.io.stdout, `expiresAt: ${result.expiresAt}`);
7856
+ if (result.detail) writeLine(ctx.io.stdout, `detail: ${result.detail}`);
7857
+ return result.ok === true ? 0 : 1;
7858
+ }
7859
+ async function runAuthorize(ctx, argv) {
7860
+ const { options, positionals } = parseOptions(argv, {
7861
+ bool: ["--help", "--write"],
7862
+ value: ["--return-to"],
7863
+ multi: ["--scope"]
7864
+ });
7865
+ if (options.help || positionals.length !== 1) {
7866
+ write(ctx.io.stdout, "Usage: openwop connections authorize <provider> [--scope <s>]... [--write] [--return-to <url>] [--json]\n");
7867
+ return options.help ? 0 : 2;
7868
+ }
7869
+ await ensureAdvertised(ctx);
7870
+ const body = {};
7871
+ if (Array.isArray(options.scope) && options.scope.length) body.scopes = options.scope;
7872
+ if (options.write) body.write = true;
7873
+ if (options.returnTo !== void 0) body.returnTo = options.returnTo;
7874
+ let res;
7875
+ try {
7876
+ res = await requestJson(ctx, `/v1/host/sample/connections/${encodeURIComponent(positionals[0])}/authorize`, { method: "POST", body });
7877
+ } catch (err) {
7878
+ if (err instanceof HttpError && err.status === 409) {
7879
+ const detail = err.body?.message ?? `OAuth is not configured for '${positionals[0]}' on this host.`;
7880
+ throw new CliError(`connections: ${detail}`, 1);
7881
+ }
7882
+ gate4042(err);
7883
+ }
7884
+ const out = redact(res.body ?? {});
7885
+ if (ctx.json) {
7886
+ writeJson(ctx.io.stdout, out);
7887
+ return 0;
7888
+ }
7889
+ if (!out.authorizeUrl) {
7890
+ throw new CliError("connections: host did not return an authorizeUrl.", 1);
7891
+ }
7892
+ writeLine(ctx.io.stdout, out.authorizeUrl);
7893
+ return 0;
7894
+ }
7895
+ async function runOAuthClients(ctx, argv) {
7896
+ const action = argv[0] ?? "list";
7897
+ if (action === "--help" || action === "-h") {
7898
+ write(ctx.io.stdout, CONNECTIONS_HELP);
7899
+ return 0;
7900
+ }
7901
+ const rest = argv.slice(["list", "get"].includes(action) ? 1 : 0);
7902
+ if (action !== "list" && action !== "get") {
7903
+ throw new CliError(`Unknown connections oauth-clients command: ${action}
7904
+ Run \`openwop connections --help\` for usage.`);
7905
+ }
7906
+ const { options, positionals } = parseOptions(rest, { bool: ["--help"] });
7907
+ if (options.help) {
7908
+ write(ctx.io.stdout, CONNECTIONS_HELP);
7909
+ return 0;
7910
+ }
7911
+ await ensureAdvertised(ctx);
7912
+ let res;
7913
+ try {
7914
+ res = await requestJson(ctx, "/v1/host/sample/connections-oauth-clients");
7915
+ } catch (err) {
7916
+ gate4042(err);
7917
+ }
7918
+ const clients = (Array.isArray(res.body?.clients) ? res.body.clients : []).map(redact);
7919
+ if (action === "get") {
7920
+ if (positionals.length !== 1) {
7921
+ write(ctx.io.stdout, "Usage: openwop connections oauth-clients get <provider> [--json]\n");
7922
+ return 2;
7923
+ }
7924
+ const client = clients.find((c) => c.provider === positionals[0]);
7925
+ if (!client) {
7926
+ throw new CliError(`connections: no OAuth client configured for provider ${positionals[0]}.`, 1);
7927
+ }
7928
+ if (ctx.json) {
7929
+ writeJson(ctx.io.stdout, client);
7930
+ return 0;
7931
+ }
7932
+ writeLine(ctx.io.stdout, `provider: ${client.provider}`);
7933
+ writeLine(ctx.io.stdout, `clientId: ${client.clientId ?? ""}`);
7934
+ writeLine(ctx.io.stdout, `configured: ${client.configured === true ? "yes" : "no"}`);
7935
+ if (client.updatedAt) writeLine(ctx.io.stdout, `updatedAt: ${client.updatedAt}`);
7936
+ if (client.updatedBy) writeLine(ctx.io.stdout, `updatedBy: ${client.updatedBy}`);
7937
+ return 0;
7938
+ }
7939
+ if (ctx.json) {
7940
+ writeJson(ctx.io.stdout, { clients });
7941
+ return 0;
7942
+ }
7943
+ if (clients.length === 0) {
7944
+ writeLine(ctx.io.stdout, "No host OAuth clients configured.");
7945
+ return 0;
7946
+ }
7947
+ const rows = clients.map((c) => ({
7948
+ provider: c.provider,
7949
+ clientId: c.clientId ?? "",
7950
+ configured: c.configured === true ? "yes" : "no",
7951
+ updatedAt: c.updatedAt ?? "",
7952
+ updatedBy: c.updatedBy ?? ""
7953
+ }));
7954
+ writeLine(ctx.io.stdout, formatTable(rows, ["provider", "clientId", "configured", "updatedAt", "updatedBy"]));
7955
+ return 0;
7956
+ }
7957
+
7958
+ // src/cli/profiles.ts
7959
+ var PROFILES_HELP = `Usage:
7960
+ openwop profiles list [--json]
7961
+ openwop profiles get [<userId>] [--json] (omit userId for your own \u2014 same as 'me')
7962
+ openwop profiles me [--json]
7963
+ openwop profiles activity [--limit <n>] [--status <s>] [--json]
7964
+ openwop profiles edit [--job-title <t>] [--department <t>] [--bio <t>] [--location <t>]
7965
+ [--equipment <x>]... [--interests <x>]...
7966
+ [--availability-status available|busy|away] [--timezone <tz>] [--hours-per-week <n>] [--json]
7967
+ openwop profiles skills set --skill <name=proficiency>... [--json]
7968
+ openwop profiles portfolio add --token <mediaToken> [--json]
7969
+ openwop profiles portfolio remove <mediaToken> [--json]
7970
+ openwop profiles pin <rosterId> [--json]
7971
+ openwop profiles unpin <rosterId> [--json]
7972
+ openwop profiles endorse <userId> <skill> [--json]
7973
+ openwop profiles unendorse <userId> <skill> [--json]
7974
+
7975
+ Self-service PERSONA: your job title, bio, contact, skills, availability, portfolio,
7976
+ and pinned agents (ADR 0005). The host is the authority and renders the resolved view \u2014
7977
+ reads are visible to any signed-in tenant member; writes only ever touch YOUR OWN profile
7978
+ (authority is intrinsic to the caller's resolved identity). Tenant-scoped throughout; a
7979
+ foreign-tenant id reads as 404. Requires a durable signed-in account (bearer via --api-key).
7980
+
7981
+ BOUNDARY: \`profiles\` is the persona/skills surface, NOT the user directory (account
7982
+ lifecycle lives in the host's users surface) and NOT RBAC (that's \`orgs\`). \`profiles list\`
7983
+ is the persona directory of the tenant, not an account-management list.
7984
+
7985
+ --job-title/--department/--bio/--location (edit) Text fields; pass empty string to clear.
7986
+ --equipment/--interests <x> (edit) Repeatable; replaces the whole list.
7987
+ --availability-status / --timezone / --hours-per-week (edit) Availability sub-fields.
7988
+ --skill <name=proficiency> (skills set) Repeatable; proficiency is 1..5. Replaces the skill list.
7989
+ --token <mediaToken> (portfolio add) A tenant media-asset token (must be an image).
7990
+ --limit <n> / --status <s> (activity) Page size (1..50) / run-status filter.
7991
+
7992
+ Exit codes: 0 success \xB7 1 host/HTTP error (incl. not found / not signed in) \xB7 2 usage error.
7993
+
7994
+ Examples:
7995
+ openwop profiles me
7996
+ openwop profiles get user_42 --json
7997
+ openwop profiles edit --job-title "Staff Engineer" --availability-status busy --timezone Europe/London
7998
+ openwop profiles skills set --skill TypeScript=5 --skill Rust=3
7999
+ openwop profiles endorse user_42 TypeScript
8000
+ openwop profiles pin r_123
8001
+ openwop profiles activity --limit 10 --status failed
8002
+ `;
8003
+ var BASE = "/v1/host/sample/profiles";
8004
+ async function profilesRequest(ctx, path, options, notFoundMsg) {
8005
+ try {
8006
+ return await requestJson(ctx, path, options);
8007
+ } catch (err) {
8008
+ if (err instanceof HttpError) {
8009
+ if (err.status === 401) {
8010
+ throw new CliError("profiles requires a durable signed-in account \u2014 pass a bearer token via --api-key.", 1);
8011
+ }
8012
+ if (err.status === 404 && notFoundMsg) {
8013
+ throw new CliError(notFoundMsg, 1);
8014
+ }
8015
+ }
8016
+ throw err;
8017
+ }
8018
+ }
8019
+ async function runProfiles(ctx, argv) {
8020
+ const sub = argv[0] ?? "me";
8021
+ if (sub === "--help" || sub === "-h") {
8022
+ write(ctx.io.stdout, PROFILES_HELP);
8023
+ return 0;
8024
+ }
8025
+ if (sub === "portfolio") {
8026
+ const psub = argv[1] ?? "";
8027
+ const rest = argv.slice(2);
8028
+ if (psub === "add") return await runPortfolioAdd(ctx, rest);
8029
+ if (psub === "remove") return await runPortfolioRemove(ctx, rest);
8030
+ throw new CliError(`Unknown profiles portfolio command: ${psub || "(none)"}
8031
+ Run \`openwop profiles --help\` for usage.`);
8032
+ }
8033
+ if (sub === "skills") {
8034
+ const psub = argv[1] ?? "";
8035
+ if (psub === "set") return await runSkillsSet(ctx, argv.slice(2));
8036
+ throw new CliError(`Unknown profiles skills command: ${psub || "(none)"}
8037
+ Run \`openwop profiles --help\` for usage.`);
8038
+ }
8039
+ const args = argv.slice(1);
8040
+ switch (sub) {
8041
+ case "list":
8042
+ return await runList2(ctx, args);
8043
+ case "get":
8044
+ return await runGet3(ctx, args);
8045
+ case "me":
8046
+ return await runGet3(ctx, []);
8047
+ // self
8048
+ case "activity":
8049
+ return await runActivity(ctx, args);
8050
+ case "edit":
8051
+ return await runEdit(ctx, args);
8052
+ case "pin":
8053
+ return await runPin(ctx, args, true);
8054
+ case "unpin":
8055
+ return await runPin(ctx, args, false);
8056
+ case "endorse":
8057
+ return await runEndorse(ctx, args, true);
8058
+ case "unendorse":
8059
+ return await runEndorse(ctx, args, false);
8060
+ default:
8061
+ throw new CliError(`Unknown profiles command: ${sub}
8062
+ Run \`openwop profiles --help\` for usage.`);
8063
+ }
8064
+ }
8065
+ function renderProfile(ctx, p) {
8066
+ const out = ctx.io.stdout;
8067
+ const who = p.userId ?? "(unknown)";
8068
+ writeLine(out, `user: ${p.displayName ? `${p.displayName} (${who})` : who}`);
8069
+ if (p.jobTitle || p.department) writeLine(out, `role: ${[p.jobTitle, p.department].filter(Boolean).join(" \xB7 ")}`);
8070
+ if (p.bio) writeLine(out, `bio: ${p.bio}`);
8071
+ if (typeof p.completeness === "number") writeLine(out, `completeness: ${p.completeness}%`);
8072
+ if (p.emailVerified !== void 0) writeLine(out, `emailVerified: ${p.emailVerified ? "yes" : "no"}`);
8073
+ if (Array.isArray(p.skills) && p.skills.length) {
8074
+ writeLine(out, `skills: ${p.skills.map((s) => `${s.name}(${s.proficiency})${Array.isArray(s.endorsements) && s.endorsements.length ? ` +${s.endorsements.length}` : ""}`).join(", ")}`);
8075
+ }
8076
+ if (p.availability && (p.availability.status || p.availability.timezone || p.availability.hoursPerWeek !== void 0)) {
8077
+ const a = p.availability;
8078
+ writeLine(out, `availability: ${[a.status, a.timezone, a.hoursPerWeek !== void 0 ? `${a.hoursPerWeek}h/wk` : ""].filter(Boolean).join(" \xB7 ")}`);
8079
+ }
8080
+ if (p.contact && (p.contact.location || Array.isArray(p.contact.links) && p.contact.links.length)) {
8081
+ const c = p.contact;
8082
+ if (c.location) writeLine(out, `location: ${c.location}`);
8083
+ if (Array.isArray(c.links) && c.links.length) writeLine(out, `links: ${c.links.map((l) => `${l.label || l.url}${l.label && l.url ? ` <${l.url}>` : ""}`).join(", ")}`);
8084
+ }
8085
+ if (Array.isArray(p.equipment) && p.equipment.length) writeLine(out, `equipment: ${p.equipment.join(", ")}`);
8086
+ if (Array.isArray(p.interests) && p.interests.length) writeLine(out, `interests: ${p.interests.join(", ")}`);
8087
+ if (Array.isArray(p.workflows) && p.workflows.length) writeLine(out, `workflows: ${p.workflows.join(", ")}`);
8088
+ if (Array.isArray(p.pinnedAgentIds) && p.pinnedAgentIds.length) writeLine(out, `pinnedAgents: ${p.pinnedAgentIds.join(", ")}`);
8089
+ if (Array.isArray(p.portfolioAssetTokens) && p.portfolioAssetTokens.length) writeLine(out, `portfolio: ${p.portfolioAssetTokens.length} asset(s)`);
8090
+ if (p.avatarAssetToken) writeLine(out, `avatar: set`);
8091
+ }
8092
+ async function runList2(ctx, argv) {
8093
+ const { options } = parseOptions(argv, { bool: ["--help"] });
8094
+ if (options.help) {
8095
+ write(ctx.io.stdout, "Usage: openwop profiles list [--json]\n");
8096
+ return 0;
8097
+ }
8098
+ const res = await profilesRequest(ctx, BASE);
8099
+ if (ctx.json) {
8100
+ writeJson(ctx.io.stdout, res.body);
8101
+ return 0;
8102
+ }
8103
+ const profiles = Array.isArray(res.body?.profiles) ? res.body.profiles : [];
8104
+ if (profiles.length === 0) {
8105
+ writeLine(ctx.io.stdout, "No profiles in this tenant.");
8106
+ return 0;
8107
+ }
8108
+ const rows = profiles.map((p) => ({
8109
+ userId: p.userId,
8110
+ name: p.displayName ?? "",
8111
+ role: [p.jobTitle, p.department].filter(Boolean).join(" / "),
8112
+ skills: Array.isArray(p.skills) ? String(p.skills.length) : "0",
8113
+ complete: typeof p.completeness === "number" ? `${p.completeness}%` : ""
8114
+ }));
8115
+ writeLine(ctx.io.stdout, formatTable(rows, ["userId", "name", "role", "skills", "complete"]));
8116
+ return 0;
8117
+ }
8118
+ async function runGet3(ctx, argv) {
8119
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
8120
+ if (options.help || positionals.length > 1) {
8121
+ write(ctx.io.stdout, "Usage: openwop profiles get [<userId>] [--json]\n");
8122
+ return options.help ? 0 : 2;
8123
+ }
8124
+ const path = positionals.length === 1 ? `${BASE}/${encodeURIComponent(positionals[0])}` : `${BASE}/me`;
8125
+ const res = await profilesRequest(ctx, path, void 0, `Profile not found: ${positionals[0] ?? "me"}`);
8126
+ if (ctx.json) {
8127
+ writeJson(ctx.io.stdout, res.body);
8128
+ return 0;
8129
+ }
8130
+ renderProfile(ctx, res.body ?? {});
8131
+ return 0;
8132
+ }
8133
+ async function runActivity(ctx, argv) {
8134
+ const { options } = parseOptions(argv, { bool: ["--help"], value: ["--limit", "--status"] });
8135
+ if (options.help) {
8136
+ write(ctx.io.stdout, "Usage: openwop profiles activity [--limit <n>] [--status <s>] [--json]\n");
8137
+ return 0;
8138
+ }
8139
+ const qs = new URLSearchParams();
8140
+ if (options.limit !== void 0) qs.set("limit", String(options.limit));
8141
+ if (options.status !== void 0) qs.set("status", String(options.status));
8142
+ const q = qs.toString();
8143
+ const res = await profilesRequest(ctx, `${BASE}/me/activity${q ? `?${q}` : ""}`);
8144
+ if (ctx.json) {
8145
+ writeJson(ctx.io.stdout, res.body);
8146
+ return 0;
8147
+ }
8148
+ const items = Array.isArray(res.body?.items) ? res.body.items : [];
8149
+ if (items.length === 0) {
8150
+ writeLine(ctx.io.stdout, "No recent activity.");
8151
+ return 0;
8152
+ }
8153
+ const rows = items.map((it) => ({
8154
+ runId: it.runId ?? "",
8155
+ workflow: it.workflowId ?? it.workflow ?? "",
8156
+ status: it.status ?? "",
8157
+ when: it.createdAt ?? it.ts ?? ""
8158
+ }));
8159
+ writeLine(ctx.io.stdout, formatTable(rows, ["runId", "workflow", "status", "when"]));
8160
+ if (res.body?.truncated) writeLine(ctx.io.stdout, "(activity scan truncated \u2014 narrow with --status or --limit)");
8161
+ return 0;
8162
+ }
8163
+ async function runEdit(ctx, argv) {
8164
+ const { options } = parseOptions(argv, {
8165
+ bool: ["--help"],
8166
+ value: ["--job-title", "--department", "--bio", "--location", "--availability-status", "--timezone", "--hours-per-week"],
8167
+ multi: ["--equipment", "--interests"]
8168
+ });
8169
+ if (options.help) {
8170
+ write(ctx.io.stdout, PROFILES_HELP);
8171
+ return 0;
8172
+ }
8173
+ const body = {};
8174
+ if (options.jobTitle !== void 0) body.jobTitle = options.jobTitle;
8175
+ if (options.department !== void 0) body.department = options.department;
8176
+ if (options.bio !== void 0) body.bio = options.bio;
8177
+ if (Array.isArray(options.equipment)) body.equipment = options.equipment;
8178
+ if (Array.isArray(options.interests)) body.interests = options.interests;
8179
+ if (options.location !== void 0) body.contact = { location: options.location };
8180
+ const avail = {};
8181
+ if (options.availabilityStatus !== void 0) {
8182
+ if (!["available", "busy", "away"].includes(options.availabilityStatus)) {
8183
+ throw new CliError("--availability-status must be one of available, busy, away", 2);
8184
+ }
8185
+ avail.status = options.availabilityStatus;
8186
+ }
8187
+ if (options.timezone !== void 0) avail.timezone = options.timezone;
8188
+ if (options.hoursPerWeek !== void 0) {
8189
+ const h = Number(options.hoursPerWeek);
8190
+ if (!Number.isFinite(h)) throw new CliError("--hours-per-week must be a number", 2);
8191
+ avail.hoursPerWeek = h;
8192
+ }
8193
+ if (Object.keys(avail).length) body.availability = avail;
8194
+ if (Object.keys(body).length === 0) throw new CliError("Nothing to edit \u2014 pass at least one field.", 2);
8195
+ const res = await profilesRequest(ctx, `${BASE}/me`, { method: "PATCH", body });
8196
+ if (ctx.json) {
8197
+ writeJson(ctx.io.stdout, res.body);
8198
+ return 0;
8199
+ }
8200
+ writeLine(ctx.io.stdout, "Updated your profile.");
8201
+ renderProfile(ctx, res.body ?? {});
8202
+ return 0;
8203
+ }
8204
+ async function runSkillsSet(ctx, argv) {
8205
+ const { options } = parseOptions(argv, { bool: ["--help"], multi: ["--skill"] });
8206
+ if (options.help) {
8207
+ write(ctx.io.stdout, "Usage: openwop profiles skills set --skill <name=proficiency>... [--json]\n");
8208
+ return 0;
8209
+ }
8210
+ const entries = Array.isArray(options.skill) ? options.skill : [];
8211
+ if (entries.length === 0) throw new CliError("Pass at least one --skill <name=proficiency>.", 2);
8212
+ const skills = entries.map((e) => {
8213
+ const eq = e.lastIndexOf("=");
8214
+ if (eq < 0) throw new CliError(`--skill must be name=proficiency (got '${e}')`, 2);
8215
+ const name = e.slice(0, eq).trim();
8216
+ const prof = Number(e.slice(eq + 1));
8217
+ if (!name) throw new CliError(`--skill name is required (got '${e}')`, 2);
8218
+ if (!Number.isFinite(prof) || prof < 1 || prof > 5) throw new CliError(`--skill proficiency must be 1..5 (got '${e}')`, 2);
8219
+ return { name, proficiency: prof };
8220
+ });
8221
+ const res = await profilesRequest(ctx, `${BASE}/me/skills`, { method: "PUT", body: { skills } });
8222
+ if (ctx.json) {
8223
+ writeJson(ctx.io.stdout, res.body);
8224
+ return 0;
8225
+ }
8226
+ writeLine(ctx.io.stdout, `Set ${skills.length} skill(s).`);
8227
+ renderProfile(ctx, res.body ?? {});
8228
+ return 0;
8229
+ }
8230
+ async function runPortfolioAdd(ctx, argv) {
8231
+ const { options } = parseOptions(argv, { bool: ["--help"], value: ["--token"] });
8232
+ if (options.help || options.token === void 0) {
8233
+ write(ctx.io.stdout, "Usage: openwop profiles portfolio add --token <mediaToken> [--json]\n");
8234
+ return options.help ? 0 : 2;
8235
+ }
8236
+ const res = await profilesRequest(ctx, `${BASE}/me/portfolio`, { method: "POST", body: { token: options.token } });
8237
+ if (ctx.json) {
8238
+ writeJson(ctx.io.stdout, res.body);
8239
+ return 0;
8240
+ }
8241
+ writeLine(ctx.io.stdout, "Added portfolio asset.");
8242
+ return 0;
8243
+ }
8244
+ async function runPortfolioRemove(ctx, argv) {
8245
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
8246
+ if (options.help || positionals.length !== 1) {
8247
+ write(ctx.io.stdout, "Usage: openwop profiles portfolio remove <mediaToken> [--json]\n");
8248
+ return options.help ? 0 : 2;
8249
+ }
8250
+ const res = await profilesRequest(ctx, `${BASE}/me/portfolio/${encodeURIComponent(positionals[0])}`, { method: "DELETE" }, `Portfolio asset not found: ${positionals[0]}`);
8251
+ if (ctx.json) {
8252
+ writeJson(ctx.io.stdout, res.body);
8253
+ return 0;
8254
+ }
8255
+ writeLine(ctx.io.stdout, "Removed portfolio asset.");
8256
+ return 0;
8257
+ }
8258
+ async function runPin(ctx, argv, pinned) {
8259
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
8260
+ if (options.help || positionals.length !== 1) {
8261
+ write(ctx.io.stdout, `Usage: openwop profiles ${pinned ? "pin" : "unpin"} <rosterId> [--json]
8262
+ `);
8263
+ return options.help ? 0 : 2;
8264
+ }
8265
+ const path = `${BASE}/me/pinned-agents/${encodeURIComponent(positionals[0])}`;
8266
+ const res = await profilesRequest(ctx, path, { method: pinned ? "PUT" : "DELETE" }, `Agent not found: ${positionals[0]}`);
8267
+ if (ctx.json) {
8268
+ writeJson(ctx.io.stdout, res.body);
8269
+ return 0;
8270
+ }
8271
+ writeLine(ctx.io.stdout, `${pinned ? "Pinned" : "Unpinned"} agent ${positionals[0]}.`);
8272
+ return 0;
8273
+ }
8274
+ async function runEndorse(ctx, argv, add) {
8275
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
8276
+ if (options.help || positionals.length !== 2) {
8277
+ write(ctx.io.stdout, `Usage: openwop profiles ${add ? "endorse" : "unendorse"} <userId> <skill> [--json]
8278
+ `);
8279
+ return options.help ? 0 : 2;
8280
+ }
8281
+ const [userId, skill] = positionals;
8282
+ const path = `${BASE}/${encodeURIComponent(userId)}/skills/${encodeURIComponent(skill)}/endorse`;
8283
+ const res = await profilesRequest(ctx, path, { method: add ? "POST" : "DELETE" }, `Profile or skill not found: ${userId} / ${skill}`);
8284
+ if (ctx.json) {
8285
+ writeJson(ctx.io.stdout, res.body);
8286
+ return 0;
8287
+ }
8288
+ writeLine(ctx.io.stdout, `${add ? "Endorsed" : "Removed endorsement for"} ${skill} on ${userId}.`);
8289
+ return 0;
8290
+ }
8291
+
8292
+ // src/cli/toggles.ts
8293
+ var TOGGLES_BASE = "/v1/host/sample/feature-toggles";
8294
+ var TOGGLES_HELP = `Usage:
8295
+ openwop toggles list [--json]
8296
+ openwop toggles get <toggleId> [--json]
8297
+
8298
+ Render the caller's RESOLVED feature-toggle assignments (sample host extension under
8299
+ ${TOGGLES_BASE}/assignments). This is a NON-NORMATIVE vendor surface \u2014 not part of the
8300
+ openwop wire contract and not advertised in /.well-known/openwop.
8301
+
8302
+ CAPABILITY HONESTY (critical): the HOST is the sole authority for toggle/variant
8303
+ resolution \u2014 it runs server-side from the authenticated principal. This command only
8304
+ RENDERS the host's resolved view (status / enabled / variant / bindings, verbatim). It
8305
+ NEVER computes, asserts, or overrides a toggle decision locally, and it does not author
8306
+ config (the superadmin /admin/configs surface is intentionally not exposed here). If the
8307
+ host doesn't serve the surface the command fails closed legibly (exit 2).
8308
+
8309
+ list 'GET /assignments' \u2014 every toggle's resolved state for the caller (incl. off).
8310
+ get <toggleId> 'GET /assignments/:id' \u2014 one toggle's resolved assignment.
8311
+
8312
+ Resolved fields (host-authored): status (on | off | beta) \xB7 enabled (bool) \xB7 variant (key | none)
8313
+ \xB7 bindings (slot \u2192 ref@version for the assigned variant, when present).
8314
+
8315
+ Exit codes: 0 ok \xB7 1 host error \xB7 2 usage error / surface not served / unknown toggle.
8316
+
8317
+ Examples:
8318
+ openwop toggles list
8319
+ openwop toggles list --json
8320
+ openwop toggles get crm.triageAgent
8321
+ `;
8322
+ async function togglesRequest(ctx, path) {
8323
+ try {
8324
+ return await requestJson(ctx, path);
8325
+ } catch (err) {
8326
+ if (err instanceof HttpError && err.status === 404 && path.endsWith("/assignments")) {
8327
+ throw new CliError(
8328
+ `Host does not serve the feature-toggle surface at ${TOGGLES_BASE} (non-normative host extension \u2014 not enabled). Failing closed.`,
8329
+ 2
8330
+ );
8331
+ }
8332
+ throw err;
8333
+ }
8334
+ }
8335
+ async function runToggles(ctx, argv) {
8336
+ const sub = argv[0] ?? "list";
8337
+ if (sub === "--help" || sub === "-h") {
8338
+ write(ctx.io.stdout, TOGGLES_HELP);
8339
+ return 0;
8340
+ }
8341
+ switch (sub) {
8342
+ case "list":
8343
+ return await runTogglesList(ctx, argv.slice(1));
8344
+ case "get":
8345
+ return await runTogglesGet(ctx, argv.slice(1));
8346
+ default:
8347
+ throw new CliError(`Unknown toggles command: ${sub}
8348
+ Run \`openwop toggles --help\` for usage.`);
8349
+ }
8350
+ }
8351
+ async function runTogglesList(ctx, argv) {
8352
+ const { options } = parseOptions(argv, { bool: ["--help"] });
8353
+ if (options.help) {
8354
+ write(ctx.io.stdout, TOGGLES_HELP);
8355
+ return 0;
8356
+ }
8357
+ const res = await togglesRequest(ctx, `${TOGGLES_BASE}/assignments`);
8358
+ if (ctx.json) {
8359
+ writeJson(ctx.io.stdout, res.body);
8360
+ return 0;
8361
+ }
8362
+ const assignments = Array.isArray(res.body?.assignments) ? res.body.assignments : [];
8363
+ if (assignments.length === 0) {
8364
+ writeLine(ctx.io.stdout, "No feature toggles resolved for this caller.");
8365
+ return 0;
8366
+ }
8367
+ const rows = assignments.map((a) => ({
8368
+ id: a.id,
8369
+ status: a.status ?? "",
8370
+ enabled: a.enabled === true ? "yes" : a.enabled === false ? "no" : "",
8371
+ variant: a.variant ?? "\u2014",
8372
+ bindings: Array.isArray(a.bindings) ? String(a.bindings.length) : "0"
8373
+ }));
8374
+ writeLine(ctx.io.stdout, formatTable(rows, ["id", "status", "enabled", "variant", "bindings"]));
8375
+ return 0;
8376
+ }
8377
+ async function runTogglesGet(ctx, argv) {
8378
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
8379
+ if (options.help || positionals.length !== 1) {
8380
+ write(ctx.io.stdout, "Usage: openwop toggles get <toggleId> [--json]\n");
8381
+ return options.help ? 0 : 2;
8382
+ }
8383
+ let res;
8384
+ try {
8385
+ res = await togglesRequest(ctx, `${TOGGLES_BASE}/assignments/${encodeURIComponent(positionals[0])}`);
8386
+ } catch (err) {
8387
+ if (err instanceof HttpError && err.status === 404) {
8388
+ throw new CliError(`No such feature toggle: ${positionals[0]}`, 2);
8389
+ }
8390
+ throw err;
8391
+ }
8392
+ if (ctx.json) {
8393
+ writeJson(ctx.io.stdout, res.body);
8394
+ return 0;
8395
+ }
8396
+ const a = res.body ?? {};
8397
+ writeLine(ctx.io.stdout, `id: ${a.id ?? positionals[0]}`);
8398
+ writeLine(ctx.io.stdout, `status: ${a.status ?? ""}`);
8399
+ writeLine(ctx.io.stdout, `enabled: ${a.enabled === true ? "yes" : a.enabled === false ? "no" : ""}`);
8400
+ writeLine(ctx.io.stdout, `variant: ${a.variant ?? "(none)"}`);
8401
+ if (Array.isArray(a.bindings) && a.bindings.length) {
8402
+ writeLine(ctx.io.stdout, "bindings:");
8403
+ for (const b of a.bindings) {
8404
+ const ref = b?.ref ?? {};
8405
+ writeLine(ctx.io.stdout, ` ${b?.slot ?? "?"} \u2192 ${ref.kind ?? "?"}:${ref.name ?? "?"}@${ref.version ?? "?"}`);
8406
+ }
8407
+ }
8408
+ return 0;
8409
+ }
8410
+
8411
+ // src/cli/users.ts
8412
+ var USERS_BASE = "/v1/host/sample/users";
8413
+ var USER_SOURCES = ["oidc", "password", "saml", "scim", "manual"];
8414
+ var USERS_HELP = `Usage:
8415
+ openwop users list [--json]
8416
+ openwop users get <userId> [--json]
8417
+ openwop users create --principal <id> [--email <e>] [--display-name <n>] [--group <g>]... [--source <s>] [--json]
8418
+ openwop users update <userId> [--email <e>] [--display-name <n>] [--group <g>]... [--json]
8419
+ openwop users disable <userId> [--json]
8420
+ openwop users enable <userId> [--json]
8421
+ openwop users delete <userId> [--yes]
8422
+ openwop users me [--display-name <n>] [--json]
8423
+
8424
+ Tenant identity DIRECTORY + account lifecycle (host users feature under ${USERS_BASE}).
8425
+ This is the durable record of WHO exists in a tenant and whether their account is active \u2014
8426
+ NOT authorization. Role/permission membership lives in \`orgs\` (RBAC); editable persona /
8427
+ self-profile lives in \`profiles\` and \`users me\`. The \`groups\` field here is the raw IdP
8428
+ \`groups[]\` captured at sign-in for the RBAC handoff \u2014 it does not itself grant anything.
8429
+
8430
+ The host is the authority: every route resolves the caller's access server-side and the
8431
+ surface 404s when the users feature isn't served for them \u2014 these commands fail closed
8432
+ legibly (exit 2) rather than guessing. Lifecycle is fail-closed: a disabled account is
8433
+ denied (403), surfaced legibly.
8434
+
8435
+ --principal <id> (create) The auth join key (e.g. oidc:<sub>) \u2014 required.
8436
+ --email <e> Email address; on update, '' clears it.
8437
+ --display-name <n> Display name; on update, '' clears it.
8438
+ --group <g> An IdP group name (repeatable). On update, replaces the set.
8439
+ --source <s> Identity source: ${USER_SOURCES.join(" | ")} (create; default manual).
8440
+ --yes (delete) Confirm the destructive removal.
8441
+
8442
+ Exit codes: 0 ok \xB7 1 host error \xB7 2 usage error / surface not served / account disabled.
8443
+
8444
+ Examples:
8445
+ openwop users list
8446
+ openwop users me
8447
+ openwop users create --principal oidc:abc123 --email a@b.dev --group eng --source oidc
8448
+ openwop users update u_1 --display-name "Ada L." --group eng --group oncall
8449
+ openwop users disable u_1
8450
+ openwop users delete u_1 --yes
8451
+ `;
8452
+ async function usersRequest(ctx, path, options) {
8453
+ try {
8454
+ return await requestJson(ctx, path, options);
8455
+ } catch (err) {
8456
+ if (err instanceof HttpError && err.status === 404 && !/\/users\/users\//.test(path)) {
8457
+ throw new CliError(
8458
+ `Host does not serve the users surface at ${USERS_BASE} (backend authority \u2014 not enabled for this caller). Failing closed.`,
8459
+ 2
8460
+ );
8461
+ }
8462
+ throw err;
8463
+ }
8464
+ }
8465
+ async function runUsers(ctx, argv) {
8466
+ const sub = argv[0] ?? "list";
8467
+ const args = argv.slice(["list", "get", "create", "update", "disable", "enable", "delete", "me"].includes(sub) ? 1 : 0);
8468
+ if (sub === "--help" || sub === "-h") {
8469
+ write(ctx.io.stdout, USERS_HELP);
8470
+ return 0;
8471
+ }
8472
+ switch (sub) {
8473
+ case "list":
8474
+ return await runUsersList(ctx, args);
8475
+ case "get":
8476
+ return await runUsersGet(ctx, args);
8477
+ case "create":
8478
+ return await runUsersCreate(ctx, args);
8479
+ case "update":
8480
+ return await runUsersUpdate(ctx, args);
8481
+ case "disable":
8482
+ case "enable":
8483
+ return await runUsersLifecycle(ctx, sub, args);
8484
+ case "delete":
8485
+ return await runUsersDelete(ctx, args);
8486
+ case "me":
8487
+ return await runUsersMe(ctx, args);
8488
+ default:
8489
+ throw new CliError(`Unknown users command: ${sub}
8490
+ Run \`openwop users --help\` for usage.`);
8491
+ }
8492
+ }
8493
+ function userRow(u) {
8494
+ return {
8495
+ userId: u.userId,
8496
+ principalId: u.principalId ?? "",
8497
+ displayName: u.displayName ?? "",
8498
+ source: u.source ?? "",
8499
+ status: u.status ?? "",
8500
+ groups: Array.isArray(u.groups) ? String(u.groups.length) : "0"
8501
+ };
8502
+ }
8503
+ var USER_COLS = ["userId", "principalId", "displayName", "source", "status", "groups"];
8504
+ function printUser(ctx, u) {
8505
+ writeLine(ctx.io.stdout, `userId: ${u.userId ?? ""}`);
8506
+ writeLine(ctx.io.stdout, `principalId: ${u.principalId ?? ""}`);
8507
+ if (u.email) writeLine(ctx.io.stdout, `email: ${u.email}`);
8508
+ if (u.displayName) writeLine(ctx.io.stdout, `displayName: ${u.displayName}`);
8509
+ writeLine(ctx.io.stdout, `source: ${u.source ?? ""}`);
8510
+ writeLine(ctx.io.stdout, `status: ${u.status ?? ""}`);
8511
+ writeLine(ctx.io.stdout, `groups: ${Array.isArray(u.groups) && u.groups.length ? u.groups.join(", ") : "(none)"}`);
8512
+ if (u.createdAt) writeLine(ctx.io.stdout, `createdAt: ${u.createdAt}`);
8513
+ }
8514
+ async function runUsersList(ctx, argv) {
8515
+ const { options } = parseOptions(argv, { bool: ["--help"] });
8516
+ if (options.help) {
8517
+ write(ctx.io.stdout, USERS_HELP);
8518
+ return 0;
8519
+ }
8520
+ const res = await usersRequest(ctx, `${USERS_BASE}/users`);
8521
+ if (ctx.json) {
8522
+ writeJson(ctx.io.stdout, res.body);
8523
+ return 0;
8524
+ }
8525
+ const users = Array.isArray(res.body?.users) ? res.body.users : [];
8526
+ if (users.length === 0) {
8527
+ writeLine(ctx.io.stdout, "No users in this tenant.");
8528
+ return 0;
8529
+ }
8530
+ writeLine(ctx.io.stdout, formatTable(users.map(userRow), USER_COLS));
8531
+ return 0;
8532
+ }
8533
+ async function runUsersGet(ctx, argv) {
8534
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
8535
+ if (options.help || positionals.length !== 1) {
8536
+ write(ctx.io.stdout, "Usage: openwop users get <userId> [--json]\n");
8537
+ return options.help ? 0 : 2;
8538
+ }
8539
+ const res = await usersRequest(ctx, `${USERS_BASE}/users/${encodeURIComponent(positionals[0])}`);
8540
+ if (ctx.json) {
8541
+ writeJson(ctx.io.stdout, res.body);
8542
+ return 0;
8543
+ }
8544
+ printUser(ctx, res.body ?? {});
8545
+ return 0;
8546
+ }
8547
+ async function runUsersCreate(ctx, argv) {
8548
+ const { options } = parseOptions(argv, {
8549
+ bool: ["--help"],
8550
+ value: ["--principal", "--email", "--display-name", "--source"],
8551
+ multi: ["--group"]
8552
+ });
8553
+ if (options.help || !options.principal) {
8554
+ write(ctx.io.stdout, "Usage: openwop users create --principal <id> [--email <e>] [--display-name <n>] [--group <g>]... [--source <s>] [--json]\n");
8555
+ return options.help ? 0 : 2;
8556
+ }
8557
+ if (options.source !== void 0 && !USER_SOURCES.includes(options.source)) {
8558
+ throw new CliError(`--source must be one of ${USER_SOURCES.join(", ")}`, 2);
8559
+ }
8560
+ const body = { principalId: options.principal };
8561
+ if (options.email !== void 0) body.email = options.email;
8562
+ if (options.displayName !== void 0) body.displayName = options.displayName;
8563
+ if (Array.isArray(options.group) && options.group.length) body.groups = options.group;
8564
+ if (options.source !== void 0) body.source = options.source;
8565
+ const res = await usersRequest(ctx, `${USERS_BASE}/users`, { method: "POST", body });
8566
+ if (ctx.json) {
8567
+ writeJson(ctx.io.stdout, res.body);
8568
+ return 0;
8569
+ }
8570
+ writeLine(ctx.io.stdout, `Created user ${res.body?.userId ?? ""} (${res.body?.principalId ?? options.principal}).`);
8571
+ return 0;
8572
+ }
8573
+ async function runUsersUpdate(ctx, argv) {
8574
+ const { options, positionals } = parseOptions(argv, {
8575
+ bool: ["--help"],
8576
+ value: ["--email", "--display-name"],
8577
+ multi: ["--group"]
8578
+ });
8579
+ if (options.help || positionals.length !== 1) {
8580
+ write(ctx.io.stdout, "Usage: openwop users update <userId> [--email <e>] [--display-name <n>] [--group <g>]... [--json]\n");
8581
+ return options.help ? 0 : 2;
8582
+ }
8583
+ const body = {};
8584
+ if (options.email !== void 0) body.email = options.email;
8585
+ if (options.displayName !== void 0) body.displayName = options.displayName;
8586
+ if (Array.isArray(options.group)) body.groups = options.group;
8587
+ if (Object.keys(body).length === 0) {
8588
+ throw new CliError("Nothing to update \u2014 pass --email, --display-name, and/or --group.", 2);
8589
+ }
8590
+ const res = await usersRequest(ctx, `${USERS_BASE}/users/${encodeURIComponent(positionals[0])}`, { method: "PATCH", body });
8591
+ if (ctx.json) {
8592
+ writeJson(ctx.io.stdout, res.body);
8593
+ return 0;
8594
+ }
8595
+ writeLine(ctx.io.stdout, `Updated user ${positionals[0]}.`);
8596
+ return 0;
8597
+ }
8598
+ async function runUsersLifecycle(ctx, verb, argv) {
8599
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
8600
+ if (options.help || positionals.length !== 1) {
8601
+ write(ctx.io.stdout, `Usage: openwop users ${verb} <userId> [--json]
8602
+ `);
8603
+ return options.help ? 0 : 2;
8604
+ }
8605
+ const res = await usersRequest(ctx, `${USERS_BASE}/users/${encodeURIComponent(positionals[0])}/${verb}`, { method: "POST", body: {} });
8606
+ if (ctx.json) {
8607
+ writeJson(ctx.io.stdout, res.body);
8608
+ return 0;
8609
+ }
8610
+ writeLine(ctx.io.stdout, `User ${positionals[0]} \u2192 ${res.body?.status ?? (verb === "disable" ? "disabled" : "active")}.`);
8611
+ return 0;
8612
+ }
8613
+ async function runUsersDelete(ctx, argv) {
8614
+ const { options, positionals } = parseOptions(argv, { bool: ["--help", "--yes"] });
8615
+ if (options.help || positionals.length !== 1) {
8616
+ write(ctx.io.stdout, "Usage: openwop users delete <userId> [--yes]\n");
8617
+ return options.help ? 0 : 2;
8618
+ }
8619
+ if (!options.yes) {
8620
+ writeLine(ctx.io.stderr, `Refusing to delete user ${positionals[0]} without --yes.`);
8621
+ return 2;
8622
+ }
8623
+ await usersRequest(ctx, `${USERS_BASE}/users/${encodeURIComponent(positionals[0])}`, { method: "DELETE" });
8624
+ writeLine(ctx.io.stdout, `Deleted user ${positionals[0]}.`);
8625
+ return 0;
8626
+ }
8627
+ async function runUsersMe(ctx, argv) {
8628
+ const { options } = parseOptions(argv, { bool: ["--help"], value: ["--display-name"] });
8629
+ if (options.help) {
8630
+ write(ctx.io.stdout, USERS_HELP);
8631
+ return 0;
8632
+ }
8633
+ const method = options.displayName !== void 0 ? "PATCH" : "GET";
8634
+ const reqOptions = method === "PATCH" ? { method: "PATCH", body: { displayName: options.displayName } } : void 0;
8635
+ let res;
8636
+ try {
8637
+ res = await usersRequest(ctx, `${USERS_BASE}/me`, reqOptions);
8638
+ } catch (err) {
8639
+ if (err instanceof HttpError && err.status === 403) {
8640
+ throw new CliError("This account is disabled (host fail-closed, 403).", 2);
8641
+ }
8642
+ throw err;
8643
+ }
8644
+ if (ctx.json) {
8645
+ writeJson(ctx.io.stdout, res.body);
8646
+ return 0;
8647
+ }
8648
+ printUser(ctx, res.body ?? {});
8649
+ return 0;
8650
+ }
8651
+
8652
+ // src/cli/workforces.ts
8653
+ var STATUSES = ["shadow", "piloting", "production"];
8654
+ var WORKFORCES_HELP = `Usage:
8655
+ openwop workforces list [--json]
8656
+ openwop workforces get <workforceId> [--json]
8657
+ openwop workforces metrics <workforceId> [--json]
8658
+ openwop workforces governance <workforceId> [--json]
8659
+ openwop workforces migration <workforceId> [--json]
8660
+ openwop workforces trace <workforceId> [--q <query>] [--json]
8661
+ openwop workforces shadow <workforceId> [--json]
8662
+ openwop workforces status <workforceId> <shadow|piloting|production> [--json]
8663
+ openwop workforces eval <workforceId> [--json]
8664
+
8665
+ Durable multi-agent orchestration at fleet scale (alias: \`fleet\`). A workforce is
8666
+ a governed bundle \u2014 agent specs, graduated-autonomy posture, and aggregate
8667
+ telemetry derived from its runs. \`status\` requests a cutover (the host gates
8668
+ promotion to \`production\` on the workforce having graduated to bounded-autonomous,
8669
+ and always allows rollback). \`eval\` runs a live shadow eval (host-gated capability).
8670
+
8671
+ This group COMPOSES with \u2014 and does not duplicate \u2014 \`kanban\` (task boards) and
8672
+ \`roster\` (standing agents): it renders the workforce's governance/metrics view,
8673
+ never re-modelling boards or roster entries. The host is the authority; the CLI
8674
+ renders its resolved view and fails closed if the surface isn't advertised.
8675
+
8676
+ Endpoints:
8677
+ list GET /v1/host/sample/workforces
8678
+ get GET /v1/host/sample/workforces/{id}
8679
+ metrics GET /v1/host/sample/workforces/{id}/metrics
8680
+ governance GET /v1/host/sample/workforces/{id}/governance
8681
+ migration GET /v1/host/sample/workforces/{id}/migration
8682
+ trace GET /v1/host/sample/workforces/{id}/trace[?q=]
8683
+ shadow GET /v1/host/sample/workforces/{id}/shadow
8684
+ status PATCH /v1/host/sample/workforces/{id} (body {status})
8685
+ eval POST /v1/host/sample/workforces/{id}/eval
8686
+
8687
+ --q <query> (trace) Match runs by correlationId / batchId / runId / outcome / status.
8688
+ --json Print the raw host response instead of the rendered view.
8689
+
8690
+ Exit codes:
8691
+ reads / status / eval 0 ok \xB7 2 usage \xB7 1 not-found / not-eligible / error
8692
+
8693
+ Examples:
8694
+ openwop workforces list
8695
+ openwop fleet get wf_support --json
8696
+ openwop workforces metrics wf_support
8697
+ openwop workforces trace wf_support --q batch_42
8698
+ openwop workforces status wf_support production
8699
+ `;
8700
+ var SUBCOMMANDS2 = ["list", "get", "metrics", "governance", "migration", "trace", "shadow", "status", "eval"];
8701
+ async function runWorkforces(ctx, argv) {
8702
+ const sub = argv[0] ?? "list";
8703
+ if (sub === "--help" || sub === "-h") {
8704
+ write(ctx.io.stdout, WORKFORCES_HELP);
8705
+ return 0;
8706
+ }
8707
+ const args = argv.slice(SUBCOMMANDS2.includes(sub) ? 1 : 0);
8708
+ switch (sub) {
8709
+ case "list":
8710
+ return await runWfList(ctx, args);
8711
+ case "get":
8712
+ return await runWfReadOne(ctx, args, "", renderWorkforce);
8713
+ case "metrics":
8714
+ return await runWfReadOne(ctx, args, "/metrics", renderMetrics);
8715
+ case "governance":
8716
+ return await runWfReadOne(ctx, args, "/governance", renderGovernance);
8717
+ case "migration":
8718
+ return await runWfReadOne(ctx, args, "/migration", renderMigration);
8719
+ case "trace":
8720
+ return await runWfTrace(ctx, args);
8721
+ case "shadow":
8722
+ return await runWfReadOne(ctx, args, "/shadow", renderShadow);
8723
+ case "status":
8724
+ return await runWfStatus(ctx, args);
8725
+ case "eval":
8726
+ return await runWfEval(ctx, args);
8727
+ default:
8728
+ throw new CliError(`Unknown workforces command: ${sub}
8729
+ Run \`openwop workforces --help\` for usage.`);
8730
+ }
8731
+ }
8732
+ async function ensureAdvertised2(ctx) {
8733
+ const wk = await safeRequest(ctx, "/.well-known/openwop", { auth: false });
8734
+ if (!wk.ok) return;
8735
+ const paths = wk.body && typeof wk.body === "object" ? wk.body.paths : void 0;
8736
+ const advertised = paths !== null && typeof paths === "object" && Object.keys(paths).some((p) => p.startsWith("/v1/host/sample/workforces"));
8737
+ if (!advertised) {
8738
+ throw new CliError(
8739
+ "workforces: this host does not advertise the workforces surface (/v1/host/sample/workforces is absent from /.well-known/openwop). The host is the authority \u2014 refusing to guess.",
8740
+ 1
8741
+ );
8742
+ }
8743
+ }
8744
+ function gate4043(err) {
8745
+ if (err instanceof HttpError && err.status === 404) {
8746
+ const detail = err.body && typeof err.body === "object" && typeof err.body.message === "string" ? err.body.message : "not found";
8747
+ throw new CliError(
8748
+ `workforces: ${detail} (the host must mount /v1/host/sample/workforces; the CLI renders the host's view, it never orchestrates locally).`,
8749
+ 1
8750
+ );
8751
+ }
8752
+ throw err;
8753
+ }
8754
+ var num = (v) => typeof v === "number" ? String(v) : v == null ? "" : String(v);
8755
+ var pct = (v) => typeof v === "number" ? `${(v * 100).toFixed(1)}%` : "";
8756
+ var ms = (v) => typeof v === "number" ? `${Math.round(v)}ms` : "";
8757
+ async function runWfList(ctx, argv) {
8758
+ const { options } = parseOptions(argv, { bool: ["--help"] });
8759
+ if (options.help) {
8760
+ write(ctx.io.stdout, WORKFORCES_HELP);
8761
+ return 0;
8762
+ }
8763
+ await ensureAdvertised2(ctx);
8764
+ let res;
8765
+ try {
8766
+ res = await requestJson(ctx, "/v1/host/sample/workforces");
8767
+ } catch (err) {
8768
+ gate4043(err);
8769
+ }
8770
+ const workforces = Array.isArray(res.body?.workforces) ? res.body.workforces : [];
8771
+ if (ctx.json) {
8772
+ writeJson(ctx.io.stdout, { workforces });
8773
+ return 0;
8774
+ }
8775
+ if (workforces.length === 0) {
8776
+ writeLine(ctx.io.stdout, "No workforces on this host.");
8777
+ return 0;
8778
+ }
8779
+ const rows = workforces.map((w) => ({
8780
+ workforceId: w.workforceId,
8781
+ name: w.name ?? "",
8782
+ businessFunction: w.businessFunction ?? "",
8783
+ status: w.status ?? "",
8784
+ autonomy: w.autonomyLevel ?? "",
8785
+ agents: Array.isArray(w.agents) ? String(w.agents.length) : "0"
8786
+ }));
8787
+ writeLine(ctx.io.stdout, formatTable(rows, ["workforceId", "name", "businessFunction", "status", "autonomy", "agents"]));
8788
+ return 0;
8789
+ }
8790
+ async function runWfReadOne(ctx, argv, suffix, render) {
8791
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
8792
+ if (options.help || positionals.length !== 1) {
8793
+ write(ctx.io.stdout, WORKFORCES_HELP);
8794
+ return options.help ? 0 : 2;
8795
+ }
8796
+ await ensureAdvertised2(ctx);
8797
+ let res;
8798
+ try {
8799
+ res = await requestJson(ctx, `/v1/host/sample/workforces/${encodeURIComponent(positionals[0])}${suffix}`);
8800
+ } catch (err) {
8801
+ gate4043(err);
8802
+ }
8803
+ if (ctx.json) {
8804
+ writeJson(ctx.io.stdout, res.body);
8805
+ return 0;
8806
+ }
8807
+ render(ctx, res.body ?? {}, positionals[0]);
8808
+ return 0;
8809
+ }
8810
+ function renderWorkforce(ctx, w, id) {
8811
+ writeLine(ctx.io.stdout, `workforceId: ${w.workforceId ?? id}`);
8812
+ writeLine(ctx.io.stdout, `name: ${w.name ?? ""}`);
8813
+ writeLine(ctx.io.stdout, `businessFunction: ${w.businessFunction ?? ""}`);
8814
+ writeLine(ctx.io.stdout, `status: ${w.status ?? ""}`);
8815
+ writeLine(ctx.io.stdout, `autonomyLevel: ${w.autonomyLevel ?? ""}`);
8816
+ if (w.purpose?.statement) writeLine(ctx.io.stdout, `purpose: ${w.purpose.statement}`);
8817
+ if (Array.isArray(w.successMetrics)) writeLine(ctx.io.stdout, `successMetrics: ${w.successMetrics.join(", ") || "(none)"}`);
8818
+ if (Array.isArray(w.workflowCatalog)) writeLine(ctx.io.stdout, `workflowCatalog: ${w.workflowCatalog.join(", ") || "(none)"}`);
8819
+ if (Array.isArray(w.agents)) {
8820
+ writeLine(ctx.io.stdout, `agents (${w.agents.length}):`);
8821
+ for (const a of w.agents) {
8822
+ writeLine(ctx.io.stdout, ` \xB7 ${a.agentRef ?? "?"} [${a.role ?? "?"}] autonomy=${a.autonomyLevel ?? "?"}`);
8823
+ }
8824
+ }
8825
+ }
8826
+ function renderMetrics(ctx, m, id) {
8827
+ writeLine(ctx.io.stdout, `workforceId: ${m.workforceId ?? id}`);
8828
+ writeLine(ctx.io.stdout, `totalRuns: ${num(m.totalRuns)} terminalRuns: ${num(m.terminalRuns)} openApprovals: ${num(m.openApprovals)}`);
8829
+ writeLine(ctx.io.stdout, `cycleTimeP50: ${ms(m.cycleTimeP50Ms)} costPerCleared: ${m.costPerClearedUsd != null ? `$${m.costPerClearedUsd}` : ""}`);
8830
+ writeLine(ctx.io.stdout, `escalationRate: ${pct(m.escalationRate)} overrideRate: ${pct(m.overrideRate)} falsePositiveRate: ${pct(m.falsePositiveRate)} recoveryRate: ${pct(m.recoveryRate)}`);
8831
+ writeLine(ctx.io.stdout, `policyViolations: ${num(m.policyViolations)}`);
8832
+ if (m.source) writeLine(ctx.io.stdout, `source: ${m.source}`);
8833
+ }
8834
+ function renderGovernance(ctx, g, id) {
8835
+ const a = g.autonomy ?? {};
8836
+ const p = g.posture ?? {};
8837
+ writeLine(ctx.io.stdout, `workforceId: ${a.workforceId ?? id}`);
8838
+ writeLine(ctx.io.stdout, `currentTier: ${a.currentTier ?? "(none)"} nextTier: ${a.nextTier ?? "(none)"} eligibleForNext: ${a.eligibleForNext === true ? "yes" : "no"}`);
8839
+ if (a.nextThreshold != null) writeLine(ctx.io.stdout, `nextThreshold: ${num(a.nextThreshold)} recentOverrideIncidence: ${pct(a.recentOverrideIncidence)}`);
8840
+ writeLine(ctx.io.stdout, `posture \u2014 overrides: ${num(p.overrides)} escalations: ${num(p.escalations)} falsePositives: ${num(p.falsePositives)} recoveries: ${num(p.recoveries)} policyViolations: ${num(p.policyViolations)}`);
8841
+ if (g.source) writeLine(ctx.io.stdout, `source: ${g.source}`);
8842
+ }
8843
+ function renderMigration(ctx, j, id) {
8844
+ writeLine(ctx.io.stdout, `workforceId: ${j.workforceId ?? id}`);
8845
+ const stages = Array.isArray(j.stages) ? j.stages : [];
8846
+ if (stages.length === 0) {
8847
+ writeJson(ctx.io.stdout, j);
8848
+ return;
8849
+ }
8850
+ for (const s of stages) {
8851
+ writeLine(ctx.io.stdout, ` \xB7 ${s.key ?? s.stage ?? "?"}: ${s.status ?? "?"}`);
8852
+ }
8853
+ }
8854
+ function renderShadow(ctx, s, id) {
8855
+ writeLine(ctx.io.stdout, `workforceId: ${s.workforceId ?? id}`);
8856
+ for (const [k, v] of Object.entries(s)) {
8857
+ if (k === "workforceId") continue;
8858
+ if (v == null || typeof v === "object") continue;
8859
+ writeLine(ctx.io.stdout, `${k}: ${String(v)}`);
8860
+ }
8861
+ }
8862
+ async function runWfTrace(ctx, argv) {
8863
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"], value: ["--q"] });
8864
+ if (options.help || positionals.length !== 1) {
8865
+ write(ctx.io.stdout, "Usage: openwop workforces trace <workforceId> [--q <query>] [--json]\n");
8866
+ return options.help ? 0 : 2;
8867
+ }
8868
+ await ensureAdvertised2(ctx);
8869
+ const q = options.q !== void 0 ? `?q=${encodeURIComponent(options.q)}` : "";
8870
+ let res;
8871
+ try {
8872
+ res = await requestJson(ctx, `/v1/host/sample/workforces/${encodeURIComponent(positionals[0])}/trace${q}`);
8873
+ } catch (err) {
8874
+ gate4043(err);
8875
+ }
8876
+ if (ctx.json) {
8877
+ writeJson(ctx.io.stdout, res.body);
8878
+ return 0;
8879
+ }
8880
+ const matches = Array.isArray(res.body?.matches) ? res.body.matches : [];
8881
+ if (matches.length === 0) {
8882
+ writeLine(ctx.io.stdout, `No trace matches for ${positionals[0]}${options.q ? ` (q=${options.q})` : ""}.`);
8883
+ return 0;
8884
+ }
8885
+ writeLine(ctx.io.stdout, formatTable(
8886
+ matches.map((m) => ({ runId: m.runId ?? "", status: m.status ?? "", outcome: m.outcome ?? "", createdAt: m.createdAt ?? m.atIso ?? "" })),
8887
+ ["runId", "status", "outcome", "createdAt"]
8888
+ ));
8889
+ return 0;
8890
+ }
8891
+ async function runWfStatus(ctx, argv) {
8892
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
8893
+ if (options.help || positionals.length !== 2) {
8894
+ write(ctx.io.stdout, "Usage: openwop workforces status <workforceId> <shadow|piloting|production> [--json]\n");
8895
+ return options.help ? 0 : 2;
8896
+ }
8897
+ const [id, status] = positionals;
8898
+ if (!STATUSES.includes(status)) {
8899
+ throw new CliError(`status must be one of: ${STATUSES.join(", ")}`, 2);
8900
+ }
8901
+ await ensureAdvertised2(ctx);
8902
+ let res;
8903
+ try {
8904
+ res = await requestJson(ctx, `/v1/host/sample/workforces/${encodeURIComponent(id)}`, { method: "PATCH", body: { status } });
8905
+ } catch (err) {
8906
+ if (err instanceof HttpError && err.status === 409) {
8907
+ const detail = err.body?.message ?? "cutover not eligible";
8908
+ throw new CliError(`workforces: ${detail}`, 1);
8909
+ }
8910
+ gate4043(err);
8911
+ }
8912
+ const out = res.body ?? {};
8913
+ if (ctx.json) {
8914
+ writeJson(ctx.io.stdout, out);
8915
+ return 0;
8916
+ }
8917
+ writeLine(ctx.io.stdout, `\u2713 ${out.workforceId ?? id} \u2192 status ${out.status ?? status}`);
8918
+ return 0;
8919
+ }
8920
+ async function runWfEval(ctx, argv) {
8921
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
8922
+ if (options.help || positionals.length !== 1) {
8923
+ write(ctx.io.stdout, "Usage: openwop workforces eval <workforceId> [--json]\n");
8924
+ return options.help ? 0 : 2;
8925
+ }
8926
+ await ensureAdvertised2(ctx);
8927
+ let res;
8928
+ try {
8929
+ res = await requestJson(ctx, `/v1/host/sample/workforces/${encodeURIComponent(positionals[0])}/eval`, { method: "POST" });
8930
+ } catch (err) {
8931
+ if (err instanceof HttpError && err.status === 501) {
8932
+ const detail = err.body?.message ?? "this host does not enable the agent eval suite";
8933
+ throw new CliError(`workforces: ${detail}`, 1);
8934
+ }
8935
+ gate4043(err);
8936
+ }
8937
+ if (ctx.json) {
8938
+ writeJson(ctx.io.stdout, res.body);
8939
+ return 0;
8940
+ }
8941
+ const s = res.body ?? {};
8942
+ writeLine(ctx.io.stdout, `eval complete for ${positionals[0]}`);
8943
+ for (const [k, v] of Object.entries(s)) {
8944
+ if (v == null || typeof v === "object") continue;
8945
+ writeLine(ctx.io.stdout, `${k}: ${String(v)}`);
8946
+ }
8947
+ return 0;
8948
+ }
8949
+
8950
+ // src/cli/auth.ts
8951
+ var AUTH_HELP = `Usage:
8952
+ openwop auth status [--json]
8953
+ openwop auth saml metadata [--json]
8954
+ openwop auth saml login-url [--return-to <path>] [--json]
8955
+ openwop auth saml validate --idp-url <url> --variant <name> [--json]
8956
+ openwop auth scim provision --op <create-user|assign-group|deactivate-user>
8957
+ [--user-name <u>] [--external-id <id>] [--email <e>] [--display-name <n>] [--group <g>] [--json]
8958
+
8959
+ Enterprise identity-provider configuration (RFC 0050): SAML 2.0 SSO + SCIM 2.0
8960
+ provisioning. The host is the authority \u2014 this command surfaces status, public SP
8961
+ metadata, and provisioning results; every auth surface 404s when the host has not
8962
+ configured it (SAML/SCIM are env-gated host-side), and the command fails closed
8963
+ legibly in that case.
8964
+
8965
+ SECRET BOUNDARY (critical): SAML signing certs, SCIM bearer tokens, and client
8966
+ secrets are HOST-SIDE only \u2014 they are never returned, printed, or logged. The CLI
8967
+ additionally runs every host response through a recursive redactor before printing,
8968
+ so a secret-looking field can never leak even if a host returns one by mistake.
8969
+ (SP metadata XML is the exception \u2014 it is PUBLIC by design, meant to be uploaded to
8970
+ your IdP, and contains only the SP's public certificate.)
8971
+
8972
+ BOUNDARY: \`auth\` is SSO/SAML/SCIM identity-provider config (hits /v1/host/sample/auth/*).
8973
+ It is NOT the user directory (\`users\`), NOT RBAC (\`orgs\`), and NOT BYOK provider
8974
+ credentials (\`byok\`/\`providers\`).
8975
+
8976
+ Subcommands \u2192 endpoint:
8977
+ status GET /.well-known/openwop + probe Which auth profiles this host advertises/serves.
8978
+ saml metadata GET /auth/saml/sso/metadata SP metadata XML (upload to your IdP).
8979
+ saml login-url GET /auth/saml/sso/login The SP-initiated IdP redirect URL (not followed).
8980
+ saml validate POST /auth/saml/validate Conformance seam: validate a synthetic-IdP assertion.
8981
+ scim provision POST /auth/scim/provision Provisioning seam: create/assign-group/deactivate.
8982
+
8983
+ --return-to <path> (saml login-url) Same-site return path after login (default /).
8984
+ --idp-url <url> (saml validate) The operator-configured synthetic IdP origin.
8985
+ --variant <name> (saml validate) The assertion variant to fetch + validate.
8986
+ --op <op> (scim provision) create-user | assign-group | deactivate-user.
8987
+ --user-name/--external-id/--email/--display-name/--group (scim provision) SCIM user/group fields.
8988
+
8989
+ Exit codes: 0 success (saml validate: 0 = authenticated) \xB7 1 host/HTTP error or not configured
8990
+ (saml validate: 1 = rejected) \xB7 2 usage error.
8991
+
8992
+ Examples:
8993
+ openwop auth status
8994
+ openwop auth saml metadata
8995
+ openwop auth saml login-url --return-to /dashboard
8996
+ openwop auth saml validate --idp-url http://localhost:9100/idp --variant valid
8997
+ openwop auth scim provision --op create-user --user-name jo@acme.com --email jo@acme.com
8998
+ `;
8999
+ var BASE2 = "/v1/host/sample/auth";
9000
+ var SECRET_KEY2 = /secret|token|password|private[-_]?key|client[-_]?secret|api[-_]?key|credential|bearer|assertion|certificate|cert|pem/i;
9001
+ function redact2(value) {
9002
+ if (Array.isArray(value)) return value.map(redact2);
9003
+ if (value && typeof value === "object") {
9004
+ const out = {};
9005
+ for (const [k, v] of Object.entries(value)) {
9006
+ out[k] = SECRET_KEY2.test(k) ? "[redacted]" : redact2(v);
9007
+ }
9008
+ return out;
9009
+ }
9010
+ return value;
9011
+ }
9012
+ async function authRequest(ctx, path, options, notConfiguredMsg) {
9013
+ try {
9014
+ return await requestJson(ctx, path, options);
9015
+ } catch (err) {
9016
+ if (err instanceof HttpError && err.status === 404) {
9017
+ throw new CliError(notConfiguredMsg, 1);
9018
+ }
9019
+ throw err;
9020
+ }
9021
+ }
9022
+ async function runAuth(ctx, argv) {
9023
+ const sub = argv[0] ?? "status";
9024
+ if (sub === "--help" || sub === "-h") {
9025
+ write(ctx.io.stdout, AUTH_HELP);
9026
+ return 0;
9027
+ }
9028
+ if (sub === "saml") {
9029
+ const psub = argv[1] ?? "";
9030
+ const rest = argv.slice(2);
9031
+ if (psub === "metadata") return await runSamlMetadata(ctx, rest);
9032
+ if (psub === "login-url") return await runSamlLoginUrl(ctx, rest);
9033
+ if (psub === "validate") return await runSamlValidate(ctx, rest);
9034
+ throw new CliError(`Unknown auth saml command: ${psub || "(none)"}
9035
+ Run \`openwop auth --help\` for usage.`);
9036
+ }
9037
+ if (sub === "scim") {
9038
+ const psub = argv[1] ?? "";
9039
+ if (psub === "provision") return await runScimProvision(ctx, argv.slice(2));
9040
+ throw new CliError(`Unknown auth scim command: ${psub || "(none)"}
9041
+ Run \`openwop auth --help\` for usage.`);
9042
+ }
9043
+ if (sub === "status") return await runStatus(ctx, argv.slice(1));
9044
+ throw new CliError(`Unknown auth command: ${sub}
9045
+ Run \`openwop auth --help\` for usage.`);
9046
+ }
9047
+ async function runStatus(ctx, argv) {
9048
+ const { options } = parseOptions(argv, { bool: ["--help"] });
9049
+ if (options.help) {
9050
+ write(ctx.io.stdout, "Usage: openwop auth status [--json]\n");
9051
+ return 0;
9052
+ }
9053
+ const disc = await safeRequest(ctx, "/.well-known/openwop", { auth: false });
9054
+ const profiles = disc.ok && Array.isArray(disc.body?.capabilities?.auth?.profiles) ? disc.body.capabilities.auth.profiles : [];
9055
+ const samlAdvertised = profiles.includes("openwop-auth-saml");
9056
+ const scimAdvertised = profiles.includes("openwop-auth-scim");
9057
+ const metaReachable = samlAdvertised ? (await probeEndpoint(ctx, `${BASE2}/saml/sso/metadata`)).ok : false;
9058
+ if (ctx.json) {
9059
+ writeJson(ctx.io.stdout, {
9060
+ authProfilesAdvertised: profiles,
9061
+ saml: { advertised: samlAdvertised, ssoMetadataReachable: metaReachable },
9062
+ scim: { advertised: scimAdvertised }
9063
+ });
9064
+ return 0;
9065
+ }
9066
+ const rows = [
9067
+ { surface: "SAML SSO (login/ACS/metadata)", advertised: samlAdvertised ? "yes" : "no", live: !samlAdvertised ? "no" : metaReachable ? "yes" : "seam-only" },
9068
+ { surface: "SCIM provisioning", advertised: scimAdvertised ? "yes" : "no", live: scimAdvertised ? "\u2014" : "no" }
9069
+ ];
9070
+ writeLine(ctx.io.stdout, formatTable(rows, ["surface", "advertised", "live"]));
9071
+ if (!samlAdvertised && !scimAdvertised) {
9072
+ writeLine(ctx.io.stdout, "This host advertises no enterprise-auth profiles (SAML/SCIM are env-gated host-side).");
9073
+ }
9074
+ return 0;
9075
+ }
9076
+ async function runSamlMetadata(ctx, argv) {
9077
+ const { options } = parseOptions(argv, { bool: ["--help"] });
9078
+ if (options.help) {
9079
+ write(ctx.io.stdout, "Usage: openwop auth saml metadata [--json]\n");
9080
+ return 0;
9081
+ }
9082
+ const res = await authRequest(ctx, `${BASE2}/saml/sso/metadata`, { auth: false }, "SAML SSO is not configured on this host (set OPENWOP_SAML_*).");
9083
+ const xml = typeof res.body?.raw === "string" ? res.body.raw : typeof res.body === "string" ? res.body : JSON.stringify(res.body);
9084
+ if (ctx.json) {
9085
+ writeJson(ctx.io.stdout, { metadata: xml });
9086
+ return 0;
9087
+ }
9088
+ write(ctx.io.stdout, xml.endsWith("\n") ? xml : `${xml}
9089
+ `);
9090
+ return 0;
9091
+ }
9092
+ async function runSamlLoginUrl(ctx, argv) {
9093
+ const { options } = parseOptions(argv, { bool: ["--help"], value: ["--return-to"] });
9094
+ if (options.help) {
9095
+ write(ctx.io.stdout, "Usage: openwop auth saml login-url [--return-to <path>] [--json]\n");
9096
+ return 0;
9097
+ }
9098
+ const returnTo = options.returnTo ?? "/";
9099
+ const base = ctx.baseUrl.endsWith("/") ? ctx.baseUrl : `${ctx.baseUrl}/`;
9100
+ const url = new URL(`${BASE2}/saml/sso/login?returnTo=${encodeURIComponent(returnTo)}`.replace(/^\//, ""), base);
9101
+ const res = await ctx.fetchImpl(url, { method: "GET", redirect: "manual" });
9102
+ if (res.status === 404) {
9103
+ throw new CliError("SAML SSO is not configured on this host (set OPENWOP_SAML_*).", 1);
9104
+ }
9105
+ const location = res.headers.get("location");
9106
+ if (!location) {
9107
+ throw new CliError(`Expected a redirect to the IdP but got HTTP ${res.status} with no Location header.`, 1);
9108
+ }
9109
+ if (ctx.json) {
9110
+ writeJson(ctx.io.stdout, { loginUrl: location });
9111
+ return 0;
9112
+ }
9113
+ writeLine(ctx.io.stdout, location);
9114
+ return 0;
9115
+ }
9116
+ async function runSamlValidate(ctx, argv) {
9117
+ const { options } = parseOptions(argv, { bool: ["--help"], value: ["--idp-url", "--variant"] });
9118
+ if (options.help || options.idpUrl === void 0 || options.variant === void 0) {
9119
+ write(ctx.io.stdout, "Usage: openwop auth saml validate --idp-url <url> --variant <name> [--json]\n");
9120
+ return options.help ? 0 : 2;
9121
+ }
9122
+ const body = { idpUrl: options.idpUrl, variant: options.variant };
9123
+ let res;
9124
+ try {
9125
+ res = await requestJson(ctx, `${BASE2}/saml/validate`, { method: "POST", body });
9126
+ } catch (err) {
9127
+ if (err instanceof HttpError && err.status === 404) {
9128
+ throw new CliError("SAML validation seam not configured on this host (set OPENWOP_TEST_SAML_IDP_URL).", 1);
9129
+ }
9130
+ if (err instanceof HttpError && err.status === 401) {
9131
+ const reason = err.body?.reason ?? "unauthenticated";
9132
+ if (ctx.json) {
9133
+ writeJson(ctx.io.stdout, redact2({ authenticated: false, reason }));
9134
+ return 1;
9135
+ }
9136
+ writeLine(ctx.io.stdout, `authenticated: no (${reason})`);
9137
+ return 1;
9138
+ }
9139
+ throw err;
9140
+ }
9141
+ const safe = redact2(res.body ?? {});
9142
+ if (ctx.json) {
9143
+ writeJson(ctx.io.stdout, safe);
9144
+ return 0;
9145
+ }
9146
+ writeLine(ctx.io.stdout, `authenticated: ${safe.authenticated === true ? "yes" : "no"}`);
9147
+ if (safe.principal !== void 0) writeLine(ctx.io.stdout, `principal: ${typeof safe.principal === "string" ? safe.principal : JSON.stringify(safe.principal)}`);
9148
+ return safe.authenticated === true ? 0 : 1;
9149
+ }
9150
+ async function runScimProvision(ctx, argv) {
9151
+ const { options } = parseOptions(argv, {
9152
+ bool: ["--help"],
9153
+ value: ["--op", "--user-name", "--external-id", "--email", "--display-name", "--group"]
9154
+ });
9155
+ const OPS = ["create-user", "assign-group", "deactivate-user"];
9156
+ if (options.help || options.op === void 0) {
9157
+ write(ctx.io.stdout, "Usage: openwop auth scim provision --op <create-user|assign-group|deactivate-user> [--user-name <u>] [--external-id <id>] [--email <e>] [--display-name <n>] [--group <g>] [--json]\n");
9158
+ return options.help ? 0 : 2;
9159
+ }
9160
+ if (!OPS.includes(options.op)) throw new CliError(`--op must be one of ${OPS.join(", ")}`, 2);
9161
+ const user = {};
9162
+ if (options.userName !== void 0) user.userName = options.userName;
9163
+ if (options.externalId !== void 0) user.externalId = options.externalId;
9164
+ if (options.email !== void 0) user.email = options.email;
9165
+ if (options.displayName !== void 0) user.displayName = options.displayName;
9166
+ const body = { op: options.op, user };
9167
+ if (options.group !== void 0) body.group = options.group;
9168
+ const res = await authRequest(ctx, `${BASE2}/scim/provision`, { method: "POST", body }, "SCIM provisioning seam not configured on this host (set OPENWOP_TEST_SCIM_URL).");
9169
+ const safe = redact2(res.body ?? {});
9170
+ if (ctx.json) {
9171
+ writeJson(ctx.io.stdout, safe);
9172
+ return 0;
9173
+ }
9174
+ writeLine(ctx.io.stdout, `op: ${safe.op ?? options.op}`);
9175
+ if (safe.principal) {
9176
+ const p = safe.principal;
9177
+ writeLine(ctx.io.stdout, `principal: ${p.userName ?? p.userId ?? ""}${p.status ? ` (${p.status})` : ""}`);
9178
+ if (Array.isArray(p.groups)) writeLine(ctx.io.stdout, `groups: ${p.groups.length ? p.groups.join(", ") : "(none)"}`);
9179
+ }
9180
+ if (safe.resolvable !== void 0) writeLine(ctx.io.stdout, `resolvable: ${safe.resolvable ? "yes" : "no"}`);
9181
+ return 0;
9182
+ }
9183
+
9184
+ // src/cli/analytics.ts
9185
+ var ANALYTICS_BASE = "/v1/host/sample/analytics/orgs";
9186
+ var PUBLIC_BASE = "/v1/host/sample/public-analytics";
9187
+ var EVENT_TYPES = ["pageview", "event", "conversion"];
9188
+ var ANALYTICS_HELP = `Usage:
9189
+ openwop analytics summary <orgId> [--json]
9190
+ openwop analytics events <orgId> [--json]
9191
+ openwop analytics collect <orgId> --session <key> [--type <t>] [--path <p>] [--name <n>] [--prop k=v]... [--json]
9192
+
9193
+ Org-scoped usage analytics (ADR 0018 host extension). Aliased as \`usage\`.
9194
+ Endpoints:
9195
+ GET ${ANALYTICS_BASE}/:orgId/summary aggregate usage rollup [authed, workspace:read]
9196
+ GET ${ANALYTICS_BASE}/:orgId/events recent raw events (max 100) [authed, workspace:read]
9197
+ POST ${PUBLIC_BASE}/:orgId/collect record one event [PUBLIC, unauthed]
9198
+
9199
+ This is USAGE / cost / observability \u2014 distinct from \`governance audit\` (the policy-decision
9200
+ log). The HOST is the authority: it aggregates server-side and gates each org read by RBAC
9201
+ (workspace:read) + the org-tenant's \`analytics\` toggle. This command only RENDERS what the
9202
+ host returns; it never computes a rollup locally. When analytics isn't served the host returns
9203
+ a uniform 404 and the command fails closed legibly (exit 2).
9204
+
9205
+ The public \`collect\` beacon is consent-gated host-side (ADR 0020): a 201 records the event;
9206
+ a 202 means analytics consent wasn't granted (honestly reported, not recorded). It is sent
9207
+ WITHOUT auth (public path) \u2014 no bearer is attached.
9208
+
9209
+ --session <key> (collect) The visitor session key (== the consent subject key). Required.
9210
+ --type <t> (collect) Event type: ${EVENT_TYPES.join(" | ")} (host default: event).
9211
+ --path <p> (collect) Page path (for pageviews).
9212
+ --name <n> (collect) Event name.
9213
+ --prop k=v (collect) A typed property (repeatable; v parsed as number/bool/string).
9214
+ --json Emit the raw host JSON.
9215
+
9216
+ Exit codes: 0 ok \xB7 1 host error \xB7 2 usage error / analytics not served / not authorized.
9217
+
9218
+ Examples:
9219
+ openwop analytics summary org_123
9220
+ openwop usage events org_123 --json
9221
+ openwop analytics collect org_123 --session s_abc --type pageview --path /pricing
9222
+ `;
9223
+ async function analyticsRequest(ctx, path, options) {
9224
+ try {
9225
+ return await requestJson(ctx, path, options);
9226
+ } catch (err) {
9227
+ if (err instanceof HttpError && err.status === 404) {
9228
+ throw new CliError(
9229
+ `Host does not serve analytics for this org (ADR 0018 \u2014 the org-tenant's 'analytics' toggle is off, or the org is unknown). Failing closed.`,
9230
+ 2
9231
+ );
9232
+ }
9233
+ throw err;
9234
+ }
9235
+ }
9236
+ async function runAnalytics(ctx, argv) {
9237
+ const sub = argv[0] ?? "summary";
9238
+ if (sub === "--help" || sub === "-h") {
9239
+ write(ctx.io.stdout, ANALYTICS_HELP);
9240
+ return 0;
9241
+ }
9242
+ switch (sub) {
9243
+ case "summary":
9244
+ return await runSummary(ctx, argv.slice(1));
9245
+ case "events":
9246
+ return await runEvents(ctx, argv.slice(1));
9247
+ case "collect":
9248
+ return await runCollect(ctx, argv.slice(1));
9249
+ default:
9250
+ throw new CliError(`Unknown analytics command: ${sub}
9251
+ Run \`openwop analytics --help\` for usage.`);
9252
+ }
9253
+ }
9254
+ async function runSummary(ctx, argv) {
9255
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
9256
+ if (options.help || positionals.length !== 1) {
9257
+ write(ctx.io.stdout, "Usage: openwop analytics summary <orgId> [--json]\n");
9258
+ return options.help ? 0 : 2;
9259
+ }
9260
+ const res = await analyticsRequest(ctx, `${ANALYTICS_BASE}/${encodeURIComponent(positionals[0])}/summary`);
9261
+ if (ctx.json) {
9262
+ writeJson(ctx.io.stdout, res.body);
9263
+ return 0;
9264
+ }
9265
+ const s = res.body?.summary ?? {};
9266
+ writeLine(ctx.io.stdout, `total events: ${s.total ?? 0}`);
9267
+ writeLine(ctx.io.stdout, `sessions: ${s.sessions ?? 0}`);
9268
+ const byType = s.byType ?? {};
9269
+ writeLine(ctx.io.stdout, `by type: ${EVENT_TYPES.map((t) => `${t}=${byType[t] ?? 0}`).join(" ")}`);
9270
+ if (Array.isArray(s.topPaths) && s.topPaths.length) {
9271
+ writeLine(ctx.io.stdout, "top paths:");
9272
+ for (const p of s.topPaths) writeLine(ctx.io.stdout, ` ${p.count} ${p.path}`);
9273
+ }
9274
+ if (Array.isArray(s.utmSources) && s.utmSources.length) {
9275
+ writeLine(ctx.io.stdout, "utm sources:");
9276
+ for (const u of s.utmSources) writeLine(ctx.io.stdout, ` ${u.count} ${u.source}`);
9277
+ }
9278
+ return 0;
9279
+ }
9280
+ async function runEvents(ctx, argv) {
9281
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
9282
+ if (options.help || positionals.length !== 1) {
9283
+ write(ctx.io.stdout, "Usage: openwop analytics events <orgId> [--json]\n");
9284
+ return options.help ? 0 : 2;
9285
+ }
9286
+ const res = await analyticsRequest(ctx, `${ANALYTICS_BASE}/${encodeURIComponent(positionals[0])}/events`);
9287
+ if (ctx.json) {
9288
+ writeJson(ctx.io.stdout, res.body);
9289
+ return 0;
9290
+ }
9291
+ const events = Array.isArray(res.body?.events) ? res.body.events : [];
9292
+ if (events.length === 0) {
9293
+ writeLine(ctx.io.stdout, "No analytics events for this org.");
9294
+ return 0;
9295
+ }
9296
+ const rows = events.map((e) => ({
9297
+ ts: e.ts ?? "",
9298
+ type: e.type ?? "",
9299
+ path: e.path ?? e.name ?? "",
9300
+ session: e.sessionKey ?? "",
9301
+ eventId: e.eventId ?? ""
9302
+ }));
9303
+ writeLine(ctx.io.stdout, formatTable(rows, ["ts", "type", "path", "session", "eventId"]));
9304
+ return 0;
9305
+ }
9306
+ async function runCollect(ctx, argv) {
9307
+ const { options, positionals } = parseOptions(argv, {
9308
+ bool: ["--help"],
9309
+ value: ["--session", "--type", "--path", "--name"],
9310
+ multi: ["--prop"]
9311
+ });
9312
+ if (options.help || positionals.length !== 1 || !options.session) {
9313
+ write(ctx.io.stdout, "Usage: openwop analytics collect <orgId> --session <key> [--type <t>] [--path <p>] [--name <n>] [--prop k=v]... [--json]\n");
9314
+ return options.help ? 0 : 2;
9315
+ }
9316
+ if (options.type !== void 0 && !EVENT_TYPES.includes(options.type)) {
9317
+ throw new CliError(`--type must be one of ${EVENT_TYPES.join(", ")}`, 2);
9318
+ }
9319
+ const body = { sessionKey: options.session };
9320
+ if (options.type !== void 0) body.type = options.type;
9321
+ if (options.path !== void 0) body.path = options.path;
9322
+ if (options.name !== void 0) body.name = options.name;
9323
+ if (Array.isArray(options.prop) && options.prop.length) {
9324
+ const props = {};
9325
+ for (const entry of options.prop) {
9326
+ const eq = String(entry).indexOf("=");
9327
+ if (eq < 0) throw new CliError(`--prop must be <key=value>, got '${entry}'`, 2);
9328
+ const k = entry.slice(0, eq);
9329
+ const raw = entry.slice(eq + 1);
9330
+ props[k] = raw === "true" ? true : raw === "false" ? false : Number.isFinite(Number(raw)) && raw.trim() !== "" ? Number(raw) : raw;
9331
+ }
9332
+ body.props = props;
9333
+ }
9334
+ const res = await analyticsRequest(ctx, `${PUBLIC_BASE}/${encodeURIComponent(positionals[0])}/collect`, {
9335
+ method: "POST",
9336
+ body,
9337
+ auth: false
9338
+ });
9339
+ if (ctx.json) {
9340
+ writeJson(ctx.io.stdout, res.body);
9341
+ return 0;
9342
+ }
9343
+ const r = res.body ?? {};
9344
+ if (r.recorded) {
9345
+ writeLine(ctx.io.stdout, `Recorded event ${r.eventId ?? ""}.`);
9346
+ } else {
9347
+ writeLine(ctx.io.stdout, `Not recorded \u2014 ${r.reason === "consent" ? "analytics consent not granted for this session" : r.reason ?? "declined by host"}.`);
9348
+ }
9349
+ return 0;
9350
+ }
9351
+
9352
+ // src/cli/proposals.ts
9353
+ var PROPOSALS_HELP = `Usage:
9354
+ openwop proposals list [--state <s>] [--kind <k>] [--json]
9355
+ openwop proposals get <proposalId> [--json]
9356
+ openwop proposals revise <proposalId> [--artifact-json '{...}'] [--note <text>] [--json]
9357
+ openwop proposals apply <proposalId> [--json]
9358
+ openwop proposals reject <proposalId> [--note <text>] [--json]
9359
+ openwop proposals archive <proposalId> [--yes]
9360
+
9361
+ Reviewable-learning proposals (RFC 0096). An agent's learned change is stored as
9362
+ an INERT proposal \u2014 it does nothing until a human applies it. 'list'/'get' render
9363
+ the host's review queue. 'revise' edits the stored draft WITHOUT activating it.
9364
+ 'apply' asks the host to materialize the byte image last persisted on the
9365
+ proposal (no re-synthesis) and route activation through the host's advertised
9366
+ mode (e.g. an approval-gate) \u2014 the CLI never activates locally. 'reject' declines;
9367
+ 'archive' soft-deletes.
9368
+
9369
+ The host is the authority for every decision \u2014 the CLI renders the host's
9370
+ resolved view and relays your verb; it never decides. If the host does not
9371
+ advertise the proposal surface (/.well-known/openwop), the group fails closed
9372
+ rather than guessing.
9373
+
9374
+ Endpoints:
9375
+ list GET /v1/host/sample/proposals[?state=&kind=]
9376
+ get GET /v1/host/sample/proposals/{id}
9377
+ revise PATCH /v1/host/sample/proposals/{id} (revise; never activates)
9378
+ apply POST /v1/host/sample/proposals/{id}/apply
9379
+ reject POST /v1/host/sample/proposals/{id}/reject
9380
+ archive DELETE /v1/host/sample/proposals/{id} (soft)
9381
+
9382
+ --state <s> (list) Filter by lifecycle state (e.g. pending, applied, rejected, archived).
9383
+ --kind <k> (list) Filter by artifact kind (RFC 0096 artifactKinds).
9384
+ --artifact-json J (revise) The revised draft artifact (validated host-side).
9385
+ --note <text> (revise/reject) Optional human note recorded with the action.
9386
+ --yes (archive) Required to confirm the soft-delete.
9387
+ --json Print the raw host response instead of the rendered view.
9388
+
9389
+ Boundary: this is NOT \`approvals\`. Approvals are the human action-queue
9390
+ (claim/reject a queued run); proposals are the stored artifact drafts a human
9391
+ reviews before they ever become live behavior.
9392
+
9393
+ Exit codes (get/apply/reject reflect the proposal's resulting state, so scripts
9394
+ can gate on the verdict):
9395
+ 0 applied 3 pending (in review) 1 rejected/archived or error
9396
+ \`list\` exits 0 on success.
9397
+
9398
+ Examples:
9399
+ openwop proposals list --state pending
9400
+ openwop proposals get prop_123 --json
9401
+ openwop proposals revise prop_123 --artifact-json '{"systemPrompt":"..."}'
9402
+ openwop proposals apply prop_123
9403
+ openwop proposals reject prop_123 --note "out of scope"
9404
+ openwop proposals archive prop_123 --yes
9405
+ `;
9406
+ var SUBCOMMANDS3 = ["list", "get", "revise", "apply", "reject", "archive"];
9407
+ async function runProposals(ctx, argv) {
9408
+ const sub = argv[0] ?? "list";
9409
+ if (sub === "--help" || sub === "-h") {
9410
+ write(ctx.io.stdout, PROPOSALS_HELP);
9411
+ return 0;
9412
+ }
9413
+ const args = argv.slice(SUBCOMMANDS3.includes(sub) ? 1 : 0);
9414
+ switch (sub) {
9415
+ case "list":
9416
+ return await runProposalsList(ctx, args);
9417
+ case "get":
9418
+ return await runProposalsGet(ctx, args);
9419
+ case "revise":
9420
+ return await runProposalsRevise(ctx, args);
9421
+ case "apply":
9422
+ return await runProposalsApply(ctx, args);
9423
+ case "reject":
9424
+ return await runProposalsReject(ctx, args);
9425
+ case "archive":
9426
+ return await runProposalsArchive(ctx, args);
9427
+ default:
9428
+ throw new CliError(`Unknown proposals command: ${sub}
9429
+ Run \`openwop proposals --help\` for usage.`);
9430
+ }
9431
+ }
9432
+ function exitForState(state) {
9433
+ if (state === "applied" || state === "active" || state === "installed") return 0;
9434
+ if (state === "pending" || state === "pending-review" || state === "in-review" || state === "draft" || state === "revised") return 3;
9435
+ return 1;
9436
+ }
9437
+ async function ensureAdvertised3(ctx) {
9438
+ const wk = await safeRequest(ctx, "/.well-known/openwop", { auth: false });
9439
+ if (!wk.ok) return;
9440
+ const body = wk.body && typeof wk.body === "object" ? wk.body : {};
9441
+ const advertised = !!(body.capabilities?.agents?.proposals ?? body.agents?.proposals);
9442
+ if (!advertised) {
9443
+ throw new CliError(
9444
+ "proposals: this host does not advertise the reviewable-learning proposal capability (capabilities.agents.proposals is absent from /.well-known/openwop). The host is the authority \u2014 refusing to guess.",
9445
+ 1
9446
+ );
9447
+ }
9448
+ }
9449
+ function gate4044(err) {
9450
+ if (err instanceof HttpError && err.status === 404) {
9451
+ const detail = err.body && typeof err.body === "object" && typeof err.body.message === "string" ? err.body.message : "not found";
9452
+ throw new CliError(
9453
+ `proposals: ${detail} (the host must mount /v1/host/sample/proposals; the CLI renders the host's queue, it never decides).`,
9454
+ 1
9455
+ );
9456
+ }
9457
+ throw err;
9458
+ }
9459
+ async function fetchOne(ctx, id) {
9460
+ let res;
9461
+ try {
9462
+ res = await requestJson(ctx, `/v1/host/sample/proposals/${encodeURIComponent(id)}`);
9463
+ } catch (err) {
9464
+ gate4044(err);
9465
+ }
9466
+ return res.body ?? {};
9467
+ }
9468
+ async function runProposalsList(ctx, argv) {
9469
+ const { options } = parseOptions(argv, { bool: ["--help"], value: ["--state", "--kind"] });
9470
+ if (options.help) {
9471
+ write(ctx.io.stdout, PROPOSALS_HELP);
9472
+ return 0;
9473
+ }
9474
+ await ensureAdvertised3(ctx);
9475
+ const query = [];
9476
+ if (options.state !== void 0) query.push(`state=${encodeURIComponent(options.state)}`);
9477
+ if (options.kind !== void 0) query.push(`kind=${encodeURIComponent(options.kind)}`);
9478
+ const path = `/v1/host/sample/proposals${query.length ? `?${query.join("&")}` : ""}`;
9479
+ let res;
9480
+ try {
9481
+ res = await requestJson(ctx, path);
9482
+ } catch (err) {
9483
+ gate4044(err);
9484
+ }
9485
+ const proposals = Array.isArray(res.body?.proposals) ? res.body.proposals : [];
9486
+ if (ctx.json) {
9487
+ writeJson(ctx.io.stdout, { proposals });
9488
+ return 0;
9489
+ }
9490
+ if (proposals.length === 0) {
9491
+ writeLine(ctx.io.stdout, `No proposals${options.state ? ` in state ${options.state}` : ""}${options.kind ? ` of kind ${options.kind}` : ""} on this host.`);
9492
+ return 0;
9493
+ }
9494
+ const rows = proposals.map((p) => ({
9495
+ proposalId: p.proposalId ?? p.id,
9496
+ state: p.state ?? "",
9497
+ kind: p.kind ?? p.artifactKind ?? "",
9498
+ activation: p.activation ?? "",
9499
+ persona: p.persona ?? p.agentId ?? "",
9500
+ title: truncate3(p.title ?? p.summary ?? "", 40),
9501
+ createdAt: p.createdAt ?? ""
9502
+ }));
9503
+ writeLine(ctx.io.stdout, formatTable(rows, ["proposalId", "state", "kind", "activation", "persona", "title", "createdAt"]));
9504
+ return 0;
9505
+ }
9506
+ async function runProposalsGet(ctx, argv) {
9507
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
9508
+ if (options.help || positionals.length !== 1) {
9509
+ write(ctx.io.stdout, "Usage: openwop proposals get <proposalId> [--json]\n");
9510
+ return options.help ? 0 : 2;
9511
+ }
9512
+ await ensureAdvertised3(ctx);
9513
+ const p = await fetchOne(ctx, positionals[0]);
9514
+ if (ctx.json) {
9515
+ writeJson(ctx.io.stdout, p);
9516
+ return exitForState(p.state);
9517
+ }
9518
+ renderProposal(ctx, p, positionals[0]);
9519
+ return exitForState(p.state);
9520
+ }
9521
+ function renderProposal(ctx, p, fallbackId) {
9522
+ writeLine(ctx.io.stdout, `proposalId: ${p.proposalId ?? p.id ?? fallbackId}`);
9523
+ writeLine(ctx.io.stdout, `state: ${p.state ?? ""}`);
9524
+ writeLine(ctx.io.stdout, `kind: ${p.kind ?? p.artifactKind ?? ""}`);
9525
+ if (p.activation) writeLine(ctx.io.stdout, `activation: ${p.activation}`);
9526
+ if (p.persona ?? p.agentId) writeLine(ctx.io.stdout, `persona: ${p.persona ?? p.agentId}`);
9527
+ if (p.title) writeLine(ctx.io.stdout, `title: ${p.title}`);
9528
+ if (p.summary) writeLine(ctx.io.stdout, `summary: ${p.summary}`);
9529
+ if (p.rationale) writeLine(ctx.io.stdout, `rationale: ${p.rationale}`);
9530
+ if (p.installedArtifactRef) writeLine(ctx.io.stdout, `installedArtifactRef: ${p.installedArtifactRef}`);
9531
+ writeLine(ctx.io.stdout, `createdAt: ${p.createdAt ?? ""}`);
9532
+ if (p.revisedAt) writeLine(ctx.io.stdout, `revisedAt: ${p.revisedAt}`);
9533
+ if (p.resolvedAt) writeLine(ctx.io.stdout, `resolvedAt: ${p.resolvedAt}`);
9534
+ if (p.note) writeLine(ctx.io.stdout, `note: ${p.note}`);
9535
+ }
9536
+ async function runProposalsRevise(ctx, argv) {
9537
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"], value: ["--artifact-json", "--note"] });
9538
+ if (options.help || positionals.length !== 1) {
9539
+ write(ctx.io.stdout, "Usage: openwop proposals revise <proposalId> [--artifact-json '{...}'] [--note <text>] [--json]\n");
9540
+ return options.help ? 0 : 2;
9541
+ }
9542
+ const body = {};
9543
+ if (options.artifactJson !== void 0) {
9544
+ try {
9545
+ body.artifact = JSON.parse(options.artifactJson);
9546
+ } catch {
9547
+ throw new CliError("--artifact-json must be valid JSON", 2);
9548
+ }
9549
+ }
9550
+ if (options.note !== void 0) body.note = options.note;
9551
+ if (Object.keys(body).length === 0) throw new CliError("Nothing to revise \u2014 pass --artifact-json and/or --note.", 2);
9552
+ await ensureAdvertised3(ctx);
9553
+ let res;
9554
+ try {
9555
+ res = await requestJson(ctx, `/v1/host/sample/proposals/${encodeURIComponent(positionals[0])}`, { method: "PATCH", body });
9556
+ } catch (err) {
9557
+ if (err instanceof HttpError && err.status === 422) {
9558
+ const detail = err.body?.message ?? "revision rejected (malformed for kind)";
9559
+ throw new CliError(`proposals: ${detail}`, 1);
9560
+ }
9561
+ gate4044(err);
9562
+ }
9563
+ const p = res.body ?? {};
9564
+ if (ctx.json) {
9565
+ writeJson(ctx.io.stdout, p);
9566
+ return exitForState(p.state);
9567
+ }
9568
+ writeLine(ctx.io.stdout, `\u2713 Revised proposal ${p.proposalId ?? p.id ?? positionals[0]} (state ${p.state ?? "unchanged"}; not activated).`);
9569
+ return exitForState(p.state);
9570
+ }
9571
+ async function runProposalsApply(ctx, argv) {
9572
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
9573
+ if (options.help || positionals.length !== 1) {
9574
+ write(ctx.io.stdout, "Usage: openwop proposals apply <proposalId> [--json]\n");
9575
+ return options.help ? 0 : 2;
9576
+ }
9577
+ await ensureAdvertised3(ctx);
9578
+ let res;
9579
+ try {
9580
+ res = await requestJson(ctx, `/v1/host/sample/proposals/${encodeURIComponent(positionals[0])}/apply`, { method: "POST" });
9581
+ } catch (err) {
9582
+ if (err instanceof HttpError && err.status === 403) {
9583
+ throw new CliError(`proposals: the host refused to apply ${positionals[0]} \u2014 you lack the scope to activate this proposal (the host is the authority).`, 1);
9584
+ }
9585
+ if (err instanceof HttpError && err.status === 422) {
9586
+ const detail = err.body?.message ?? "proposal is malformed for its kind";
9587
+ throw new CliError(`proposals: ${detail}`, 1);
9588
+ }
9589
+ gate4044(err);
9590
+ }
9591
+ const out = res.body ?? {};
9592
+ if (ctx.json) {
9593
+ writeJson(ctx.io.stdout, out);
9594
+ return out.state ? exitForState(out.state) : 0;
9595
+ }
9596
+ if (out.installedArtifactRef) {
9597
+ writeLine(ctx.io.stdout, `\u2713 Applied proposal ${positionals[0]} \u2192 installed ${out.installedArtifactRef}.`);
9598
+ } else if (out.state === "pending" || out.activation === "approval-gate") {
9599
+ writeLine(ctx.io.stdout, `Proposal ${positionals[0]} routed to the host's activation gate (state ${out.state ?? "pending"}); awaiting approval.`);
9600
+ } else {
9601
+ writeLine(ctx.io.stdout, `\u2713 Applied proposal ${positionals[0]}${out.state ? ` \u2192 ${out.state}` : ""}.`);
9602
+ }
9603
+ return out.state ? exitForState(out.state) : 0;
9604
+ }
9605
+ async function runProposalsReject(ctx, argv) {
9606
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"], value: ["--note"] });
9607
+ if (options.help || positionals.length !== 1) {
9608
+ write(ctx.io.stdout, "Usage: openwop proposals reject <proposalId> [--note <text>] [--json]\n");
9609
+ return options.help ? 0 : 2;
9610
+ }
9611
+ await ensureAdvertised3(ctx);
9612
+ const body = options.note !== void 0 ? { note: options.note } : void 0;
9613
+ let res;
9614
+ try {
9615
+ res = await requestJson(ctx, `/v1/host/sample/proposals/${encodeURIComponent(positionals[0])}/reject`, { method: "POST", ...body !== void 0 ? { body } : {} });
9616
+ } catch (err) {
9617
+ gate4044(err);
9618
+ }
9619
+ const p = res.body ?? {};
9620
+ if (ctx.json) {
9621
+ writeJson(ctx.io.stdout, p);
9622
+ return exitForState(p.state ?? "rejected");
9623
+ }
9624
+ writeLine(ctx.io.stdout, `\u2713 Rejected proposal ${p.proposalId ?? p.id ?? positionals[0]} \u2192 ${p.state ?? "rejected"}.`);
9625
+ return exitForState(p.state ?? "rejected");
9626
+ }
9627
+ async function runProposalsArchive(ctx, argv) {
9628
+ const { options, positionals } = parseOptions(argv, { bool: ["--help", "--yes"] });
9629
+ if (options.help || positionals.length !== 1) {
9630
+ write(ctx.io.stdout, "Usage: openwop proposals archive <proposalId> [--yes]\n");
9631
+ return options.help ? 0 : 2;
9632
+ }
9633
+ if (!options.yes) {
9634
+ writeLine(ctx.io.stderr, `Refusing to archive proposal ${positionals[0]} without --yes.`);
9635
+ return 2;
9636
+ }
9637
+ await ensureAdvertised3(ctx);
9638
+ let res;
9639
+ try {
9640
+ res = await requestJson(ctx, `/v1/host/sample/proposals/${encodeURIComponent(positionals[0])}`, { method: "DELETE" });
9641
+ } catch (err) {
9642
+ gate4044(err);
9643
+ }
9644
+ const p = res.body ?? {};
9645
+ if (ctx.json) {
9646
+ writeJson(ctx.io.stdout, p);
9647
+ return exitForState(p.state ?? "archived");
9648
+ }
9649
+ writeLine(ctx.io.stdout, `\u2713 Archived proposal ${p.proposalId ?? p.id ?? positionals[0]} \u2192 ${p.state ?? "archived"}.`);
9650
+ return exitForState(p.state ?? "archived");
9651
+ }
9652
+ function truncate3(s, n) {
9653
+ return s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
9654
+ }
9655
+
9656
+ // src/cli/goals.ts
9657
+ var GOALS_HELP = `Usage:
9658
+ openwop goals list [--state <s>] [--json]
9659
+ openwop goals get <goalId> [--json]
9660
+ openwop goals create --objective <text> [--judge host|verifier] [--continuation <mode>]
9661
+ [--max-iterations <n>] [--max-cost <usd>] [--timeout-ms <ms>] [--json]
9662
+ openwop goals pause <goalId> [--json]
9663
+ openwop goals resume <goalId> [--json]
9664
+ openwop goals abandon <goalId> [--yes]
9665
+
9666
+ Standing goals (RFC 0097). A goal is an objective the host pursues across runs
9667
+ until a JUDGE (RFC 0090) verdicts it satisfied or a BOUND (RFC 0058) stops it.
9668
+ 'list'/'get' render the host's goals. 'create' opens a goal with its judge,
9669
+ continuation arm(s), and bounds. 'pause'/'resume' suspend/restart continuation;
9670
+ 'abandon' closes the goal.
9671
+
9672
+ Completion is the host judge's verdict \u2014 there is no \`complete\`/\`satisfy\` verb
9673
+ and the CLI never sets satisfied; you cannot declare victory from the client. The
9674
+ host is the authority; the CLI renders its resolved view and relays your verb. If
9675
+ the host does not advertise the goal surface (/.well-known/openwop), the group
9676
+ fails closed rather than guessing.
9677
+
9678
+ Endpoints:
9679
+ list GET /v1/host/sample/goals[?state=]
9680
+ get GET /v1/host/sample/goals/{id}
9681
+ create POST /v1/host/sample/goals (422 if the host requires bounds and none given)
9682
+ pause POST /v1/host/sample/goals/{id}/pause
9683
+ resume POST /v1/host/sample/goals/{id}/resume
9684
+ abandon POST /v1/host/sample/goals/{id}/abandon
9685
+
9686
+ --state <s> (list) Filter by state (e.g. active, satisfied, escalated, bound-exceeded, paused).
9687
+ --objective <text> (create) The goal objective (required).
9688
+ --judge host|verifier (create) completion.check \u2014 who decides completion (default: host).
9689
+ --continuation <mode> (create) continuation.mode \u2014 the arm that re-engages work (e.g. schedule | commitment | manual).
9690
+ --max-iterations <n> (create) Bound: bounds.maxLoopIterations.
9691
+ --max-cost <usd> (create) Bound: bounds.maxCostUsd (accumulated USD).
9692
+ --timeout-ms <ms> (create) Bound: bounds.runTimeoutMs (wall-clock, milliseconds).
9693
+ --yes (abandon) Required to confirm.
9694
+ --json Print the raw host response instead of the rendered view.
9695
+
9696
+ Boundary: a goal is NOT an RFC 0068 commitment \u2014 a goal *uses* a
9697
+ commitment/schedule/heartbeat as a continuation arm and adds a judge loop + a
9698
+ termination guarantee (bounds).
9699
+
9700
+ Exit codes (get/create/pause/resume reflect the goal's state, so scripts can gate
9701
+ on the verdict):
9702
+ 0 satisfied 3 escalated or still open (active/paused) 1 bound-exceeded/abandoned or error
9703
+ \`list\` exits 0 on success.
9704
+
9705
+ Examples:
9706
+ openwop goals list --state active
9707
+ openwop goals get goal_123 --json
9708
+ openwop goals create --objective "Keep the triage backlog under 20" --judge verifier --continuation schedule --max-iterations 50 --max-cost 5.00 --timeout-ms 600000
9709
+ openwop goals pause goal_123
9710
+ openwop goals abandon goal_123 --yes
9711
+ `;
9712
+ var SUBCOMMANDS4 = ["list", "get", "create", "pause", "resume", "abandon"];
9713
+ async function runGoals(ctx, argv) {
9714
+ const sub = argv[0] ?? "list";
9715
+ if (sub === "--help" || sub === "-h") {
9716
+ write(ctx.io.stdout, GOALS_HELP);
9717
+ return 0;
9718
+ }
9719
+ const args = argv.slice(SUBCOMMANDS4.includes(sub) ? 1 : 0);
9720
+ switch (sub) {
9721
+ case "list":
9722
+ return await runGoalsList(ctx, args);
9723
+ case "get":
9724
+ return await runGoalsGet(ctx, args);
9725
+ case "create":
9726
+ return await runGoalsCreate(ctx, args);
9727
+ case "pause":
9728
+ return await runGoalsTransition(ctx, args, "pause");
9729
+ case "resume":
9730
+ return await runGoalsTransition(ctx, args, "resume");
9731
+ case "abandon":
9732
+ return await runGoalsAbandon(ctx, args);
9733
+ default:
9734
+ throw new CliError(`Unknown goals command: ${sub}
9735
+ Run \`openwop goals --help\` for usage.`);
9736
+ }
9737
+ }
9738
+ function exitForState2(state) {
9739
+ if (state === "satisfied") return 0;
9740
+ if (state === "escalated" || state === "active" || state === "paused" || state === "running" || state === "pending") return 3;
9741
+ return 1;
9742
+ }
9743
+ async function ensureAdvertised4(ctx) {
9744
+ const wk = await safeRequest(ctx, "/.well-known/openwop", { auth: false });
9745
+ if (!wk.ok) return;
9746
+ const body = wk.body && typeof wk.body === "object" ? wk.body : {};
9747
+ const advertised = !!(body.capabilities?.agents?.goals ?? body.agents?.goals);
9748
+ if (!advertised) {
9749
+ throw new CliError(
9750
+ "goals: this host does not advertise the standing-goals capability (capabilities.agents.goals is absent from /.well-known/openwop). The host is the authority \u2014 refusing to guess.",
9751
+ 1
9752
+ );
9753
+ }
9754
+ }
9755
+ function gate4045(err) {
9756
+ if (err instanceof HttpError && err.status === 404) {
9757
+ const detail = err.body && typeof err.body === "object" && typeof err.body.message === "string" ? err.body.message : "not found";
9758
+ throw new CliError(
9759
+ `goals: ${detail} (the host must mount /v1/host/sample/goals; the CLI renders the host's view, it never decides).`,
9760
+ 1
9761
+ );
9762
+ }
9763
+ throw err;
9764
+ }
9765
+ async function runGoalsList(ctx, argv) {
9766
+ const { options } = parseOptions(argv, { bool: ["--help"], value: ["--state"] });
9767
+ if (options.help) {
9768
+ write(ctx.io.stdout, GOALS_HELP);
9769
+ return 0;
9770
+ }
9771
+ await ensureAdvertised4(ctx);
9772
+ const path = options.state !== void 0 ? `/v1/host/sample/goals?state=${encodeURIComponent(options.state)}` : "/v1/host/sample/goals";
9773
+ let res;
9774
+ try {
9775
+ res = await requestJson(ctx, path);
9776
+ } catch (err) {
9777
+ gate4045(err);
9778
+ }
9779
+ const goals = Array.isArray(res.body?.goals) ? res.body.goals : [];
9780
+ if (ctx.json) {
9781
+ writeJson(ctx.io.stdout, { goals });
9782
+ return 0;
9783
+ }
9784
+ if (goals.length === 0) {
9785
+ writeLine(ctx.io.stdout, `No goals${options.state ? ` in state ${options.state}` : ""} on this host.`);
9786
+ return 0;
9787
+ }
9788
+ const rows = goals.map((g) => ({
9789
+ goalId: g.goalId ?? g.id,
9790
+ state: g.state ?? "",
9791
+ check: g.completion?.check ?? "",
9792
+ objective: truncate4(g.objective ?? "", 40),
9793
+ iterations: progressIterations(g),
9794
+ createdAt: g.createdAt ?? ""
9795
+ }));
9796
+ writeLine(ctx.io.stdout, formatTable(rows, ["goalId", "state", "check", "objective", "iterations", "createdAt"]));
9797
+ return 0;
9798
+ }
9799
+ function progressIterations(g) {
9800
+ const it = g.progress?.iterations ?? g.iterations;
9801
+ const max = g.bounds?.maxLoopIterations;
9802
+ if (it === void 0) return "";
9803
+ return max !== void 0 ? `${it}/${max}` : String(it);
9804
+ }
9805
+ async function runGoalsGet(ctx, argv) {
9806
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
9807
+ if (options.help || positionals.length !== 1) {
9808
+ write(ctx.io.stdout, "Usage: openwop goals get <goalId> [--json]\n");
9809
+ return options.help ? 0 : 2;
9810
+ }
9811
+ await ensureAdvertised4(ctx);
9812
+ let res;
9813
+ try {
9814
+ res = await requestJson(ctx, `/v1/host/sample/goals/${encodeURIComponent(positionals[0])}`);
9815
+ } catch (err) {
9816
+ gate4045(err);
9817
+ }
9818
+ const g = res.body ?? {};
9819
+ if (ctx.json) {
9820
+ writeJson(ctx.io.stdout, g);
9821
+ return exitForState2(g.state);
9822
+ }
9823
+ renderGoal(ctx, g, positionals[0]);
9824
+ return exitForState2(g.state);
9825
+ }
9826
+ function renderGoal(ctx, g, fallbackId) {
9827
+ writeLine(ctx.io.stdout, `goalId: ${g.goalId ?? g.id ?? fallbackId}`);
9828
+ writeLine(ctx.io.stdout, `state: ${g.state ?? ""}`);
9829
+ if (g.objective) writeLine(ctx.io.stdout, `objective: ${g.objective}`);
9830
+ writeLine(ctx.io.stdout, `completion.check: ${g.completion?.check ?? ""}`);
9831
+ if (g.continuation?.mode) writeLine(ctx.io.stdout, `continuation: ${g.continuation.mode}`);
9832
+ if (g.bounds) {
9833
+ const b = g.bounds;
9834
+ const parts = [
9835
+ b.maxLoopIterations !== void 0 ? `maxLoopIterations=${b.maxLoopIterations}` : null,
9836
+ b.runTimeoutMs !== void 0 ? `runTimeoutMs=${b.runTimeoutMs}` : null,
9837
+ b.maxCostUsd !== void 0 ? `maxCostUsd=${b.maxCostUsd}` : null
9838
+ ].filter(Boolean);
9839
+ writeLine(ctx.io.stdout, `bounds: ${parts.length ? parts.join(", ") : "(none)"}`);
9840
+ }
9841
+ if (g.completion?.lastVerdict) writeLine(ctx.io.stdout, `lastVerdict: ${JSON.stringify(g.completion.lastVerdict)}`);
9842
+ const it = progressIterations(g);
9843
+ if (it) writeLine(ctx.io.stdout, `iterations: ${it}`);
9844
+ if (typeof g.confidence === "number") writeLine(ctx.io.stdout, `confidence: ${g.confidence}`);
9845
+ if (Array.isArray(g.progress?.contributingRunIds)) writeLine(ctx.io.stdout, `contributingRuns: ${g.progress.contributingRunIds.length}`);
9846
+ if (g.finalState) writeLine(ctx.io.stdout, `finalState: ${g.finalState}`);
9847
+ writeLine(ctx.io.stdout, `createdAt: ${g.createdAt ?? ""}`);
9848
+ if (g.closedAt) writeLine(ctx.io.stdout, `closedAt: ${g.closedAt}`);
9849
+ }
9850
+ async function runGoalsCreate(ctx, argv) {
9851
+ const { options } = parseOptions(argv, {
9852
+ bool: ["--help"],
9853
+ value: ["--objective", "--judge", "--continuation", "--max-iterations", "--max-cost", "--timeout-ms"]
9854
+ });
9855
+ if (options.help || !options.objective) {
9856
+ write(ctx.io.stdout, "Usage: openwop goals create --objective <text> [--judge host|verifier] [--continuation <mode>] [--max-iterations <n>] [--max-cost <usd>] [--timeout-ms <ms>] [--json]\n");
9857
+ return options.help ? 0 : 2;
9858
+ }
9859
+ if (options.judge !== void 0 && !["host", "verifier"].includes(options.judge)) {
9860
+ throw new CliError("--judge must be one of: host, verifier", 2);
9861
+ }
9862
+ const body = {
9863
+ objective: options.objective,
9864
+ completion: { check: options.judge ?? "host" },
9865
+ continuation: { mode: options.continuation ?? "manual" }
9866
+ };
9867
+ const bounds = {};
9868
+ if (options.maxIterations !== void 0) {
9869
+ const n = Number(options.maxIterations);
9870
+ if (!Number.isInteger(n) || n <= 0) throw new CliError("--max-iterations must be a positive integer", 2);
9871
+ bounds.maxLoopIterations = n;
9872
+ }
9873
+ if (options.maxCost !== void 0) {
9874
+ const c = Number(options.maxCost);
9875
+ if (!Number.isFinite(c) || c < 0) throw new CliError("--max-cost must be a non-negative number (USD)", 2);
9876
+ bounds.maxCostUsd = c;
9877
+ }
9878
+ if (options.timeoutMs !== void 0) {
9879
+ const t = Number(options.timeoutMs);
9880
+ if (!Number.isInteger(t) || t < 0) throw new CliError("--timeout-ms must be a non-negative integer (milliseconds)", 2);
9881
+ bounds.runTimeoutMs = t;
9882
+ }
9883
+ if (Object.keys(bounds).length) body.bounds = bounds;
9884
+ await ensureAdvertised4(ctx);
9885
+ let res;
9886
+ try {
9887
+ res = await requestJson(ctx, "/v1/host/sample/goals", { method: "POST", body });
9888
+ } catch (err) {
9889
+ if (err instanceof HttpError && err.status === 422) {
9890
+ const detail = err.body?.message ?? "this host requires bounds on an active goal \u2014 pass --max-iterations, --max-cost, and/or --timeout-ms";
9891
+ throw new CliError(`goals: ${detail}`, 1);
9892
+ }
9893
+ gate4045(err);
9894
+ }
9895
+ const g = res.body ?? {};
9896
+ if (ctx.json) {
9897
+ writeJson(ctx.io.stdout, g);
9898
+ return exitForState2(g.state);
9899
+ }
9900
+ writeLine(ctx.io.stdout, `\u2713 Created goal ${g.goalId ?? g.id ?? ""} (state ${g.state ?? "active"}, check ${g.completion?.check ?? options.judge ?? "host"}).`);
9901
+ return exitForState2(g.state);
9902
+ }
9903
+ async function runGoalsTransition(ctx, argv, verb) {
9904
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
9905
+ if (options.help || positionals.length !== 1) {
9906
+ write(ctx.io.stdout, `Usage: openwop goals ${verb} <goalId> [--json]
9907
+ `);
9908
+ return options.help ? 0 : 2;
9909
+ }
9910
+ await ensureAdvertised4(ctx);
9911
+ let res;
9912
+ try {
9913
+ res = await requestJson(ctx, `/v1/host/sample/goals/${encodeURIComponent(positionals[0])}/${verb}`, { method: "POST" });
9914
+ } catch (err) {
9915
+ if (err instanceof HttpError && err.status === 409) {
9916
+ const status = err.body?.state;
9917
+ throw new CliError(`goals: cannot ${verb} ${positionals[0]} \u2014 the host reports it is ${status ?? "in a conflicting state"}.`, 1);
9918
+ }
9919
+ gate4045(err);
9920
+ }
9921
+ const g = res.body ?? {};
9922
+ if (ctx.json) {
9923
+ writeJson(ctx.io.stdout, g);
9924
+ return exitForState2(g.state);
9925
+ }
9926
+ writeLine(ctx.io.stdout, `\u2713 ${verb === "pause" ? "Paused" : "Resumed"} goal ${g.goalId ?? g.id ?? positionals[0]} \u2192 ${g.state ?? (verb === "pause" ? "paused" : "active")}.`);
9927
+ return exitForState2(g.state);
9928
+ }
9929
+ async function runGoalsAbandon(ctx, argv) {
9930
+ const { options, positionals } = parseOptions(argv, { bool: ["--help", "--yes"] });
9931
+ if (options.help || positionals.length !== 1) {
9932
+ write(ctx.io.stdout, "Usage: openwop goals abandon <goalId> [--yes]\n");
9933
+ return options.help ? 0 : 2;
9934
+ }
9935
+ if (!options.yes) {
9936
+ writeLine(ctx.io.stderr, `Refusing to abandon goal ${positionals[0]} without --yes.`);
9937
+ return 2;
9938
+ }
9939
+ await ensureAdvertised4(ctx);
9940
+ let res;
9941
+ try {
9942
+ res = await requestJson(ctx, `/v1/host/sample/goals/${encodeURIComponent(positionals[0])}/abandon`, { method: "POST" });
9943
+ } catch (err) {
9944
+ gate4045(err);
9945
+ }
9946
+ const g = res.body ?? {};
9947
+ if (ctx.json) {
9948
+ writeJson(ctx.io.stdout, g);
9949
+ return exitForState2(g.state ?? "abandoned");
9950
+ }
9951
+ writeLine(ctx.io.stdout, `\u2713 Abandoned goal ${g.goalId ?? g.id ?? positionals[0]} \u2192 ${g.state ?? "abandoned"}.`);
9952
+ return exitForState2(g.state ?? "abandoned");
9953
+ }
9954
+ function truncate4(s, n) {
9955
+ return s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
9956
+ }
9957
+
9958
+ // src/cli/portability.ts
9959
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "node:fs";
9960
+ import { resolve } from "node:path";
9961
+ var EXPORT_HELP = `Usage:
9962
+ openwop export [--kinds <kind>]... [--out <file>] [--json]
9963
+
9964
+ Produce a portable ExportBundle of this tenant's reusable estate \u2014 agents, packs,
9965
+ schedules, rosters, templates (RFC 0098). The bundle carries REFERENCES only: no
9966
+ credential values ever travel in it, just \`secretsToRebind\` refs the operator
9967
+ rebinds at the destination. Filter what's included with --kinds (repeatable).
9968
+
9969
+ The host produces the bundle; the CLI renders/saves what it returns and runs it
9970
+ through a secret redactor first (defense-in-depth \u2014 the bundle is already
9971
+ refs-only). Gated on \`capabilities.portability\`; fails closed when the host
9972
+ doesn't advertise the surface.
9973
+
9974
+ --kinds <kind> Limit the export to a kind (repeatable): agent | pack | schedule | roster | template.
9975
+ --out <file> Write the bundle JSON to a file instead of stdout.
9976
+ --json Print the raw (redacted) bundle JSON to stdout.
9977
+
9978
+ Exit codes: 0 ok \xB7 1 error / surface not served.
9979
+
9980
+ Examples:
9981
+ openwop export --json
9982
+ openwop export --kinds agent --kinds schedule --out estate.json
9983
+ `;
9984
+ var IMPORT_HELP = `Usage:
9985
+ openwop import <bundle-file> [--dry-run] [--json]
9986
+
9987
+ Import a portable ExportBundle into this tenant (RFC 0098). ALWAYS dry-run first:
9988
+ \`--dry-run\` returns a PLAN \u2014 what would be created / updated / skipped, and any
9989
+ CONFLICTS \u2014 and makes ZERO writes. Without --dry-run the host applies the bundle
9990
+ idempotently and re-owns every imported entity to your identity.
9991
+
9992
+ Secrets are refs, never values: the host rejects (422) a bundle that carries a
9993
+ literal credential value before it applies anything; the CLI never prints secret
9994
+ material (responses run through a redactor). The result reports \`secretsToRebind\`
9995
+ references you bind via the host's credential flow afterward.
9996
+
9997
+ --dry-run Preview the plan (creates/updates/skips/conflicts); no writes.
9998
+ --json Print the raw (redacted) plan/result JSON.
9999
+
10000
+ Exit codes:
10001
+ 0 applied (or a clean dry-run plan)
10002
+ 2 the plan has conflicts (nothing destructive was overwritten)
10003
+ 1 error \u2014 e.g. the bundle carried a literal credential or a dependsOn cycle (422), or you lack import scope (403)
10004
+
10005
+ Examples:
10006
+ openwop import estate.json --dry-run # preview first (no writes)
10007
+ openwop import estate.json # apply (idempotent, re-owned to you)
10008
+ `;
10009
+ var SECRET_KEY3 = /secret|token|password|private[-_]?key|client[-_]?secret|api[-_]?key|credential/i;
10010
+ var REF_ONLY_KEY = /^secretsToRebind$/;
10011
+ function redact3(value) {
10012
+ if (Array.isArray(value)) return value.map(redact3);
10013
+ if (value && typeof value === "object") {
10014
+ const out = {};
10015
+ for (const [k, v] of Object.entries(value)) {
10016
+ out[k] = SECRET_KEY3.test(k) && !REF_ONLY_KEY.test(k) ? "[redacted]" : redact3(v);
10017
+ }
10018
+ return out;
10019
+ }
10020
+ return value;
10021
+ }
10022
+ async function ensureAdvertised5(ctx) {
10023
+ const wk = await safeRequest(ctx, "/.well-known/openwop", { auth: false });
10024
+ if (!wk.ok) return;
10025
+ const body = wk.body && typeof wk.body === "object" ? wk.body : {};
10026
+ const advertised = !!(body.capabilities?.portability ?? body.portability);
10027
+ if (!advertised) {
10028
+ throw new CliError(
10029
+ "portability: this host does not advertise the export/import capability (capabilities.portability is absent from /.well-known/openwop). The host is the authority \u2014 refusing to guess.",
10030
+ 1
10031
+ );
10032
+ }
10033
+ }
10034
+ function gate4046(err, verb) {
10035
+ if (err instanceof HttpError && err.status === 404) {
10036
+ throw new CliError(
10037
+ `${verb}: the host does not mount /v1/host/sample/${verb} (the CLI renders the host's portability surface, it never fabricates a bundle).`,
10038
+ 1
10039
+ );
10040
+ }
10041
+ throw err;
10042
+ }
10043
+ async function runExport(ctx, argv) {
10044
+ const { options } = parseOptions(argv, { bool: ["--help"], value: ["--out"], multi: ["--kinds"] });
10045
+ if (options.help) {
10046
+ write(ctx.io.stdout, EXPORT_HELP);
10047
+ return 0;
10048
+ }
10049
+ await ensureAdvertised5(ctx);
10050
+ const kinds = Array.isArray(options.kinds) ? options.kinds : [];
10051
+ const path = kinds.length ? `/v1/host/sample/export?kinds=${encodeURIComponent(kinds.join(","))}` : "/v1/host/sample/export";
10052
+ let res;
10053
+ try {
10054
+ res = await requestJson(ctx, path);
10055
+ } catch (err) {
10056
+ gate4046(err, "export");
10057
+ }
10058
+ const bundle = redact3(res.body ?? {});
10059
+ if (options.out !== void 0) {
10060
+ const dest = resolve(ctx.cwd, options.out);
10061
+ writeFileSync9(dest, `${JSON.stringify(bundle, null, 2)}
10062
+ `);
10063
+ writeLine(ctx.io.stdout, `\u2713 Wrote export bundle to ${dest} (${summarizeKinds(bundle)}; refs only, no secret values).`);
10064
+ return 0;
10065
+ }
10066
+ if (ctx.json) {
10067
+ writeJson(ctx.io.stdout, bundle);
10068
+ return 0;
10069
+ }
10070
+ const items = Array.isArray(bundle.items) ? bundle.items : [];
10071
+ if (items.length === 0) {
10072
+ writeLine(ctx.io.stdout, "Export bundle is empty (no exportable estate for the requested kinds).");
10073
+ return 0;
10074
+ }
10075
+ const byKind = {};
10076
+ for (const it of items) byKind[it.kind ?? "unknown"] = (byKind[it.kind ?? "unknown"] ?? 0) + 1;
10077
+ const rows = Object.entries(byKind).map(([kind, count]) => ({ kind, count: String(count) }));
10078
+ writeLine(ctx.io.stdout, formatTable(rows, ["kind", "count"]));
10079
+ const rebind = Array.isArray(bundle.secretsToRebind) ? bundle.secretsToRebind : [];
10080
+ writeLine(ctx.io.stdout, `
10081
+ secretsToRebind: ${rebind.length} reference(s)${rebind.length ? ` \u2014 ${rebind.map((r) => r.provider ?? r.ref ?? r).join(", ")}` : ""} (refs only \u2014 bind at the destination).`);
10082
+ if (bundle.source?.origin) writeLine(ctx.io.stdout, `source.origin: ${bundle.source.origin}`);
10083
+ return 0;
10084
+ }
10085
+ function summarizeKinds(bundle) {
10086
+ const items = Array.isArray(bundle.items) ? bundle.items : [];
10087
+ return `${items.length} item(s)`;
10088
+ }
10089
+ async function runImport(ctx, argv) {
10090
+ const { options, positionals } = parseOptions(argv, { bool: ["--help", "--dry-run"] });
10091
+ if (options.help || positionals.length !== 1) {
10092
+ write(ctx.io.stdout, IMPORT_HELP);
10093
+ return options.help ? 0 : 2;
10094
+ }
10095
+ let bundle;
10096
+ try {
10097
+ const raw = readFileSync9(resolve(ctx.cwd, positionals[0]), "utf8");
10098
+ bundle = JSON.parse(raw);
10099
+ } catch (err) {
10100
+ throw new CliError(`import: could not read bundle file ${positionals[0]} \u2014 ${err instanceof Error ? err.message : "invalid JSON"}`, 2);
10101
+ }
10102
+ await ensureAdvertised5(ctx);
10103
+ const path = options.dryRun ? "/v1/host/sample/import?dryRun=true" : "/v1/host/sample/import";
10104
+ let res;
10105
+ try {
10106
+ res = await requestJson(ctx, path, { method: "POST", body: { bundle } });
10107
+ } catch (err) {
10108
+ if (err instanceof HttpError && err.status === 422) {
10109
+ const detail = err.body?.message ?? "the bundle carried a literal credential value or a dependsOn cycle";
10110
+ throw new CliError(`import: the host refused the bundle (422) \u2014 ${detail}.`, 1);
10111
+ }
10112
+ if (err instanceof HttpError && err.status === 403) {
10113
+ throw new CliError(`import: the host refused the import (403) \u2014 you lack import scope (the host is the authority).`, 1);
10114
+ }
10115
+ gate4046(err, "import");
10116
+ }
10117
+ const out = redact3(res.body ?? {});
10118
+ if (ctx.json) {
10119
+ writeJson(ctx.io.stdout, out);
10120
+ return exitForImport(out);
10121
+ }
10122
+ return options.dryRun ? renderPlan(ctx, out) : renderResult(ctx, out);
10123
+ }
10124
+ function exitForImport(out) {
10125
+ const conflicts = Array.isArray(out.conflicts) ? out.conflicts : Array.isArray(out.items) ? out.items.filter((i) => i.result === "conflict") : [];
10126
+ return conflicts.length > 0 ? 2 : 0;
10127
+ }
10128
+ function renderPlan(ctx, plan) {
10129
+ const creates = Array.isArray(plan.creates) ? plan.creates : [];
10130
+ const updates = Array.isArray(plan.updates) ? plan.updates : [];
10131
+ const skips = Array.isArray(plan.skips) ? plan.skips : [];
10132
+ const conflicts = Array.isArray(plan.conflicts) ? plan.conflicts : [];
10133
+ writeLine(ctx.io.stdout, `Import plan (dry-run \u2014 no writes made):`);
10134
+ writeLine(ctx.io.stdout, ` create: ${creates.length}`);
10135
+ writeLine(ctx.io.stdout, ` update: ${updates.length}`);
10136
+ writeLine(ctx.io.stdout, ` skip: ${skips.length}`);
10137
+ writeLine(ctx.io.stdout, ` conflict: ${conflicts.length}`);
10138
+ if (conflicts.length) {
10139
+ writeLine(ctx.io.stdout, `
10140
+ Conflicts (resolve before applying \u2014 nothing will be overwritten):`);
10141
+ for (const c of conflicts) writeLine(ctx.io.stdout, ` \xB7 ${c.kind ?? ""} ${c.id ?? c.key ?? ""}${c.reason ? `: ${c.reason}` : ""}`);
10142
+ }
10143
+ const rebind = Array.isArray(plan.secretsToRebind) ? plan.secretsToRebind : [];
10144
+ if (rebind.length) writeLine(ctx.io.stdout, `
10145
+ secretsToRebind after apply: ${rebind.length} reference(s) (refs only).`);
10146
+ return exitForImport(plan);
10147
+ }
10148
+ function renderResult(ctx, result) {
10149
+ const items = Array.isArray(result.items) ? result.items : [];
10150
+ const counts = result.counts ?? tally(items);
10151
+ writeLine(ctx.io.stdout, `Import applied (idempotent; entities re-owned to you):`);
10152
+ writeLine(ctx.io.stdout, ` created: ${counts.created ?? 0}`);
10153
+ writeLine(ctx.io.stdout, ` updated: ${counts.updated ?? 0}`);
10154
+ writeLine(ctx.io.stdout, ` skipped: ${counts.skipped ?? 0}`);
10155
+ if (counts.conflict) writeLine(ctx.io.stdout, ` conflict: ${counts.conflict}`);
10156
+ if (counts.failed) writeLine(ctx.io.stdout, ` failed: ${counts.failed}`);
10157
+ const rebind = Array.isArray(result.secretsToRebind) ? result.secretsToRebind : [];
10158
+ writeLine(ctx.io.stdout, `
10159
+ secretsToRebind: ${rebind.length} reference(s)${rebind.length ? ` \u2014 ${rebind.map((r) => r.provider ?? r.ref ?? r).join(", ")}` : ""} (bind via the host's credential flow).`);
10160
+ return exitForImport(result);
10161
+ }
10162
+ function tally(items) {
10163
+ const c = {};
10164
+ for (const it of items) c[it.result ?? "unknown"] = (c[it.result ?? "unknown"] ?? 0) + 1;
10165
+ return { created: c.created ?? 0, updated: c.updated ?? 0, skipped: c.skipped ?? 0, conflict: c.conflict ?? 0, failed: c.failed ?? 0 };
10166
+ }
10167
+
10168
+ // src/cli/triggers.ts
10169
+ var TRIGGERS_HELP = `Usage:
10170
+ openwop triggers register --source <webhook|email|form> --workflow <id> [--dedup] [--verification <mode>] [--json]
10171
+ openwop triggers list [--state <s>] [--source <s>] [--json]
10172
+ openwop triggers get <subscriptionId> [--json]
10173
+
10174
+ External-event trigger subscriptions (RFC 0099). A subscription binds an external
10175
+ SOURCE (webhook / email / form) to a WORKFLOW the caller can start; a verified
10176
+ inbound event then starts a run with the event as \`ctx.triggerData\`. 'register'
10177
+ creates the subscription and returns its source-specific binding (the ingest URL /
10178
+ address); 'list'/'get' render the host's subscription state machine
10179
+ (active | paused | failed | dead-lettered).
10180
+
10181
+ The host is the authority \u2014 it verifies the source, dedups, and runs the durable
10182
+ delivery state machine; the CLI renders its resolved view and relays the
10183
+ registration. Gated on \`capabilities.triggerBridge\`; fails closed when the host
10184
+ doesn't advertise the bridge (or, for register, the requested source under
10185
+ \`triggerBridge.ingestion.externalSources\`).
10186
+
10187
+ Endpoints:
10188
+ register POST /v1/trigger-subscriptions
10189
+ list GET /v1/trigger-subscriptions
10190
+ get GET /v1/trigger-subscriptions/{id} (falls back to filtering the list)
10191
+
10192
+ --source <s> (register/list) webhook | email | form.
10193
+ --workflow <id> (register) The workflow the subscription starts (must be one you can start).
10194
+ --dedup (register) Enable effectively-once dedup on the subscription.
10195
+ --verification <mode> (register) The host's verification policy (e.g. none | optional | required).
10196
+ --state <s> (list) Filter by subscription state.
10197
+ --json Print the raw host response instead of the rendered view.
10198
+
10199
+ Secret boundary: a webhook binding secret is returned ONCE at creation \u2014 store it
10200
+ then; the CLI never persists it and re-reads show only the secret fingerprint.
10201
+
10202
+ Exit codes (get reflects the subscription's state, so scripts can gate):
10203
+ 0 active 3 paused 1 failed / dead-lettered or error
10204
+ \`list\` exits 0 on success.
10205
+
10206
+ Examples:
10207
+ openwop triggers register --source webhook --workflow wf_intake --dedup --verification required
10208
+ openwop triggers list --state active
10209
+ openwop triggers get sub_123 --json
10210
+ `;
10211
+ var SUBCOMMANDS5 = ["register", "list", "get"];
10212
+ async function runTriggers(ctx, argv) {
10213
+ const sub = argv[0] ?? "list";
10214
+ if (sub === "--help" || sub === "-h") {
10215
+ write(ctx.io.stdout, TRIGGERS_HELP);
10216
+ return 0;
10217
+ }
10218
+ const args = argv.slice(SUBCOMMANDS5.includes(sub) ? 1 : 0);
10219
+ switch (sub) {
10220
+ case "register":
10221
+ return await runRegister(ctx, args);
10222
+ case "list":
10223
+ return await runList3(ctx, args);
10224
+ case "get":
10225
+ return await runGet4(ctx, args);
10226
+ default:
10227
+ throw new CliError(`Unknown triggers command: ${sub}
10228
+ Run \`openwop triggers --help\` for usage.`);
10229
+ }
10230
+ }
10231
+ function exitForState3(state) {
10232
+ if (state === "active") return 0;
10233
+ if (state === "paused") return 3;
10234
+ return 1;
10235
+ }
10236
+ async function triggerBridgeCaps(ctx) {
10237
+ const wk = await safeRequest(ctx, "/.well-known/openwop", { auth: false });
10238
+ if (!wk.ok) return void 0;
10239
+ const body = wk.body && typeof wk.body === "object" ? wk.body : {};
10240
+ return body.capabilities?.triggerBridge ?? body.triggerBridge ?? null;
10241
+ }
10242
+ async function ensureAdvertised6(ctx) {
10243
+ const tb = await triggerBridgeCaps(ctx);
10244
+ if (tb === void 0) return;
10245
+ if (!tb) {
10246
+ throw new CliError(
10247
+ "triggers: this host does not advertise the trigger bridge (capabilities.triggerBridge is absent from /.well-known/openwop). The host is the authority \u2014 refusing to guess.",
10248
+ 1
10249
+ );
10250
+ }
10251
+ }
10252
+ function gate4047(err) {
10253
+ if (err instanceof HttpError && err.status === 404) {
10254
+ const detail = err.body && typeof err.body === "object" && typeof err.body.message === "string" ? err.body.message : "not found";
10255
+ throw new CliError(
10256
+ `triggers: ${detail} (the host must serve /v1/trigger-subscriptions; the CLI renders the host's subscriptions, it never fabricates one).`,
10257
+ 1
10258
+ );
10259
+ }
10260
+ throw err;
10261
+ }
10262
+ async function runRegister(ctx, argv) {
10263
+ const { options } = parseOptions(argv, {
10264
+ bool: ["--help", "--dedup"],
10265
+ value: ["--source", "--workflow", "--verification"]
10266
+ });
10267
+ if (options.help || !options.source || !options.workflow) {
10268
+ write(ctx.io.stdout, "Usage: openwop triggers register --source <webhook|email|form> --workflow <id> [--dedup] [--verification <mode>] [--json]\n");
10269
+ return options.help ? 0 : 2;
10270
+ }
10271
+ if (!["webhook", "email", "form"].includes(options.source)) {
10272
+ throw new CliError("--source must be one of: webhook, email, form", 2);
10273
+ }
10274
+ const tb = await triggerBridgeCaps(ctx);
10275
+ if (tb) {
10276
+ const ext = tb.ingestion?.externalSources;
10277
+ if (Array.isArray(ext) && !ext.includes(options.source)) {
10278
+ throw new CliError(
10279
+ `triggers: this host does not externally-ingest the '${options.source}' source (capabilities.triggerBridge.ingestion.externalSources = ${JSON.stringify(ext)}).`,
10280
+ 1
10281
+ );
10282
+ }
10283
+ if (tb.ingestion === void 0 && tb.supported) {
10284
+ throw new CliError(
10285
+ "triggers: this host advertises triggerBridge but not external ingestion (capabilities.triggerBridge.ingestion is absent) \u2014 it cannot register an external-event subscription.",
10286
+ 1
10287
+ );
10288
+ }
10289
+ if (tb.ingestion && tb.ingestion.registrationEndpoint === false) {
10290
+ throw new CliError(
10291
+ "triggers: this host does not serve the registration endpoint (capabilities.triggerBridge.ingestion.registrationEndpoint is false) \u2014 POST /v1/trigger-subscriptions is unavailable here.",
10292
+ 1
10293
+ );
10294
+ }
10295
+ } else if (tb === null) {
10296
+ throw new CliError("triggers: this host does not advertise the trigger bridge. The host is the authority \u2014 refusing to guess.", 1);
10297
+ }
10298
+ const body = { source: options.source, workflowId: options.workflow };
10299
+ if (options.dedup) body.dedupEnabled = true;
10300
+ if (options.verification !== void 0) body.verification = { mode: options.verification };
10301
+ let res;
10302
+ try {
10303
+ res = await requestJson(ctx, "/v1/trigger-subscriptions", { method: "POST", body });
10304
+ } catch (err) {
10305
+ if (err instanceof HttpError && err.status === 403) {
10306
+ throw new CliError(`triggers: the host refused the registration (403) \u2014 you cannot bind workflow ${options.workflow} (you must be able to start it; the host is the authority).`, 1);
10307
+ }
10308
+ if (err instanceof HttpError && err.status === 422) {
10309
+ const detail = err.body?.message ?? "registration rejected";
10310
+ throw new CliError(`triggers: ${detail}`, 1);
10311
+ }
10312
+ if (err instanceof HttpError && err.status === 501) {
10313
+ throw new CliError("triggers: the host does not implement the registration endpoint (501) \u2014 it does not advertise triggerBridge.ingestion.registrationEndpoint.", 1);
10314
+ }
10315
+ gate4047(err);
10316
+ }
10317
+ const out = res.body ?? {};
10318
+ if (ctx.json) {
10319
+ writeJson(ctx.io.stdout, out);
10320
+ return exitForState3(out.subscription?.state ?? out.state);
10321
+ }
10322
+ const s = out.subscription ?? out;
10323
+ writeLine(ctx.io.stdout, `\u2713 Registered ${s.source ?? options.source} subscription ${s.subscriptionId ?? s.id ?? ""} (state ${s.state ?? "active"}).`);
10324
+ const binding = out.binding ?? s.binding;
10325
+ if (binding) {
10326
+ if (binding.ingestUrl) writeLine(ctx.io.stdout, `ingestUrl: ${binding.ingestUrl}`);
10327
+ if (binding.ingestAddress) writeLine(ctx.io.stdout, `ingestAddress: ${binding.ingestAddress}`);
10328
+ if (binding.secretFingerprint) writeLine(ctx.io.stdout, `secretFingerprint: ${binding.secretFingerprint}`);
10329
+ if (binding.secret) {
10330
+ writeLine(ctx.io.stdout, `secret: ${binding.secret}`);
10331
+ writeLine(ctx.io.stderr, "\u26A0 This binding secret is shown ONCE and is not stored by the CLI \u2014 copy it now. Re-reading the subscription returns only the fingerprint.");
10332
+ }
10333
+ }
10334
+ return exitForState3(s.state);
10335
+ }
10336
+ async function fetchList(ctx, query) {
10337
+ const path = `/v1/trigger-subscriptions${query.length ? `?${query.join("&")}` : ""}`;
10338
+ let res;
10339
+ try {
10340
+ res = await requestJson(ctx, path);
10341
+ } catch (err) {
10342
+ gate4047(err);
10343
+ }
10344
+ const b = res.body;
10345
+ return Array.isArray(b?.subscriptions) ? b.subscriptions : Array.isArray(b?.items) ? b.items : Array.isArray(b) ? b : [];
10346
+ }
10347
+ async function runList3(ctx, argv) {
10348
+ const { options } = parseOptions(argv, { bool: ["--help"], value: ["--state", "--source"] });
10349
+ if (options.help) {
10350
+ write(ctx.io.stdout, TRIGGERS_HELP);
10351
+ return 0;
10352
+ }
10353
+ await ensureAdvertised6(ctx);
10354
+ const query = [];
10355
+ if (options.state !== void 0) query.push(`state=${encodeURIComponent(options.state)}`);
10356
+ if (options.source !== void 0) query.push(`source=${encodeURIComponent(options.source)}`);
10357
+ const subs = await fetchList(ctx, query);
10358
+ if (ctx.json) {
10359
+ writeJson(ctx.io.stdout, { subscriptions: subs });
10360
+ return 0;
10361
+ }
10362
+ if (subs.length === 0) {
10363
+ writeLine(ctx.io.stdout, `No trigger subscriptions${options.state ? ` in state ${options.state}` : ""}${options.source ? ` for source ${options.source}` : ""} on this host.`);
10364
+ return 0;
10365
+ }
10366
+ const rows = subs.map((s) => ({
10367
+ subscriptionId: s.subscriptionId ?? s.id,
10368
+ source: s.source ?? "",
10369
+ state: s.state ?? "",
10370
+ workflow: s.workflowId ?? "",
10371
+ dedup: s.dedupEnabled ? "yes" : "no",
10372
+ createdAt: s.createdAt ?? ""
10373
+ }));
10374
+ writeLine(ctx.io.stdout, formatTable(rows, ["subscriptionId", "source", "state", "workflow", "dedup", "createdAt"]));
10375
+ return 0;
10376
+ }
10377
+ async function runGet4(ctx, argv) {
10378
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
10379
+ if (options.help || positionals.length !== 1) {
10380
+ write(ctx.io.stdout, "Usage: openwop triggers get <subscriptionId> [--json]\n");
10381
+ return options.help ? 0 : 2;
10382
+ }
10383
+ await ensureAdvertised6(ctx);
10384
+ const id = positionals[0];
10385
+ let s;
10386
+ try {
10387
+ const res = await requestJson(ctx, `/v1/trigger-subscriptions/${encodeURIComponent(id)}`);
10388
+ s = res.body ?? {};
10389
+ } catch (err) {
10390
+ if (err instanceof HttpError && (err.status === 404 || err.status === 405)) {
10391
+ const found = (await fetchList(ctx, [])).find((x) => (x.subscriptionId ?? x.id) === id);
10392
+ if (!found) throw new CliError(`triggers: no subscription found with id ${id}.`, 1);
10393
+ s = found;
10394
+ } else {
10395
+ gate4047(err);
10396
+ }
10397
+ }
10398
+ if (ctx.json) {
10399
+ writeJson(ctx.io.stdout, s);
10400
+ return exitForState3(s.state);
10401
+ }
10402
+ writeLine(ctx.io.stdout, `subscriptionId: ${s.subscriptionId ?? s.id ?? id}`);
10403
+ writeLine(ctx.io.stdout, `source: ${s.source ?? ""}`);
10404
+ writeLine(ctx.io.stdout, `state: ${s.state ?? ""}`);
10405
+ if (s.workflowId) writeLine(ctx.io.stdout, `workflowId: ${s.workflowId}`);
10406
+ writeLine(ctx.io.stdout, `dedupEnabled: ${s.dedupEnabled ? "yes" : "no"}`);
10407
+ if (s.secretFingerprint) writeLine(ctx.io.stdout, `secretFingerprint: ${s.secretFingerprint}`);
10408
+ if (s.retryPolicy) writeLine(ctx.io.stdout, `retryPolicy: ${JSON.stringify(s.retryPolicy)}`);
10409
+ if (s.lastDelivery) writeLine(ctx.io.stdout, `lastDelivery: ${JSON.stringify(s.lastDelivery)}`);
10410
+ writeLine(ctx.io.stdout, `createdAt: ${s.createdAt ?? ""}`);
10411
+ return exitForState3(s.state);
10412
+ }
10413
+
10414
+ // src/cli/a2a.ts
10415
+ var A2A_HELP = `Usage:
10416
+ openwop a2a status [--json]
10417
+ openwop a2a task <taskId> [--json]
10418
+
10419
+ Async / durable A2A tasks (RFC 0100). When a host advertises
10420
+ \`capabilities.a2a.durableTasks\`, every backing run has a durable A2ATaskState
10421
+ (taskId === runId) that survives caller disconnect, host restart, and HITL pauses.
10422
+ 'status' shows what the host advertises; 'task' reads one durable task's live
10423
+ state.
10424
+
10425
+ The host is the authority \u2014 the A2ATaskState is the persisted projection of the
10426
+ run's status; the CLI renders it and never derives a task state locally. The
10427
+ record is content-free (no run inputs/outputs/artifacts/credentials). Gated on
10428
+ \`capabilities.a2a\`; the durable read needs \`durableTasks: true\`. Fails closed
10429
+ when the host doesn't advertise A2A.
10430
+
10431
+ Endpoints:
10432
+ status reads /.well-known/openwop \u2192 capabilities.a2a
10433
+ task GET /v1/host/sample/a2a/tasks/{taskId}
10434
+
10435
+ --json Print the raw host response instead of the rendered view.
10436
+
10437
+ Exit codes (task reflects the A2A task state, so scripts can gate):
10438
+ 0 completed
10439
+ 3 in progress / needs input (submitted | working | input-required | auth-required)
10440
+ 1 failed | canceled | rejected, or error
10441
+ \`status\` exits 0 when A2A is advertised, 1 when it is not.
10442
+
10443
+ Examples:
10444
+ openwop a2a status
10445
+ openwop a2a task run_abc123 --json
10446
+ `;
10447
+ var SUBCOMMANDS6 = ["status", "task"];
10448
+ async function runA2a(ctx, argv) {
10449
+ const sub = argv[0] ?? "status";
10450
+ if (sub === "--help" || sub === "-h") {
10451
+ write(ctx.io.stdout, A2A_HELP);
10452
+ return 0;
10453
+ }
10454
+ const args = argv.slice(SUBCOMMANDS6.includes(sub) ? 1 : 0);
10455
+ switch (sub) {
10456
+ case "status":
10457
+ return await runStatus2(ctx, args);
10458
+ case "task":
10459
+ return await runTask(ctx, args);
10460
+ default:
10461
+ throw new CliError(`Unknown a2a command: ${sub}
10462
+ Run \`openwop a2a --help\` for usage.`);
10463
+ }
10464
+ }
10465
+ function exitForState4(state) {
10466
+ if (state === "completed") return 0;
10467
+ if (state === "submitted" || state === "working" || state === "input-required" || state === "auth-required") return 3;
10468
+ return 1;
10469
+ }
10470
+ async function a2aCaps(ctx) {
10471
+ const wk = await safeRequest(ctx, "/.well-known/openwop", { auth: false });
10472
+ if (!wk.ok) return void 0;
10473
+ const body = wk.body && typeof wk.body === "object" ? wk.body : {};
10474
+ return body.capabilities?.a2a ?? body.a2a ?? null;
10475
+ }
10476
+ async function runStatus2(ctx, argv) {
10477
+ const { options } = parseOptions(argv, { bool: ["--help"] });
10478
+ if (options.help) {
10479
+ write(ctx.io.stdout, A2A_HELP);
10480
+ return 0;
10481
+ }
10482
+ const a2a = await a2aCaps(ctx);
10483
+ if (ctx.json) {
10484
+ writeJson(ctx.io.stdout, { a2a: a2a ?? null });
10485
+ return a2a ? 0 : 1;
10486
+ }
10487
+ if (!a2a) {
10488
+ writeLine(ctx.io.stdout, "This host does not advertise A2A (capabilities.a2a absent).");
10489
+ return 1;
10490
+ }
10491
+ writeLine(ctx.io.stdout, `a2a.supported: ${a2a.supported ? "yes" : "no"}`);
10492
+ if (a2a.agentCardUrl) writeLine(ctx.io.stdout, `agentCardUrl: ${a2a.agentCardUrl}`);
10493
+ writeLine(ctx.io.stdout, `streaming: ${a2a.streaming ? "yes" : "no"}`);
10494
+ writeLine(ctx.io.stdout, `pushNotifications: ${a2a.pushNotifications ? "yes" : "no"}`);
10495
+ writeLine(ctx.io.stdout, `durableTasks: ${a2a.durableTasks ? "yes" : "no"}`);
10496
+ if (!a2a.durableTasks) writeLine(ctx.io.stdout, "(durable task reads require durableTasks: true)");
10497
+ return 0;
10498
+ }
10499
+ async function runTask(ctx, argv) {
10500
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
10501
+ if (options.help || positionals.length !== 1) {
10502
+ write(ctx.io.stdout, "Usage: openwop a2a task <taskId> [--json]\n");
10503
+ return options.help ? 0 : 2;
10504
+ }
10505
+ const a2a = await a2aCaps(ctx);
10506
+ if (a2a === null) {
10507
+ throw new CliError("a2a: this host does not advertise A2A (capabilities.a2a absent from /.well-known/openwop). The host is the authority \u2014 refusing to guess.", 1);
10508
+ }
10509
+ if (a2a && a2a.durableTasks === false) {
10510
+ throw new CliError("a2a: this host advertises A2A but not durable tasks (capabilities.a2a.durableTasks is false) \u2014 there is no durable task to read.", 1);
10511
+ }
10512
+ const id = positionals[0];
10513
+ let t;
10514
+ try {
10515
+ const res = await requestJson(ctx, `/v1/host/sample/a2a/tasks/${encodeURIComponent(id)}`);
10516
+ t = res.body ?? {};
10517
+ } catch (err) {
10518
+ if (err instanceof HttpError && err.status === 404) {
10519
+ throw new CliError(`a2a: no durable task ${id} (the host must serve /v1/host/sample/a2a/tasks/{taskId}; taskId equals the backing runId).`, 1);
10520
+ }
10521
+ throw err;
10522
+ }
10523
+ if (ctx.json) {
10524
+ writeJson(ctx.io.stdout, t);
10525
+ return exitForState4(t.state);
10526
+ }
10527
+ writeLine(ctx.io.stdout, `taskId: ${t.taskId ?? id}`);
10528
+ writeLine(ctx.io.stdout, `runId: ${t.runId ?? ""}`);
10529
+ writeLine(ctx.io.stdout, `state: ${t.state ?? ""}`);
10530
+ if (t.interruptKind) writeLine(ctx.io.stdout, `interruptKind: ${t.interruptKind}`);
10531
+ if (t.contextId) writeLine(ctx.io.stdout, `contextId: ${t.contextId}`);
10532
+ if (t.pushConfig) writeLine(ctx.io.stdout, `pushConfig: ${JSON.stringify(t.pushConfig)}`);
10533
+ writeLine(ctx.io.stdout, `updatedAt: ${t.updatedAt ?? ""}`);
10534
+ return exitForState4(t.state);
10535
+ }
10536
+
6659
10537
  // src/cli.ts
6660
10538
  async function runCli(argv, options = {}) {
6661
10539
  const io = options.io ?? {
@@ -6780,6 +10658,46 @@ async function runCli(argv, options = {}) {
6780
10658
  return await runAccount(ctx, commandArgs);
6781
10659
  case "admin":
6782
10660
  return await runAdmin(ctx, commandArgs);
10661
+ case "governance":
10662
+ case "policy":
10663
+ return await runGovernance(ctx, commandArgs);
10664
+ case "approvals":
10665
+ case "approval":
10666
+ return await runApprovals(ctx, commandArgs);
10667
+ case "consent":
10668
+ return await runConsent(ctx, commandArgs);
10669
+ case "mcp":
10670
+ return await runMcp(ctx, commandArgs);
10671
+ case "connections":
10672
+ case "conn":
10673
+ return await runConnections(ctx, commandArgs);
10674
+ case "profiles":
10675
+ return await runProfiles(ctx, commandArgs);
10676
+ case "toggles":
10677
+ return await runToggles(ctx, commandArgs);
10678
+ case "users":
10679
+ return await runUsers(ctx, commandArgs);
10680
+ case "workforces":
10681
+ case "fleet":
10682
+ return await runWorkforces(ctx, commandArgs);
10683
+ case "auth":
10684
+ case "sso":
10685
+ return await runAuth(ctx, commandArgs);
10686
+ case "analytics":
10687
+ case "usage":
10688
+ return await runAnalytics(ctx, commandArgs);
10689
+ case "proposals":
10690
+ return await runProposals(ctx, commandArgs);
10691
+ case "goals":
10692
+ return await runGoals(ctx, commandArgs);
10693
+ case "export":
10694
+ return await runExport(ctx, commandArgs);
10695
+ case "import":
10696
+ return await runImport(ctx, commandArgs);
10697
+ case "triggers":
10698
+ return await runTriggers(ctx, commandArgs);
10699
+ case "a2a":
10700
+ return await runA2a(ctx, commandArgs);
6783
10701
  default:
6784
10702
  throw new CliError(`Unknown command: ${command}
6785
10703
  Run \`openwop --help\` for usage.`);
@@ -6846,7 +10764,30 @@ function showHelp(io, command) {
6846
10764
  prompt: PROMPTS_HELP,
6847
10765
  notify: NOTIFY_HELP,
6848
10766
  account: ACCOUNT_HELP,
6849
- admin: ADMIN_HELP
10767
+ admin: ADMIN_HELP,
10768
+ governance: GOVERNANCE_HELP,
10769
+ policy: GOVERNANCE_HELP,
10770
+ approvals: APPROVALS_HELP,
10771
+ approval: APPROVALS_HELP,
10772
+ consent: CONSENT_HELP,
10773
+ mcp: MCP_HELP,
10774
+ connections: CONNECTIONS_HELP,
10775
+ conn: CONNECTIONS_HELP,
10776
+ profiles: PROFILES_HELP,
10777
+ toggles: TOGGLES_HELP,
10778
+ users: USERS_HELP,
10779
+ workforces: WORKFORCES_HELP,
10780
+ fleet: WORKFORCES_HELP,
10781
+ auth: AUTH_HELP,
10782
+ sso: AUTH_HELP,
10783
+ analytics: ANALYTICS_HELP,
10784
+ usage: ANALYTICS_HELP,
10785
+ proposals: PROPOSALS_HELP,
10786
+ goals: GOALS_HELP,
10787
+ export: EXPORT_HELP,
10788
+ import: IMPORT_HELP,
10789
+ triggers: TRIGGERS_HELP,
10790
+ a2a: A2A_HELP
6850
10791
  };
6851
10792
  write(io.stdout, map[command] ?? ROOT_HELP);
6852
10793
  return 0;
@@ -6933,8 +10874,9 @@ Commands:
6933
10874
  webhooks add Register a webhook subscription
6934
10875
  webhooks remove Delete a webhook subscription
6935
10876
  webhooks test Fire a signed test delivery
6936
- cron list List scheduled (cron) jobs
10877
+ cron list List scheduled (cron) jobs (--roster <id> to filter)
6937
10878
  cron add Schedule a cron job
10879
+ cron enable|disable Toggle a scheduled job active/inert
6938
10880
  cron remove Delete a scheduled job
6939
10881
  cron trigger Fire a scheduled job once now
6940
10882
  messaging connectors Manage messaging relay connectors
@@ -6961,7 +10903,31 @@ Commands:
6961
10903
  media generate-image Generate an image via the demo media route (stubbed)
6962
10904
  media transcribe Transcribe an audio file (stubbed)
6963
10905
  media synthesize Synthesize speech from text (stubbed)
10906
+ governance policy Get/set tenant governance policy (alias: policy)
10907
+ governance audit Read the host audit log (tenant-scoped)
6964
10908
  conformance Run the OpenWOP conformance CLI from this repo
10909
+ approvals list List the approval inbox (agents propose, humans dispose)
10910
+ approvals claim Approve a proposal (starts the run); reject to dismiss
10911
+ consent policy Show/set the tenant consent policy (org-scoped)
10912
+ consent records List or read tenant consent records (+ GDPR erase)
10913
+ consent public Read/record a visitor's consent (public, unauthed)
10914
+ mcp tools|resources|prompts MCP client for the host JSON-RPC server mount (RFC 0020)
10915
+ connections list List host connections + status (test/authorize; alias: conn)
10916
+ connections authorize Mint an OAuth consent URL for a provider (URL only)
10917
+ profiles me Show/edit your self-service profile (persona/skills/portfolio)
10918
+ profiles get View a tenant member's profile; endorse/pin via subcommands
10919
+ toggles list|get Render the host's resolved feature-toggle assignments (read-only)
10920
+ users list|me Tenant identity directory + account lifecycle (not RBAC; see orgs)
10921
+ workforces list List governed workforces (durable orchestration; alias: fleet)
10922
+ workforces metrics Aggregate telemetry + governance/autonomy for a workforce
10923
+ auth status Enterprise SSO/SAML/SCIM status + SP metadata (alias: sso)
10924
+ auth saml|scim SAML SSO metadata/login/validate; SCIM provisioning seam
10925
+ analytics summary Org-scoped usage analytics: summary/events + public collect (alias: usage)
10926
+ proposals list Reviewable-learning proposals (RFC 0096): list/get/revise/apply/reject/archive
10927
+ goals list Standing goals (RFC 0097): list/get/create/pause/resume/abandon \u2014 judge-based completion
10928
+ export / import Agent-platform portability (RFC 0098): export a refs-only bundle; import --dry-run then apply
10929
+ triggers register External-event trigger subscriptions (RFC 0099): register/list/get a source\u2192workflow binding
10930
+ a2a task Async/durable A2A tasks (RFC 0100): read a durable task's live state (+ a2a status)
6965
10931
 
6966
10932
  Examples:
6967
10933
  openwop onboard