@openwop/cli 1.2.0 → 1.2.1
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 +1 -1
- package/dist/cli.js +72 -44
- package/dist/openwop.js +70 -44
- package/package.json +1 -1
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.
|
|
581
|
+
version: "1.2.1",
|
|
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
|
-
|
|
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}` : ""}
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
1787
|
-
|
|
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 (!(
|
|
1813
|
+
const arm = (after = ms2) => {
|
|
1814
|
+
if (!(after > 0)) return;
|
|
1792
1815
|
if (timer !== void 0) clearTimeout(timer);
|
|
1793
|
-
timer = setTimeout(expire,
|
|
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
|
|
1832
|
+
out.error(new StreamSilent(ms2, false));
|
|
1805
1833
|
source.cancel().catch(() => {
|
|
1806
1834
|
});
|
|
1807
1835
|
};
|
|
@@ -2650,7 +2678,8 @@ async function runInterrupts(ctx, argv) {
|
|
|
2650
2678
|
if (b.key) writeLine(ctx.io.stdout, `key: ${b.key}`);
|
|
2651
2679
|
writeLine(ctx.io.stdout, `resolved: ${b.resolved ? "yes" : "no"}`);
|
|
2652
2680
|
if (b.expiresAt) writeLine(ctx.io.stdout, `expiresAt: ${b.expiresAt}`);
|
|
2653
|
-
|
|
2681
|
+
const prompt = b.data && typeof b.data === "object" ? b.data.prompt : void 0;
|
|
2682
|
+
if (typeof prompt === "string") writeLine(ctx.io.stdout, `prompt: ${prompt}`);
|
|
2654
2683
|
if (b.resumeSchema !== void 0) writeLine(ctx.io.stdout, `resumeSchema: ${JSON.stringify(b.resumeSchema)}`);
|
|
2655
2684
|
return 0;
|
|
2656
2685
|
}
|
|
@@ -12417,7 +12446,9 @@ honouring the server's \`retry:\` field), and no event is printed twice.
|
|
|
12417
12446
|
(\`values\` never combines). A host that does not serve
|
|
12418
12447
|
the mode answers 400 unsupported_stream_mode.
|
|
12419
12448
|
--idle-timeout-ms N a connection that delivers no bytes for N ms (default
|
|
12420
|
-
45000; 0 disables) is treated as dropped and resumed.
|
|
12449
|
+
45000; 0 disables) is treated as dropped and resumed. A
|
|
12450
|
+
stream whose headers do not arrive within 10 s is given
|
|
12451
|
+
up on and followed by polling, with a hint on stderr.
|
|
12421
12452
|
Streams are read from the stream origin: --stream-base-url (global), else the
|
|
12422
12453
|
host's advertised \`streamBase\` (https only), else --base-url \u2014 some front
|
|
12423
12454
|
doors buffer event streams entirely (e.g. a CDN rewrite).
|
|
@@ -17404,6 +17435,24 @@ async function runPromptsGet(ctx, argv) {
|
|
|
17404
17435
|
return 0;
|
|
17405
17436
|
}
|
|
17406
17437
|
|
|
17438
|
+
// src/redact.ts
|
|
17439
|
+
var BASE_SECRET_KEY = /secret|token|password|private[-_]?key|client[-_]?secret|api[-_]?key|credential/i;
|
|
17440
|
+
function redactSecrets(value, rules = {}) {
|
|
17441
|
+
const secret = rules.secret ?? BASE_SECRET_KEY;
|
|
17442
|
+
const walk = (v) => {
|
|
17443
|
+
if (Array.isArray(v)) return v.map(walk);
|
|
17444
|
+
if (v && typeof v === "object") {
|
|
17445
|
+
const out = {};
|
|
17446
|
+
for (const [k, inner] of Object.entries(v)) {
|
|
17447
|
+
out[k] = secret.test(k) && !(rules.keep?.test(k) ?? false) ? "[redacted]" : walk(inner);
|
|
17448
|
+
}
|
|
17449
|
+
return out;
|
|
17450
|
+
}
|
|
17451
|
+
return v;
|
|
17452
|
+
};
|
|
17453
|
+
return walk(value);
|
|
17454
|
+
}
|
|
17455
|
+
|
|
17407
17456
|
// src/cli/connections.ts
|
|
17408
17457
|
var CONN = "/v1/host/openwop-app/connections/:id";
|
|
17409
17458
|
var CONNECTIONS_ROUTES = [
|
|
@@ -17479,15 +17528,7 @@ Examples:
|
|
|
17479
17528
|
`;
|
|
17480
17529
|
var SECRET_KEY = /secret|token|password|private[-_]?key|client[-_]?secret|api[-_]?key|credential/i;
|
|
17481
17530
|
function redact(value) {
|
|
17482
|
-
|
|
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;
|
|
17531
|
+
return redactSecrets(value, { secret: SECRET_KEY });
|
|
17491
17532
|
}
|
|
17492
17533
|
var TOP = ["list", "get", "test", "authorize", "oauth-clients", "oauth-client"];
|
|
17493
17534
|
async function runConnections(ctx, argv) {
|
|
@@ -17691,7 +17732,8 @@ Run \`openwop connections --help\` for usage.`);
|
|
|
17691
17732
|
} catch (err) {
|
|
17692
17733
|
gate4042(err);
|
|
17693
17734
|
}
|
|
17694
|
-
const
|
|
17735
|
+
const rawClients = Array.isArray(res.body?.clients) ? res.body.clients : [];
|
|
17736
|
+
const clients = rawClients.map(redact);
|
|
17695
17737
|
if (action === "get") {
|
|
17696
17738
|
if (positionals.length !== 1) {
|
|
17697
17739
|
write(ctx.io.stdout, "Usage: openwop connections oauth-clients get <provider> [--json]\n");
|
|
@@ -19207,15 +19249,7 @@ Examples:
|
|
|
19207
19249
|
var BASE11 = "/v1/host/openwop-app/auth";
|
|
19208
19250
|
var SECRET_KEY2 = /secret|token|password|private[-_]?key|client[-_]?secret|api[-_]?key|credential|bearer|assertion|certificate|cert|pem/i;
|
|
19209
19251
|
function redact2(value) {
|
|
19210
|
-
|
|
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;
|
|
19252
|
+
return redactSecrets(value, { secret: SECRET_KEY2 });
|
|
19219
19253
|
}
|
|
19220
19254
|
async function authRequest(ctx, path, options, notConfiguredMsg) {
|
|
19221
19255
|
try {
|
|
@@ -20327,15 +20361,7 @@ Examples:
|
|
|
20327
20361
|
var SECRET_KEY3 = /secret|token|password|private[-_]?key|client[-_]?secret|api[-_]?key|credential/i;
|
|
20328
20362
|
var REF_ONLY_KEY = /^secretsToRebind$/;
|
|
20329
20363
|
function redact3(value) {
|
|
20330
|
-
|
|
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;
|
|
20364
|
+
return redactSecrets(value, { secret: SECRET_KEY3, keep: REF_ONLY_KEY });
|
|
20339
20365
|
}
|
|
20340
20366
|
async function ensureAdvertised5(ctx) {
|
|
20341
20367
|
const wk = await safeRequest(ctx, "/.well-known/openwop", { auth: false });
|
|
@@ -21991,7 +22017,7 @@ async function startJob(ctx, path, body) {
|
|
|
21991
22017
|
const res = await requestJson(ctx, path, { method: "POST", body });
|
|
21992
22018
|
return renderDone(ctx, res.body, `Video job ${res.body?.jobId ?? ""} ${res.body?.status ?? "started"}.`);
|
|
21993
22019
|
} catch (err) {
|
|
21994
|
-
const b = err instanceof HttpError ? err.body : void 0;
|
|
22020
|
+
const b = err instanceof HttpError && err.body && typeof err.body === "object" ? err.body : void 0;
|
|
21995
22021
|
if (err instanceof HttpError && b && b.status === "failed") {
|
|
21996
22022
|
if (ctx.json) writeJson(ctx.io.stdout, b);
|
|
21997
22023
|
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" : ""}`);
|
|
@@ -30443,6 +30469,7 @@ Examples:
|
|
|
30443
30469
|
openwop packs install community.openwop-team.demo --version 0.1.0
|
|
30444
30470
|
`;
|
|
30445
30471
|
export {
|
|
30472
|
+
BASE_SECRET_KEY,
|
|
30446
30473
|
CLI_PROTOCOL_VERSION_BY_MAJOR,
|
|
30447
30474
|
COMMAND_NAMES,
|
|
30448
30475
|
DEFAULT_BASE_URL,
|
|
@@ -30488,6 +30515,7 @@ export {
|
|
|
30488
30515
|
projectTenantBoundId,
|
|
30489
30516
|
readConfigSafe,
|
|
30490
30517
|
readDaemonRecord,
|
|
30518
|
+
redactSecrets,
|
|
30491
30519
|
renderEvent,
|
|
30492
30520
|
requestNormativeOrHost,
|
|
30493
30521
|
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.
|
|
582
|
+
version: "1.2.1",
|
|
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
|
-
|
|
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}` : ""}
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
1787
|
-
|
|
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 (!(
|
|
1813
|
+
const arm = (after = ms2) => {
|
|
1814
|
+
if (!(after > 0)) return;
|
|
1792
1815
|
if (timer !== void 0) clearTimeout(timer);
|
|
1793
|
-
timer = setTimeout(expire,
|
|
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
|
|
1832
|
+
out.error(new StreamSilent(ms2, false));
|
|
1805
1833
|
source.cancel().catch(() => {
|
|
1806
1834
|
});
|
|
1807
1835
|
};
|
|
@@ -2650,7 +2678,8 @@ async function runInterrupts(ctx, argv) {
|
|
|
2650
2678
|
if (b.key) writeLine(ctx.io.stdout, `key: ${b.key}`);
|
|
2651
2679
|
writeLine(ctx.io.stdout, `resolved: ${b.resolved ? "yes" : "no"}`);
|
|
2652
2680
|
if (b.expiresAt) writeLine(ctx.io.stdout, `expiresAt: ${b.expiresAt}`);
|
|
2653
|
-
|
|
2681
|
+
const prompt = b.data && typeof b.data === "object" ? b.data.prompt : void 0;
|
|
2682
|
+
if (typeof prompt === "string") writeLine(ctx.io.stdout, `prompt: ${prompt}`);
|
|
2654
2683
|
if (b.resumeSchema !== void 0) writeLine(ctx.io.stdout, `resumeSchema: ${JSON.stringify(b.resumeSchema)}`);
|
|
2655
2684
|
return 0;
|
|
2656
2685
|
}
|
|
@@ -12417,7 +12446,9 @@ honouring the server's \`retry:\` field), and no event is printed twice.
|
|
|
12417
12446
|
(\`values\` never combines). A host that does not serve
|
|
12418
12447
|
the mode answers 400 unsupported_stream_mode.
|
|
12419
12448
|
--idle-timeout-ms N a connection that delivers no bytes for N ms (default
|
|
12420
|
-
45000; 0 disables) is treated as dropped and resumed.
|
|
12449
|
+
45000; 0 disables) is treated as dropped and resumed. A
|
|
12450
|
+
stream whose headers do not arrive within 10 s is given
|
|
12451
|
+
up on and followed by polling, with a hint on stderr.
|
|
12421
12452
|
Streams are read from the stream origin: --stream-base-url (global), else the
|
|
12422
12453
|
host's advertised \`streamBase\` (https only), else --base-url \u2014 some front
|
|
12423
12454
|
doors buffer event streams entirely (e.g. a CDN rewrite).
|
|
@@ -17404,6 +17435,24 @@ async function runPromptsGet(ctx, argv) {
|
|
|
17404
17435
|
return 0;
|
|
17405
17436
|
}
|
|
17406
17437
|
|
|
17438
|
+
// src/redact.ts
|
|
17439
|
+
var BASE_SECRET_KEY = /secret|token|password|private[-_]?key|client[-_]?secret|api[-_]?key|credential/i;
|
|
17440
|
+
function redactSecrets(value, rules = {}) {
|
|
17441
|
+
const secret = rules.secret ?? BASE_SECRET_KEY;
|
|
17442
|
+
const walk = (v) => {
|
|
17443
|
+
if (Array.isArray(v)) return v.map(walk);
|
|
17444
|
+
if (v && typeof v === "object") {
|
|
17445
|
+
const out = {};
|
|
17446
|
+
for (const [k, inner] of Object.entries(v)) {
|
|
17447
|
+
out[k] = secret.test(k) && !(rules.keep?.test(k) ?? false) ? "[redacted]" : walk(inner);
|
|
17448
|
+
}
|
|
17449
|
+
return out;
|
|
17450
|
+
}
|
|
17451
|
+
return v;
|
|
17452
|
+
};
|
|
17453
|
+
return walk(value);
|
|
17454
|
+
}
|
|
17455
|
+
|
|
17407
17456
|
// src/cli/connections.ts
|
|
17408
17457
|
var CONN = "/v1/host/openwop-app/connections/:id";
|
|
17409
17458
|
var CONNECTIONS_ROUTES = [
|
|
@@ -17479,15 +17528,7 @@ Examples:
|
|
|
17479
17528
|
`;
|
|
17480
17529
|
var SECRET_KEY = /secret|token|password|private[-_]?key|client[-_]?secret|api[-_]?key|credential/i;
|
|
17481
17530
|
function redact(value) {
|
|
17482
|
-
|
|
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;
|
|
17531
|
+
return redactSecrets(value, { secret: SECRET_KEY });
|
|
17491
17532
|
}
|
|
17492
17533
|
var TOP = ["list", "get", "test", "authorize", "oauth-clients", "oauth-client"];
|
|
17493
17534
|
async function runConnections(ctx, argv) {
|
|
@@ -17691,7 +17732,8 @@ Run \`openwop connections --help\` for usage.`);
|
|
|
17691
17732
|
} catch (err) {
|
|
17692
17733
|
gate4042(err);
|
|
17693
17734
|
}
|
|
17694
|
-
const
|
|
17735
|
+
const rawClients = Array.isArray(res.body?.clients) ? res.body.clients : [];
|
|
17736
|
+
const clients = rawClients.map(redact);
|
|
17695
17737
|
if (action === "get") {
|
|
17696
17738
|
if (positionals.length !== 1) {
|
|
17697
17739
|
write(ctx.io.stdout, "Usage: openwop connections oauth-clients get <provider> [--json]\n");
|
|
@@ -19207,15 +19249,7 @@ Examples:
|
|
|
19207
19249
|
var BASE11 = "/v1/host/openwop-app/auth";
|
|
19208
19250
|
var SECRET_KEY2 = /secret|token|password|private[-_]?key|client[-_]?secret|api[-_]?key|credential|bearer|assertion|certificate|cert|pem/i;
|
|
19209
19251
|
function redact2(value) {
|
|
19210
|
-
|
|
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;
|
|
19252
|
+
return redactSecrets(value, { secret: SECRET_KEY2 });
|
|
19219
19253
|
}
|
|
19220
19254
|
async function authRequest(ctx, path, options, notConfiguredMsg) {
|
|
19221
19255
|
try {
|
|
@@ -20327,15 +20361,7 @@ Examples:
|
|
|
20327
20361
|
var SECRET_KEY3 = /secret|token|password|private[-_]?key|client[-_]?secret|api[-_]?key|credential/i;
|
|
20328
20362
|
var REF_ONLY_KEY = /^secretsToRebind$/;
|
|
20329
20363
|
function redact3(value) {
|
|
20330
|
-
|
|
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;
|
|
20364
|
+
return redactSecrets(value, { secret: SECRET_KEY3, keep: REF_ONLY_KEY });
|
|
20339
20365
|
}
|
|
20340
20366
|
async function ensureAdvertised5(ctx) {
|
|
20341
20367
|
const wk = await safeRequest(ctx, "/.well-known/openwop", { auth: false });
|
|
@@ -21991,7 +22017,7 @@ async function startJob(ctx, path, body) {
|
|
|
21991
22017
|
const res = await requestJson(ctx, path, { method: "POST", body });
|
|
21992
22018
|
return renderDone(ctx, res.body, `Video job ${res.body?.jobId ?? ""} ${res.body?.status ?? "started"}.`);
|
|
21993
22019
|
} catch (err) {
|
|
21994
|
-
const b = err instanceof HttpError ? err.body : void 0;
|
|
22020
|
+
const b = err instanceof HttpError && err.body && typeof err.body === "object" ? err.body : void 0;
|
|
21995
22021
|
if (err instanceof HttpError && b && b.status === "failed") {
|
|
21996
22022
|
if (ctx.json) writeJson(ctx.io.stdout, b);
|
|
21997
22023
|
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" : ""}`);
|