@openwop/cli 1.2.0 → 1.2.2

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
@@ -98,7 +98,7 @@ What else the CLI does as a v2 client (audited against `spec/v2/core/*` at corpu
98
98
  | Discovery (`capabilities.md`) | `openwop capabilities` renders the v2 root (records by status, `minClientVersion`, `eventLogSchemaVersion`, `extensions`) from the negotiation read. |
99
99
  | Run list (`runs.md` §List) | `runs list --cursor` / `--workflow-id`; the `nextCursor` hint is printed. |
100
100
  | Stream origin (host extension; openwop-app ADR 0761) | Some front doors buffer event streams entirely — `https://app.openwop.dev/api` delivered **0 bytes of an event stream in 25 s** (measured 2026-09-27), and `runs.md` keeps `eventsUrl` on the request's origin, so the protocol cannot redirect. Every stream (`runs watch`, `chat`, `notifications stream`, `kanban watch`, `present`) is therefore read from the **stream origin**: `--stream-base-url` > `OPENWOP_STREAM_BASE_URL` > config `host.streamBaseUrl` > a host-advertised `extensions.*.streamBase` (accepted only if `https:` with no credentials/query/fragment, read from an `https:` host; loopback `http:` only for a loopback base) > `--base-url`. Non-stream requests always use `--base-url`. To keep streams on `--base-url` too (e.g. an egress proxy), set `--stream-base-url` to the same URL; `openwop doctor`'s **stream origin** row shows which origin is in use and why. |
101
- | Stalled streams | A run stream that delivers **no bytes** (keep-alives included) for `--idle-timeout-ms` (default 45000; 0 disables) is aborted and resumed with `Last-Event-ID` — a half-open connection no longer hangs the command. `scripts/live-sse-resume.mjs` checks resume against a real run (`--mode drop|stall`). |
101
+ | Stalled streams | A run stream that delivers **no bytes** (keep-alives included) for `--idle-timeout-ms` (default 45000; 0 disables) is aborted and resumed with `Last-Event-ID` — a half-open connection no longer hangs the command. A stream whose response headers do not arrive within 10 s (a front door that buffers streams) is followed by polling instead, with one stderr line naming `--stream-base-url`; `--verbose` says whether each reconnect was a drop or a stall. `scripts/live-sse-resume.mjs` checks resume against a real run (`--mode drop|stall`). |
102
102
 
103
103
  When the host's `minClientVersion` is above the CLI, every command prints one warning (from the discovery read it already made); refusing is left to the host (`426`). Not exposed: the `bufferMs` batching hint (batched frames from a host that batches anyway are consumed).
104
104
 
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.2.0",
581
+ version: "1.2.2",
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",
@@ -1236,11 +1236,15 @@ function isLoopback(url) {
1236
1236
  }
1237
1237
  function explicitStreamBase(raw) {
1238
1238
  if (typeof raw !== "string" || raw.trim() === "") return void 0;
1239
+ let url;
1239
1240
  try {
1240
- new URL(raw.trim());
1241
+ url = new URL(raw.trim());
1241
1242
  } catch {
1242
1243
  throw new CliError(`--stream-base-url / OPENWOP_STREAM_BASE_URL is not a URL: ${raw}`);
1243
1244
  }
1245
+ if (url.username !== "" || url.password !== "") {
1246
+ throw new CliError("--stream-base-url / OPENWOP_STREAM_BASE_URL must not carry credentials (user:pass@); pass them with --api-key.");
1247
+ }
1244
1248
  return raw.trim().replace(/\/+$/, "");
1245
1249
  }
1246
1250
  function streamBaseFrom(extensions, baseUrl) {
@@ -1481,6 +1485,10 @@ async function streamRunEvents(ctx, runId, opts = {}) {
1481
1485
  } catch (err) {
1482
1486
  if (err instanceof HttpError && err.status >= 400 && err.status < 500 && (opts.streamMode || opts.lastEventId !== void 0 || opts.afterSequence !== void 0)) throw err;
1483
1487
  if (err instanceof SseResumeExhausted) throw err.reason instanceof HttpError ? err.reason : err;
1488
+ if (err instanceof StreamSilent && err.beforeHeaders && !ctx.quiet) {
1489
+ ctx.io.stderr.write(`openwop: the event stream sent nothing for ${err.ms >= 1e3 ? `${Math.round(err.ms / 1e3)} s` : `${err.ms} ms`} \u2014 the host's front door may be buffering streams. Following by polling instead; for live events pass --stream-base-url <a direct origin> (see \`openwop doctor\`).
1490
+ `);
1491
+ }
1484
1492
  }
1485
1493
  }
1486
1494
  if (opts.streamMode && opts.streamMode !== "updates" && !ctx.quiet) {
@@ -1530,8 +1538,10 @@ async function streamViaSse(ctx, runId, deliver, opts) {
1530
1538
  try {
1531
1539
  const { url, headers } = await resolveStreamRequest(ctx, `/v1/runs/${encodeURIComponent(runId)}/events${query}`, {});
1532
1540
  if (lastEventId !== void 0 && lastEventId !== "") headers["last-event-id"] = lastEventId;
1533
- idle = idleWatchdog(opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS);
1541
+ const idleMs = opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
1542
+ idle = idleWatchdog(idleMs, opts.headersTimeoutMs ?? (idleMs > 0 ? Math.min(idleMs, DEFAULT_HEADERS_TIMEOUT_MS) : 0));
1534
1543
  const res = await ctx.fetchImpl(url, { method: "GET", headers, signal: idle.signal });
1544
+ idle.headersArrived();
1535
1545
  if (!res.ok) {
1536
1546
  const text = await res.text().catch(() => "");
1537
1547
  let body = null;
@@ -1575,12 +1585,15 @@ async function streamViaSse(ctx, runId, deliver, opts) {
1575
1585
  if (await runIsTerminal(ctx, runId)) return true;
1576
1586
  failures = progressed ? 1 : failures + 1;
1577
1587
  if (failures > maxReconnects) {
1578
- throw new SseResumeExhausted(`events stream for ${runId} dropped ${failures} times without progress${dropError instanceof Error ? `: ${dropError.message}` : ""}`, dropError);
1588
+ throw new SseResumeExhausted(`events stream for ${runId} dropped ${failures} times without progress${dropError instanceof Error ? `: ${dropError.message}` : ""}. Re-run with --since <last sequence printed> to continue, or --no-stream to follow by polling.`, dropError);
1579
1589
  }
1580
1590
  const delayMs = Math.min(retryMs * 2 ** (failures - 1), 3e4);
1581
1591
  opts.onReconnect?.({ attempt: failures, lastEventId, delayMs });
1582
- if (ctx.verbose) ctx.io.stderr.write(`openwop: events stream dropped; reconnecting in ${delayMs}ms with Last-Event-ID ${lastEventId ?? "(none)"}
1592
+ if (ctx.verbose) {
1593
+ const why = dropError instanceof StreamSilent ? `stalled (${dropError.message})` : "dropped";
1594
+ ctx.io.stderr.write(`openwop: events stream ${why}; reconnecting in ${delayMs}ms with Last-Event-ID ${lastEventId ?? "(none)"}
1583
1595
  `);
1596
+ }
1584
1597
  await sleep(delayMs);
1585
1598
  }
1586
1599
  }
@@ -1779,29 +1792,44 @@ function defaultReadTurn(ctx) {
1779
1792
  });
1780
1793
  }
1781
1794
  var DEFAULT_IDLE_TIMEOUT_MS = 45e3;
1782
- function idleWatchdog(ms2) {
1795
+ var DEFAULT_HEADERS_TIMEOUT_MS = 1e4;
1796
+ var StreamSilent = class extends Error {
1797
+ constructor(ms2, beforeHeaders) {
1798
+ super(beforeHeaders ? `no response headers within ${ms2}ms` : `no bytes for ${ms2}ms`);
1799
+ this.ms = ms2;
1800
+ this.beforeHeaders = beforeHeaders;
1801
+ this.name = "StreamSilent";
1802
+ }
1803
+ };
1804
+ function idleWatchdog(ms2, headersMs = ms2) {
1783
1805
  const controller = new AbortController();
1784
1806
  let timer;
1785
1807
  let onExpire;
1786
- const expire = () => {
1787
- controller.abort(new Error(`no bytes for ${ms2}ms`));
1808
+ let beforeHeaders = true;
1809
+ const expire = (after) => {
1810
+ controller.abort(new StreamSilent(after, beforeHeaders));
1788
1811
  onExpire?.();
1789
1812
  };
1790
- const arm = () => {
1791
- if (!(ms2 > 0)) return;
1813
+ const arm = (after = ms2) => {
1814
+ if (!(after > 0)) return;
1792
1815
  if (timer !== void 0) clearTimeout(timer);
1793
- timer = setTimeout(expire, ms2);
1816
+ timer = setTimeout(() => expire(after), after);
1794
1817
  };
1795
- arm();
1818
+ arm(headersMs);
1796
1819
  return {
1797
1820
  signal: controller.signal,
1821
+ /** The response headers arrived: from here on only the idle timeout applies. */
1822
+ headersArrived() {
1823
+ beforeHeaders = false;
1824
+ arm();
1825
+ },
1798
1826
  watch(body) {
1799
1827
  if (!(ms2 > 0) || typeof body?.getReader !== "function") return body;
1800
1828
  const source = body.getReader();
1801
1829
  return new ReadableStream({
1802
1830
  start(out) {
1803
1831
  onExpire = () => {
1804
- out.error(new Error(`events stream idle for ${ms2}ms`));
1832
+ out.error(new StreamSilent(ms2, false));
1805
1833
  source.cancel().catch(() => {
1806
1834
  });
1807
1835
  };
@@ -1976,7 +2004,7 @@ function formatCheckTable(checks) {
1976
2004
 
1977
2005
  // src/cli/routeKit.ts
1978
2006
  var errText2 = (err) => err instanceof Error ? err.message : String(err);
1979
- var DEFAULT_MESSAGES = {
2007
+ var MSG = {
1980
2008
  required: (flag, usage6) => `${usage6}
1981
2009
  ${flag} is required.`,
1982
2010
  refusal: (words, positionals) => `Refusing to ${words.join(" ")} ${positionals.join(" ")} without --yes.`.replace(/\s+/g, " ").trim(),
@@ -2055,14 +2083,14 @@ function setPath(target, key, value) {
2055
2083
  }
2056
2084
  node[parts[parts.length - 1]] = value;
2057
2085
  }
2058
- function coerce(f, raw, cmd, msg) {
2086
+ function coerce(f, raw) {
2059
2087
  const t = f.type ?? "string";
2060
2088
  if (t === "boolean") return raw;
2061
2089
  if (t === "bool") {
2062
2090
  const s2 = String(raw);
2063
2091
  if (s2 === "true" || s2 === "yes" || s2 === "1") return true;
2064
2092
  if (s2 === "false" || s2 === "no" || s2 === "0") return false;
2065
- throw new CliError(msg.invalidBool(f.flag, s2), 2);
2093
+ throw new CliError(MSG.invalidBool(f.flag, s2), 2);
2066
2094
  }
2067
2095
  if (t === "list") return Array.isArray(raw) ? raw.map(String) : [String(raw)];
2068
2096
  if (t === "map") {
@@ -2079,7 +2107,7 @@ function coerce(f, raw, cmd, msg) {
2079
2107
  const s = String(raw);
2080
2108
  if (t === "number") {
2081
2109
  const n = Number(s);
2082
- if (!Number.isFinite(n) || cmd.strictNumbers && s.trim() === "") throw new CliError(msg.invalidNumber(f.flag, s), 2);
2110
+ if (s.trim() === "" || !Number.isFinite(n)) throw new CliError(MSG.invalidNumber(f.flag, s), 2);
2083
2111
  return n;
2084
2112
  }
2085
2113
  if (t === "json-file") {
@@ -2087,7 +2115,7 @@ function coerce(f, raw, cmd, msg) {
2087
2115
  try {
2088
2116
  text = readFileSync4(s, "utf8");
2089
2117
  } catch (err) {
2090
- throw new CliError(msg.unreadable(f.flag, s, err), 2);
2118
+ throw new CliError(MSG.unreadable(f.flag, s, err), 2);
2091
2119
  }
2092
2120
  try {
2093
2121
  return JSON.parse(text);
@@ -2099,14 +2127,14 @@ function coerce(f, raw, cmd, msg) {
2099
2127
  try {
2100
2128
  return readFileSync4(s).toString("base64");
2101
2129
  } catch (err) {
2102
- throw new CliError(msg.unreadable(f.flag, s, err), 2);
2130
+ throw new CliError(MSG.unreadable(f.flag, s, err), 2);
2103
2131
  }
2104
2132
  }
2105
2133
  if (t === "file") {
2106
2134
  try {
2107
2135
  return readFileSync4(s, "utf8").replace(/\r?\n$/, "");
2108
2136
  } catch (err) {
2109
- throw new CliError(msg.unreadable(f.flag, s, err), 2);
2137
+ throw new CliError(MSG.unreadable(f.flag, s, err), 2);
2110
2138
  }
2111
2139
  }
2112
2140
  if (t === "csv") return s.split(",").map((x) => x.trim()).filter(Boolean);
@@ -2114,19 +2142,19 @@ function coerce(f, raw, cmd, msg) {
2114
2142
  try {
2115
2143
  return JSON.parse(s);
2116
2144
  } catch {
2117
- throw new CliError(msg.invalidJson(f.flag, s), 2);
2145
+ throw new CliError(MSG.invalidJson(f.flag), 2);
2118
2146
  }
2119
2147
  }
2120
2148
  return s;
2121
2149
  }
2122
- function readBodyOptions(options, msg) {
2150
+ function readBodyOptions(options) {
2123
2151
  let raw;
2124
2152
  if (options.bodyFile !== void 0) {
2125
2153
  if (String(options.bodyFile).trim() === "") throw new CliError("--body-file needs a path (got an empty value).", 2);
2126
2154
  try {
2127
2155
  raw = readFileSync4(String(options.bodyFile), "utf8");
2128
2156
  } catch (err) {
2129
- throw new CliError(msg.unreadableBody(String(options.bodyFile), err), 2);
2157
+ throw new CliError(MSG.unreadableBody(String(options.bodyFile), err), 2);
2130
2158
  }
2131
2159
  } else if (options.body !== void 0) {
2132
2160
  raw = String(options.body);
@@ -2136,9 +2164,9 @@ function readBodyOptions(options, msg) {
2136
2164
  try {
2137
2165
  parsed = JSON.parse(raw);
2138
2166
  } catch {
2139
- throw new CliError(msg.invalidBodyJson, 2);
2167
+ throw new CliError(MSG.invalidBodyJson, 2);
2140
2168
  }
2141
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new CliError(msg.bodyNotObject, 2);
2169
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new CliError(MSG.bodyNotObject, 2);
2142
2170
  return parsed;
2143
2171
  }
2144
2172
  function hostError(err) {
@@ -2149,7 +2177,6 @@ function hostError(err) {
2149
2177
  ${hint}` : line, code);
2150
2178
  }
2151
2179
  async function runRoute(ctx, group, cmd, argv) {
2152
- const msg = { ...DEFAULT_MESSAGES, ...cmd.messages ?? {} };
2153
2180
  const rest = argv.slice(cmd.words.length);
2154
2181
  const fields = [...cmd.query ?? [], ...cmd.body ?? []];
2155
2182
  const bool = [...COMMON_BOOL, ...fields.filter((f) => f.type === "boolean").flatMap((f) => [f.flag, f.flag.replace(/^--/, "--no-")])];
@@ -2162,8 +2189,7 @@ async function runRoute(ctx, group, cmd, argv) {
2162
2189
  const multi = fields.filter(isMulti).map((f) => f.flag);
2163
2190
  const { options, positionals } = parseOptions(rest, { bool, value, multi });
2164
2191
  if (options.help) {
2165
- if (cmd.usageText !== void 0) write(ctx.io.stdout, cmd.usageText);
2166
- else writeLine(ctx.io.stdout, `Usage: ${usageLine(group, cmd)}
2192
+ writeLine(ctx.io.stdout, `Usage: ${usageLine(group, cmd)}
2167
2193
 
2168
2194
  ${cmd.summary}
2169
2195
  Endpoint: ${cmd.method} ${templates(cmd).join(" | ")}`);
@@ -2171,12 +2197,12 @@ Endpoint: ${cmd.method} ${templates(cmd).join(" | ")}`);
2171
2197
  }
2172
2198
  const template = templates(cmd).find((p) => positionalNames(cmd, p).length === positionals.length);
2173
2199
  if (!template) {
2174
- write(ctx.io.stderr, cmd.usageText ?? `Usage: ${usageLine(group, cmd)}
2200
+ write(ctx.io.stderr, `Usage: ${usageLine(group, cmd)}
2175
2201
  `);
2176
2202
  return 2;
2177
2203
  }
2178
2204
  const path = fillPath(cmd, template, positionals, options.org);
2179
- const requiredError = (f) => new CliError(msg.required(f.flag, cmd.usageText ?? usageLine(group, cmd)), 2);
2205
+ const requiredError = (f) => new CliError(MSG.required(f.flag, usageLine(group, cmd)), 2);
2180
2206
  const optionOf = (f) => {
2181
2207
  const name = f.flag.replace(/^--/, "").replace(/-([a-z])/g, (_, c) => c.toUpperCase());
2182
2208
  if (f.type === "boolean") {
@@ -2194,7 +2220,7 @@ Endpoint: ${cmd.method} ${templates(cmd).join(" | ")}`);
2194
2220
  if (f.required) throw requiredError(f);
2195
2221
  continue;
2196
2222
  }
2197
- const c = coerce(f, v, cmd, msg);
2223
+ const c = coerce(f, v);
2198
2224
  if (f.type === "csv") qs2.set(f.key, c.join(","));
2199
2225
  else if (Array.isArray(c)) c.forEach((x) => qs2.append(f.key, String(x)));
2200
2226
  else qs2.set(f.key, c !== null && typeof c === "object" ? JSON.stringify(c) : String(c));
@@ -2202,7 +2228,7 @@ Endpoint: ${cmd.method} ${templates(cmd).join(" | ")}`);
2202
2228
  const url = qs2.size ? `${path}${path.includes("?") ? "&" : "?"}${qs2.toString()}` : path;
2203
2229
  const confirm2 = cmd.confirm ?? cmd.method === "DELETE";
2204
2230
  const gate = () => {
2205
- if (confirm2 && !options.yes) throw new CliError(msg.refusal(cmd.words, positionals), 2);
2231
+ if (confirm2 && !options.yes) throw new CliError(MSG.refusal(cmd.words, positionals), 2);
2206
2232
  };
2207
2233
  if (!cmd.validateFirst) gate();
2208
2234
  const isWrite = cmd.method !== "GET" && cmd.method !== "DELETE";
@@ -2210,10 +2236,10 @@ Endpoint: ${cmd.method} ${templates(cmd).join(" | ")}`);
2210
2236
  let explicit = {};
2211
2237
  const overlay = {};
2212
2238
  if (takesBody) {
2213
- explicit = readBodyOptions(options, msg);
2239
+ explicit = readBodyOptions(options);
2214
2240
  for (const f of cmd.body ?? []) {
2215
2241
  const v = optionOf(f);
2216
- if (v !== void 0) setPath(overlay, f.key, coerce(f, v, cmd, msg));
2242
+ if (v !== void 0) setPath(overlay, f.key, coerce(f, v));
2217
2243
  else if (cmd.validateFirst && f.required && getPath(explicit, f.key) === void 0) throw requiredError(f);
2218
2244
  }
2219
2245
  }
@@ -2250,7 +2276,7 @@ Endpoint: ${cmd.method} ${templates(cmd).join(" | ")}`);
2250
2276
  render(ctx, { ...cmd, method: "GET" }, positionals, err.status, err.body);
2251
2277
  return 1;
2252
2278
  }
2253
- if (err instanceof HttpError && cmd.hostErrors !== false) throw hostError(err);
2279
+ if (err instanceof HttpError) throw hostError(err);
2254
2280
  throw err;
2255
2281
  }
2256
2282
  }
@@ -2261,7 +2287,7 @@ async function send(ctx, cmd, url, init) {
2261
2287
  try {
2262
2288
  return await requestJson(ctx, url, { method: init.method, ...init.body !== void 0 ? { body: init.body } : {}, ...cmd.anonymous ? { auth: false } : {} });
2263
2289
  } catch (err) {
2264
- if (err instanceof HttpError && cmd.hostErrors !== false) throw hostError(err);
2290
+ if (err instanceof HttpError) throw hostError(err);
2265
2291
  throw err;
2266
2292
  }
2267
2293
  }
@@ -2311,7 +2337,7 @@ function render(ctx, cmd, positionals, status, rawBody) {
2311
2337
  return;
2312
2338
  }
2313
2339
  }
2314
- if (cmd.method === "GET" || cmd.writeOutput === "json") {
2340
+ if (cmd.method === "GET") {
2315
2341
  writeJson(ctx.io.stdout, body);
2316
2342
  return;
2317
2343
  }
@@ -2650,7 +2676,8 @@ async function runInterrupts(ctx, argv) {
2650
2676
  if (b.key) writeLine(ctx.io.stdout, `key: ${b.key}`);
2651
2677
  writeLine(ctx.io.stdout, `resolved: ${b.resolved ? "yes" : "no"}`);
2652
2678
  if (b.expiresAt) writeLine(ctx.io.stdout, `expiresAt: ${b.expiresAt}`);
2653
- if (b.data && typeof b.data === "object" && typeof b.data.prompt === "string") writeLine(ctx.io.stdout, `prompt: ${b.data.prompt}`);
2679
+ const prompt = b.data && typeof b.data === "object" ? b.data.prompt : void 0;
2680
+ if (typeof prompt === "string") writeLine(ctx.io.stdout, `prompt: ${prompt}`);
2654
2681
  if (b.resumeSchema !== void 0) writeLine(ctx.io.stdout, `resumeSchema: ${JSON.stringify(b.resumeSchema)}`);
2655
2682
  return 0;
2656
2683
  }
@@ -6282,25 +6309,8 @@ function parseField(spec) {
6282
6309
  const flag = `--${flagOverride ?? key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`).replace(/_/g, "-")}`;
6283
6310
  return { key, flag, type, required };
6284
6311
  }
6285
- function hasOrg(route) {
6286
- return /:org(?![A-Za-z0-9_])/.test(route);
6287
- }
6288
- function paramsOf(route) {
6289
- return (route.match(/:[A-Za-z][A-Za-z0-9_]*/g) ?? []).map((p) => p.slice(1)).filter((p) => p !== "org");
6290
- }
6291
6312
  function usageLine2(group, spec) {
6292
- const parts = [`openwop ${group}`, ...spec.cmd, ...paramsOf(spec.route).map((p) => `<${p}>`)];
6293
- if (hasOrg(spec.route)) parts.push("--org <orgId>");
6294
- const fmt = (f) => {
6295
- const v = f.type === "bool" ? "true|false" : f.type === "json" ? "json" : f.type === "list" ? "a,b" : f.type === "number" ? "n" : f.type === "file" ? "path" : "v";
6296
- return f.required ? `${f.flag} <${v}>` : `[${f.flag} <${v}>]`;
6297
- };
6298
- for (const f of (spec.query ?? []).map(parseField)) parts.push(fmt(f));
6299
- for (const f of (spec.body ?? []).map(parseField)) parts.push(fmt(f));
6300
- if (spec.body || spec.rawBody) parts.push("[--body <json> | --body-file <path>]");
6301
- if (spec.confirm) parts.push("--yes");
6302
- parts.push("[--json]");
6303
- return parts.join(" ");
6313
+ return usageLine(group, toRouteCmd(spec));
6304
6314
  }
6305
6315
  function buildGroupHelp(group, intro, specs, footer = "") {
6306
6316
  const lines = ["Usage:"];
@@ -6329,10 +6339,7 @@ function toField(spec) {
6329
6339
  const f = parseField(spec);
6330
6340
  return { flag: f.flag, key: f.key, type: TYPES[f.type] ?? "string", required: f.required };
6331
6341
  }
6332
- function toRouteCmd(group, spec) {
6333
- const usage6 = `Usage: ${usageLine2(group, spec)}
6334
- ${spec.method} ${spec.route} \u2014 ${spec.summary}
6335
- `;
6342
+ function toRouteCmd(spec) {
6336
6343
  const rmwKey = spec.rmwKey;
6337
6344
  return {
6338
6345
  words: spec.cmd,
@@ -6352,30 +6359,14 @@ function toRouteCmd(group, spec) {
6352
6359
  rawText: true,
6353
6360
  // every spec, as before — `text` only documents the route
6354
6361
  noContent: spec.method === "DELETE" ? "Deleted." : "Done (HTTP {status}).",
6355
- writeOutput: "json",
6356
- hostErrors: false,
6357
- validateFirst: true,
6358
- strictNumbers: true,
6359
- usageText: usage6,
6360
- messages: {
6361
- required: (flag, usageText) => `${flag} is required.
6362
- ${usageText}`,
6363
- refusal: () => `Refusing to ${spec.summary.charAt(0).toLowerCase()}${spec.summary.slice(1).replace(/\.$/, "")} without --yes.`,
6364
- invalidNumber: (flag, raw) => `${flag} must be a number, got: ${raw}`,
6365
- invalidBool: (flag, raw) => `${flag} must be true or false, got: ${raw}`,
6366
- invalidJson: (flag) => `${flag} must be valid JSON`,
6367
- unreadable: (flag, path) => `Cannot read ${flag} ${path}`,
6368
- unreadableBody: (path) => `Cannot read --body-file ${path}`,
6369
- invalidBodyJson: "--body/--body-file must be valid JSON",
6370
- bodyNotObject: "--body/--body-file must be a JSON object"
6371
- }
6362
+ validateFirst: true
6372
6363
  };
6373
6364
  }
6374
6365
  function matchSpec(specs, argv) {
6375
6366
  return matchRoute(specs.map((spec) => ({ words: spec.cmd, spec })), argv)?.spec;
6376
6367
  }
6377
6368
  async function runSpec(ctx, group, spec, argv) {
6378
- return runRoute(ctx, group, toRouteCmd(group, spec), [...spec.cmd, ...argv]);
6369
+ return runRoute(ctx, group, toRouteCmd(spec), [...spec.cmd, ...argv]);
6379
6370
  }
6380
6371
  async function dispatchSpecs(ctx, group, specs, argv) {
6381
6372
  const spec = matchSpec(specs, argv);
@@ -12417,7 +12408,9 @@ honouring the server's \`retry:\` field), and no event is printed twice.
12417
12408
  (\`values\` never combines). A host that does not serve
12418
12409
  the mode answers 400 unsupported_stream_mode.
12419
12410
  --idle-timeout-ms N a connection that delivers no bytes for N ms (default
12420
- 45000; 0 disables) is treated as dropped and resumed.
12411
+ 45000; 0 disables) is treated as dropped and resumed. A
12412
+ stream whose headers do not arrive within 10 s is given
12413
+ up on and followed by polling, with a hint on stderr.
12421
12414
  Streams are read from the stream origin: --stream-base-url (global), else the
12422
12415
  host's advertised \`streamBase\` (https only), else --base-url \u2014 some front
12423
12416
  doors buffer event streams entirely (e.g. a CDN rewrite).
@@ -17404,6 +17397,24 @@ async function runPromptsGet(ctx, argv) {
17404
17397
  return 0;
17405
17398
  }
17406
17399
 
17400
+ // src/redact.ts
17401
+ var BASE_SECRET_KEY = /secret|token|password|private[-_]?key|client[-_]?secret|api[-_]?key|credential/i;
17402
+ function redactSecrets(value, rules = {}) {
17403
+ const secret = rules.secret ?? BASE_SECRET_KEY;
17404
+ const walk = (v) => {
17405
+ if (Array.isArray(v)) return v.map(walk);
17406
+ if (v && typeof v === "object") {
17407
+ const out = {};
17408
+ for (const [k, inner] of Object.entries(v)) {
17409
+ out[k] = secret.test(k) && !(rules.keep?.test(k) ?? false) ? "[redacted]" : walk(inner);
17410
+ }
17411
+ return out;
17412
+ }
17413
+ return v;
17414
+ };
17415
+ return walk(value);
17416
+ }
17417
+
17407
17418
  // src/cli/connections.ts
17408
17419
  var CONN = "/v1/host/openwop-app/connections/:id";
17409
17420
  var CONNECTIONS_ROUTES = [
@@ -17479,15 +17490,7 @@ Examples:
17479
17490
  `;
17480
17491
  var SECRET_KEY = /secret|token|password|private[-_]?key|client[-_]?secret|api[-_]?key|credential/i;
17481
17492
  function redact(value) {
17482
- if (Array.isArray(value)) return value.map(redact);
17483
- if (value && typeof value === "object") {
17484
- const out = {};
17485
- for (const [k, v] of Object.entries(value)) {
17486
- out[k] = SECRET_KEY.test(k) ? "[redacted]" : redact(v);
17487
- }
17488
- return out;
17489
- }
17490
- return value;
17493
+ return redactSecrets(value, { secret: SECRET_KEY });
17491
17494
  }
17492
17495
  var TOP = ["list", "get", "test", "authorize", "oauth-clients", "oauth-client"];
17493
17496
  async function runConnections(ctx, argv) {
@@ -17691,7 +17694,8 @@ Run \`openwop connections --help\` for usage.`);
17691
17694
  } catch (err) {
17692
17695
  gate4042(err);
17693
17696
  }
17694
- const clients = (Array.isArray(res.body?.clients) ? res.body.clients : []).map(redact);
17697
+ const rawClients = Array.isArray(res.body?.clients) ? res.body.clients : [];
17698
+ const clients = rawClients.map(redact);
17695
17699
  if (action === "get") {
17696
17700
  if (positionals.length !== 1) {
17697
17701
  write(ctx.io.stdout, "Usage: openwop connections oauth-clients get <provider> [--json]\n");
@@ -19207,15 +19211,7 @@ Examples:
19207
19211
  var BASE11 = "/v1/host/openwop-app/auth";
19208
19212
  var SECRET_KEY2 = /secret|token|password|private[-_]?key|client[-_]?secret|api[-_]?key|credential|bearer|assertion|certificate|cert|pem/i;
19209
19213
  function redact2(value) {
19210
- if (Array.isArray(value)) return value.map(redact2);
19211
- if (value && typeof value === "object") {
19212
- const out = {};
19213
- for (const [k, v] of Object.entries(value)) {
19214
- out[k] = SECRET_KEY2.test(k) ? "[redacted]" : redact2(v);
19215
- }
19216
- return out;
19217
- }
19218
- return value;
19214
+ return redactSecrets(value, { secret: SECRET_KEY2 });
19219
19215
  }
19220
19216
  async function authRequest(ctx, path, options, notConfiguredMsg) {
19221
19217
  try {
@@ -20327,15 +20323,7 @@ Examples:
20327
20323
  var SECRET_KEY3 = /secret|token|password|private[-_]?key|client[-_]?secret|api[-_]?key|credential/i;
20328
20324
  var REF_ONLY_KEY = /^secretsToRebind$/;
20329
20325
  function redact3(value) {
20330
- if (Array.isArray(value)) return value.map(redact3);
20331
- if (value && typeof value === "object") {
20332
- const out = {};
20333
- for (const [k, v] of Object.entries(value)) {
20334
- out[k] = SECRET_KEY3.test(k) && !REF_ONLY_KEY.test(k) ? "[redacted]" : redact3(v);
20335
- }
20336
- return out;
20337
- }
20338
- return value;
20326
+ return redactSecrets(value, { secret: SECRET_KEY3, keep: REF_ONLY_KEY });
20339
20327
  }
20340
20328
  async function ensureAdvertised5(ctx) {
20341
20329
  const wk = await safeRequest(ctx, "/.well-known/openwop", { auth: false });
@@ -21991,7 +21979,7 @@ async function startJob(ctx, path, body) {
21991
21979
  const res = await requestJson(ctx, path, { method: "POST", body });
21992
21980
  return renderDone(ctx, res.body, `Video job ${res.body?.jobId ?? ""} ${res.body?.status ?? "started"}.`);
21993
21981
  } catch (err) {
21994
- const b = err instanceof HttpError ? err.body : void 0;
21982
+ const b = err instanceof HttpError && err.body && typeof err.body === "object" ? err.body : void 0;
21995
21983
  if (err instanceof HttpError && b && b.status === "failed") {
21996
21984
  if (ctx.json) writeJson(ctx.io.stdout, b);
21997
21985
  else writeLine(ctx.io.stderr, `openwop: video generation failed (HTTP ${err.status}): ${b.error ?? "unknown error"}${b.error === "no_connection" ? " \u2014 connect the video provider under Connections first" : ""}`);
@@ -29495,8 +29483,9 @@ async function runCapabilityFirewall(ctx, argv) {
29495
29483
  try {
29496
29484
  return await runResourceGroup(ctx, "capability-firewall", CAPABILITY_FIREWALL_HELP, CAPABILITY_FIREWALL_SPECS, argv);
29497
29485
  } catch (err) {
29498
- if (argv[0] === "platform" && err instanceof HttpError && (err.status === 401 || err.status === 403)) {
29499
- throw new CliError(gateAdvice("superadmin", "The capability-firewall platform baseline"), 4);
29486
+ if (argv[0] === "platform" && err instanceof CliError && err.code === 4) {
29487
+ throw new CliError(`${gateAdvice("superadmin", "The capability-firewall platform baseline")}
29488
+ ${err.message}`, 4);
29500
29489
  }
29501
29490
  throw err;
29502
29491
  }
@@ -30443,6 +30432,7 @@ Examples:
30443
30432
  openwop packs install community.openwop-team.demo --version 0.1.0
30444
30433
  `;
30445
30434
  export {
30435
+ BASE_SECRET_KEY,
30446
30436
  CLI_PROTOCOL_VERSION_BY_MAJOR,
30447
30437
  COMMAND_NAMES,
30448
30438
  DEFAULT_BASE_URL,
@@ -30488,6 +30478,7 @@ export {
30488
30478
  projectTenantBoundId,
30489
30479
  readConfigSafe,
30490
30480
  readDaemonRecord,
30481
+ redactSecrets,
30491
30482
  renderEvent,
30492
30483
  requestNormativeOrHost,
30493
30484
  resolveStreamRequest,
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.2.0",
582
+ version: "1.2.2",
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",
@@ -1236,11 +1236,15 @@ function isLoopback(url) {
1236
1236
  }
1237
1237
  function explicitStreamBase(raw) {
1238
1238
  if (typeof raw !== "string" || raw.trim() === "") return void 0;
1239
+ let url;
1239
1240
  try {
1240
- new URL(raw.trim());
1241
+ url = new URL(raw.trim());
1241
1242
  } catch {
1242
1243
  throw new CliError(`--stream-base-url / OPENWOP_STREAM_BASE_URL is not a URL: ${raw}`);
1243
1244
  }
1245
+ if (url.username !== "" || url.password !== "") {
1246
+ throw new CliError("--stream-base-url / OPENWOP_STREAM_BASE_URL must not carry credentials (user:pass@); pass them with --api-key.");
1247
+ }
1244
1248
  return raw.trim().replace(/\/+$/, "");
1245
1249
  }
1246
1250
  function streamBaseFrom(extensions, baseUrl) {
@@ -1481,6 +1485,10 @@ async function streamRunEvents(ctx, runId, opts = {}) {
1481
1485
  } catch (err) {
1482
1486
  if (err instanceof HttpError && err.status >= 400 && err.status < 500 && (opts.streamMode || opts.lastEventId !== void 0 || opts.afterSequence !== void 0)) throw err;
1483
1487
  if (err instanceof SseResumeExhausted) throw err.reason instanceof HttpError ? err.reason : err;
1488
+ if (err instanceof StreamSilent && err.beforeHeaders && !ctx.quiet) {
1489
+ ctx.io.stderr.write(`openwop: the event stream sent nothing for ${err.ms >= 1e3 ? `${Math.round(err.ms / 1e3)} s` : `${err.ms} ms`} \u2014 the host's front door may be buffering streams. Following by polling instead; for live events pass --stream-base-url <a direct origin> (see \`openwop doctor\`).
1490
+ `);
1491
+ }
1484
1492
  }
1485
1493
  }
1486
1494
  if (opts.streamMode && opts.streamMode !== "updates" && !ctx.quiet) {
@@ -1530,8 +1538,10 @@ async function streamViaSse(ctx, runId, deliver, opts) {
1530
1538
  try {
1531
1539
  const { url, headers } = await resolveStreamRequest(ctx, `/v1/runs/${encodeURIComponent(runId)}/events${query}`, {});
1532
1540
  if (lastEventId !== void 0 && lastEventId !== "") headers["last-event-id"] = lastEventId;
1533
- idle = idleWatchdog(opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS);
1541
+ const idleMs = opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
1542
+ idle = idleWatchdog(idleMs, opts.headersTimeoutMs ?? (idleMs > 0 ? Math.min(idleMs, DEFAULT_HEADERS_TIMEOUT_MS) : 0));
1534
1543
  const res = await ctx.fetchImpl(url, { method: "GET", headers, signal: idle.signal });
1544
+ idle.headersArrived();
1535
1545
  if (!res.ok) {
1536
1546
  const text = await res.text().catch(() => "");
1537
1547
  let body = null;
@@ -1575,12 +1585,15 @@ async function streamViaSse(ctx, runId, deliver, opts) {
1575
1585
  if (await runIsTerminal(ctx, runId)) return true;
1576
1586
  failures = progressed ? 1 : failures + 1;
1577
1587
  if (failures > maxReconnects) {
1578
- throw new SseResumeExhausted(`events stream for ${runId} dropped ${failures} times without progress${dropError instanceof Error ? `: ${dropError.message}` : ""}`, dropError);
1588
+ throw new SseResumeExhausted(`events stream for ${runId} dropped ${failures} times without progress${dropError instanceof Error ? `: ${dropError.message}` : ""}. Re-run with --since <last sequence printed> to continue, or --no-stream to follow by polling.`, dropError);
1579
1589
  }
1580
1590
  const delayMs = Math.min(retryMs * 2 ** (failures - 1), 3e4);
1581
1591
  opts.onReconnect?.({ attempt: failures, lastEventId, delayMs });
1582
- if (ctx.verbose) ctx.io.stderr.write(`openwop: events stream dropped; reconnecting in ${delayMs}ms with Last-Event-ID ${lastEventId ?? "(none)"}
1592
+ if (ctx.verbose) {
1593
+ const why = dropError instanceof StreamSilent ? `stalled (${dropError.message})` : "dropped";
1594
+ ctx.io.stderr.write(`openwop: events stream ${why}; reconnecting in ${delayMs}ms with Last-Event-ID ${lastEventId ?? "(none)"}
1583
1595
  `);
1596
+ }
1584
1597
  await sleep(delayMs);
1585
1598
  }
1586
1599
  }
@@ -1779,29 +1792,44 @@ function defaultReadTurn(ctx) {
1779
1792
  });
1780
1793
  }
1781
1794
  var DEFAULT_IDLE_TIMEOUT_MS = 45e3;
1782
- function idleWatchdog(ms2) {
1795
+ var DEFAULT_HEADERS_TIMEOUT_MS = 1e4;
1796
+ var StreamSilent = class extends Error {
1797
+ constructor(ms2, beforeHeaders) {
1798
+ super(beforeHeaders ? `no response headers within ${ms2}ms` : `no bytes for ${ms2}ms`);
1799
+ this.ms = ms2;
1800
+ this.beforeHeaders = beforeHeaders;
1801
+ this.name = "StreamSilent";
1802
+ }
1803
+ };
1804
+ function idleWatchdog(ms2, headersMs = ms2) {
1783
1805
  const controller = new AbortController();
1784
1806
  let timer;
1785
1807
  let onExpire;
1786
- const expire = () => {
1787
- controller.abort(new Error(`no bytes for ${ms2}ms`));
1808
+ let beforeHeaders = true;
1809
+ const expire = (after) => {
1810
+ controller.abort(new StreamSilent(after, beforeHeaders));
1788
1811
  onExpire?.();
1789
1812
  };
1790
- const arm = () => {
1791
- if (!(ms2 > 0)) return;
1813
+ const arm = (after = ms2) => {
1814
+ if (!(after > 0)) return;
1792
1815
  if (timer !== void 0) clearTimeout(timer);
1793
- timer = setTimeout(expire, ms2);
1816
+ timer = setTimeout(() => expire(after), after);
1794
1817
  };
1795
- arm();
1818
+ arm(headersMs);
1796
1819
  return {
1797
1820
  signal: controller.signal,
1821
+ /** The response headers arrived: from here on only the idle timeout applies. */
1822
+ headersArrived() {
1823
+ beforeHeaders = false;
1824
+ arm();
1825
+ },
1798
1826
  watch(body) {
1799
1827
  if (!(ms2 > 0) || typeof body?.getReader !== "function") return body;
1800
1828
  const source = body.getReader();
1801
1829
  return new ReadableStream({
1802
1830
  start(out) {
1803
1831
  onExpire = () => {
1804
- out.error(new Error(`events stream idle for ${ms2}ms`));
1832
+ out.error(new StreamSilent(ms2, false));
1805
1833
  source.cancel().catch(() => {
1806
1834
  });
1807
1835
  };
@@ -1976,7 +2004,7 @@ function formatCheckTable(checks) {
1976
2004
 
1977
2005
  // src/cli/routeKit.ts
1978
2006
  var errText2 = (err) => err instanceof Error ? err.message : String(err);
1979
- var DEFAULT_MESSAGES = {
2007
+ var MSG = {
1980
2008
  required: (flag, usage6) => `${usage6}
1981
2009
  ${flag} is required.`,
1982
2010
  refusal: (words, positionals) => `Refusing to ${words.join(" ")} ${positionals.join(" ")} without --yes.`.replace(/\s+/g, " ").trim(),
@@ -2055,14 +2083,14 @@ function setPath(target, key, value) {
2055
2083
  }
2056
2084
  node[parts[parts.length - 1]] = value;
2057
2085
  }
2058
- function coerce(f, raw, cmd, msg) {
2086
+ function coerce(f, raw) {
2059
2087
  const t = f.type ?? "string";
2060
2088
  if (t === "boolean") return raw;
2061
2089
  if (t === "bool") {
2062
2090
  const s2 = String(raw);
2063
2091
  if (s2 === "true" || s2 === "yes" || s2 === "1") return true;
2064
2092
  if (s2 === "false" || s2 === "no" || s2 === "0") return false;
2065
- throw new CliError(msg.invalidBool(f.flag, s2), 2);
2093
+ throw new CliError(MSG.invalidBool(f.flag, s2), 2);
2066
2094
  }
2067
2095
  if (t === "list") return Array.isArray(raw) ? raw.map(String) : [String(raw)];
2068
2096
  if (t === "map") {
@@ -2079,7 +2107,7 @@ function coerce(f, raw, cmd, msg) {
2079
2107
  const s = String(raw);
2080
2108
  if (t === "number") {
2081
2109
  const n = Number(s);
2082
- if (!Number.isFinite(n) || cmd.strictNumbers && s.trim() === "") throw new CliError(msg.invalidNumber(f.flag, s), 2);
2110
+ if (s.trim() === "" || !Number.isFinite(n)) throw new CliError(MSG.invalidNumber(f.flag, s), 2);
2083
2111
  return n;
2084
2112
  }
2085
2113
  if (t === "json-file") {
@@ -2087,7 +2115,7 @@ function coerce(f, raw, cmd, msg) {
2087
2115
  try {
2088
2116
  text = readFileSync4(s, "utf8");
2089
2117
  } catch (err) {
2090
- throw new CliError(msg.unreadable(f.flag, s, err), 2);
2118
+ throw new CliError(MSG.unreadable(f.flag, s, err), 2);
2091
2119
  }
2092
2120
  try {
2093
2121
  return JSON.parse(text);
@@ -2099,14 +2127,14 @@ function coerce(f, raw, cmd, msg) {
2099
2127
  try {
2100
2128
  return readFileSync4(s).toString("base64");
2101
2129
  } catch (err) {
2102
- throw new CliError(msg.unreadable(f.flag, s, err), 2);
2130
+ throw new CliError(MSG.unreadable(f.flag, s, err), 2);
2103
2131
  }
2104
2132
  }
2105
2133
  if (t === "file") {
2106
2134
  try {
2107
2135
  return readFileSync4(s, "utf8").replace(/\r?\n$/, "");
2108
2136
  } catch (err) {
2109
- throw new CliError(msg.unreadable(f.flag, s, err), 2);
2137
+ throw new CliError(MSG.unreadable(f.flag, s, err), 2);
2110
2138
  }
2111
2139
  }
2112
2140
  if (t === "csv") return s.split(",").map((x) => x.trim()).filter(Boolean);
@@ -2114,19 +2142,19 @@ function coerce(f, raw, cmd, msg) {
2114
2142
  try {
2115
2143
  return JSON.parse(s);
2116
2144
  } catch {
2117
- throw new CliError(msg.invalidJson(f.flag, s), 2);
2145
+ throw new CliError(MSG.invalidJson(f.flag), 2);
2118
2146
  }
2119
2147
  }
2120
2148
  return s;
2121
2149
  }
2122
- function readBodyOptions(options, msg) {
2150
+ function readBodyOptions(options) {
2123
2151
  let raw;
2124
2152
  if (options.bodyFile !== void 0) {
2125
2153
  if (String(options.bodyFile).trim() === "") throw new CliError("--body-file needs a path (got an empty value).", 2);
2126
2154
  try {
2127
2155
  raw = readFileSync4(String(options.bodyFile), "utf8");
2128
2156
  } catch (err) {
2129
- throw new CliError(msg.unreadableBody(String(options.bodyFile), err), 2);
2157
+ throw new CliError(MSG.unreadableBody(String(options.bodyFile), err), 2);
2130
2158
  }
2131
2159
  } else if (options.body !== void 0) {
2132
2160
  raw = String(options.body);
@@ -2136,9 +2164,9 @@ function readBodyOptions(options, msg) {
2136
2164
  try {
2137
2165
  parsed = JSON.parse(raw);
2138
2166
  } catch {
2139
- throw new CliError(msg.invalidBodyJson, 2);
2167
+ throw new CliError(MSG.invalidBodyJson, 2);
2140
2168
  }
2141
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new CliError(msg.bodyNotObject, 2);
2169
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new CliError(MSG.bodyNotObject, 2);
2142
2170
  return parsed;
2143
2171
  }
2144
2172
  function hostError(err) {
@@ -2149,7 +2177,6 @@ function hostError(err) {
2149
2177
  ${hint}` : line, code2);
2150
2178
  }
2151
2179
  async function runRoute(ctx, group, cmd, argv) {
2152
- const msg = { ...DEFAULT_MESSAGES, ...cmd.messages ?? {} };
2153
2180
  const rest = argv.slice(cmd.words.length);
2154
2181
  const fields = [...cmd.query ?? [], ...cmd.body ?? []];
2155
2182
  const bool = [...COMMON_BOOL, ...fields.filter((f) => f.type === "boolean").flatMap((f) => [f.flag, f.flag.replace(/^--/, "--no-")])];
@@ -2162,8 +2189,7 @@ async function runRoute(ctx, group, cmd, argv) {
2162
2189
  const multi = fields.filter(isMulti).map((f) => f.flag);
2163
2190
  const { options, positionals } = parseOptions(rest, { bool, value, multi });
2164
2191
  if (options.help) {
2165
- if (cmd.usageText !== void 0) write(ctx.io.stdout, cmd.usageText);
2166
- else writeLine(ctx.io.stdout, `Usage: ${usageLine(group, cmd)}
2192
+ writeLine(ctx.io.stdout, `Usage: ${usageLine(group, cmd)}
2167
2193
 
2168
2194
  ${cmd.summary}
2169
2195
  Endpoint: ${cmd.method} ${templates(cmd).join(" | ")}`);
@@ -2171,12 +2197,12 @@ Endpoint: ${cmd.method} ${templates(cmd).join(" | ")}`);
2171
2197
  }
2172
2198
  const template = templates(cmd).find((p) => positionalNames(cmd, p).length === positionals.length);
2173
2199
  if (!template) {
2174
- write(ctx.io.stderr, cmd.usageText ?? `Usage: ${usageLine(group, cmd)}
2200
+ write(ctx.io.stderr, `Usage: ${usageLine(group, cmd)}
2175
2201
  `);
2176
2202
  return 2;
2177
2203
  }
2178
2204
  const path = fillPath(cmd, template, positionals, options.org);
2179
- const requiredError = (f) => new CliError(msg.required(f.flag, cmd.usageText ?? usageLine(group, cmd)), 2);
2205
+ const requiredError = (f) => new CliError(MSG.required(f.flag, usageLine(group, cmd)), 2);
2180
2206
  const optionOf = (f) => {
2181
2207
  const name = f.flag.replace(/^--/, "").replace(/-([a-z])/g, (_, c) => c.toUpperCase());
2182
2208
  if (f.type === "boolean") {
@@ -2194,7 +2220,7 @@ Endpoint: ${cmd.method} ${templates(cmd).join(" | ")}`);
2194
2220
  if (f.required) throw requiredError(f);
2195
2221
  continue;
2196
2222
  }
2197
- const c = coerce(f, v, cmd, msg);
2223
+ const c = coerce(f, v);
2198
2224
  if (f.type === "csv") qs2.set(f.key, c.join(","));
2199
2225
  else if (Array.isArray(c)) c.forEach((x) => qs2.append(f.key, String(x)));
2200
2226
  else qs2.set(f.key, c !== null && typeof c === "object" ? JSON.stringify(c) : String(c));
@@ -2202,7 +2228,7 @@ Endpoint: ${cmd.method} ${templates(cmd).join(" | ")}`);
2202
2228
  const url = qs2.size ? `${path}${path.includes("?") ? "&" : "?"}${qs2.toString()}` : path;
2203
2229
  const confirm2 = cmd.confirm ?? cmd.method === "DELETE";
2204
2230
  const gate = () => {
2205
- if (confirm2 && !options.yes) throw new CliError(msg.refusal(cmd.words, positionals), 2);
2231
+ if (confirm2 && !options.yes) throw new CliError(MSG.refusal(cmd.words, positionals), 2);
2206
2232
  };
2207
2233
  if (!cmd.validateFirst) gate();
2208
2234
  const isWrite = cmd.method !== "GET" && cmd.method !== "DELETE";
@@ -2210,10 +2236,10 @@ Endpoint: ${cmd.method} ${templates(cmd).join(" | ")}`);
2210
2236
  let explicit = {};
2211
2237
  const overlay = {};
2212
2238
  if (takesBody) {
2213
- explicit = readBodyOptions(options, msg);
2239
+ explicit = readBodyOptions(options);
2214
2240
  for (const f of cmd.body ?? []) {
2215
2241
  const v = optionOf(f);
2216
- if (v !== void 0) setPath(overlay, f.key, coerce(f, v, cmd, msg));
2242
+ if (v !== void 0) setPath(overlay, f.key, coerce(f, v));
2217
2243
  else if (cmd.validateFirst && f.required && getPath(explicit, f.key) === void 0) throw requiredError(f);
2218
2244
  }
2219
2245
  }
@@ -2250,7 +2276,7 @@ Endpoint: ${cmd.method} ${templates(cmd).join(" | ")}`);
2250
2276
  render(ctx, { ...cmd, method: "GET" }, positionals, err.status, err.body);
2251
2277
  return 1;
2252
2278
  }
2253
- if (err instanceof HttpError && cmd.hostErrors !== false) throw hostError(err);
2279
+ if (err instanceof HttpError) throw hostError(err);
2254
2280
  throw err;
2255
2281
  }
2256
2282
  }
@@ -2261,7 +2287,7 @@ async function send(ctx, cmd, url, init) {
2261
2287
  try {
2262
2288
  return await requestJson(ctx, url, { method: init.method, ...init.body !== void 0 ? { body: init.body } : {}, ...cmd.anonymous ? { auth: false } : {} });
2263
2289
  } catch (err) {
2264
- if (err instanceof HttpError && cmd.hostErrors !== false) throw hostError(err);
2290
+ if (err instanceof HttpError) throw hostError(err);
2265
2291
  throw err;
2266
2292
  }
2267
2293
  }
@@ -2311,7 +2337,7 @@ function render(ctx, cmd, positionals, status, rawBody) {
2311
2337
  return;
2312
2338
  }
2313
2339
  }
2314
- if (cmd.method === "GET" || cmd.writeOutput === "json") {
2340
+ if (cmd.method === "GET") {
2315
2341
  writeJson(ctx.io.stdout, body);
2316
2342
  return;
2317
2343
  }
@@ -2650,7 +2676,8 @@ async function runInterrupts(ctx, argv) {
2650
2676
  if (b.key) writeLine(ctx.io.stdout, `key: ${b.key}`);
2651
2677
  writeLine(ctx.io.stdout, `resolved: ${b.resolved ? "yes" : "no"}`);
2652
2678
  if (b.expiresAt) writeLine(ctx.io.stdout, `expiresAt: ${b.expiresAt}`);
2653
- if (b.data && typeof b.data === "object" && typeof b.data.prompt === "string") writeLine(ctx.io.stdout, `prompt: ${b.data.prompt}`);
2679
+ const prompt = b.data && typeof b.data === "object" ? b.data.prompt : void 0;
2680
+ if (typeof prompt === "string") writeLine(ctx.io.stdout, `prompt: ${prompt}`);
2654
2681
  if (b.resumeSchema !== void 0) writeLine(ctx.io.stdout, `resumeSchema: ${JSON.stringify(b.resumeSchema)}`);
2655
2682
  return 0;
2656
2683
  }
@@ -6282,25 +6309,8 @@ function parseField(spec) {
6282
6309
  const flag = `--${flagOverride ?? key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`).replace(/_/g, "-")}`;
6283
6310
  return { key, flag, type, required };
6284
6311
  }
6285
- function hasOrg(route) {
6286
- return /:org(?![A-Za-z0-9_])/.test(route);
6287
- }
6288
- function paramsOf(route) {
6289
- return (route.match(/:[A-Za-z][A-Za-z0-9_]*/g) ?? []).map((p) => p.slice(1)).filter((p) => p !== "org");
6290
- }
6291
6312
  function usageLine2(group, spec) {
6292
- const parts = [`openwop ${group}`, ...spec.cmd, ...paramsOf(spec.route).map((p) => `<${p}>`)];
6293
- if (hasOrg(spec.route)) parts.push("--org <orgId>");
6294
- const fmt = (f) => {
6295
- const v = f.type === "bool" ? "true|false" : f.type === "json" ? "json" : f.type === "list" ? "a,b" : f.type === "number" ? "n" : f.type === "file" ? "path" : "v";
6296
- return f.required ? `${f.flag} <${v}>` : `[${f.flag} <${v}>]`;
6297
- };
6298
- for (const f of (spec.query ?? []).map(parseField)) parts.push(fmt(f));
6299
- for (const f of (spec.body ?? []).map(parseField)) parts.push(fmt(f));
6300
- if (spec.body || spec.rawBody) parts.push("[--body <json> | --body-file <path>]");
6301
- if (spec.confirm) parts.push("--yes");
6302
- parts.push("[--json]");
6303
- return parts.join(" ");
6313
+ return usageLine(group, toRouteCmd(spec));
6304
6314
  }
6305
6315
  function buildGroupHelp(group, intro, specs, footer = "") {
6306
6316
  const lines = ["Usage:"];
@@ -6329,10 +6339,7 @@ function toField(spec) {
6329
6339
  const f = parseField(spec);
6330
6340
  return { flag: f.flag, key: f.key, type: TYPES[f.type] ?? "string", required: f.required };
6331
6341
  }
6332
- function toRouteCmd(group, spec) {
6333
- const usage6 = `Usage: ${usageLine2(group, spec)}
6334
- ${spec.method} ${spec.route} \u2014 ${spec.summary}
6335
- `;
6342
+ function toRouteCmd(spec) {
6336
6343
  const rmwKey = spec.rmwKey;
6337
6344
  return {
6338
6345
  words: spec.cmd,
@@ -6352,30 +6359,14 @@ function toRouteCmd(group, spec) {
6352
6359
  rawText: true,
6353
6360
  // every spec, as before — `text` only documents the route
6354
6361
  noContent: spec.method === "DELETE" ? "Deleted." : "Done (HTTP {status}).",
6355
- writeOutput: "json",
6356
- hostErrors: false,
6357
- validateFirst: true,
6358
- strictNumbers: true,
6359
- usageText: usage6,
6360
- messages: {
6361
- required: (flag, usageText) => `${flag} is required.
6362
- ${usageText}`,
6363
- refusal: () => `Refusing to ${spec.summary.charAt(0).toLowerCase()}${spec.summary.slice(1).replace(/\.$/, "")} without --yes.`,
6364
- invalidNumber: (flag, raw) => `${flag} must be a number, got: ${raw}`,
6365
- invalidBool: (flag, raw) => `${flag} must be true or false, got: ${raw}`,
6366
- invalidJson: (flag) => `${flag} must be valid JSON`,
6367
- unreadable: (flag, path) => `Cannot read ${flag} ${path}`,
6368
- unreadableBody: (path) => `Cannot read --body-file ${path}`,
6369
- invalidBodyJson: "--body/--body-file must be valid JSON",
6370
- bodyNotObject: "--body/--body-file must be a JSON object"
6371
- }
6362
+ validateFirst: true
6372
6363
  };
6373
6364
  }
6374
6365
  function matchSpec(specs, argv) {
6375
6366
  return matchRoute(specs.map((spec) => ({ words: spec.cmd, spec })), argv)?.spec;
6376
6367
  }
6377
6368
  async function runSpec(ctx, group, spec, argv) {
6378
- return runRoute(ctx, group, toRouteCmd(group, spec), [...spec.cmd, ...argv]);
6369
+ return runRoute(ctx, group, toRouteCmd(spec), [...spec.cmd, ...argv]);
6379
6370
  }
6380
6371
  async function dispatchSpecs(ctx, group, specs, argv) {
6381
6372
  const spec = matchSpec(specs, argv);
@@ -12417,7 +12408,9 @@ honouring the server's \`retry:\` field), and no event is printed twice.
12417
12408
  (\`values\` never combines). A host that does not serve
12418
12409
  the mode answers 400 unsupported_stream_mode.
12419
12410
  --idle-timeout-ms N a connection that delivers no bytes for N ms (default
12420
- 45000; 0 disables) is treated as dropped and resumed.
12411
+ 45000; 0 disables) is treated as dropped and resumed. A
12412
+ stream whose headers do not arrive within 10 s is given
12413
+ up on and followed by polling, with a hint on stderr.
12421
12414
  Streams are read from the stream origin: --stream-base-url (global), else the
12422
12415
  host's advertised \`streamBase\` (https only), else --base-url \u2014 some front
12423
12416
  doors buffer event streams entirely (e.g. a CDN rewrite).
@@ -17404,6 +17397,24 @@ async function runPromptsGet(ctx, argv) {
17404
17397
  return 0;
17405
17398
  }
17406
17399
 
17400
+ // src/redact.ts
17401
+ var BASE_SECRET_KEY = /secret|token|password|private[-_]?key|client[-_]?secret|api[-_]?key|credential/i;
17402
+ function redactSecrets(value, rules = {}) {
17403
+ const secret = rules.secret ?? BASE_SECRET_KEY;
17404
+ const walk = (v) => {
17405
+ if (Array.isArray(v)) return v.map(walk);
17406
+ if (v && typeof v === "object") {
17407
+ const out = {};
17408
+ for (const [k, inner] of Object.entries(v)) {
17409
+ out[k] = secret.test(k) && !(rules.keep?.test(k) ?? false) ? "[redacted]" : walk(inner);
17410
+ }
17411
+ return out;
17412
+ }
17413
+ return v;
17414
+ };
17415
+ return walk(value);
17416
+ }
17417
+
17407
17418
  // src/cli/connections.ts
17408
17419
  var CONN = "/v1/host/openwop-app/connections/:id";
17409
17420
  var CONNECTIONS_ROUTES = [
@@ -17479,15 +17490,7 @@ Examples:
17479
17490
  `;
17480
17491
  var SECRET_KEY = /secret|token|password|private[-_]?key|client[-_]?secret|api[-_]?key|credential/i;
17481
17492
  function redact(value) {
17482
- if (Array.isArray(value)) return value.map(redact);
17483
- if (value && typeof value === "object") {
17484
- const out = {};
17485
- for (const [k, v] of Object.entries(value)) {
17486
- out[k] = SECRET_KEY.test(k) ? "[redacted]" : redact(v);
17487
- }
17488
- return out;
17489
- }
17490
- return value;
17493
+ return redactSecrets(value, { secret: SECRET_KEY });
17491
17494
  }
17492
17495
  var TOP = ["list", "get", "test", "authorize", "oauth-clients", "oauth-client"];
17493
17496
  async function runConnections(ctx, argv) {
@@ -17691,7 +17694,8 @@ Run \`openwop connections --help\` for usage.`);
17691
17694
  } catch (err) {
17692
17695
  gate4042(err);
17693
17696
  }
17694
- const clients = (Array.isArray(res.body?.clients) ? res.body.clients : []).map(redact);
17697
+ const rawClients = Array.isArray(res.body?.clients) ? res.body.clients : [];
17698
+ const clients = rawClients.map(redact);
17695
17699
  if (action === "get") {
17696
17700
  if (positionals.length !== 1) {
17697
17701
  write(ctx.io.stdout, "Usage: openwop connections oauth-clients get <provider> [--json]\n");
@@ -19207,15 +19211,7 @@ Examples:
19207
19211
  var BASE11 = "/v1/host/openwop-app/auth";
19208
19212
  var SECRET_KEY2 = /secret|token|password|private[-_]?key|client[-_]?secret|api[-_]?key|credential|bearer|assertion|certificate|cert|pem/i;
19209
19213
  function redact2(value) {
19210
- if (Array.isArray(value)) return value.map(redact2);
19211
- if (value && typeof value === "object") {
19212
- const out = {};
19213
- for (const [k, v] of Object.entries(value)) {
19214
- out[k] = SECRET_KEY2.test(k) ? "[redacted]" : redact2(v);
19215
- }
19216
- return out;
19217
- }
19218
- return value;
19214
+ return redactSecrets(value, { secret: SECRET_KEY2 });
19219
19215
  }
19220
19216
  async function authRequest(ctx, path, options, notConfiguredMsg) {
19221
19217
  try {
@@ -20327,15 +20323,7 @@ Examples:
20327
20323
  var SECRET_KEY3 = /secret|token|password|private[-_]?key|client[-_]?secret|api[-_]?key|credential/i;
20328
20324
  var REF_ONLY_KEY = /^secretsToRebind$/;
20329
20325
  function redact3(value) {
20330
- if (Array.isArray(value)) return value.map(redact3);
20331
- if (value && typeof value === "object") {
20332
- const out = {};
20333
- for (const [k, v] of Object.entries(value)) {
20334
- out[k] = SECRET_KEY3.test(k) && !REF_ONLY_KEY.test(k) ? "[redacted]" : redact3(v);
20335
- }
20336
- return out;
20337
- }
20338
- return value;
20326
+ return redactSecrets(value, { secret: SECRET_KEY3, keep: REF_ONLY_KEY });
20339
20327
  }
20340
20328
  async function ensureAdvertised5(ctx) {
20341
20329
  const wk = await safeRequest(ctx, "/.well-known/openwop", { auth: false });
@@ -21991,7 +21979,7 @@ async function startJob(ctx, path, body) {
21991
21979
  const res = await requestJson(ctx, path, { method: "POST", body });
21992
21980
  return renderDone(ctx, res.body, `Video job ${res.body?.jobId ?? ""} ${res.body?.status ?? "started"}.`);
21993
21981
  } catch (err) {
21994
- const b = err instanceof HttpError ? err.body : void 0;
21982
+ const b = err instanceof HttpError && err.body && typeof err.body === "object" ? err.body : void 0;
21995
21983
  if (err instanceof HttpError && b && b.status === "failed") {
21996
21984
  if (ctx.json) writeJson(ctx.io.stdout, b);
21997
21985
  else writeLine(ctx.io.stderr, `openwop: video generation failed (HTTP ${err.status}): ${b.error ?? "unknown error"}${b.error === "no_connection" ? " \u2014 connect the video provider under Connections first" : ""}`);
@@ -29495,8 +29483,9 @@ async function runCapabilityFirewall(ctx, argv) {
29495
29483
  try {
29496
29484
  return await runResourceGroup(ctx, "capability-firewall", CAPABILITY_FIREWALL_HELP, CAPABILITY_FIREWALL_SPECS, argv);
29497
29485
  } catch (err) {
29498
- if (argv[0] === "platform" && err instanceof HttpError && (err.status === 401 || err.status === 403)) {
29499
- throw new CliError(gateAdvice("superadmin", "The capability-firewall platform baseline"), 4);
29486
+ if (argv[0] === "platform" && err instanceof CliError && err.code === 4) {
29487
+ throw new CliError(`${gateAdvice("superadmin", "The capability-firewall platform baseline")}
29488
+ ${err.message}`, 4);
29500
29489
  }
29501
29490
  throw err;
29502
29491
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/cli",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
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",