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