@openwop/cli 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -565,6 +565,12 @@ function httpErrorHint(err, env) {
565
565
  }
566
566
  return void 0;
567
567
  }
568
+ function isNoCredentialChallenge(err) {
569
+ if (!(err instanceof HttpError) || err.status !== 401) return false;
570
+ const challenge = err.headers?.get?.("www-authenticate");
571
+ if (!challenge || !/^\s*Bearer\b/i.test(challenge)) return false;
572
+ return !/\berror\s*=/i.test(challenge);
573
+ }
568
574
  function errText(err) {
569
575
  return err instanceof Error ? err.message : String(err);
570
576
  }
@@ -572,7 +578,7 @@ function errText(err) {
572
578
  // package.json
573
579
  var package_default = {
574
580
  name: "@openwop/cli",
575
- version: "1.1.0",
581
+ version: "1.2.0",
576
582
  description: "OpenWOP CLI for operating the workflow-engine demo app and OpenWOP-compatible hosts.",
577
583
  license: "Apache-2.0",
578
584
  type: "module",
@@ -703,6 +709,7 @@ function prefixChunk(stream, label, chunk) {
703
709
  function extractGlobalOptions(argv, env = process.env) {
704
710
  const globals = {
705
711
  baseUrl: void 0,
712
+ streamBaseUrl: void 0,
706
713
  apiKey: void 0,
707
714
  profile: void 0,
708
715
  json: false,
@@ -720,6 +727,10 @@ function extractGlobalOptions(argv, env = process.env) {
720
727
  globals.baseUrl = value ?? takeValue(argv, ++i, "--base-url");
721
728
  continue;
722
729
  }
730
+ if (flag === "--stream-base-url") {
731
+ globals.streamBaseUrl = value ?? takeValue(argv, ++i, "--stream-base-url");
732
+ continue;
733
+ }
723
734
  if (flag === "--api-key") {
724
735
  globals.apiKey = value ?? takeValue(argv, ++i, "--api-key");
725
736
  continue;
@@ -1199,6 +1210,7 @@ async function negotiateMajor(ctx) {
1199
1210
  const doc = JSON.parse(await res.text());
1200
1211
  if (res.ok) {
1201
1212
  ctx.discovery = { doc, servedVersion: res.headers?.get?.("openwop-version") ?? void 0 };
1213
+ ctx.discoveredStreamBase = streamBaseFrom(doc?.extensions, ctx.baseUrl);
1202
1214
  major = selectMajor(doc?.protocolVersions);
1203
1215
  if (major === 2) roots = hostRootsFrom(doc?.extensions);
1204
1216
  } else if (res.status === 406) {
@@ -1219,6 +1231,63 @@ async function resolveRequest(ctx, path, headers) {
1219
1231
  if (rooted !== null) return { path: rooted, headers };
1220
1232
  return { path, headers };
1221
1233
  }
1234
+ function isLoopback(url) {
1235
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "::1";
1236
+ }
1237
+ function explicitStreamBase(raw) {
1238
+ if (typeof raw !== "string" || raw.trim() === "") return void 0;
1239
+ try {
1240
+ new URL(raw.trim());
1241
+ } catch {
1242
+ throw new CliError(`--stream-base-url / OPENWOP_STREAM_BASE_URL is not a URL: ${raw}`);
1243
+ }
1244
+ return raw.trim().replace(/\/+$/, "");
1245
+ }
1246
+ function streamBaseFrom(extensions, baseUrl) {
1247
+ if (!extensions || typeof extensions !== "object" || Array.isArray(extensions)) return void 0;
1248
+ let base16;
1249
+ try {
1250
+ base16 = new URL(baseUrl);
1251
+ } catch {
1252
+ return void 0;
1253
+ }
1254
+ for (const value of Object.values(extensions)) {
1255
+ const raw = value && typeof value === "object" ? value.streamBase : void 0;
1256
+ if (typeof raw !== "string") continue;
1257
+ let url;
1258
+ try {
1259
+ url = new URL(raw);
1260
+ } catch {
1261
+ continue;
1262
+ }
1263
+ if (url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "") continue;
1264
+ const secure = url.protocol === "https:" && (base16.protocol === "https:" || isLoopback(base16));
1265
+ const localDev = url.protocol === "http:" && isLoopback(url) && isLoopback(base16);
1266
+ if (!secure && !localDev) continue;
1267
+ return raw.replace(/\/+$/, "");
1268
+ }
1269
+ return void 0;
1270
+ }
1271
+ async function resolveStreamRequest(ctx, requestedPath, headers) {
1272
+ const resolved = await resolveRequest(ctx, requestedPath, { accept: "text/event-stream", ...headers });
1273
+ const { origin, source } = streamOrigin(ctx);
1274
+ if (ctx.verbose && source !== "base") ctx.io.stderr.write(`openwop: reading the event stream from ${origin} (${STREAM_SOURCE_LABEL[source]})
1275
+ `);
1276
+ const url = new URL(resolved.path.replace(/^\//, ""), origin.endsWith("/") ? origin : `${origin}/`);
1277
+ const out = { ...resolved.headers };
1278
+ if (ctx.apiKey) out.authorization = `Bearer ${ctx.apiKey}`;
1279
+ return { url, headers: out };
1280
+ }
1281
+ var STREAM_SOURCE_LABEL = {
1282
+ setting: "your --stream-base-url / OPENWOP_STREAM_BASE_URL / config setting",
1283
+ advertised: "the host's advertised streamBase",
1284
+ base: "--base-url"
1285
+ };
1286
+ function streamOrigin(ctx) {
1287
+ if (ctx.streamBaseUrl) return { origin: ctx.streamBaseUrl, source: "setting" };
1288
+ if (ctx.discoveredStreamBase) return { origin: ctx.discoveredStreamBase, source: "advertised" };
1289
+ return { origin: ctx.baseUrl, source: "base" };
1290
+ }
1222
1291
 
1223
1292
  // src/wire.ts
1224
1293
  import { randomUUID } from "node:crypto";
@@ -1457,12 +1526,12 @@ async function streamViaSse(ctx, runId, deliver, opts) {
1457
1526
  for (; ; ) {
1458
1527
  let progressed = false;
1459
1528
  let dropError;
1529
+ let idle;
1460
1530
  try {
1461
- const { path, headers } = await resolveRequest(ctx, `/v1/runs/${encodeURIComponent(runId)}/events${query}`, { accept: "text/event-stream" });
1462
- const url = new URL(path.replace(/^\//, ""), ctx.baseUrl.endsWith("/") ? ctx.baseUrl : `${ctx.baseUrl}/`);
1463
- if (ctx.apiKey) headers.authorization = `Bearer ${ctx.apiKey}`;
1531
+ const { url, headers } = await resolveStreamRequest(ctx, `/v1/runs/${encodeURIComponent(runId)}/events${query}`, {});
1464
1532
  if (lastEventId !== void 0 && lastEventId !== "") headers["last-event-id"] = lastEventId;
1465
- const res = await ctx.fetchImpl(url, { method: "GET", headers });
1533
+ idle = idleWatchdog(opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS);
1534
+ const res = await ctx.fetchImpl(url, { method: "GET", headers, signal: idle.signal });
1466
1535
  if (!res.ok) {
1467
1536
  const text = await res.text().catch(() => "");
1468
1537
  let body = null;
@@ -1479,7 +1548,7 @@ async function streamViaSse(ctx, runId, deliver, opts) {
1479
1548
  throw new HttpError("resumed events stream is not text/event-stream", res.status, null, res.headers);
1480
1549
  }
1481
1550
  opened = true;
1482
- await consumeSse(res.body, (frame) => {
1551
+ await consumeSse(idle.watch(res.body), (frame) => {
1483
1552
  if (frame.retry !== void 0) retryMs = frame.retry;
1484
1553
  if (frame.id !== void 0) lastEventId = frame.id;
1485
1554
  if (frame.data === void 0) return;
@@ -1498,6 +1567,8 @@ async function streamViaSse(ctx, runId, deliver, opts) {
1498
1567
  if (!opened) throw err;
1499
1568
  if (err instanceof HttpError && err.status !== 429 && err.status < 500) throw new SseResumeExhausted(err.message, err);
1500
1569
  dropError = err;
1570
+ } finally {
1571
+ idle?.stop();
1501
1572
  }
1502
1573
  if (terminal) return true;
1503
1574
  if (lastEventId === void 0 && deliver.highest() >= 0) lastEventId = String(deliver.highest());
@@ -1707,6 +1778,59 @@ function defaultReadTurn(ctx) {
1707
1778
  rl.once("close", onClose);
1708
1779
  });
1709
1780
  }
1781
+ var DEFAULT_IDLE_TIMEOUT_MS = 45e3;
1782
+ function idleWatchdog(ms2) {
1783
+ const controller = new AbortController();
1784
+ let timer;
1785
+ let onExpire;
1786
+ const expire = () => {
1787
+ controller.abort(new Error(`no bytes for ${ms2}ms`));
1788
+ onExpire?.();
1789
+ };
1790
+ const arm = () => {
1791
+ if (!(ms2 > 0)) return;
1792
+ if (timer !== void 0) clearTimeout(timer);
1793
+ timer = setTimeout(expire, ms2);
1794
+ };
1795
+ arm();
1796
+ return {
1797
+ signal: controller.signal,
1798
+ watch(body) {
1799
+ if (!(ms2 > 0) || typeof body?.getReader !== "function") return body;
1800
+ const source = body.getReader();
1801
+ return new ReadableStream({
1802
+ start(out) {
1803
+ onExpire = () => {
1804
+ out.error(new Error(`events stream idle for ${ms2}ms`));
1805
+ source.cancel().catch(() => {
1806
+ });
1807
+ };
1808
+ },
1809
+ async pull(out) {
1810
+ try {
1811
+ const { value, done: done2 } = await source.read();
1812
+ if (done2) {
1813
+ out.close();
1814
+ return;
1815
+ }
1816
+ arm();
1817
+ out.enqueue(value);
1818
+ } catch (err) {
1819
+ out.error(err);
1820
+ }
1821
+ },
1822
+ cancel(reason) {
1823
+ return source.cancel(reason);
1824
+ }
1825
+ });
1826
+ },
1827
+ stop() {
1828
+ if (timer !== void 0) clearTimeout(timer);
1829
+ timer = void 0;
1830
+ onExpire = void 0;
1831
+ }
1832
+ };
1833
+ }
1710
1834
 
1711
1835
  // src/cli/chatShared.ts
1712
1836
  var enc = (value) => encodeURIComponent(value);
@@ -1731,9 +1855,7 @@ function writeFields(ctx, fields) {
1731
1855
  }
1732
1856
  }
1733
1857
  async function streamHostSse(ctx, requestedPath, opts) {
1734
- const { path, headers } = await resolveRequest(ctx, requestedPath, { accept: "text/event-stream" });
1735
- const url = new URL(path.replace(/^\//, ""), ctx.baseUrl.endsWith("/") ? ctx.baseUrl : `${ctx.baseUrl}/`);
1736
- if (ctx.apiKey) headers.authorization = `Bearer ${ctx.apiKey}`;
1858
+ const { url, headers } = await resolveStreamRequest(ctx, requestedPath, {});
1737
1859
  const controller = new AbortController();
1738
1860
  let timer;
1739
1861
  if (opts.timeoutMs && opts.timeoutMs > 0) timer = setTimeout(() => controller.abort(), opts.timeoutMs);
@@ -1790,6 +1912,82 @@ function renderFrame(frame) {
1790
1912
 
1791
1913
  // src/cli/routeKit.ts
1792
1914
  import { readFileSync as readFileSync4 } from "node:fs";
1915
+
1916
+ // src/cli/shared.ts
1917
+ function requireOrg(org2) {
1918
+ if (!org2) throw new CliError("This command is org-scoped \u2014 pass --org <orgId>.", 2);
1919
+ return String(org2);
1920
+ }
1921
+ function buildInputs(options) {
1922
+ const fromJson = options.inputsJson ? JSON.parse(options.inputsJson) : {};
1923
+ if (fromJson === null || typeof fromJson !== "object" || Array.isArray(fromJson)) {
1924
+ throw new CliError("--inputs-json must be a JSON object");
1925
+ }
1926
+ const inputs = { ...fromJson };
1927
+ for (const pair of options.input ?? []) {
1928
+ const eq = pair.indexOf("=");
1929
+ if (eq <= 0) throw new CliError(`--input must be key=value, got: ${pair}`);
1930
+ inputs[pair.slice(0, eq)] = parseInputValue(pair.slice(eq + 1));
1931
+ }
1932
+ return inputs;
1933
+ }
1934
+ function parseInputValue(value) {
1935
+ try {
1936
+ return JSON.parse(value);
1937
+ } catch {
1938
+ return value;
1939
+ }
1940
+ }
1941
+ function parseNodeVersion(version) {
1942
+ const [major, minor, patch] = version.split(".").map((v) => Number(v));
1943
+ return { major: major || 0, minor: minor || 0, patch: patch || 0 };
1944
+ }
1945
+ function defaultApiKeyFor(baseUrl) {
1946
+ try {
1947
+ const host = new URL(baseUrl).hostname;
1948
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1") return DEFAULT_API_KEY;
1949
+ } catch {
1950
+ return void 0;
1951
+ }
1952
+ return void 0;
1953
+ }
1954
+ function normalizeBaseUrl(value) {
1955
+ if (!value) return DEFAULT_BASE_URL;
1956
+ return value.endsWith("/") ? value.slice(0, -1) : value;
1957
+ }
1958
+ function npmCommand() {
1959
+ return process.platform === "win32" ? "npm.cmd" : "npm";
1960
+ }
1961
+ function ok(name, message) {
1962
+ return { status: "ok", name, message };
1963
+ }
1964
+ function warn(name, message) {
1965
+ return { status: "warn", name, message };
1966
+ }
1967
+ function fail(name, message) {
1968
+ return { status: "fail", name, message };
1969
+ }
1970
+ function formatCheckTable(checks) {
1971
+ return formatTable(
1972
+ checks.map((c) => ({ status: c.status.toUpperCase(), check: c.name, message: c.message })),
1973
+ ["status", "check", "message"]
1974
+ );
1975
+ }
1976
+
1977
+ // src/cli/routeKit.ts
1978
+ var errText2 = (err) => err instanceof Error ? err.message : String(err);
1979
+ var DEFAULT_MESSAGES = {
1980
+ required: (flag, usage6) => `${usage6}
1981
+ ${flag} is required.`,
1982
+ refusal: (words, positionals) => `Refusing to ${words.join(" ")} ${positionals.join(" ")} without --yes.`.replace(/\s+/g, " ").trim(),
1983
+ invalidNumber: (flag, raw) => `${flag} must be a number (got "${raw}").`,
1984
+ invalidBool: (flag, raw) => `${flag} must be true or false (got "${raw}").`,
1985
+ invalidJson: (flag) => `${flag} must be valid JSON.`,
1986
+ unreadable: (flag, path, err) => `Could not read ${path} for ${flag}: ${errText2(err)}`,
1987
+ unreadableBody: (path, err) => `Could not read ${path}: ${errText2(err)}`,
1988
+ invalidBodyJson: "--body / --body-file must be a JSON object.",
1989
+ bodyNotObject: "--body / --body-file must be a JSON object."
1990
+ };
1793
1991
  var COMMON_BOOL = ["--help", "--yes"];
1794
1992
  function templates(cmd) {
1795
1993
  return Array.isArray(cmd.path) ? cmd.path : [cmd.path];
@@ -1797,18 +1995,31 @@ function templates(cmd) {
1797
1995
  function paramNames(path) {
1798
1996
  return [...path.matchAll(/:([A-Za-z][A-Za-z0-9_]*)/g)].map((m) => m[1]);
1799
1997
  }
1998
+ function positionalNames(cmd, path) {
1999
+ return paramNames(path).filter((p) => !(cmd.orgFlag && p === "org"));
2000
+ }
2001
+ function bindsOrg(cmd) {
2002
+ return Boolean(cmd.orgFlag) && templates(cmd).some((p) => paramNames(p).includes("org"));
2003
+ }
2004
+ function fillPath(cmd, template, positionals, org2) {
2005
+ let i = 0;
2006
+ return template.replace(/:([A-Za-z][A-Za-z0-9_]*)/g, (_m, name) => {
2007
+ if (cmd.orgFlag && name === "org") return encodeURIComponent(requireOrg(org2));
2008
+ return encodeURIComponent(positionals[i++] ?? "");
2009
+ });
2010
+ }
1800
2011
  function flagHint(f) {
1801
2012
  const t = f.type ?? "string";
1802
- const v = t === "boolean" ? "" : t === "file" || t === "file64" || t === "json-file" ? " <path>" : t === "map" ? " <key=value>..." : t === "list" ? ` <${f.key}>...` : t === "json" ? ` <json>` : ` <${f.key.split(".").pop()}>`;
2013
+ const v = t === "boolean" ? "" : t === "bool" ? " <true|false>" : t === "file" || t === "file64" || t === "json-file" ? " <path>" : t === "map" ? " <key=value>..." : t === "list" ? ` <${f.key}>...` : t === "json" ? ` <json>` : ` <${f.key.split(".").pop()}>`;
1803
2014
  return f.required ? `${f.flag}${v}` : `[${f.flag}${v}]`;
1804
2015
  }
1805
2016
  function usageLine(group, cmd) {
1806
2017
  const paths = templates(cmd);
1807
- const longest = paths.reduce((a, b) => paramNames(b).length > paramNames(a).length ? b : a);
1808
- const shortest = paths.reduce((a, b) => paramNames(b).length < paramNames(a).length ? b : a);
1809
- const req2 = paramNames(shortest);
1810
- const params = paramNames(longest).map((p, i) => i < req2.length ? `<${p}>` : `[<${p}>]`);
1811
- const flags = [...cmd.query ?? [], ...cmd.body ?? []].map(flagHint);
2018
+ const longest = paths.reduce((a, b) => positionalNames(cmd, b).length > positionalNames(cmd, a).length ? b : a);
2019
+ const shortest = paths.reduce((a, b) => positionalNames(cmd, b).length < positionalNames(cmd, a).length ? b : a);
2020
+ const req2 = positionalNames(cmd, shortest);
2021
+ const params = positionalNames(cmd, longest).map((p, i) => i < req2.length ? `<${p}>` : `[<${p}>]`);
2022
+ const flags = [...bindsOrg(cmd) ? ["--org <orgId>"] : [], ...[...cmd.query ?? [], ...cmd.body ?? []].map(flagHint)];
1812
2023
  if (cmd.body?.some((f) => f.type === "json")) flags.push("[--body <json>|--body-file <path>]");
1813
2024
  if (cmd.confirm ?? cmd.method === "DELETE") flags.push("--yes");
1814
2025
  if (cmd.method === "GET") flags.push("[--json]");
@@ -1844,9 +2055,15 @@ function setPath(target, key, value) {
1844
2055
  }
1845
2056
  node[parts[parts.length - 1]] = value;
1846
2057
  }
1847
- function coerce(f, raw) {
2058
+ function coerce(f, raw, cmd, msg) {
1848
2059
  const t = f.type ?? "string";
1849
2060
  if (t === "boolean") return raw;
2061
+ if (t === "bool") {
2062
+ const s2 = String(raw);
2063
+ if (s2 === "true" || s2 === "yes" || s2 === "1") return true;
2064
+ if (s2 === "false" || s2 === "no" || s2 === "0") return false;
2065
+ throw new CliError(msg.invalidBool(f.flag, s2), 2);
2066
+ }
1850
2067
  if (t === "list") return Array.isArray(raw) ? raw.map(String) : [String(raw)];
1851
2068
  if (t === "map") {
1852
2069
  const out = {};
@@ -1862,7 +2079,7 @@ function coerce(f, raw) {
1862
2079
  const s = String(raw);
1863
2080
  if (t === "number") {
1864
2081
  const n = Number(s);
1865
- if (!Number.isFinite(n)) throw new CliError(`${f.flag} must be a number (got "${s}").`, 2);
2082
+ if (!Number.isFinite(n) || cmd.strictNumbers && s.trim() === "") throw new CliError(msg.invalidNumber(f.flag, s), 2);
1866
2083
  return n;
1867
2084
  }
1868
2085
  if (t === "json-file") {
@@ -1870,7 +2087,7 @@ function coerce(f, raw) {
1870
2087
  try {
1871
2088
  text = readFileSync4(s, "utf8");
1872
2089
  } catch (err) {
1873
- throw new CliError(`Could not read ${s} for ${f.flag}: ${err instanceof Error ? err.message : String(err)}`, 2);
2090
+ throw new CliError(msg.unreadable(f.flag, s, err), 2);
1874
2091
  }
1875
2092
  try {
1876
2093
  return JSON.parse(text);
@@ -1882,14 +2099,14 @@ function coerce(f, raw) {
1882
2099
  try {
1883
2100
  return readFileSync4(s).toString("base64");
1884
2101
  } catch (err) {
1885
- throw new CliError(`Could not read ${s} for ${f.flag}: ${err instanceof Error ? err.message : String(err)}`, 2);
2102
+ throw new CliError(msg.unreadable(f.flag, s, err), 2);
1886
2103
  }
1887
2104
  }
1888
2105
  if (t === "file") {
1889
2106
  try {
1890
2107
  return readFileSync4(s, "utf8").replace(/\r?\n$/, "");
1891
2108
  } catch (err) {
1892
- throw new CliError(`Could not read ${s} for ${f.flag}: ${err instanceof Error ? err.message : String(err)}`, 2);
2109
+ throw new CliError(msg.unreadable(f.flag, s, err), 2);
1893
2110
  }
1894
2111
  }
1895
2112
  if (t === "csv") return s.split(",").map((x) => x.trim()).filter(Boolean);
@@ -1897,18 +2114,19 @@ function coerce(f, raw) {
1897
2114
  try {
1898
2115
  return JSON.parse(s);
1899
2116
  } catch {
1900
- throw new CliError(`${f.flag} must be valid JSON.`, 2);
2117
+ throw new CliError(msg.invalidJson(f.flag, s), 2);
1901
2118
  }
1902
2119
  }
1903
2120
  return s;
1904
2121
  }
1905
- function readBodyOptions(options) {
2122
+ function readBodyOptions(options, msg) {
1906
2123
  let raw;
1907
2124
  if (options.bodyFile !== void 0) {
2125
+ if (String(options.bodyFile).trim() === "") throw new CliError("--body-file needs a path (got an empty value).", 2);
1908
2126
  try {
1909
2127
  raw = readFileSync4(String(options.bodyFile), "utf8");
1910
2128
  } catch (err) {
1911
- throw new CliError(`Could not read ${String(options.bodyFile)}: ${err instanceof Error ? err.message : String(err)}`, 2);
2129
+ throw new CliError(msg.unreadableBody(String(options.bodyFile), err), 2);
1912
2130
  }
1913
2131
  } else if (options.body !== void 0) {
1914
2132
  raw = String(options.body);
@@ -1918,9 +2136,9 @@ function readBodyOptions(options) {
1918
2136
  try {
1919
2137
  parsed = JSON.parse(raw);
1920
2138
  } catch {
1921
- throw new CliError("--body / --body-file must be a JSON object.", 2);
2139
+ throw new CliError(msg.invalidBodyJson, 2);
1922
2140
  }
1923
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new CliError("--body / --body-file must be a JSON object.", 2);
2141
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new CliError(msg.bodyNotObject, 2);
1924
2142
  return parsed;
1925
2143
  }
1926
2144
  function hostError(err) {
@@ -1931,30 +2149,34 @@ function hostError(err) {
1931
2149
  ${hint}` : line, code);
1932
2150
  }
1933
2151
  async function runRoute(ctx, group, cmd, argv) {
2152
+ const msg = { ...DEFAULT_MESSAGES, ...cmd.messages ?? {} };
1934
2153
  const rest = argv.slice(cmd.words.length);
1935
2154
  const fields = [...cmd.query ?? [], ...cmd.body ?? []];
1936
2155
  const bool = [...COMMON_BOOL, ...fields.filter((f) => f.type === "boolean").flatMap((f) => [f.flag, f.flag.replace(/^--/, "--no-")])];
1937
2156
  const isMulti = (f) => f.type === "list" || f.type === "map";
1938
- const value = ["--body", "--body-file", ...fields.filter((f) => f.type !== "boolean" && !isMulti(f)).map((f) => f.flag)];
2157
+ const value = [
2158
+ ...cmd.bodyFlags !== false ? ["--body", "--body-file"] : [],
2159
+ ...bindsOrg(cmd) ? ["--org"] : [],
2160
+ ...fields.filter((f) => f.type !== "boolean" && !isMulti(f)).map((f) => f.flag)
2161
+ ];
1939
2162
  const multi = fields.filter(isMulti).map((f) => f.flag);
1940
2163
  const { options, positionals } = parseOptions(rest, { bool, value, multi });
1941
2164
  if (options.help) {
1942
- writeLine(ctx.io.stdout, `Usage: ${usageLine(group, cmd)}
2165
+ if (cmd.usageText !== void 0) write(ctx.io.stdout, cmd.usageText);
2166
+ else writeLine(ctx.io.stdout, `Usage: ${usageLine(group, cmd)}
1943
2167
 
1944
2168
  ${cmd.summary}
1945
2169
  Endpoint: ${cmd.method} ${templates(cmd).join(" | ")}`);
1946
2170
  return 0;
1947
2171
  }
1948
- const template = templates(cmd).find((p) => paramNames(p).length === positionals.length);
2172
+ const template = templates(cmd).find((p) => positionalNames(cmd, p).length === positionals.length);
1949
2173
  if (!template) {
1950
- write(ctx.io.stderr, `Usage: ${usageLine(group, cmd)}
2174
+ write(ctx.io.stderr, cmd.usageText ?? `Usage: ${usageLine(group, cmd)}
1951
2175
  `);
1952
2176
  return 2;
1953
2177
  }
1954
- let path = template;
1955
- paramNames(template).forEach((name, i) => {
1956
- path = path.replace(`:${name}`, encodeURIComponent(positionals[i]));
1957
- });
2178
+ const path = fillPath(cmd, template, positionals, options.org);
2179
+ const requiredError = (f) => new CliError(msg.required(f.flag, cmd.usageText ?? usageLine(group, cmd)), 2);
1958
2180
  const optionOf = (f) => {
1959
2181
  const name = f.flag.replace(/^--/, "").replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1960
2182
  if (f.type === "boolean") {
@@ -1969,34 +2191,39 @@ Endpoint: ${cmd.method} ${templates(cmd).join(" | ")}`);
1969
2191
  for (const f of cmd.query ?? []) {
1970
2192
  const v = optionOf(f);
1971
2193
  if (v === void 0) {
1972
- if (f.required) throw new CliError(`${usageLine(group, cmd)}
1973
- ${f.flag} is required.`, 2);
2194
+ if (f.required) throw requiredError(f);
1974
2195
  continue;
1975
2196
  }
1976
- const c = coerce(f, v);
1977
- if (Array.isArray(c)) c.forEach((x) => qs2.append(f.key, String(x)));
1978
- else qs2.set(f.key, String(c));
2197
+ const c = coerce(f, v, cmd, msg);
2198
+ if (f.type === "csv") qs2.set(f.key, c.join(","));
2199
+ else if (Array.isArray(c)) c.forEach((x) => qs2.append(f.key, String(x)));
2200
+ else qs2.set(f.key, c !== null && typeof c === "object" ? JSON.stringify(c) : String(c));
1979
2201
  }
1980
2202
  const url = qs2.size ? `${path}${path.includes("?") ? "&" : "?"}${qs2.toString()}` : path;
1981
2203
  const confirm2 = cmd.confirm ?? cmd.method === "DELETE";
1982
- if (confirm2 && !options.yes) throw new CliError(`Refusing to ${cmd.words.join(" ")} ${positionals.join(" ")} without --yes.`.replace(/\s+/g, " ").trim(), 2);
1983
- let body;
2204
+ const gate = () => {
2205
+ if (confirm2 && !options.yes) throw new CliError(msg.refusal(cmd.words, positionals), 2);
2206
+ };
2207
+ if (!cmd.validateFirst) gate();
1984
2208
  const isWrite = cmd.method !== "GET" && cmd.method !== "DELETE";
1985
- if (isWrite || cmd.body?.length) {
1986
- const explicit = readBodyOptions(options);
1987
- const overlay = {};
2209
+ const takesBody = isWrite || Boolean(cmd.body?.length) || cmd.bodyFlags === true;
2210
+ let explicit = {};
2211
+ const overlay = {};
2212
+ if (takesBody) {
2213
+ explicit = readBodyOptions(options, msg);
1988
2214
  for (const f of cmd.body ?? []) {
1989
2215
  const v = optionOf(f);
1990
- if (v === void 0) continue;
1991
- setPath(overlay, f.key, coerce(f, v));
2216
+ if (v !== void 0) setPath(overlay, f.key, coerce(f, v, cmd, msg));
2217
+ else if (cmd.validateFirst && f.required && getPath(explicit, f.key) === void 0) throw requiredError(f);
1992
2218
  }
2219
+ }
2220
+ if (cmd.validateFirst) gate();
2221
+ let body;
2222
+ if (takesBody) {
1993
2223
  let base16 = {};
1994
2224
  if (cmd.rmw) {
1995
- let from = cmd.rmw.from ?? template;
1996
- paramNames(from).forEach((name, i) => {
1997
- from = from.replace(`:${name}`, encodeURIComponent(positionals[i]));
1998
- });
1999
- const current = await send(ctx, from, { method: "GET" }, cmd.anonymous);
2225
+ const from = fillPath(cmd, cmd.rmw.from ?? template, positionals, options.org);
2226
+ const current = await send(ctx, cmd, from, { method: "GET" });
2000
2227
  const picked = cmd.rmw.pick ? cmd.rmw.pick(current.body) : current.body;
2001
2228
  base16 = picked && typeof picked === "object" && !Array.isArray(picked) ? { ...picked } : {};
2002
2229
  }
@@ -2005,13 +2232,15 @@ ${f.flag} is required.`, 2);
2005
2232
  const v = getPath(overlay, f.key);
2006
2233
  if (v !== void 0) setPath(body, f.key, v);
2007
2234
  }
2008
- for (const f of cmd.body ?? []) {
2009
- if (f.required && getPath(body, f.key) === void 0) throw new CliError(`${usageLine(group, cmd)}
2010
- ${f.flag} is required.`, 2);
2235
+ if (!cmd.validateFirst) {
2236
+ for (const f of cmd.body ?? []) {
2237
+ if (f.required && getPath(body, f.key) === void 0) throw requiredError(f);
2238
+ }
2011
2239
  }
2012
2240
  if (!isWrite && Object.keys(body).length === 0) body = void 0;
2013
2241
  if (isWrite && body && Object.keys(body).length === 0 && cmd.emptyBody === false) body = void 0;
2014
2242
  }
2243
+ if (cmd.notice !== void 0) writeLine(ctx.io.stderr, cmd.notice);
2015
2244
  try {
2016
2245
  const res = await requestJson(ctx, url, { method: cmd.method, ...body !== void 0 ? { body } : {}, ...cmd.anonymous ? { auth: false } : {} });
2017
2246
  render(ctx, cmd, positionals, res.status, res.body);
@@ -2021,25 +2250,42 @@ ${f.flag} is required.`, 2);
2021
2250
  render(ctx, { ...cmd, method: "GET" }, positionals, err.status, err.body);
2022
2251
  return 1;
2023
2252
  }
2024
- if (err instanceof HttpError) throw hostError(err);
2253
+ if (err instanceof HttpError && cmd.hostErrors !== false) throw hostError(err);
2025
2254
  throw err;
2026
2255
  }
2027
2256
  }
2028
2257
  function getPath(obj, key) {
2029
2258
  return key.split(".").reduce((node, part) => node && typeof node === "object" ? node[part] : void 0, obj);
2030
2259
  }
2031
- async function send(ctx, url, init, anonymous) {
2260
+ async function send(ctx, cmd, url, init) {
2032
2261
  try {
2033
- return await requestJson(ctx, url, { method: init.method, ...init.body !== void 0 ? { body: init.body } : {}, ...anonymous ? { auth: false } : {} });
2262
+ return await requestJson(ctx, url, { method: init.method, ...init.body !== void 0 ? { body: init.body } : {}, ...cmd.anonymous ? { auth: false } : {} });
2034
2263
  } catch (err) {
2035
- if (err instanceof HttpError) throw hostError(err);
2264
+ if (err instanceof HttpError && cmd.hostErrors !== false) throw hostError(err);
2036
2265
  throw err;
2037
2266
  }
2038
2267
  }
2268
+ function tableRows(table3, body) {
2269
+ if (!table3.anyArray) return table3.key ? getPath(body ?? {}, table3.key) : body;
2270
+ if (Array.isArray(body)) return body;
2271
+ const keyed = table3.key ? getPath(body ?? {}, table3.key) : void 0;
2272
+ if (Array.isArray(keyed)) return keyed;
2273
+ return Object.values(body ?? {}).find((v) => Array.isArray(v)) ?? [];
2274
+ }
2039
2275
  function render(ctx, cmd, positionals, status, rawBody) {
2040
2276
  const body = cmd.transform ? cmd.transform(rawBody) : rawBody;
2277
+ const noContent = body === null || body === void 0 ? cmd.noContent : void 0;
2041
2278
  if (ctx.json) {
2042
- writeJson(ctx.io.stdout, body);
2279
+ writeJson(ctx.io.stdout, noContent !== void 0 ? { ok: true, status } : body);
2280
+ return;
2281
+ }
2282
+ if (cmd.rawText && body && typeof body === "object" && typeof body.raw === "string" && Object.keys(body).length === 1) {
2283
+ write(ctx.io.stdout, body.raw.endsWith("\n") ? body.raw : `${body.raw}
2284
+ `);
2285
+ return;
2286
+ }
2287
+ if (noContent !== void 0) {
2288
+ writeLine(ctx.io.stdout, noContent.replace("{status}", String(status)));
2043
2289
  return;
2044
2290
  }
2045
2291
  if (cmd.human) {
@@ -2047,7 +2293,7 @@ function render(ctx, cmd, positionals, status, rawBody) {
2047
2293
  return;
2048
2294
  }
2049
2295
  if (cmd.table) {
2050
- const rows = cmd.table.key ? getPath(body ?? {}, cmd.table.key) : body;
2296
+ const rows = tableRows(cmd.table, body);
2051
2297
  if (rows === null || rows === void 0) {
2052
2298
  writeLine(ctx.io.stdout, cmd.table.empty ?? "None.");
2053
2299
  return;
@@ -2065,7 +2311,7 @@ function render(ctx, cmd, positionals, status, rawBody) {
2065
2311
  return;
2066
2312
  }
2067
2313
  }
2068
- if (cmd.method === "GET") {
2314
+ if (cmd.method === "GET" || cmd.writeOutput === "json") {
2069
2315
  writeJson(ctx.io.stdout, body);
2070
2316
  return;
2071
2317
  }
@@ -2460,67 +2706,6 @@ function lifecycleError(err) {
2460
2706
  throw err;
2461
2707
  }
2462
2708
 
2463
- // src/cli/shared.ts
2464
- function requireOrg(org2) {
2465
- if (!org2) throw new CliError("This command is org-scoped \u2014 pass --org <orgId>.", 2);
2466
- return String(org2);
2467
- }
2468
- function buildInputs(options) {
2469
- const fromJson = options.inputsJson ? JSON.parse(options.inputsJson) : {};
2470
- if (fromJson === null || typeof fromJson !== "object" || Array.isArray(fromJson)) {
2471
- throw new CliError("--inputs-json must be a JSON object");
2472
- }
2473
- const inputs = { ...fromJson };
2474
- for (const pair of options.input ?? []) {
2475
- const eq = pair.indexOf("=");
2476
- if (eq <= 0) throw new CliError(`--input must be key=value, got: ${pair}`);
2477
- inputs[pair.slice(0, eq)] = parseInputValue(pair.slice(eq + 1));
2478
- }
2479
- return inputs;
2480
- }
2481
- function parseInputValue(value) {
2482
- try {
2483
- return JSON.parse(value);
2484
- } catch {
2485
- return value;
2486
- }
2487
- }
2488
- function parseNodeVersion(version) {
2489
- const [major, minor, patch] = version.split(".").map((v) => Number(v));
2490
- return { major: major || 0, minor: minor || 0, patch: patch || 0 };
2491
- }
2492
- function defaultApiKeyFor(baseUrl) {
2493
- try {
2494
- const host = new URL(baseUrl).hostname;
2495
- if (host === "localhost" || host === "127.0.0.1" || host === "::1") return DEFAULT_API_KEY;
2496
- } catch {
2497
- return void 0;
2498
- }
2499
- return void 0;
2500
- }
2501
- function normalizeBaseUrl(value) {
2502
- if (!value) return DEFAULT_BASE_URL;
2503
- return value.endsWith("/") ? value.slice(0, -1) : value;
2504
- }
2505
- function npmCommand() {
2506
- return process.platform === "win32" ? "npm.cmd" : "npm";
2507
- }
2508
- function ok(name, message) {
2509
- return { status: "ok", name, message };
2510
- }
2511
- function warn(name, message) {
2512
- return { status: "warn", name, message };
2513
- }
2514
- function fail(name, message) {
2515
- return { status: "fail", name, message };
2516
- }
2517
- function formatCheckTable(checks) {
2518
- return formatTable(
2519
- checks.map((c) => ({ status: c.status.toUpperCase(), check: c.name, message: c.message })),
2520
- ["status", "check", "message"]
2521
- );
2522
- }
2523
-
2524
2709
  // src/cli/contentHelpers.ts
2525
2710
  import { readFileSync as readFileSync5 } from "node:fs";
2526
2711
  import { resolve as resolvePath2 } from "node:path";
@@ -2873,19 +3058,37 @@ import { readFileSync as readFileSync6 } from "node:fs";
2873
3058
 
2874
3059
  // src/cli/requestHelpers.ts
2875
3060
  var NOT_SERVED = /* @__PURE__ */ new Set([404, 405, 501]);
3061
+ function presentsCredential(ctx, req2) {
3062
+ if (req2.auth !== false && ctx.apiKey) return true;
3063
+ return Object.keys(req2.headers ?? {}).some((h) => /^(authorization|cookie)$/i.test(h));
3064
+ }
2876
3065
  async function requestNormativeOrHost(ctx, normativePath, hostPath, options = {}) {
2877
3066
  const { forceHost, ...req2 } = options;
3067
+ let anonymousRetryOf;
2878
3068
  if (!forceHost) {
2879
3069
  try {
2880
3070
  const res2 = await requestJson(ctx, normativePath, req2);
2881
3071
  if (ctx.verbose) writeLine(ctx.io.stderr, `openwop: served by the normative path ${normativePath}`);
2882
3072
  return { ...res2, path: normativePath, via: "normative" };
2883
3073
  } catch (err) {
2884
- if (!(err instanceof HttpError) || !NOT_SERVED.has(err.status)) throw err;
3074
+ if (!(err instanceof HttpError)) throw err;
3075
+ if (isNoCredentialChallenge(err) && !presentsCredential(ctx, req2)) anonymousRetryOf = err;
3076
+ else if (!NOT_SERVED.has(err.status)) throw err;
2885
3077
  }
2886
3078
  }
2887
- const res = await requestJson(ctx, hostPath, req2);
2888
- if (ctx.verbose) writeLine(ctx.io.stderr, `openwop: served by the host-extension path ${hostPath}`);
3079
+ let res;
3080
+ try {
3081
+ res = await requestJson(ctx, hostPath, req2);
3082
+ } catch (err) {
3083
+ if (anonymousRetryOf && err instanceof HttpError && (NOT_SERVED.has(err.status) || err.status === 401)) throw anonymousRetryOf;
3084
+ throw err;
3085
+ }
3086
+ if (anonymousRetryOf) {
3087
+ const sent = (await resolveRequest(ctx, hostPath, {})).path;
3088
+ writeLine(ctx.io.stderr, `openwop: no credential sent \u2014 showing this host's anonymous view (${sent}), not your workspace. Pass --api-key, or run \`openwop onboard\` to save one.`);
3089
+ } else if (ctx.verbose) {
3090
+ writeLine(ctx.io.stderr, `openwop: served by the host-extension path ${hostPath}`);
3091
+ }
2889
3092
  return { ...res, path: hostPath, via: "host" };
2890
3093
  }
2891
3094
  function failClosedOn404(err, surface) {
@@ -4847,6 +5050,8 @@ GET /v1/agents and GET /v1/agents/{agentId}. When the host does not serve the
4847
5050
  normative read (404/405/501) they fall back to the host-extension alias
4848
5051
  GET /v1/host/openwop-app/agents[/{agentId}]; --host forces the alias (which also
4849
5052
  reports the host's runtime posture); --verbose names the path that answered.
5053
+ With no credential, a v2 host refuses the normative read (401); the command
5054
+ then shows the host's anonymous view from the alias and says so on stderr.
4850
5055
  'run' dispatches one agent turn via
4851
5056
  POST /v1/host/openwop-app/agents/{agentId}/dispatch \u2014 the tool surface is filtered to
4852
5057
  the agent's toolAllowlist (RFC 0002 \xA7A14), task/return payloads are validated
@@ -5170,6 +5375,8 @@ GET /v1/agents/roster. The normative view carries only protocol-shaped entries
5170
5375
  (the host may omit host-only entries such as advisors); when the host does not
5171
5376
  serve it (404/405/501), or with --host, 'list' reads the host-extension
5172
5377
  GET /v1/host/openwop-app/roster instead (--verbose names the path that answered).
5378
+ With no credential, a v2 host refuses the normative read (401); 'list' then
5379
+ shows the host's anonymous view from the alias and says so on stderr.
5173
5380
  'get'/'create'/'update'/'delete' drive the host-extension surface
5174
5381
  GET/POST/PATCH/DELETE /v1/host/openwop-app/roster[/{rosterId}].
5175
5382
 
@@ -5352,7 +5559,9 @@ field).
5352
5559
  GET /v1/agents/org-chart/{departmentId}. When the host does not serve it
5353
5560
  (404/405/501 on 'get'), or with --host, they read the host-extension
5354
5561
  GET /v1/host/openwop-app/org-chart[/{departmentId}] (--verbose names the path
5355
- that answered). 'set'/'clear' drive the host-extension PUT/DELETE
5562
+ that answered). With no credential, a v2 host refuses the normative read (401);
5563
+ 'get'/'dept' then show the host's anonymous view from the alias and say so on
5564
+ stderr. 'set'/'clear' drive the host-extension PUT/DELETE
5356
5565
  /v1/host/openwop-app/org-chart. 'set' replaces the whole chart from a JSON file
5357
5566
  with { "departments": [...], "members": [...] }.
5358
5567
 
@@ -5734,9 +5943,7 @@ async function boardWatch(ctx, argv) {
5734
5943
  write(ctx.io.stdout, "Usage: openwop kanban watch <boardId>\n");
5735
5944
  return options.help ? 0 : 2;
5736
5945
  }
5737
- const { path, headers } = await resolveRequest(ctx, `/v1/host/openwop-app/kanban/boards/${encodeURIComponent(positionals[0])}/events`, { accept: "text/event-stream" });
5738
- const url = new URL(path.replace(/^\//, ""), ctx.baseUrl.endsWith("/") ? ctx.baseUrl : `${ctx.baseUrl}/`);
5739
- if (ctx.apiKey) headers.authorization = `Bearer ${ctx.apiKey}`;
5946
+ const { url, headers } = await resolveStreamRequest(ctx, `/v1/host/openwop-app/kanban/boards/${encodeURIComponent(positionals[0])}/events`, {});
5740
5947
  const res = await ctx.fetchImpl(url, { headers });
5741
5948
  if (!res.ok || !res.body) {
5742
5949
  throw new CliError(`Could not open the board event stream (HTTP ${res.status}).`, 1);
@@ -6061,7 +6268,6 @@ async function brandAsset(ctx, argv) {
6061
6268
  }
6062
6269
 
6063
6270
  // src/cli/resourceCommands.ts
6064
- import { readFileSync as readFileSync9 } from "node:fs";
6065
6271
  function parseField(spec) {
6066
6272
  let s = spec;
6067
6273
  let flagOverride;
@@ -6076,35 +6282,6 @@ function parseField(spec) {
6076
6282
  const flag = `--${flagOverride ?? key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`).replace(/_/g, "-")}`;
6077
6283
  return { key, flag, type, required };
6078
6284
  }
6079
- function coerce2(field, raw) {
6080
- switch (field.type) {
6081
- case "number": {
6082
- const n = Number(raw);
6083
- if (raw.trim() === "" || !Number.isFinite(n)) throw new CliError(`${field.flag} must be a number, got: ${raw}`);
6084
- return n;
6085
- }
6086
- case "bool":
6087
- if (raw === "true" || raw === "yes" || raw === "1") return true;
6088
- if (raw === "false" || raw === "no" || raw === "0") return false;
6089
- throw new CliError(`${field.flag} must be true or false, got: ${raw}`);
6090
- case "json":
6091
- try {
6092
- return JSON.parse(raw);
6093
- } catch {
6094
- throw new CliError(`${field.flag} must be valid JSON`);
6095
- }
6096
- case "list":
6097
- return raw.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
6098
- case "file":
6099
- try {
6100
- return readFileSync9(raw, "utf8").replace(/\r?\n$/, "");
6101
- } catch {
6102
- throw new CliError(`Cannot read ${field.flag} ${raw}`);
6103
- }
6104
- default:
6105
- return raw;
6106
- }
6107
- }
6108
6285
  function hasOrg(route) {
6109
6286
  return /:org(?![A-Za-z0-9_])/.test(route);
6110
6287
  }
@@ -6147,142 +6324,62 @@ ${footer ? `
6147
6324
  ${footer.trim()}
6148
6325
  ` : ""}`;
6149
6326
  }
6150
- function matchSpec(specs, argv) {
6151
- let best = null;
6152
- for (const spec of specs) {
6153
- if (spec.cmd.length > argv.length) continue;
6154
- if (spec.cmd.every((w, i) => argv[i] === w) && (!best || spec.cmd.length > best.spec.cmd.length)) {
6155
- best = { spec, rest: argv.slice(spec.cmd.length) };
6156
- }
6157
- }
6158
- return best;
6159
- }
6160
- function readBodyOption2(options) {
6161
- let raw;
6162
- if (options.bodyFile) {
6163
- try {
6164
- raw = readFileSync9(String(options.bodyFile), "utf8");
6165
- } catch {
6166
- throw new CliError(`Cannot read --body-file ${options.bodyFile}`);
6167
- }
6168
- } else if (options.body !== void 0) {
6169
- raw = String(options.body);
6170
- }
6171
- if (raw === void 0) return {};
6172
- let parsed;
6173
- try {
6174
- parsed = JSON.parse(raw);
6175
- } catch {
6176
- throw new CliError("--body/--body-file must be valid JSON");
6177
- }
6178
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new CliError("--body/--body-file must be a JSON object");
6179
- return parsed;
6327
+ var TYPES = { string: "string", number: "number", bool: "bool", json: "json", list: "csv", file: "file" };
6328
+ function toField(spec) {
6329
+ const f = parseField(spec);
6330
+ return { flag: f.flag, key: f.key, type: TYPES[f.type] ?? "string", required: f.required };
6180
6331
  }
6181
- async function runSpec(ctx, group, spec, argv) {
6182
- const queryFields = (spec.query ?? []).map(parseField);
6183
- const bodyFields = (spec.body ?? []).map(parseField);
6184
- const allowBody = Boolean(spec.body || spec.rawBody);
6185
- const valueFlags = [...queryFields, ...bodyFields].map((f) => f.flag);
6186
- if (hasOrg(spec.route)) valueFlags.push("--org");
6187
- if (allowBody) valueFlags.push("--body", "--body-file");
6188
- const { options, positionals } = parseOptions(argv, { bool: ["--help", "--yes"], value: valueFlags });
6332
+ function toRouteCmd(group, spec) {
6189
6333
  const usage6 = `Usage: ${usageLine2(group, spec)}
6190
6334
  ${spec.method} ${spec.route} \u2014 ${spec.summary}
6191
6335
  `;
6192
- if (options.help) {
6193
- write(ctx.io.stdout, usage6);
6194
- return 0;
6195
- }
6196
- const params = paramsOf(spec.route);
6197
- if (positionals.length !== params.length) {
6198
- write(ctx.io.stderr, usage6);
6199
- return 2;
6200
- }
6201
- const optionValue = (f) => options[f.flag.slice(2).replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase())];
6202
- const fill = (route) => {
6203
- let i = 0;
6204
- return route.replace(/:[A-Za-z][A-Za-z0-9_]*/g, (m) => {
6205
- if (m === ":org") return encodeURIComponent(requireOrg(options.org));
6206
- return encodeURIComponent(positionals[i++] ?? "");
6207
- });
6208
- };
6209
- let path = fill(spec.route);
6210
- const qs2 = new URLSearchParams();
6211
- for (const f of queryFields) {
6212
- const v = optionValue(f);
6213
- if (v === void 0) {
6214
- if (f.required) throw new CliError(`${f.flag} is required.
6215
- ${usage6}`);
6216
- continue;
6217
- }
6218
- const c = coerce2(f, String(v));
6219
- qs2.set(f.key, Array.isArray(c) ? c.join(",") : typeof c === "object" ? JSON.stringify(c) : String(c));
6220
- }
6221
- const q = qs2.toString();
6222
- if (q) path += `${path.includes("?") ? "&" : "?"}${q}`;
6223
- let body;
6224
- if (allowBody || spec.method === "POST" || spec.method === "PUT" || spec.method === "PATCH") {
6225
- body = allowBody ? readBodyOption2(options) : {};
6226
- for (const f of bodyFields) {
6227
- const v = optionValue(f);
6228
- if (v !== void 0) body[f.key] = coerce2(f, String(v));
6229
- else if (f.required && body[f.key] === void 0) throw new CliError(`${f.flag} is required.
6230
- ${usage6}`);
6231
- }
6232
- if (spec.method === "DELETE" && Object.keys(body).length === 0) body = void 0;
6233
- }
6234
- if (spec.confirm && !options.yes) {
6235
- throw new CliError(`Refusing to ${spec.summary.charAt(0).toLowerCase()}${spec.summary.slice(1).replace(/\.$/, "")} without --yes.`, 2);
6236
- }
6237
- if (spec.rmw) {
6238
- const current = await requestJson(ctx, fill(spec.rmw), { auth: spec.auth });
6239
- const base16 = spec.rmwKey ? current.body?.[spec.rmwKey] : current.body;
6240
- body = { ...base16 && typeof base16 === "object" && !Array.isArray(base16) ? base16 : {}, ...body ?? {} };
6241
- }
6242
- if (spec.notice) writeLine(ctx.io.stderr, spec.notice);
6243
- const res = await requestJson(ctx, path, {
6336
+ const rmwKey = spec.rmwKey;
6337
+ return {
6338
+ words: spec.cmd,
6244
6339
  method: spec.method,
6245
- ...body !== void 0 ? { body } : {},
6246
- ...spec.auth === false ? { auth: false } : {}
6247
- });
6248
- return render2(ctx, spec, res.status, res.body);
6249
- }
6250
- function render2(ctx, spec, status, body) {
6251
- if (ctx.json) {
6252
- writeJson(ctx.io.stdout, body ?? { ok: true, status });
6253
- return 0;
6254
- }
6255
- if (body && typeof body === "object" && typeof body.raw === "string" && Object.keys(body).length === 1) {
6256
- write(ctx.io.stdout, body.raw.endsWith("\n") ? body.raw : `${body.raw}
6257
- `);
6258
- return 0;
6259
- }
6260
- if (body === null || body === void 0) {
6261
- writeLine(ctx.io.stdout, spec.method === "DELETE" ? "Deleted." : `Done (HTTP ${status}).`);
6262
- return 0;
6263
- }
6264
- if (spec.list) {
6265
- const items = Array.isArray(body) ? body : spec.list.key && Array.isArray(body?.[spec.list.key]) ? body[spec.list.key] : Object.values(body ?? {}).find((v) => Array.isArray(v)) ?? [];
6266
- if (items.length === 0) {
6267
- writeLine(ctx.io.stdout, spec.list.empty ?? "None.");
6268
- return 0;
6340
+ path: spec.route,
6341
+ summary: spec.summary,
6342
+ query: (spec.query ?? []).map(toField),
6343
+ body: (spec.body ?? []).map(toField),
6344
+ orgFlag: true,
6345
+ bodyFlags: Boolean(spec.body || spec.rawBody),
6346
+ emptyBody: true,
6347
+ confirm: Boolean(spec.confirm),
6348
+ anonymous: spec.auth === false,
6349
+ ...spec.list ? { table: { key: spec.list.key, columns: spec.list.columns, empty: spec.list.empty ?? "None.", anyArray: true } } : {},
6350
+ ...spec.rmw ? { rmw: { from: spec.rmw, pick: (body) => rmwKey ? body?.[rmwKey] : body } } : {},
6351
+ ...spec.notice !== void 0 ? { notice: spec.notice } : {},
6352
+ rawText: true,
6353
+ // every spec, as before — `text` only documents the route
6354
+ 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"
6269
6371
  }
6270
- writeLine(ctx.io.stdout, formatTable(items.map((row2) => {
6271
- const out = {};
6272
- for (const col of spec.list.columns) {
6273
- const v = row2?.[col];
6274
- out[col] = v === void 0 || v === null ? "" : typeof v === "object" ? JSON.stringify(v) : String(v);
6275
- }
6276
- return out;
6277
- }), spec.list.columns));
6278
- return 0;
6279
- }
6280
- writeJson(ctx.io.stdout, body);
6281
- return 0;
6372
+ };
6373
+ }
6374
+ function matchSpec(specs, argv) {
6375
+ return matchRoute(specs.map((spec) => ({ words: spec.cmd, spec })), argv)?.spec;
6376
+ }
6377
+ async function runSpec(ctx, group, spec, argv) {
6378
+ return runRoute(ctx, group, toRouteCmd(group, spec), [...spec.cmd, ...argv]);
6282
6379
  }
6283
6380
  async function dispatchSpecs(ctx, group, specs, argv) {
6284
- const match = matchSpec(specs, argv);
6285
- return match ? runSpec(ctx, group, match.spec, match.rest) : void 0;
6381
+ const spec = matchSpec(specs, argv);
6382
+ return spec ? runSpec(ctx, group, spec, argv.slice(spec.cmd.length)) : void 0;
6286
6383
  }
6287
6384
  function specsUsage(group, specs) {
6288
6385
  return specs.map((s) => ` ${usageLine2(group, s)}
@@ -6293,12 +6390,12 @@ async function runResourceGroup(ctx, group, help, specs, argv) {
6293
6390
  write(ctx.io.stdout, help);
6294
6391
  return 0;
6295
6392
  }
6296
- const match = matchSpec(specs, argv);
6297
- if (!match) {
6393
+ const spec = matchSpec(specs, argv);
6394
+ if (!spec) {
6298
6395
  throw new CliError(`Unknown ${group} command: ${argv.filter((a) => !a.startsWith("-")).slice(0, 2).join(" ")}
6299
6396
  Run \`openwop ${group} --help\` for usage.`);
6300
6397
  }
6301
- return runSpec(ctx, group, match.spec, match.rest);
6398
+ return runSpec(ctx, group, spec, argv.slice(spec.cmd.length));
6302
6399
  }
6303
6400
 
6304
6401
  // src/cli/crmOrg.ts
@@ -6975,7 +7072,7 @@ async function sharingFrameViews(ctx, argv) {
6975
7072
  }
6976
7073
 
6977
7074
  // src/cli/marketingShared.ts
6978
- import { readFileSync as readFileSync10 } from "node:fs";
7075
+ import { readFileSync as readFileSync9 } from "node:fs";
6979
7076
  import { resolve as resolvePath5 } from "node:path";
6980
7077
  var APP = "/v1/host/openwop-app";
6981
7078
  async function dispatchTable(ctx, group, help, table3, argv, defaultSub) {
@@ -7033,14 +7130,14 @@ function jsonObject(text, flag) {
7033
7130
  }
7034
7131
  function readText(ctx, path, flag) {
7035
7132
  try {
7036
- return readFileSync10(resolvePath5(ctx.cwd, path), "utf8");
7133
+ return readFileSync9(resolvePath5(ctx.cwd, path), "utf8");
7037
7134
  } catch (err) {
7038
7135
  throw new CliError(`Cannot read ${flag} ${path}: ${err instanceof Error ? err.message : String(err)}`, 2);
7039
7136
  }
7040
7137
  }
7041
7138
  function readBase64(ctx, path, flag) {
7042
7139
  try {
7043
- return readFileSync10(resolvePath5(ctx.cwd, path)).toString("base64");
7140
+ return readFileSync9(resolvePath5(ctx.cwd, path)).toString("base64");
7044
7141
  } catch (err) {
7045
7142
  throw new CliError(`Cannot read ${flag} ${path}: ${err instanceof Error ? err.message : String(err)}`, 2);
7046
7143
  }
@@ -8652,10 +8749,10 @@ Run \`openwop documents --help\` for usage.`);
8652
8749
  if (!id) return usage(ctx, "add-version <documentId> --org <orgId> (--content <text> | --content-file <path>)");
8653
8750
  let content = options.content !== void 0 ? String(options.content) : void 0;
8654
8751
  if (options.contentFile) {
8655
- const { readFileSync: readFileSync23 } = await import("node:fs");
8752
+ const { readFileSync: readFileSync22 } = await import("node:fs");
8656
8753
  const { resolve: resolve5 } = await import("node:path");
8657
8754
  try {
8658
- content = readFileSync23(resolve5(ctx.cwd, String(options.contentFile)), "utf8");
8755
+ content = readFileSync22(resolve5(ctx.cwd, String(options.contentFile)), "utf8");
8659
8756
  } catch (err) {
8660
8757
  throw new CliError(`Cannot read --content-file ${String(options.contentFile)}: ${err instanceof Error ? err.message : String(err)}`);
8661
8758
  }
@@ -9242,7 +9339,7 @@ Run \`openwop reviews --help\` for usage.`);
9242
9339
  }
9243
9340
 
9244
9341
  // src/cli/adminShared.ts
9245
- import { readFileSync as readFileSync11 } from "node:fs";
9342
+ import { readFileSync as readFileSync10 } from "node:fs";
9246
9343
  import { resolve as resolve2 } from "node:path";
9247
9344
 
9248
9345
  // src/prompt.ts
@@ -9361,7 +9458,7 @@ async function readSecretInput(ctx, options, label) {
9361
9458
  let value;
9362
9459
  if (options.valueFile !== void 0) {
9363
9460
  try {
9364
- value = readFileSync11(resolve2(ctx.cwd, String(options.valueFile)), "utf8").replace(/\r?\n$/, "");
9461
+ value = readFileSync10(resolve2(ctx.cwd, String(options.valueFile)), "utf8").replace(/\r?\n$/, "");
9365
9462
  } catch (err) {
9366
9463
  throw new CliError(`Cannot read --value-file ${options.valueFile}: ${err instanceof Error ? err.message : String(err)}`, 2);
9367
9464
  }
@@ -10315,7 +10412,7 @@ async function runCampaignsOrch(ctx, argv) {
10315
10412
  }
10316
10413
 
10317
10414
  // src/cli/notebooks.ts
10318
- import { readFileSync as readFileSync12 } from "node:fs";
10415
+ import { readFileSync as readFileSync11 } from "node:fs";
10319
10416
  import { extname as extname3, resolve as resolvePath7 } from "node:path";
10320
10417
  var BASE9 = "/v1/host/openwop-app/notebooks";
10321
10418
  var NOTEBOOKS_HELP = `Usage:
@@ -10390,7 +10487,7 @@ var MIME = {
10390
10487
  function readFileBase64(ctx, file, contentType) {
10391
10488
  let buf;
10392
10489
  try {
10393
- buf = readFileSync12(resolvePath7(ctx.cwd, file));
10490
+ buf = readFileSync11(resolvePath7(ctx.cwd, file));
10394
10491
  } catch (err) {
10395
10492
  throw new CliError(`Cannot read file ${file}: ${err instanceof Error ? err.message : String(err)}`);
10396
10493
  }
@@ -12319,6 +12416,11 @@ honouring the server's \`retry:\` field), and no event is printed twice.
12319
12416
  debug, or a comma list of updates/messages/debug
12320
12417
  (\`values\` never combines). A host that does not serve
12321
12418
  the mode answers 400 unsupported_stream_mode.
12419
+ --idle-timeout-ms N a connection that delivers no bytes for N ms (default
12420
+ 45000; 0 disables) is treated as dropped and resumed.
12421
+ Streams are read from the stream origin: --stream-base-url (global), else the
12422
+ host's advertised \`streamBase\` (https only), else --base-url \u2014 some front
12423
+ doors buffer event streams entirely (e.g. a CDN rewrite).
12322
12424
  Exit 0 when the run completes, 1 when it fails or is cancelled. \`runs list\` pages with --cursor: pass the
12323
12425
  \`nextCursor\` the previous page printed.
12324
12426
 
@@ -12541,7 +12643,7 @@ async function runRunsBulkCancel(ctx, argv) {
12541
12643
  return 0;
12542
12644
  }
12543
12645
  async function runRunsEvents(ctx, argv) {
12544
- const { options, positionals } = parseOptions(argv, { bool: ["--help", "--follow", "--no-stream"], value: ["--since", "--limit", "--last-event-id", "--stream-mode", "--timeout-ms"] });
12646
+ const { options, positionals } = parseOptions(argv, { bool: ["--help", "--follow", "--no-stream"], value: ["--since", "--limit", "--last-event-id", "--stream-mode", "--timeout-ms", "--idle-timeout-ms"] });
12545
12647
  if (options.help || positionals.length !== 1) {
12546
12648
  write(ctx.io.stdout, "Usage: openwop runs events <runId> [--since <sequence>] [--limit <n>] [--json]\n openwop runs events <runId> --follow [--since <sequence> | --last-event-id <id>] [--stream-mode <mode>] [--no-stream] [--json]\n");
12547
12649
  return options.help ? 0 : 2;
@@ -12579,6 +12681,7 @@ async function followRunEvents(ctx, runId, options) {
12579
12681
  ...start,
12580
12682
  useStream: !options.noStream,
12581
12683
  timeoutMs,
12684
+ ...options.idleTimeoutMs !== void 0 ? { idleTimeoutMs: nonNegativeInt(options.idleTimeoutMs, "--idle-timeout-ms") } : {},
12582
12685
  onEvent: (ev) => {
12583
12686
  if (isTerminalRunEvent(ev)) terminalType = canonicalEventType(ev.type);
12584
12687
  if (ctx.json) {
@@ -12818,10 +12921,15 @@ async function runRunsRedrive(ctx, argv) {
12818
12921
  })), ["runId", "redriveRunId", "error"]));
12819
12922
  return anyFailed ? 1 : 0;
12820
12923
  }
12924
+ function nonNegativeInt(value, flag) {
12925
+ const n = Number(value);
12926
+ if (!Number.isInteger(n) || n < 0) throw new CliError(`${flag} must be a non-negative integer (got ${String(value)})`);
12927
+ return n;
12928
+ }
12821
12929
 
12822
12930
  // src/cli/chat.ts
12823
12931
  import { randomUUID as randomUUID2 } from "node:crypto";
12824
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "node:fs";
12932
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "node:fs";
12825
12933
  var CHAT_BASE = "/v1/host/openwop-app/chat";
12826
12934
  var CHAT_SUBCOMMANDS = ["repl", "sessions", "session", "messages", "message", "participants", "open", "feedback", "models", "search", "export", "import", "tools"];
12827
12935
  var CHAT_HELP = `Usage:
@@ -13411,7 +13519,7 @@ Requires a signed-in account.
13411
13519
  }
13412
13520
  let data;
13413
13521
  try {
13414
- data = JSON.parse(readFileSync13(String(options.file), "utf8"));
13522
+ data = JSON.parse(readFileSync12(String(options.file), "utf8"));
13415
13523
  } catch (err) {
13416
13524
  throw new CliError(`--file: cannot read JSON from ${options.file} (${err instanceof Error ? err.message : String(err)})`, 2);
13417
13525
  }
@@ -13505,7 +13613,7 @@ Run \`openwop chat tools --help\` for usage.`, 2);
13505
13613
  }
13506
13614
 
13507
13615
  // src/cli/relayShared.ts
13508
- import { existsSync as existsSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync8, mkdirSync as mkdirSync3, rmSync as rmSync2, chmodSync as chmodSync3 } from "node:fs";
13616
+ import { existsSync as existsSync4, readFileSync as readFileSync13, writeFileSync as writeFileSync8, mkdirSync as mkdirSync3, rmSync as rmSync2, chmodSync as chmodSync3 } from "node:fs";
13509
13617
  import { dirname as dirname4, join as join5 } from "node:path";
13510
13618
  var MESSAGING_BASE = "/v1/host/openwop-app/messaging";
13511
13619
  var DEVICE_TOKEN_HEADER = "x-openwop-device-token";
@@ -13516,7 +13624,7 @@ function relayCredsPath(env) {
13516
13624
  function loadRelayConfig(ctx) {
13517
13625
  try {
13518
13626
  const p = relayCredsPath(ctx.env);
13519
- if (existsSync4(p)) return JSON.parse(readFileSync14(p, "utf8"));
13627
+ if (existsSync4(p)) return JSON.parse(readFileSync13(p, "utf8"));
13520
13628
  } catch {
13521
13629
  }
13522
13630
  return {};
@@ -13552,7 +13660,7 @@ function relayLogPath(env) {
13552
13660
  function readRelayRecord(env) {
13553
13661
  try {
13554
13662
  const p = relayPidPath(env);
13555
- return existsSync4(p) ? JSON.parse(readFileSync14(p, "utf8")) : null;
13663
+ return existsSync4(p) ? JSON.parse(readFileSync13(p, "utf8")) : null;
13556
13664
  } catch {
13557
13665
  return null;
13558
13666
  }
@@ -14147,7 +14255,7 @@ Usage: openwop notify <email|sms> --to <addr> --text <msg> [--subject s]`);
14147
14255
 
14148
14256
  // src/cli/relay.ts
14149
14257
  import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
14150
- import { existsSync as existsSync5, readFileSync as readFileSync15, writeFileSync as writeFileSync9, mkdirSync as mkdirSync4, rmSync as rmSync3 } from "node:fs";
14258
+ import { existsSync as existsSync5, readFileSync as readFileSync14, writeFileSync as writeFileSync9, mkdirSync as mkdirSync4, rmSync as rmSync3 } from "node:fs";
14151
14259
  import { dirname as dirname5 } from "node:path";
14152
14260
  import { fileURLToPath } from "node:url";
14153
14261
  var RELAY_HELP = `Usage:
@@ -14498,7 +14606,7 @@ async function runRelayLogs(ctx, argv) {
14498
14606
  const r = spawnSync3("tail", ["-f", logPath], { stdio: "inherit" });
14499
14607
  if (r.status === 0 || r.signal) return 0;
14500
14608
  }
14501
- write(ctx.io.stdout, readFileSync15(logPath, "utf8"));
14609
+ write(ctx.io.stdout, readFileSync14(logPath, "utf8"));
14502
14610
  return 0;
14503
14611
  }
14504
14612
 
@@ -14867,7 +14975,7 @@ async function runProvidersTest(ctx, argv) {
14867
14975
  // src/cli/packs.ts
14868
14976
  import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, sign as ed25519Sign, verify as ed25519Verify } from "node:crypto";
14869
14977
  import { gunzipSync, gzipSync } from "node:zlib";
14870
- import { existsSync as existsSync6, readFileSync as readFileSync16, writeFileSync as writeFileSync10, mkdirSync as mkdirSync5, readdirSync, statSync } from "node:fs";
14978
+ import { existsSync as existsSync6, readFileSync as readFileSync15, writeFileSync as writeFileSync10, mkdirSync as mkdirSync5, readdirSync, statSync } from "node:fs";
14871
14979
  import { join as join6, resolve as resolvePath8 } from "node:path";
14872
14980
  import { homedir as homedir3 } from "node:os";
14873
14981
  var PACKS_HELP = `Usage:
@@ -15124,7 +15232,7 @@ async function runPacksPublish(ctx, argv) {
15124
15232
  if (!existsSync6(manifestPath)) {
15125
15233
  throw new CliError(`No pack.json found in ${absPackDir}.`);
15126
15234
  }
15127
- const manifest = JSON.parse(readFileSync16(manifestPath, "utf8"));
15235
+ const manifest = JSON.parse(readFileSync15(manifestPath, "utf8"));
15128
15236
  const name = manifest.name;
15129
15237
  const version = manifest.version;
15130
15238
  if (!name || !version) throw new CliError('pack.json must declare both "name" and "version".');
@@ -15137,11 +15245,11 @@ async function runPacksPublish(ctx, argv) {
15137
15245
  let privateKey;
15138
15246
  let ephemeralPublicB64 = null;
15139
15247
  if (options.key) {
15140
- privateKey = createPrivateKey({ key: readFileSync16(resolvePath8(ctx.cwd, options.key), "utf8"), format: "pem" });
15248
+ privateKey = createPrivateKey({ key: readFileSync15(resolvePath8(ctx.cwd, options.key), "utf8"), format: "pem" });
15141
15249
  } else {
15142
15250
  const conventional = join6(homedir3(), ".openwop-keys", `${keyId}.private.pem`);
15143
15251
  if (existsSync6(conventional)) {
15144
- privateKey = createPrivateKey({ key: readFileSync16(conventional, "utf8"), format: "pem" });
15252
+ privateKey = createPrivateKey({ key: readFileSync15(conventional, "utf8"), format: "pem" });
15145
15253
  } else {
15146
15254
  const kp = generateKeyPairSync("ed25519");
15147
15255
  privateKey = kp.privateKey;
@@ -15220,7 +15328,7 @@ async function runPacksYank(ctx, argv) {
15220
15328
  throw new CliError(`Version manifest not found: ${manifestPath}
15221
15329
  (packs yank edits a local registry checkout; the published change lands via PR.)`);
15222
15330
  }
15223
- const manifest = JSON.parse(readFileSync16(manifestPath, "utf8"));
15331
+ const manifest = JSON.parse(readFileSync15(manifestPath, "utf8"));
15224
15332
  const newValue = !options.undo;
15225
15333
  if (Boolean(manifest.yanked) === newValue) {
15226
15334
  writeLine(ctx.io.stdout, `${name}@${version} is already ${newValue ? "yanked" : "un-yanked"}; no change.`);
@@ -15277,11 +15385,11 @@ function walkPackDir(packDir) {
15277
15385
  const st = statSync(full);
15278
15386
  if (st.isFile()) {
15279
15387
  if (!ALLOWED_TOPS.has(entry)) continue;
15280
- entries2.push({ name: entry, content: readFileSync16(full) });
15388
+ entries2.push({ name: entry, content: readFileSync15(full) });
15281
15389
  } else if (st.isDirectory() && ALLOWED_DIRS.has(entry)) {
15282
15390
  for (const f of readdirSync(full).sort()) {
15283
15391
  if (!f.endsWith(".json") && !f.endsWith(".pem") && !f.endsWith(".sig")) continue;
15284
- entries2.push({ name: `${entry}/${f}`, content: readFileSync16(join6(full, f)) });
15392
+ entries2.push({ name: `${entry}/${f}`, content: readFileSync15(join6(full, f)) });
15285
15393
  }
15286
15394
  }
15287
15395
  }
@@ -15450,6 +15558,8 @@ async function protocolChecks(ctx) {
15450
15558
  } else if (floor.required !== void 0) {
15451
15559
  rows.push(warn("min client", `host minClientVersion ${floor.required} is not a <major>.<minor> version`));
15452
15560
  }
15561
+ const stream = streamOrigin(ctx);
15562
+ rows.push(stream.source === "base" ? ok("stream origin", `event streams use --base-url (${stream.origin})`) : ok("stream origin", `event streams use ${stream.origin} \u2014 ${STREAM_SOURCE_LABEL[stream.source]}; set --stream-base-url to your --base-url to keep them there`));
15453
15563
  return rows;
15454
15564
  }
15455
15565
 
@@ -15841,7 +15951,7 @@ async function runGovernanceEgress(ctx, argv) {
15841
15951
 
15842
15952
  // src/cli/demo.ts
15843
15953
  import { spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
15844
- import { existsSync as existsSync8, readFileSync as readFileSync17, writeFileSync as writeFileSync12, createReadStream, openSync as openSync2, statSync as statSync2, mkdirSync as mkdirSync6, rmSync as rmSync4, chmodSync as chmodSync4, watch } from "node:fs";
15954
+ import { existsSync as existsSync8, readFileSync as readFileSync16, writeFileSync as writeFileSync12, createReadStream, openSync as openSync2, statSync as statSync2, mkdirSync as mkdirSync6, rmSync as rmSync4, chmodSync as chmodSync4, watch } from "node:fs";
15845
15955
  import { join as join7, dirname as dirname7, relative as relative2 } from "node:path";
15846
15956
  var DEMO_HELP = `Usage:
15847
15957
  openwop demo status [--json]
@@ -16208,7 +16318,7 @@ async function runDemoLogs(ctx, argv) {
16208
16318
  return 2;
16209
16319
  }
16210
16320
  const lineCount = Number(options.lines ?? 50);
16211
- const existing = readFileSync17(logPath, "utf8");
16321
+ const existing = readFileSync16(logPath, "utf8");
16212
16322
  const lines = existing.split("\n");
16213
16323
  const tail = lines.slice(Math.max(0, lines.length - lineCount - 1));
16214
16324
  write(ctx.io.stdout, tail.join("\n"));
@@ -18873,7 +18983,7 @@ async function runWfList(ctx, argv) {
18873
18983
  writeLine(ctx.io.stdout, formatTable(rows, ["workforceId", "name", "businessFunction", "status", "autonomy", "agents"]));
18874
18984
  return 0;
18875
18985
  }
18876
- async function runWfReadOne(ctx, argv, suffix, render3) {
18986
+ async function runWfReadOne(ctx, argv, suffix, render2) {
18877
18987
  const { options, positionals } = parseOptions(argv, { bool: ["--help"] });
18878
18988
  if (options.help || positionals.length !== 1) {
18879
18989
  write(ctx.io.stdout, WORKFORCES_HELP);
@@ -18890,7 +19000,7 @@ async function runWfReadOne(ctx, argv, suffix, render3) {
18890
19000
  writeJson(ctx.io.stdout, res.body);
18891
19001
  return 0;
18892
19002
  }
18893
- render3(ctx, res.body ?? {}, positionals[0]);
19003
+ render2(ctx, res.body ?? {}, positionals[0]);
18894
19004
  return 0;
18895
19005
  }
18896
19006
  function renderWorkforce(ctx, w, id) {
@@ -20164,7 +20274,7 @@ function truncate4(s, n) {
20164
20274
  }
20165
20275
 
20166
20276
  // src/cli/portability.ts
20167
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync13 } from "node:fs";
20277
+ import { readFileSync as readFileSync17, writeFileSync as writeFileSync13 } from "node:fs";
20168
20278
  import { resolve as resolve4 } from "node:path";
20169
20279
  var EXPORT_HELP = `Usage:
20170
20280
  openwop export [--kinds <kind>]... [--out <file>] [--json]
@@ -20302,7 +20412,7 @@ async function runImport(ctx, argv) {
20302
20412
  }
20303
20413
  let bundle;
20304
20414
  try {
20305
- const raw = readFileSync18(resolve4(ctx.cwd, positionals[0]), "utf8");
20415
+ const raw = readFileSync17(resolve4(ctx.cwd, positionals[0]), "utf8");
20306
20416
  bundle = JSON.parse(raw);
20307
20417
  } catch (err) {
20308
20418
  throw new CliError(`import: could not read bundle file ${positionals[0]} \u2014 ${err instanceof Error ? err.message : "invalid JSON"}`, 2);
@@ -21074,7 +21184,7 @@ Run \`openwop knowledge-sync --help\` for usage.`);
21074
21184
  }
21075
21185
 
21076
21186
  // src/cli/entities.ts
21077
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync14 } from "node:fs";
21187
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync14 } from "node:fs";
21078
21188
  import { resolve as resolvePath9 } from "node:path";
21079
21189
  var BASE13 = "/v1/host/openwop-app/entities";
21080
21190
  var PUBLIC_BASE2 = "/v1/host/openwop-app/public-entities";
@@ -21310,7 +21420,7 @@ async function types(ctx, sub, pos, o, project4, q) {
21310
21420
  if (!name || !o.file) return usage5(ctx, "types import <type> --file <file.ndjson|file.json>");
21311
21421
  let raw;
21312
21422
  try {
21313
- raw = readFileSync19(resolvePath9(ctx.cwd, String(o.file)), "utf8");
21423
+ raw = readFileSync18(resolvePath9(ctx.cwd, String(o.file)), "utf8");
21314
21424
  } catch (err) {
21315
21425
  throw new CliError(`Cannot read --file ${String(o.file)}: ${err instanceof Error ? err.message : String(err)}`);
21316
21426
  }
@@ -22744,8 +22854,7 @@ async function followEvents(ctx, requestedPath, options) {
22744
22854
  const max = options.max !== void 0 ? Number(options.max) : Infinity;
22745
22855
  const timeoutMs = options.timeout !== void 0 ? Number(options.timeout) * 1e3 : Infinity;
22746
22856
  if (Number.isNaN(max) || Number.isNaN(timeoutMs)) throw new CliError("--max and --timeout must be numbers.");
22747
- const { path, headers } = await resolveRequest(ctx, requestedPath, { accept: "text/event-stream" });
22748
- const url = new URL(path.replace(/^\//, ""), ctx.baseUrl.endsWith("/") ? ctx.baseUrl : `${ctx.baseUrl}/`);
22857
+ const { url, headers } = await resolveStreamRequest(ctx, requestedPath, {});
22749
22858
  const controller = new AbortController();
22750
22859
  const timer = Number.isFinite(timeoutMs) ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
22751
22860
  let seen = 0;
@@ -26552,7 +26661,7 @@ function renderKnowledge(ctx, agentId, k) {
26552
26661
  }
26553
26662
 
26554
26663
  // src/cli/compatEndpoints.ts
26555
- import { readFileSync as readFileSync20 } from "node:fs";
26664
+ import { readFileSync as readFileSync19 } from "node:fs";
26556
26665
  var BASE32 = "/v1/host/openwop-app/compat-endpoints";
26557
26666
  var COMPAT_ENDPOINTS_HELP = `Usage:
26558
26667
  openwop compat-endpoints list --org <orgId> [--json]
@@ -26666,7 +26775,7 @@ function readApiKey(ctx, options) {
26666
26775
  if (options.apiKeyFile && options.apiKeyEnv) throw new CliError("Pass only one of --api-key-file / --api-key-env.", 2);
26667
26776
  if (options.apiKeyFile) {
26668
26777
  try {
26669
- return readFileSync20(String(options.apiKeyFile), "utf8").trim() || void 0;
26778
+ return readFileSync19(String(options.apiKeyFile), "utf8").trim() || void 0;
26670
26779
  } catch (err) {
26671
26780
  throw new CliError(`Could not read ${options.apiKeyFile}: ${err instanceof Error ? err.message : String(err)}`, 2);
26672
26781
  }
@@ -27868,7 +27977,7 @@ Run \`openwop scheduled-chats --help\` for usage.`, 2);
27868
27977
  }
27869
27978
 
27870
27979
  // src/cli/voice.ts
27871
- import { readFileSync as readFileSync21, writeFileSync as writeFileSync19 } from "node:fs";
27980
+ import { readFileSync as readFileSync20, writeFileSync as writeFileSync19 } from "node:fs";
27872
27981
  var SESSION = "/v1/host/openwop-app/voice/session";
27873
27982
  var REALTIME = "/v1/host/openwop-app/voice/realtime";
27874
27983
  var AUDIO_CHUNK_BYTES = 192 * 1024;
@@ -27973,7 +28082,7 @@ Run \`openwop voice --help\` for usage.`, 2);
27973
28082
  if (options.file) {
27974
28083
  let bytes;
27975
28084
  try {
27976
- bytes = readFileSync21(String(options.file));
28085
+ bytes = readFileSync20(String(options.file));
27977
28086
  } catch (err) {
27978
28087
  throw new CliError(`--file: cannot read ${options.file} (${err instanceof Error ? err.message : String(err)})`, 2);
27979
28088
  }
@@ -28115,7 +28224,7 @@ async function runRealtime(ctx, argv) {
28115
28224
  if (!options.sdpFile) throw new CliError("voice realtime connect requires --sdp-file <offer.sdp>.", 2);
28116
28225
  let sdp;
28117
28226
  try {
28118
- sdp = readFileSync21(String(options.sdpFile), "utf8");
28227
+ sdp = readFileSync20(String(options.sdpFile), "utf8");
28119
28228
  } catch (err) {
28120
28229
  throw new CliError(`--sdp-file: cannot read ${options.sdpFile} (${err instanceof Error ? err.message : String(err)})`, 2);
28121
28230
  }
@@ -28170,10 +28279,10 @@ async function runRealtimeConfig(ctx, argv) {
28170
28279
  write(ctx.io.stdout, VOICE_HELP);
28171
28280
  return 0;
28172
28281
  }
28173
- const render3 = (b) => writeFields(ctx, [["provider", b?.provider], ["credentialRef", b?.credentialRef], ["model", b?.model]]);
28282
+ const render2 = (b) => writeFields(ctx, [["provider", b?.provider], ["credentialRef", b?.credentialRef], ["model", b?.model]]);
28174
28283
  if (sub === "get") {
28175
28284
  const res = await requestJson(ctx, `${REALTIME}/config`);
28176
- return emit(ctx, res.body, () => render3(res.body));
28285
+ return emit(ctx, res.body, () => render2(res.body));
28177
28286
  }
28178
28287
  if (sub === "set") {
28179
28288
  if (!options.provider && options.credentialRef === void 0 && options.model === void 0) {
@@ -28188,7 +28297,7 @@ async function runRealtimeConfig(ctx, argv) {
28188
28297
  const res = await requestJson(ctx, `${REALTIME}/config`, { method: "PUT", body });
28189
28298
  return emit(ctx, res.body, () => {
28190
28299
  writeLine(ctx.io.stdout, "Updated realtime voice config.");
28191
- render3(res.body);
28300
+ render2(res.body);
28192
28301
  });
28193
28302
  }
28194
28303
  throw new CliError(`Unknown voice realtime config command: ${sub}
@@ -28196,7 +28305,7 @@ Run \`openwop voice --help\` for usage.`, 2);
28196
28305
  }
28197
28306
 
28198
28307
  // src/cli/ai.ts
28199
- import { readFileSync as readFileSync22 } from "node:fs";
28308
+ import { readFileSync as readFileSync21 } from "node:fs";
28200
28309
  var AI = "/v1/host/openwop-app/ai";
28201
28310
  var AI_HELP = `Usage:
28202
28311
  openwop ai call (--message <text> | --messages-json <json>) [--provider p] [--model m] [--json]
@@ -28294,7 +28403,7 @@ async function runAi(ctx, argv) {
28294
28403
  let value;
28295
28404
  if (options.valueFile) {
28296
28405
  try {
28297
- value = readFileSync22(String(options.valueFile), "utf8").trim();
28406
+ value = readFileSync21(String(options.valueFile), "utf8").trim();
28298
28407
  } catch (err) {
28299
28408
  throw new CliError(`--value-file: cannot read ${options.valueFile} (${err instanceof Error ? err.message : String(err)})`, 2);
28300
28409
  }
@@ -29501,6 +29610,7 @@ async function runCli(argv, options = {}) {
29501
29610
  parsed.globals.baseUrl ?? env.OPENWOP_BASE_URL ?? savedConfig?.host?.baseUrl ?? DEFAULT_BASE_URL
29502
29611
  ),
29503
29612
  apiKey: parsed.globals.apiKey ?? env.OPENWOP_API_KEY ?? savedConfig?.host?.apiKey,
29613
+ streamBaseUrl: explicitStreamBase(parsed.globals.streamBaseUrl ?? env.OPENWOP_STREAM_BASE_URL ?? savedConfig?.host?.streamBaseUrl),
29504
29614
  json: parsed.globals.json,
29505
29615
  quiet: parsed.globals.quiet,
29506
29616
  verbose: parsed.globals.verbose,
@@ -30061,6 +30171,9 @@ Usage:
30061
30171
 
30062
30172
  Global options:
30063
30173
  --base-url <url> Host base URL (default: --base-url > OPENWOP_BASE_URL > saved config > http://localhost:8080)
30174
+ --stream-base-url <url>
30175
+ Where to read event streams (default: OPENWOP_STREAM_BASE_URL >
30176
+ saved config > the host's advertised https streamBase > --base-url)
30064
30177
  --api-key <key> Bearer API key (default: OPENWOP_API_KEY > saved config > sample-token for localhost)
30065
30178
  --profile <name> Use a named config profile (~/.openwop-<name>); also OPENWOP_PROFILE
30066
30179
  --json Print machine-readable JSON where supported
@@ -30353,6 +30466,7 @@ export {
30353
30466
  describeHttpError,
30354
30467
  detectChannelAvailability,
30355
30468
  errorEnvelope,
30469
+ explicitStreamBase,
30356
30470
  extractAssistantText,
30357
30471
  extractGlobalOptions,
30358
30472
  findRepoRoot,
@@ -30361,6 +30475,7 @@ export {
30361
30475
  hostRootFor,
30362
30476
  hostRootsFrom,
30363
30477
  idempotencyHeaders,
30478
+ isNoCredentialChallenge,
30364
30479
  isTerminalRunEvent,
30365
30480
  openwopHomeDir,
30366
30481
  parseDiscordMessage,
@@ -30374,9 +30489,12 @@ export {
30374
30489
  readConfigSafe,
30375
30490
  readDaemonRecord,
30376
30491
  renderEvent,
30492
+ requestNormativeOrHost,
30493
+ resolveStreamRequest,
30377
30494
  runCli,
30378
30495
  saveConfig,
30379
30496
  startInboundReceive,
30497
+ streamBaseFrom,
30380
30498
  streamRunEvents,
30381
30499
  submitTurn,
30382
30500
  summarizeCapabilities