@openwop/cli 1.4.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -134,7 +134,7 @@ openwop packs search ads # browse the signed pack registry
134
134
 
135
135
  The `onboard` wizard walks you through:
136
136
 
137
- 1. **Host URL** — `https://app.openwop.dev/api` (shared demo), `http://localhost:8080` (local), or a custom URL.
137
+ 1. **Host URL** — `https://api.openwop.dev` (shared demo; `https://app.openwop.dev/api` also works), `http://localhost:8080` (local), or a custom URL.
138
138
  2. **AI provider** — `anthropic`, `openai`, `google`, or `minimax` (matches what the demo backend dispatches to).
139
139
  3. **API key** — auto-detects `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GOOGLE_API_KEY` / `MINIMAX_API_KEY`, or hidden-input via raw-mode stdin. The key is POSTed to `/v1/host/openwop-app/byok/secrets` on the configured host. **The key is never written to your local config file** — only a credential ref pointer is stored.
140
140
  4. **Model** — provider-specific recommended defaults plus a custom option.
package/dist/cli.js CHANGED
@@ -578,7 +578,7 @@ function errText(err) {
578
578
  // package.json
579
579
  var package_default = {
580
580
  name: "@openwop/cli",
581
- version: "1.4.1",
581
+ version: "1.6.0",
582
582
  description: "OpenWOP CLI for operating the workflow-engine demo app and OpenWOP-compatible hosts.",
583
583
  license: "Apache-2.0",
584
584
  type: "module",
@@ -667,7 +667,10 @@ var PROVIDER_CATALOG = {
667
667
  }
668
668
  };
669
669
  var HOST_PRESETS = [
670
- { key: "shared", label: "Shared demo at https://app.openwop.dev/api (recommended for trying things out)", url: "https://app.openwop.dev/api" },
670
+ // openwop-app ADR 0827: the protocol origin, served straight from Cloud Run with
671
+ // no CDN in front (`https://app.openwop.dev/api` still works; its edge drops
672
+ // conditional requests). `openwop login` opens the host-named web page.
673
+ { key: "shared", label: "Shared demo at https://api.openwop.dev (recommended for trying things out)", url: "https://api.openwop.dev" },
671
674
  { key: "local", label: "Local demo at http://localhost:8080 (run `openwop demo start` to launch)", url: "http://localhost:8080" }
672
675
  ];
673
676
 
@@ -1206,6 +1209,14 @@ function hostRootFor(path, roots) {
1206
1209
  }
1207
1210
  return best === null ? null : `${roots[best]}${path.slice(best.length)}`;
1208
1211
  }
1212
+ function hostSurfaceAdvertised(discovery, v1Prefix) {
1213
+ if (!discovery || typeof discovery !== "object") return false;
1214
+ const { paths, extensions } = discovery;
1215
+ if (paths !== null && typeof paths === "object") {
1216
+ return Object.keys(paths).some((p) => p.startsWith(v1Prefix));
1217
+ }
1218
+ return hostRootFor(v1Prefix, hostRootsFrom(extensions)) !== null;
1219
+ }
1209
1220
  async function negotiateMajor(ctx) {
1210
1221
  if (ctx.protocolMajor !== void 0) return ctx.protocolMajor;
1211
1222
  const pinned = ctx.env?.OPENWOP_PROTOCOL_MAJOR;
@@ -14925,13 +14936,21 @@ Approve a code ONLY if you just ran \`openwop login\` yourself.
14925
14936
  Exit codes: 0 signed in / signed out \xB7 1 denied, expired or not available on this host \xB7 2 usage.
14926
14937
 
14927
14938
  Examples:
14928
- export OPENWOP_BASE_URL=https://app.openwop.dev/api
14939
+ export OPENWOP_BASE_URL=https://api.openwop.dev
14929
14940
  openwop login
14930
14941
  openwop login --label "work laptop"
14931
14942
  openwop logout
14932
14943
  `;
14933
14944
  var sleep2 = (ms2) => new Promise((r) => setTimeout(r, ms2));
14934
- function approvalUrl(baseUrl, path) {
14945
+ function approvalUrl(baseUrl, path, uri) {
14946
+ if (typeof uri === "string") {
14947
+ try {
14948
+ const u = new URL(uri);
14949
+ const loopback = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]";
14950
+ if (u.protocol === "https:" || u.protocol === "http:" && loopback) return u.toString();
14951
+ } catch {
14952
+ }
14953
+ }
14935
14954
  const p = typeof path === "string" && path.startsWith("/") ? path : "/access?tab=api-keys";
14936
14955
  try {
14937
14956
  return `${new URL(baseUrl).origin}${p}`;
@@ -14969,7 +14988,7 @@ async function runLogin(ctx, argv) {
14969
14988
  const userCode = started?.userCode;
14970
14989
  if (typeof deviceCode !== "string" || typeof userCode !== "string") throw notAvailable(ctx);
14971
14990
  writeLine(ctx.io.stderr, "");
14972
- writeLine(ctx.io.stderr, ` 1. Open ${approvalUrl(ctx.baseUrl, started.verificationPath)}`);
14991
+ writeLine(ctx.io.stderr, ` 1. Open ${approvalUrl(ctx.baseUrl, started.verificationPath, started.verificationUri)}`);
14973
14992
  writeLine(ctx.io.stderr, ` 2. Enter ${userCode} under "Sign in the OpenWOP CLI", and approve.`);
14974
14993
  writeLine(ctx.io.stderr, "");
14975
14994
  writeLine(ctx.io.stderr, "Waiting for approval\u2026");
@@ -16887,8 +16906,8 @@ var APPROVALS_ROUTES = [
16887
16906
  var APPROVALS_HELP = `Usage:
16888
16907
  openwop approvals list [--status pending|approved|rejected] [--json]
16889
16908
  openwop approvals get <approvalId> [--json]
16890
- openwop approvals claim <approvalId> [--note <text>] [--acted-for <subject>] [--expected-hash <h>] [--json]
16891
- openwop approvals reject <approvalId> [--note <text>] [--acted-for <subject>] [--expected-hash <h>] [--json]
16909
+ openwop approvals claim <approvalId> [--note <text>] [--acted-for <subject>] [--expected-hash <h>] [--content-hash <h>] [--json]
16910
+ openwop approvals reject <approvalId> [--note <text>] [--acted-for <subject>] [--expected-hash <h>] [--content-hash <h>] [--json]
16892
16911
  openwop approvals sla-policy [--json]
16893
16912
  openwop approvals sla-policy set (--enabled | --disabled) [--remind-after-ms n] [--escalate-after-ms n] [--expire-after-ms n] [--json]
16894
16913
  openwop approvals email-pref [--json]
@@ -16926,6 +16945,11 @@ Endpoints:
16926
16945
  --acted-for <s> (claim/reject) As a delegate covering several people, whose approval this is.
16927
16946
  --expected-hash <h> (claim/reject) The definition hash you reviewed (composed-workflow proposals;
16928
16947
  the host refuses the decision if the proposal changed since).
16948
+ --content-hash <h> (claim/reject) The contentHash of the assistant-action card you reviewed
16949
+ (\`approvals get\` prints it under the card). REQUIRED to claim an
16950
+ assistant action (ADR 0862): the host refuses a claim without it (exit 2),
16951
+ and refuses one whose action changed since you read it (exit 1; re-read
16952
+ it with \`approvals get\` and review the new card).
16929
16953
  --status <s> (list) Filter the queue: pending | approved | rejected (default: all).
16930
16954
  --note <text> (claim/reject) Optional human note recorded with the decision.
16931
16955
  --json Print the raw host response instead of the rendered view.
@@ -16942,6 +16966,8 @@ Examples:
16942
16966
  openwop approvals list --status pending
16943
16967
  openwop approvals get appr_123 --json
16944
16968
  openwop approvals claim appr_123 --note "LGTM, ship it"
16969
+ openwop approvals get appr_456 # an assistant action: read the card
16970
+ openwop approvals claim appr_456 --content-hash 9f2c\u2026 # \u2026then approve exactly what you read
16945
16971
  openwop approvals reject appr_123 --note "out of policy"
16946
16972
 
16947
16973
  Teams approval delivery:
@@ -16988,8 +17014,7 @@ function exitForStatus(status) {
16988
17014
  async function ensureApprovalsAdvertised(ctx) {
16989
17015
  const wk = await safeRequest(ctx, "/.well-known/openwop", { auth: false });
16990
17016
  if (!wk.ok) return;
16991
- const paths = wk.body && typeof wk.body === "object" ? wk.body.paths : void 0;
16992
- const advertised = paths !== null && typeof paths === "object" && Object.keys(paths).some((p) => p.startsWith("/v1/host/openwop-app/approvals"));
17017
+ const advertised = hostSurfaceAdvertised(wk.body, "/v1/host/openwop-app/approvals");
16993
17018
  if (!advertised) {
16994
17019
  throw new CliError(
16995
17020
  "approvals: this host does not advertise the approval-inbox surface (/v1/host/openwop-app/approvals is absent from /.well-known/openwop). The host is the authority \u2014 refusing to guess.",
@@ -17074,12 +17099,31 @@ async function runApprovalsGet(ctx, argv) {
17074
17099
  if (approval.resolvedAt) writeLine(ctx.io.stdout, `resolvedAt: ${approval.resolvedAt}`);
17075
17100
  if (approval.runId) writeLine(ctx.io.stdout, `runId: ${approval.runId}`);
17076
17101
  if (approval.note) writeLine(ctx.io.stdout, `note: ${approval.note}`);
17102
+ if (approval.action && typeof approval.action === "object") renderActionCard(ctx, approval.action);
17077
17103
  return exitForStatus(approval.status);
17078
17104
  }
17105
+ function renderActionCard(ctx, action) {
17106
+ writeLine(ctx.io.stdout, "action:");
17107
+ if (action.actionId) writeLine(ctx.io.stdout, ` actionId: ${action.actionId}`);
17108
+ if (action.kind) writeLine(ctx.io.stdout, ` kind: ${action.kind}`);
17109
+ if (action.riskLevel) writeLine(ctx.io.stdout, ` riskLevel: ${action.riskLevel}`);
17110
+ if (action.reason) writeLine(ctx.io.stdout, ` reason: ${action.reason}`);
17111
+ if (typeof action.draft === "string" && action.draft.length) writeLine(ctx.io.stdout, ` draft: ${action.draft}`);
17112
+ if (action.payload && typeof action.payload === "object") {
17113
+ for (const [k, v] of Object.entries(action.payload)) {
17114
+ writeLine(ctx.io.stdout, ` ${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`);
17115
+ }
17116
+ }
17117
+ if (action.contentHash) writeLine(ctx.io.stdout, ` contentHash: ${action.contentHash} (claim with --content-hash ${action.contentHash})`);
17118
+ }
17119
+ function detailsOf(body) {
17120
+ const d = body && typeof body === "object" ? body.details : void 0;
17121
+ return d && typeof d === "object" ? d : {};
17122
+ }
17079
17123
  async function runApprovalsResolve(ctx, argv, verb) {
17080
- const { options, positionals } = parseOptions(argv, { bool: ["--help"], value: ["--note", "--acted-for", "--expected-hash"] });
17124
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"], value: ["--note", "--acted-for", "--expected-hash", "--content-hash"] });
17081
17125
  if (options.help || positionals.length !== 1) {
17082
- write(ctx.io.stdout, `Usage: openwop approvals ${verb} <approvalId> [--note <text>] [--acted-for <subject>] [--expected-hash <h>] [--json]
17126
+ write(ctx.io.stdout, `Usage: openwop approvals ${verb} <approvalId> [--note <text>] [--acted-for <subject>] [--expected-hash <h>] [--content-hash <h>] [--json]
17083
17127
  `);
17084
17128
  return options.help ? 0 : 2;
17085
17129
  }
@@ -17089,11 +17133,25 @@ async function runApprovalsResolve(ctx, argv, verb) {
17089
17133
  if (options.note !== void 0) decision.note = options.note;
17090
17134
  if (options.actedFor !== void 0) decision.actedFor = options.actedFor;
17091
17135
  if (options.expectedHash !== void 0) decision.expectedDefinitionHash = options.expectedHash;
17136
+ if (options.contentHash !== void 0) decision.expectedContentHash = options.contentHash;
17092
17137
  const body = Object.keys(decision).length ? decision : void 0;
17093
17138
  let res;
17094
17139
  try {
17095
17140
  res = await requestJson(ctx, path, { method: "POST", ...body !== void 0 ? { body } : {} });
17096
17141
  } catch (err) {
17142
+ if (err instanceof HttpError && err.status === 409 && detailsOf(err.body).reason === "action_changed") {
17143
+ const now = detailsOf(err.body).contentHash;
17144
+ throw new CliError(
17145
+ `approvals: the action behind ${positionals[0]} changed since you reviewed it \u2014 nothing was ${verb === "claim" ? "approved" : "rejected"}. Re-read it with \`openwop approvals get ${positionals[0]}\`${now ? ` (its card now hashes to ${now})` : ""} and decide on what it says now.`,
17146
+ 1
17147
+ );
17148
+ }
17149
+ if (err instanceof HttpError && err.status === 400 && detailsOf(err.body).reason === "content_hash_required") {
17150
+ throw new CliError(
17151
+ `approvals: ${positionals[0]} is an assistant action \u2014 read its card with \`openwop approvals get ${positionals[0]}\`, then pass the contentHash it shows: --content-hash <hash>.`,
17152
+ 2
17153
+ );
17154
+ }
17097
17155
  if (err instanceof HttpError && err.status === 409) {
17098
17156
  const status = err.body?.status;
17099
17157
  throw new CliError(`approvals: ${positionals[0]} is already ${status ?? "resolved"} \u2014 the host rejected a second decision.`, 1);
@@ -17975,8 +18033,7 @@ Run \`openwop connections --help\` for usage.`);
17975
18033
  async function ensureAdvertised(ctx) {
17976
18034
  const wk = await safeRequest(ctx, "/.well-known/openwop", { auth: false });
17977
18035
  if (!wk.ok) return;
17978
- const paths = wk.body && typeof wk.body === "object" ? wk.body.paths : void 0;
17979
- const advertised = paths !== null && typeof paths === "object" && Object.keys(paths).some((p) => p.startsWith("/v1/host/openwop-app/connections"));
18036
+ const advertised = hostSurfaceAdvertised(wk.body, "/v1/host/openwop-app/connections");
17980
18037
  if (!advertised) {
17981
18038
  throw new CliError(
17982
18039
  "connections: this host does not advertise the connections surface (/v1/host/openwop-app/connections is absent from /.well-known/openwop). The host is the authority \u2014 refusing to guess.",
@@ -19385,8 +19442,7 @@ Run \`openwop workforces --help\` for usage.`);
19385
19442
  async function ensureAdvertised2(ctx) {
19386
19443
  const wk = await safeRequest(ctx, "/.well-known/openwop", { auth: false });
19387
19444
  if (!wk.ok) return;
19388
- const paths = wk.body && typeof wk.body === "object" ? wk.body.paths : void 0;
19389
- const advertised = paths !== null && typeof paths === "object" && Object.keys(paths).some((p) => p.startsWith("/v1/host/openwop-app/workforces"));
19445
+ const advertised = hostSurfaceAdvertised(wk.body, "/v1/host/openwop-app/workforces");
19390
19446
  if (!advertised) {
19391
19447
  throw new CliError(
19392
19448
  "workforces: this host does not advertise the workforces surface (/v1/host/openwop-app/workforces is absent from /.well-known/openwop). The host is the authority \u2014 refusing to guess.",
@@ -30933,6 +30989,7 @@ export {
30933
30989
  getChannelPlugin,
30934
30990
  hostRootFor,
30935
30991
  hostRootsFrom,
30992
+ hostSurfaceAdvertised,
30936
30993
  idempotencyHeaders,
30937
30994
  isNoCredentialChallenge,
30938
30995
  isTerminalRunEvent,
package/dist/openwop.js CHANGED
@@ -579,7 +579,7 @@ function errText(err) {
579
579
  // package.json
580
580
  var package_default = {
581
581
  name: "@openwop/cli",
582
- version: "1.4.1",
582
+ version: "1.6.0",
583
583
  description: "OpenWOP CLI for operating the workflow-engine demo app and OpenWOP-compatible hosts.",
584
584
  license: "Apache-2.0",
585
585
  type: "module",
@@ -668,7 +668,10 @@ var PROVIDER_CATALOG = {
668
668
  }
669
669
  };
670
670
  var HOST_PRESETS = [
671
- { key: "shared", label: "Shared demo at https://app.openwop.dev/api (recommended for trying things out)", url: "https://app.openwop.dev/api" },
671
+ // openwop-app ADR 0827: the protocol origin, served straight from Cloud Run with
672
+ // no CDN in front (`https://app.openwop.dev/api` still works; its edge drops
673
+ // conditional requests). `openwop login` opens the host-named web page.
674
+ { key: "shared", label: "Shared demo at https://api.openwop.dev (recommended for trying things out)", url: "https://api.openwop.dev" },
672
675
  { key: "local", label: "Local demo at http://localhost:8080 (run `openwop demo start` to launch)", url: "http://localhost:8080" }
673
676
  ];
674
677
 
@@ -1206,6 +1209,14 @@ function hostRootFor(path, roots) {
1206
1209
  }
1207
1210
  return best === null ? null : `${roots[best]}${path.slice(best.length)}`;
1208
1211
  }
1212
+ function hostSurfaceAdvertised(discovery, v1Prefix) {
1213
+ if (!discovery || typeof discovery !== "object") return false;
1214
+ const { paths, extensions } = discovery;
1215
+ if (paths !== null && typeof paths === "object") {
1216
+ return Object.keys(paths).some((p) => p.startsWith(v1Prefix));
1217
+ }
1218
+ return hostRootFor(v1Prefix, hostRootsFrom(extensions)) !== null;
1219
+ }
1209
1220
  async function negotiateMajor(ctx) {
1210
1221
  if (ctx.protocolMajor !== void 0) return ctx.protocolMajor;
1211
1222
  const pinned = ctx.env?.OPENWOP_PROTOCOL_MAJOR;
@@ -14925,13 +14936,21 @@ Approve a code ONLY if you just ran \`openwop login\` yourself.
14925
14936
  Exit codes: 0 signed in / signed out \xB7 1 denied, expired or not available on this host \xB7 2 usage.
14926
14937
 
14927
14938
  Examples:
14928
- export OPENWOP_BASE_URL=https://app.openwop.dev/api
14939
+ export OPENWOP_BASE_URL=https://api.openwop.dev
14929
14940
  openwop login
14930
14941
  openwop login --label "work laptop"
14931
14942
  openwop logout
14932
14943
  `;
14933
14944
  var sleep2 = (ms2) => new Promise((r) => setTimeout(r, ms2));
14934
- function approvalUrl(baseUrl, path) {
14945
+ function approvalUrl(baseUrl, path, uri) {
14946
+ if (typeof uri === "string") {
14947
+ try {
14948
+ const u = new URL(uri);
14949
+ const loopback = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]";
14950
+ if (u.protocol === "https:" || u.protocol === "http:" && loopback) return u.toString();
14951
+ } catch {
14952
+ }
14953
+ }
14935
14954
  const p = typeof path === "string" && path.startsWith("/") ? path : "/access?tab=api-keys";
14936
14955
  try {
14937
14956
  return `${new URL(baseUrl).origin}${p}`;
@@ -14969,7 +14988,7 @@ async function runLogin(ctx, argv) {
14969
14988
  const userCode = started?.userCode;
14970
14989
  if (typeof deviceCode !== "string" || typeof userCode !== "string") throw notAvailable(ctx);
14971
14990
  writeLine(ctx.io.stderr, "");
14972
- writeLine(ctx.io.stderr, ` 1. Open ${approvalUrl(ctx.baseUrl, started.verificationPath)}`);
14991
+ writeLine(ctx.io.stderr, ` 1. Open ${approvalUrl(ctx.baseUrl, started.verificationPath, started.verificationUri)}`);
14973
14992
  writeLine(ctx.io.stderr, ` 2. Enter ${userCode} under "Sign in the OpenWOP CLI", and approve.`);
14974
14993
  writeLine(ctx.io.stderr, "");
14975
14994
  writeLine(ctx.io.stderr, "Waiting for approval\u2026");
@@ -16887,8 +16906,8 @@ var APPROVALS_ROUTES = [
16887
16906
  var APPROVALS_HELP = `Usage:
16888
16907
  openwop approvals list [--status pending|approved|rejected] [--json]
16889
16908
  openwop approvals get <approvalId> [--json]
16890
- openwop approvals claim <approvalId> [--note <text>] [--acted-for <subject>] [--expected-hash <h>] [--json]
16891
- openwop approvals reject <approvalId> [--note <text>] [--acted-for <subject>] [--expected-hash <h>] [--json]
16909
+ openwop approvals claim <approvalId> [--note <text>] [--acted-for <subject>] [--expected-hash <h>] [--content-hash <h>] [--json]
16910
+ openwop approvals reject <approvalId> [--note <text>] [--acted-for <subject>] [--expected-hash <h>] [--content-hash <h>] [--json]
16892
16911
  openwop approvals sla-policy [--json]
16893
16912
  openwop approvals sla-policy set (--enabled | --disabled) [--remind-after-ms n] [--escalate-after-ms n] [--expire-after-ms n] [--json]
16894
16913
  openwop approvals email-pref [--json]
@@ -16926,6 +16945,11 @@ Endpoints:
16926
16945
  --acted-for <s> (claim/reject) As a delegate covering several people, whose approval this is.
16927
16946
  --expected-hash <h> (claim/reject) The definition hash you reviewed (composed-workflow proposals;
16928
16947
  the host refuses the decision if the proposal changed since).
16948
+ --content-hash <h> (claim/reject) The contentHash of the assistant-action card you reviewed
16949
+ (\`approvals get\` prints it under the card). REQUIRED to claim an
16950
+ assistant action (ADR 0862): the host refuses a claim without it (exit 2),
16951
+ and refuses one whose action changed since you read it (exit 1; re-read
16952
+ it with \`approvals get\` and review the new card).
16929
16953
  --status <s> (list) Filter the queue: pending | approved | rejected (default: all).
16930
16954
  --note <text> (claim/reject) Optional human note recorded with the decision.
16931
16955
  --json Print the raw host response instead of the rendered view.
@@ -16942,6 +16966,8 @@ Examples:
16942
16966
  openwop approvals list --status pending
16943
16967
  openwop approvals get appr_123 --json
16944
16968
  openwop approvals claim appr_123 --note "LGTM, ship it"
16969
+ openwop approvals get appr_456 # an assistant action: read the card
16970
+ openwop approvals claim appr_456 --content-hash 9f2c\u2026 # \u2026then approve exactly what you read
16945
16971
  openwop approvals reject appr_123 --note "out of policy"
16946
16972
 
16947
16973
  Teams approval delivery:
@@ -16988,8 +17014,7 @@ function exitForStatus(status) {
16988
17014
  async function ensureApprovalsAdvertised(ctx) {
16989
17015
  const wk = await safeRequest(ctx, "/.well-known/openwop", { auth: false });
16990
17016
  if (!wk.ok) return;
16991
- const paths = wk.body && typeof wk.body === "object" ? wk.body.paths : void 0;
16992
- const advertised = paths !== null && typeof paths === "object" && Object.keys(paths).some((p) => p.startsWith("/v1/host/openwop-app/approvals"));
17017
+ const advertised = hostSurfaceAdvertised(wk.body, "/v1/host/openwop-app/approvals");
16993
17018
  if (!advertised) {
16994
17019
  throw new CliError(
16995
17020
  "approvals: this host does not advertise the approval-inbox surface (/v1/host/openwop-app/approvals is absent from /.well-known/openwop). The host is the authority \u2014 refusing to guess.",
@@ -17074,12 +17099,31 @@ async function runApprovalsGet(ctx, argv) {
17074
17099
  if (approval.resolvedAt) writeLine(ctx.io.stdout, `resolvedAt: ${approval.resolvedAt}`);
17075
17100
  if (approval.runId) writeLine(ctx.io.stdout, `runId: ${approval.runId}`);
17076
17101
  if (approval.note) writeLine(ctx.io.stdout, `note: ${approval.note}`);
17102
+ if (approval.action && typeof approval.action === "object") renderActionCard(ctx, approval.action);
17077
17103
  return exitForStatus(approval.status);
17078
17104
  }
17105
+ function renderActionCard(ctx, action) {
17106
+ writeLine(ctx.io.stdout, "action:");
17107
+ if (action.actionId) writeLine(ctx.io.stdout, ` actionId: ${action.actionId}`);
17108
+ if (action.kind) writeLine(ctx.io.stdout, ` kind: ${action.kind}`);
17109
+ if (action.riskLevel) writeLine(ctx.io.stdout, ` riskLevel: ${action.riskLevel}`);
17110
+ if (action.reason) writeLine(ctx.io.stdout, ` reason: ${action.reason}`);
17111
+ if (typeof action.draft === "string" && action.draft.length) writeLine(ctx.io.stdout, ` draft: ${action.draft}`);
17112
+ if (action.payload && typeof action.payload === "object") {
17113
+ for (const [k, v] of Object.entries(action.payload)) {
17114
+ writeLine(ctx.io.stdout, ` ${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`);
17115
+ }
17116
+ }
17117
+ if (action.contentHash) writeLine(ctx.io.stdout, ` contentHash: ${action.contentHash} (claim with --content-hash ${action.contentHash})`);
17118
+ }
17119
+ function detailsOf(body) {
17120
+ const d = body && typeof body === "object" ? body.details : void 0;
17121
+ return d && typeof d === "object" ? d : {};
17122
+ }
17079
17123
  async function runApprovalsResolve(ctx, argv, verb) {
17080
- const { options, positionals } = parseOptions(argv, { bool: ["--help"], value: ["--note", "--acted-for", "--expected-hash"] });
17124
+ const { options, positionals } = parseOptions(argv, { bool: ["--help"], value: ["--note", "--acted-for", "--expected-hash", "--content-hash"] });
17081
17125
  if (options.help || positionals.length !== 1) {
17082
- write(ctx.io.stdout, `Usage: openwop approvals ${verb} <approvalId> [--note <text>] [--acted-for <subject>] [--expected-hash <h>] [--json]
17126
+ write(ctx.io.stdout, `Usage: openwop approvals ${verb} <approvalId> [--note <text>] [--acted-for <subject>] [--expected-hash <h>] [--content-hash <h>] [--json]
17083
17127
  `);
17084
17128
  return options.help ? 0 : 2;
17085
17129
  }
@@ -17089,11 +17133,25 @@ async function runApprovalsResolve(ctx, argv, verb) {
17089
17133
  if (options.note !== void 0) decision.note = options.note;
17090
17134
  if (options.actedFor !== void 0) decision.actedFor = options.actedFor;
17091
17135
  if (options.expectedHash !== void 0) decision.expectedDefinitionHash = options.expectedHash;
17136
+ if (options.contentHash !== void 0) decision.expectedContentHash = options.contentHash;
17092
17137
  const body = Object.keys(decision).length ? decision : void 0;
17093
17138
  let res;
17094
17139
  try {
17095
17140
  res = await requestJson(ctx, path, { method: "POST", ...body !== void 0 ? { body } : {} });
17096
17141
  } catch (err) {
17142
+ if (err instanceof HttpError && err.status === 409 && detailsOf(err.body).reason === "action_changed") {
17143
+ const now = detailsOf(err.body).contentHash;
17144
+ throw new CliError(
17145
+ `approvals: the action behind ${positionals[0]} changed since you reviewed it \u2014 nothing was ${verb === "claim" ? "approved" : "rejected"}. Re-read it with \`openwop approvals get ${positionals[0]}\`${now ? ` (its card now hashes to ${now})` : ""} and decide on what it says now.`,
17146
+ 1
17147
+ );
17148
+ }
17149
+ if (err instanceof HttpError && err.status === 400 && detailsOf(err.body).reason === "content_hash_required") {
17150
+ throw new CliError(
17151
+ `approvals: ${positionals[0]} is an assistant action \u2014 read its card with \`openwop approvals get ${positionals[0]}\`, then pass the contentHash it shows: --content-hash <hash>.`,
17152
+ 2
17153
+ );
17154
+ }
17097
17155
  if (err instanceof HttpError && err.status === 409) {
17098
17156
  const status = err.body?.status;
17099
17157
  throw new CliError(`approvals: ${positionals[0]} is already ${status ?? "resolved"} \u2014 the host rejected a second decision.`, 1);
@@ -17975,8 +18033,7 @@ Run \`openwop connections --help\` for usage.`);
17975
18033
  async function ensureAdvertised(ctx) {
17976
18034
  const wk = await safeRequest(ctx, "/.well-known/openwop", { auth: false });
17977
18035
  if (!wk.ok) return;
17978
- const paths = wk.body && typeof wk.body === "object" ? wk.body.paths : void 0;
17979
- const advertised = paths !== null && typeof paths === "object" && Object.keys(paths).some((p) => p.startsWith("/v1/host/openwop-app/connections"));
18036
+ const advertised = hostSurfaceAdvertised(wk.body, "/v1/host/openwop-app/connections");
17980
18037
  if (!advertised) {
17981
18038
  throw new CliError(
17982
18039
  "connections: this host does not advertise the connections surface (/v1/host/openwop-app/connections is absent from /.well-known/openwop). The host is the authority \u2014 refusing to guess.",
@@ -19385,8 +19442,7 @@ Run \`openwop workforces --help\` for usage.`);
19385
19442
  async function ensureAdvertised2(ctx) {
19386
19443
  const wk = await safeRequest(ctx, "/.well-known/openwop", { auth: false });
19387
19444
  if (!wk.ok) return;
19388
- const paths = wk.body && typeof wk.body === "object" ? wk.body.paths : void 0;
19389
- const advertised = paths !== null && typeof paths === "object" && Object.keys(paths).some((p) => p.startsWith("/v1/host/openwop-app/workforces"));
19445
+ const advertised = hostSurfaceAdvertised(wk.body, "/v1/host/openwop-app/workforces");
19390
19446
  if (!advertised) {
19391
19447
  throw new CliError(
19392
19448
  "workforces: this host does not advertise the workforces surface (/v1/host/openwop-app/workforces is absent from /.well-known/openwop). The host is the authority \u2014 refusing to guess.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/cli",
3
- "version": "1.4.1",
3
+ "version": "1.6.0",
4
4
  "description": "OpenWOP CLI for operating the workflow-engine demo app and OpenWOP-compatible hosts.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",