jeopi-cli 16.4.1 → 16.4.3
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/CHANGELOG.md +80 -0
- package/dist/cli.js +3179 -3140
- package/dist/types/advisor/config.d.ts +4 -3
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/update-cli.d.ts +4 -1
- package/dist/types/cli/usage-error.d.ts +4 -0
- package/dist/types/commit/agentic/agent.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +7 -1
- package/dist/types/config/settings-schema.d.ts +3 -3
- package/dist/types/eval/py/tool-bridge.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -6
- package/dist/types/internal-urls/types.d.ts +7 -0
- package/dist/types/irc/bus.d.ts +4 -0
- package/dist/types/lsp/deferred-diagnostics.d.ts +11 -0
- package/dist/types/modes/components/move-overlay.d.ts +1 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
- package/dist/types/modes/components/welcome.d.ts +4 -0
- package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/modes/orchestrate.d.ts +1 -1
- package/dist/types/modes/ultrathink.d.ts +1 -1
- package/dist/types/modes/workflow.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +5 -0
- package/dist/types/session/auth-broker-config.d.ts +2 -2
- package/dist/types/tools/bash-skill-urls.d.ts +3 -2
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +3 -1
- package/dist/types/tools/browser/cmux/rpc.d.ts +20 -0
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/browser/run-cancellation.d.ts +17 -2
- package/dist/types/tools/browser/run-output.d.ts +25 -0
- package/dist/types/tools/browser/tab-worker.d.ts +22 -0
- package/dist/types/utils/changelog.d.ts +35 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +13 -0
- package/src/advisor/__tests__/config.test.ts +36 -0
- package/src/advisor/config.ts +11 -8
- package/src/autolearn/controller.ts +14 -2
- package/src/cli/args.ts +12 -0
- package/src/cli/flag-tables.ts +21 -7
- package/src/cli/update-cli.ts +63 -13
- package/src/cli/usage-error.ts +7 -0
- package/src/commands/acp.ts +11 -2
- package/src/commands/launch.ts +12 -3
- package/src/commit/agentic/agent.ts +4 -0
- package/src/commit/agentic/index.ts +46 -21
- package/src/config/model-resolver.ts +24 -6
- package/src/config/settings-schema.ts +3 -3
- package/src/edit/index.ts +10 -76
- package/src/edit/renderer.ts +13 -10
- package/src/eval/__tests__/agent-bridge.test.ts +19 -14
- package/src/eval/__tests__/bridge-timeout.test.ts +60 -0
- package/src/eval/executor-base.ts +84 -7
- package/src/eval/js/shared/rewrite-imports.ts +31 -13
- package/src/eval/py/tool-bridge.ts +13 -14
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/export/ttsr.ts +0 -3
- package/src/extensibility/custom-tools/loader.ts +70 -37
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/plugins/manager.ts +21 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/memory-protocol.ts +13 -8
- package/src/internal-urls/skill-protocol.ts +1 -1
- package/src/internal-urls/types.ts +7 -0
- package/src/irc/bus.ts +48 -18
- package/src/lsp/client.ts +7 -1
- package/src/lsp/deferred-diagnostics.ts +66 -0
- package/src/main.ts +13 -13
- package/src/mcp/tool-bridge.ts +57 -6
- package/src/mcp/transports/stdio.test.ts +45 -1
- package/src/mcp/transports/stdio.ts +6 -3
- package/src/modes/acp/acp-agent.ts +73 -2
- package/src/modes/acp/acp-event-mapper.ts +5 -0
- package/src/modes/acp/acp-mode.ts +11 -0
- package/src/modes/components/__tests__/move-overlay.test.ts +16 -1
- package/src/modes/components/advisor-config.ts +15 -7
- package/src/modes/components/move-overlay.ts +2 -3
- package/src/modes/components/plan-review-overlay.ts +7 -0
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/welcome.ts +13 -14
- package/src/modes/controllers/command-controller.ts +8 -10
- package/src/modes/controllers/event-controller.ts +17 -30
- package/src/modes/controllers/input-controller.ts +9 -6
- package/src/modes/controllers/selector-controller.ts +3 -2
- package/src/modes/controllers/tan-command-controller.ts +40 -1
- package/src/modes/interactive-mode.ts +13 -0
- package/src/modes/magic-keyword-boundary.ts +23 -0
- package/src/modes/orchestrate.ts +6 -5
- package/src/modes/print-mode.ts +9 -0
- package/src/modes/ultrathink.ts +6 -5
- package/src/modes/workflow.ts +5 -4
- package/src/prompts/system/eager-task.md +2 -2
- package/src/prompts/system/tan-context-switch.md +17 -0
- package/src/prompts/tools/browser.md +5 -5
- package/src/sdk.ts +3 -0
- package/src/session/agent-session.ts +237 -50
- package/src/session/auth-broker-config.ts +4 -4
- package/src/session/snapcompact-inline.ts +3 -19
- package/src/session/streaming-output.ts +40 -12
- package/src/slash-commands/builtin-registry.ts +8 -8
- package/src/task/executor.ts +8 -0
- package/src/tools/bash-skill-urls.ts +7 -4
- package/src/tools/bash.ts +30 -22
- package/src/tools/browser/cmux/cmux-tab.ts +49 -50
- package/src/tools/browser/cmux/rpc.ts +50 -0
- package/src/tools/browser/launch.ts +8 -4
- package/src/tools/browser/run-cancellation.ts +44 -5
- package/src/tools/browser/run-output.ts +76 -0
- package/src/tools/browser/tab-worker.ts +280 -133
- package/src/tools/eval-render.ts +16 -14
- package/src/tools/glob.ts +20 -6
- package/src/tools/grep.ts +1 -0
- package/src/tools/image-gen.ts +3 -4
- package/src/tools/irc.ts +27 -13
- package/src/tools/read.ts +14 -7
- package/src/tools/write.ts +22 -5
- package/src/tools/yield.ts +29 -1
- package/src/utils/changelog.ts +106 -9
- package/src/utils/git.ts +0 -1
- package/src/utils/title-generator.ts +68 -7
- package/src/web/search/index.ts +14 -6
- package/src/web/search/providers/perplexity.ts +18 -2
|
@@ -735,6 +735,7 @@ export class OutputSink {
|
|
|
735
735
|
#truncated = false;
|
|
736
736
|
#lastChunkTime = 0;
|
|
737
737
|
#pendingChunk = "";
|
|
738
|
+
#pendingChunkTimer: Timer | undefined;
|
|
738
739
|
|
|
739
740
|
// Per-line column cap streaming state (persists across `push` calls so a
|
|
740
741
|
// long line split across chunks still trips the same trigger).
|
|
@@ -806,20 +807,18 @@ export class OutputSink {
|
|
|
806
807
|
push(chunk: string): void {
|
|
807
808
|
chunk = sanitizeWithOptionalSixelPassthrough(chunk, sanitizeText);
|
|
808
809
|
|
|
809
|
-
// Throttled onChunk: coalesce chunks arriving inside the throttle window
|
|
810
|
-
//
|
|
811
|
-
// final
|
|
810
|
+
// Throttled onChunk: coalesce chunks arriving inside the throttle window.
|
|
811
|
+
// A timer flushes quiet tails at the throttle boundary; dump() catches a
|
|
812
|
+
// final pending chunk when the process exits before that timer fires.
|
|
812
813
|
// Live preview gets the raw (pre-cap) chunk so the TUI never lags behind
|
|
813
814
|
// what reached the sink — the column cap is for the persisted LLM view.
|
|
814
815
|
if (this.#onChunk) {
|
|
815
816
|
const now = Date.now();
|
|
816
817
|
if (now - this.#lastChunkTime >= this.#chunkThrottleMs) {
|
|
817
|
-
this.#
|
|
818
|
-
const merged = this.#pendingChunk + chunk;
|
|
819
|
-
this.#pendingChunk = "";
|
|
820
|
-
this.#onChunk(merged);
|
|
818
|
+
this.#emitPendingChunkWith(chunk, now);
|
|
821
819
|
} else {
|
|
822
820
|
this.#pendingChunk += chunk;
|
|
821
|
+
this.#schedulePendingChunkFlush();
|
|
823
822
|
}
|
|
824
823
|
}
|
|
825
824
|
|
|
@@ -1121,6 +1120,7 @@ export class OutputSink {
|
|
|
1121
1120
|
* branch in `dump()` against stale totals.
|
|
1122
1121
|
*/
|
|
1123
1122
|
replace(text: string): void {
|
|
1123
|
+
this.#clearPendingChunkTimer();
|
|
1124
1124
|
this.#buffer = text;
|
|
1125
1125
|
this.#bufferBytes = Buffer.byteLength(text, "utf-8");
|
|
1126
1126
|
this.#head = "";
|
|
@@ -1138,6 +1138,38 @@ export class OutputSink {
|
|
|
1138
1138
|
this.#pendingChunk = "";
|
|
1139
1139
|
}
|
|
1140
1140
|
|
|
1141
|
+
#clearPendingChunkTimer(): void {
|
|
1142
|
+
if (!this.#pendingChunkTimer) return;
|
|
1143
|
+
clearTimeout(this.#pendingChunkTimer);
|
|
1144
|
+
this.#pendingChunkTimer = undefined;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
#emitPendingChunkWith(chunk: string, now: number): void {
|
|
1148
|
+
this.#clearPendingChunkTimer();
|
|
1149
|
+
this.#lastChunkTime = now;
|
|
1150
|
+
const merged = this.#pendingChunk + chunk;
|
|
1151
|
+
this.#pendingChunk = "";
|
|
1152
|
+
this.#onChunk?.(merged);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
#flushPendingChunk(): void {
|
|
1156
|
+
if (this.#pendingChunk.length === 0) {
|
|
1157
|
+
this.#clearPendingChunkTimer();
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
this.#emitPendingChunkWith("", Date.now());
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
#schedulePendingChunkFlush(): void {
|
|
1164
|
+
if (this.#chunkThrottleMs <= 0 || this.#pendingChunkTimer) return;
|
|
1165
|
+
const elapsed = Date.now() - this.#lastChunkTime;
|
|
1166
|
+
const delay = Math.max(0, this.#chunkThrottleMs - elapsed);
|
|
1167
|
+
this.#pendingChunkTimer = setTimeout(() => {
|
|
1168
|
+
this.#pendingChunkTimer = undefined;
|
|
1169
|
+
this.#flushPendingChunk();
|
|
1170
|
+
}, delay);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1141
1173
|
/**
|
|
1142
1174
|
* Replay the rolling tail ring back into the artifact sink. When bytes
|
|
1143
1175
|
* were actually dropped from the middle (the head budget was exhausted
|
|
@@ -1179,11 +1211,7 @@ export class OutputSink {
|
|
|
1179
1211
|
|
|
1180
1212
|
// Flush any chunk still held back by the throttle so the live preview
|
|
1181
1213
|
// ends with the complete stream.
|
|
1182
|
-
|
|
1183
|
-
const pending = this.#pendingChunk;
|
|
1184
|
-
this.#pendingChunk = "";
|
|
1185
|
-
this.#onChunk(pending);
|
|
1186
|
-
}
|
|
1214
|
+
this.#flushPendingChunk();
|
|
1187
1215
|
const totalLines = this.#sawData ? this.#totalLines + 1 : 0;
|
|
1188
1216
|
|
|
1189
1217
|
if (this.#file) {
|
|
@@ -34,7 +34,12 @@ import { resolveResumableSession } from "../session/session-listing";
|
|
|
34
34
|
import { formatShakeSummary, type ShakeMode } from "../session/shake-types";
|
|
35
35
|
import { expandTilde, resolveToCwd } from "../tools/path-utils";
|
|
36
36
|
import { urlHyperlinkAlways } from "../tui";
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
getChangelogPath,
|
|
39
|
+
parseChangelog,
|
|
40
|
+
RECENT_CHANGELOG_ENTRY_LIMIT,
|
|
41
|
+
renderChangelogEntries,
|
|
42
|
+
} from "../utils/changelog";
|
|
38
43
|
import { copyToClipboard } from "../utils/clipboard";
|
|
39
44
|
import { CollabQrCodeComponent } from "./helpers/collab-qrcode";
|
|
40
45
|
import { buildContextReportText } from "./helpers/context-report";
|
|
@@ -1100,17 +1105,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1100
1105
|
const changelogPath = getChangelogPath();
|
|
1101
1106
|
const allEntries = await parseChangelog(changelogPath);
|
|
1102
1107
|
const showFull = command.args.trim().toLowerCase() === "full";
|
|
1103
|
-
const entriesToShow = showFull ? allEntries : allEntries.slice(0,
|
|
1108
|
+
const entriesToShow = showFull ? allEntries : allEntries.slice(0, RECENT_CHANGELOG_ENTRY_LIMIT);
|
|
1104
1109
|
if (entriesToShow.length === 0) {
|
|
1105
1110
|
await runtime.output("No changelog entries found.");
|
|
1106
1111
|
return commandConsumed();
|
|
1107
1112
|
}
|
|
1108
|
-
await runtime.output(
|
|
1109
|
-
[...entriesToShow]
|
|
1110
|
-
.reverse()
|
|
1111
|
-
.map(entry => entry.content)
|
|
1112
|
-
.join("\n\n"),
|
|
1113
|
-
);
|
|
1113
|
+
await runtime.output(renderChangelogEntries(entriesToShow).markdown);
|
|
1114
1114
|
return commandConsumed();
|
|
1115
1115
|
},
|
|
1116
1116
|
handleTui: async (command, runtime) => {
|
package/src/task/executor.ts
CHANGED
|
@@ -2038,14 +2038,22 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2038
2038
|
thinkingLevel: resolvedThinkingLevel,
|
|
2039
2039
|
explicitThinkingLevel,
|
|
2040
2040
|
authFallbackUsed,
|
|
2041
|
+
warning: modelResolutionWarning,
|
|
2041
2042
|
} = await awaitAbortable(
|
|
2042
2043
|
resolveModelOverrideWithAuthFallback(
|
|
2043
2044
|
modelPatterns,
|
|
2044
2045
|
options.parentActiveModelPattern,
|
|
2045
2046
|
modelRegistry,
|
|
2046
2047
|
settings,
|
|
2048
|
+
id,
|
|
2047
2049
|
),
|
|
2048
2050
|
);
|
|
2051
|
+
if (modelResolutionWarning) {
|
|
2052
|
+
logger.warn("Subagent model resolution warning", {
|
|
2053
|
+
warning: modelResolutionWarning,
|
|
2054
|
+
requested: modelPatterns,
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2049
2057
|
if (authFallbackUsed && model) {
|
|
2050
2058
|
logger.warn("Subagent model has no working credentials; falling back to parent session model", {
|
|
2051
2059
|
requested: modelPatterns,
|
|
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import type { Skill } from "../extensibility/skills";
|
|
4
4
|
import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
5
5
|
import { validateRelativePath } from "../internal-urls/skill-protocol";
|
|
6
|
-
import type { InternalResource } from "../internal-urls/types";
|
|
6
|
+
import type { InternalResource, ResolveContext } from "../internal-urls/types";
|
|
7
7
|
import { normalizeLocalScheme } from "./path-utils";
|
|
8
8
|
import { ToolError } from "./tool-errors";
|
|
9
9
|
|
|
@@ -19,7 +19,7 @@ type SupportedInternalScheme = (typeof SUPPORTED_INTERNAL_SCHEMES)[number];
|
|
|
19
19
|
|
|
20
20
|
interface InternalUrlResolver {
|
|
21
21
|
canHandle(input: string): boolean;
|
|
22
|
-
resolve(input: string): Promise<InternalResource>;
|
|
22
|
+
resolve(input: string, context?: ResolveContext): Promise<InternalResource>;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export interface InternalUrlExpansionOptions {
|
|
@@ -27,6 +27,7 @@ export interface InternalUrlExpansionOptions {
|
|
|
27
27
|
noEscape?: boolean;
|
|
28
28
|
internalRouter?: InternalUrlResolver;
|
|
29
29
|
localOptions?: LocalProtocolOptions;
|
|
30
|
+
cwd?: string;
|
|
30
31
|
ensureLocalParentDirs?: boolean;
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -66,7 +67,7 @@ export function resolveSkillUrlToPath(url: string, skills: readonly Skill[]): st
|
|
|
66
67
|
const hasRelativePath = rawPath !== "" && rawPath !== "/";
|
|
67
68
|
|
|
68
69
|
if (!hasRelativePath) {
|
|
69
|
-
return path.resolve(skill.
|
|
70
|
+
return path.resolve(skill.baseDir);
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
let relativePath: string;
|
|
@@ -151,6 +152,7 @@ async function resolveInternalUrlToPath(
|
|
|
151
152
|
internalRouter?: InternalUrlResolver,
|
|
152
153
|
localOptions?: LocalProtocolOptions,
|
|
153
154
|
ensureLocalParentDirs?: boolean,
|
|
155
|
+
cwd?: string,
|
|
154
156
|
): Promise<string> {
|
|
155
157
|
const url = normalizeLocalScheme(rawUrl);
|
|
156
158
|
const scheme = extractScheme(url);
|
|
@@ -184,7 +186,7 @@ async function resolveInternalUrlToPath(
|
|
|
184
186
|
|
|
185
187
|
let resource: InternalResource;
|
|
186
188
|
try {
|
|
187
|
-
resource = await internalRouter.resolve(url);
|
|
189
|
+
resource = await internalRouter.resolve(url, { cwd });
|
|
188
190
|
} catch (error) {
|
|
189
191
|
const message = error instanceof Error ? error.message : String(error);
|
|
190
192
|
throw new ToolError(`Failed to resolve ${scheme}:// URL in bash command: ${url}\n${message}`);
|
|
@@ -239,6 +241,7 @@ export async function expandInternalUrls(command: string, options: InternalUrlEx
|
|
|
239
241
|
options.internalRouter,
|
|
240
242
|
options.localOptions,
|
|
241
243
|
options.ensureLocalParentDirs,
|
|
244
|
+
options.cwd,
|
|
242
245
|
);
|
|
243
246
|
const replacement = options.noEscape ? resolvedPath : shellEscape(resolvedPath);
|
|
244
247
|
expanded = `${expanded.slice(0, index)}${replacement}${expanded.slice(index + token.length)}`;
|
package/src/tools/bash.ts
CHANGED
|
@@ -163,10 +163,9 @@ type ManagedBashJobCompletion =
|
|
|
163
163
|
|
|
164
164
|
interface ManagedBashJobHandle {
|
|
165
165
|
jobId: string;
|
|
166
|
-
label: string;
|
|
167
166
|
completion: Promise<ManagedBashJobCompletion>;
|
|
168
167
|
getLatestText: () => string;
|
|
169
|
-
|
|
168
|
+
stopUpdates: () => void;
|
|
170
169
|
}
|
|
171
170
|
|
|
172
171
|
function normalizeResultOutput(result: BashResult | BashInteractiveResult): string {
|
|
@@ -297,6 +296,10 @@ function formatExitCodeNotice(exitCode: number): string {
|
|
|
297
296
|
return `Command exited with code ${exitCode}`;
|
|
298
297
|
}
|
|
299
298
|
|
|
299
|
+
function formatBackgroundNotice(jobId: string): string {
|
|
300
|
+
return `Backgrounded as job ${jobId}; result will be delivered automatically.`;
|
|
301
|
+
}
|
|
302
|
+
|
|
300
303
|
/**
|
|
301
304
|
* Strip the trailing occurrence of `notice` (plus a single surrounding newline
|
|
302
305
|
* on each side) so the TUI can echo the value via a styled footer label
|
|
@@ -325,6 +328,11 @@ function stripExitCodeNotice(text: string, exitCode: number | undefined): string
|
|
|
325
328
|
return stripTrailingNotice(text, formatExitCodeNotice(exitCode));
|
|
326
329
|
}
|
|
327
330
|
|
|
331
|
+
function stripBackgroundNotice(text: string, async: BashToolDetails["async"] | undefined): string {
|
|
332
|
+
if (async?.state !== "running") return text;
|
|
333
|
+
return stripTrailingNotice(text, formatBackgroundNotice(async.jobId));
|
|
334
|
+
}
|
|
335
|
+
|
|
328
336
|
/**
|
|
329
337
|
* Bash tool implementation.
|
|
330
338
|
*
|
|
@@ -473,7 +481,6 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
473
481
|
|
|
474
482
|
#buildBackgroundStartResult(
|
|
475
483
|
jobId: string,
|
|
476
|
-
label: string,
|
|
477
484
|
previewText: string,
|
|
478
485
|
timeoutSec: number,
|
|
479
486
|
options: { requestedTimeoutSec?: number; notices?: readonly string[] } = {},
|
|
@@ -493,11 +500,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
493
500
|
if (options.notices?.length) {
|
|
494
501
|
lines.push(...options.notices, "");
|
|
495
502
|
}
|
|
496
|
-
lines.push(
|
|
497
|
-
lines.push("Result will be delivered automatically when complete.");
|
|
498
|
-
lines.push(
|
|
499
|
-
`You can use \`job\` to poll until complete, but prefer to continue with another task in the meanwhile if it's not blocking.`,
|
|
500
|
-
);
|
|
503
|
+
lines.push(formatBackgroundNotice(jobId));
|
|
501
504
|
return {
|
|
502
505
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
503
506
|
details,
|
|
@@ -518,7 +521,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
518
521
|
|
|
519
522
|
resolvedEnv?: Record<string, string>;
|
|
520
523
|
onUpdate?: AgentToolUpdateCallback<BashToolDetails>;
|
|
521
|
-
|
|
524
|
+
forwardUpdates: boolean;
|
|
522
525
|
}): ManagedBashJobHandle {
|
|
523
526
|
const manager = this.session.asyncJobManager;
|
|
524
527
|
if (!manager) {
|
|
@@ -527,7 +530,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
527
530
|
|
|
528
531
|
const label = options.command.length > 120 ? `${options.command.slice(0, 117)}...` : options.command;
|
|
529
532
|
let latestText = "";
|
|
530
|
-
let
|
|
533
|
+
let forwardUpdates = options.forwardUpdates;
|
|
531
534
|
const completion = Promise.withResolvers<ManagedBashJobCompletion>();
|
|
532
535
|
|
|
533
536
|
const jobId = manager.register(
|
|
@@ -583,11 +586,12 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
583
586
|
},
|
|
584
587
|
{
|
|
585
588
|
ownerId: this.session.getAgentId?.() ?? undefined,
|
|
586
|
-
onProgress: async
|
|
589
|
+
onProgress: async text => {
|
|
587
590
|
latestText = text;
|
|
591
|
+
if (!forwardUpdates) return;
|
|
588
592
|
await options.onUpdate?.({
|
|
589
593
|
content: [{ type: "text", text }],
|
|
590
|
-
details:
|
|
594
|
+
details: {},
|
|
591
595
|
});
|
|
592
596
|
},
|
|
593
597
|
},
|
|
@@ -595,11 +599,10 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
595
599
|
|
|
596
600
|
return {
|
|
597
601
|
jobId,
|
|
598
|
-
label,
|
|
599
602
|
completion: completion.promise,
|
|
600
603
|
getLatestText: () => latestText,
|
|
601
|
-
|
|
602
|
-
|
|
604
|
+
stopUpdates: () => {
|
|
605
|
+
forwardUpdates = false;
|
|
603
606
|
},
|
|
604
607
|
};
|
|
605
608
|
}
|
|
@@ -701,6 +704,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
701
704
|
const internalUrlOptions: InternalUrlExpansionOptions = {
|
|
702
705
|
skills: this.session.skills ?? [],
|
|
703
706
|
internalRouter: InternalUrlRouter.instance(),
|
|
707
|
+
cwd: this.session.cwd,
|
|
704
708
|
localOptions: {
|
|
705
709
|
getArtifactsDir: this.session.getArtifactsDir,
|
|
706
710
|
getSessionId: this.session.getSessionId,
|
|
@@ -769,9 +773,9 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
769
773
|
|
|
770
774
|
resolvedEnv,
|
|
771
775
|
onUpdate,
|
|
772
|
-
|
|
776
|
+
forwardUpdates: false,
|
|
773
777
|
});
|
|
774
|
-
return this.#buildBackgroundStartResult(job.jobId,
|
|
778
|
+
return this.#buildBackgroundStartResult(job.jobId, "", timeoutSec, {
|
|
775
779
|
requestedTimeoutSec,
|
|
776
780
|
notices: pendingNotices,
|
|
777
781
|
});
|
|
@@ -807,10 +811,10 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
807
811
|
|
|
808
812
|
resolvedEnv,
|
|
809
813
|
onUpdate,
|
|
810
|
-
startBackgrounded,
|
|
814
|
+
forwardUpdates: !startBackgrounded,
|
|
811
815
|
});
|
|
812
816
|
if (startBackgrounded) {
|
|
813
|
-
return this.#buildBackgroundStartResult(job.jobId,
|
|
817
|
+
return this.#buildBackgroundStartResult(job.jobId, "", timeoutSec, {
|
|
814
818
|
requestedTimeoutSec,
|
|
815
819
|
notices: pendingNotices,
|
|
816
820
|
});
|
|
@@ -830,9 +834,9 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
830
834
|
autoBgManager.cancel(job.jobId);
|
|
831
835
|
throw new ToolAbortError(job.getLatestText() || "Command aborted");
|
|
832
836
|
}
|
|
833
|
-
job.
|
|
837
|
+
job.stopUpdates();
|
|
834
838
|
autoBgManager.resumeDeliveries([job.jobId]);
|
|
835
|
-
return this.#buildBackgroundStartResult(job.jobId, job.
|
|
839
|
+
return this.#buildBackgroundStartResult(job.jobId, job.getLatestText(), timeoutSec, {
|
|
836
840
|
requestedTimeoutSec,
|
|
837
841
|
notices: pendingNotices,
|
|
838
842
|
});
|
|
@@ -1249,7 +1253,8 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1249
1253
|
) {
|
|
1250
1254
|
return cachedLines;
|
|
1251
1255
|
}
|
|
1252
|
-
const
|
|
1256
|
+
const withoutBackground = stripBackgroundNotice(rawOutput, details?.async);
|
|
1257
|
+
const strippedOutput = stripOutputNotice(withoutBackground, details?.meta);
|
|
1253
1258
|
const withoutExit = stripExitCodeNotice(strippedOutput, details?.exitCode);
|
|
1254
1259
|
const withoutWall = stripWallTimeNotice(withoutExit, details?.wallTimeMs);
|
|
1255
1260
|
const rawOutputArtifact = stripRawOutputArtifactNotice(withoutWall);
|
|
@@ -1262,6 +1267,9 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1262
1267
|
const requestedTimeoutSeconds = details?.requestedTimeoutSeconds;
|
|
1263
1268
|
const wallTimeMs = details?.wallTimeMs;
|
|
1264
1269
|
const statsParts: string[] = [];
|
|
1270
|
+
if (details?.async?.state === "running") {
|
|
1271
|
+
statsParts.push(`Backgrounded: ${details.async.jobId}`);
|
|
1272
|
+
}
|
|
1265
1273
|
if (wallTimeMs !== undefined) {
|
|
1266
1274
|
statsParts.push(`Wall: ${formatWallTimeSeconds(wallTimeMs)}s`);
|
|
1267
1275
|
}
|
|
@@ -3,7 +3,6 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { logger, Snowflake, untilAborted } from "jeopi-utils";
|
|
5
5
|
import { JsRuntime, type RuntimeHooks } from "../../../eval/js/shared/runtime";
|
|
6
|
-
import type { JsDisplayOutput } from "../../../eval/js/shared/types";
|
|
7
6
|
import { callSessionTool } from "../../../eval/js/tool-bridge";
|
|
8
7
|
import { resizeImage } from "../../../utils/image-resize";
|
|
9
8
|
import type { ToolSession } from "../../index";
|
|
@@ -13,7 +12,8 @@ import { ToolAbortError, ToolError, throwIfAborted } from "../../tool-errors";
|
|
|
13
12
|
import { type AriaSnapshotOptions, buildAriaSnapshotScript } from "../aria/aria-snapshot";
|
|
14
13
|
import { DEFAULT_VIEWPORT } from "../launch";
|
|
15
14
|
import { extractReadableFromHtml, type ReadableFormat } from "../readable";
|
|
16
|
-
import { bindBrowserRunFacade, waitForBrowserRun } from "../run-cancellation";
|
|
15
|
+
import { bindBrowserRunFacade, type WaitPredicateOptions, waitForBrowserRun } from "../run-cancellation";
|
|
16
|
+
import { cloneSafe, RunOutput } from "../run-output";
|
|
17
17
|
import type { Observation, ReadyInfo, RunResultOk, ScreenshotResult, SessionSnapshot } from "../tab-protocol";
|
|
18
18
|
import {
|
|
19
19
|
type CmuxEvalResult,
|
|
@@ -24,7 +24,8 @@ import {
|
|
|
24
24
|
cmuxSnapshotToObservation,
|
|
25
25
|
GEOMETRY_SCRIPT,
|
|
26
26
|
mapWaitUntil,
|
|
27
|
-
|
|
27
|
+
serializeEvalWithEnvelope,
|
|
28
|
+
unwrapEvalEnvelope,
|
|
28
29
|
} from "./rpc";
|
|
29
30
|
import type { CmuxSocketClient } from "./socket-client";
|
|
30
31
|
|
|
@@ -43,7 +44,7 @@ interface ObserveOptions {
|
|
|
43
44
|
|
|
44
45
|
interface RunContext {
|
|
45
46
|
session: SessionSnapshot;
|
|
46
|
-
|
|
47
|
+
output: RunOutput;
|
|
47
48
|
screenshots: ScreenshotResult[];
|
|
48
49
|
signal: AbortSignal;
|
|
49
50
|
timeoutMs: number;
|
|
@@ -445,10 +446,14 @@ export class CmuxTab {
|
|
|
445
446
|
fn: string | ((...args: TArgs) => TResult | Promise<TResult>),
|
|
446
447
|
...args: TArgs
|
|
447
448
|
): Promise<TResult> {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
return
|
|
449
|
+
// A script that throws inside the daemon comes back as a bare
|
|
450
|
+
// `js_error: A JavaScript exception occurred` with no message or stack.
|
|
451
|
+
// Catch page-side instead so the exception is diagnosable, and turn the
|
|
452
|
+
// daemon's other blind spot — Promise return values it cannot
|
|
453
|
+
// serialize — into an actionable error instead of "unsupported type".
|
|
454
|
+
const script = serializeEvalWithEnvelope(fn as string | ((...args: unknown[]) => unknown), args);
|
|
455
|
+
const result = (await this.#request("browser.eval", { script })) as CmuxEvalResult;
|
|
456
|
+
return unwrapEvalEnvelope<TResult>(result.value, "tab.evaluate()");
|
|
452
457
|
}
|
|
453
458
|
|
|
454
459
|
async scrollIntoView(selector: string): Promise<void> {
|
|
@@ -479,10 +484,22 @@ export class CmuxTab {
|
|
|
479
484
|
|
|
480
485
|
async screenshot(opts: ScreenshotOptions = {}): Promise<ScreenshotResult> {
|
|
481
486
|
const context = this.#requireRunContext("tab.screenshot()");
|
|
487
|
+
// The cmux daemon's `browser.screenshot` captures the surface viewport
|
|
488
|
+
// only — it has no element-clip or full-page mode, and Bun.Image cannot
|
|
489
|
+
// crop locally. Degrade transparently instead of silently mislabeling
|
|
490
|
+
// the capture: scroll the element into view, then TELL the model the
|
|
491
|
+
// image is the full viewport (reports showed selector captures being
|
|
492
|
+
// consumed as element crops).
|
|
493
|
+
const captureNotes: string[] = [];
|
|
482
494
|
if (opts.selector) {
|
|
483
495
|
await this.scrollIntoView(opts.selector);
|
|
496
|
+
captureNotes.push(
|
|
497
|
+
`selector ${JSON.stringify(opts.selector)} was scrolled into view, but this surface cannot clip to an element — the image is the full viewport`,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
if (opts.fullPage) {
|
|
501
|
+
captureNotes.push("fullPage is unavailable on this surface — the image is the viewport only");
|
|
484
502
|
}
|
|
485
|
-
void opts.fullPage;
|
|
486
503
|
const result = await this.#captureScreenshotPng(context.timeoutMs);
|
|
487
504
|
const buffer = Buffer.from(result.png_base64, "base64");
|
|
488
505
|
const captureMime = "image/png";
|
|
@@ -528,8 +545,11 @@ export class CmuxTab {
|
|
|
528
545
|
dest,
|
|
529
546
|
resized,
|
|
530
547
|
});
|
|
531
|
-
|
|
532
|
-
|
|
548
|
+
if (captureNotes.length > 0) {
|
|
549
|
+
lines.push(`[cmux surface: ${captureNotes.join("; ")}]`);
|
|
550
|
+
}
|
|
551
|
+
context.output.push({ type: "text", text: lines.join("\n") });
|
|
552
|
+
context.output.push({ type: "image", data: resized.data, mimeType: resized.mimeType });
|
|
533
553
|
}
|
|
534
554
|
return info;
|
|
535
555
|
}
|
|
@@ -724,7 +744,12 @@ export class CmuxTab {
|
|
|
724
744
|
const callable = (0, eval)("(" + source + ")");
|
|
725
745
|
return callable(element, ...args);
|
|
726
746
|
})()`;
|
|
727
|
-
|
|
747
|
+
// Envelope so a stale selector or a throwing callback reports its actual
|
|
748
|
+
// error instead of the daemon's generic js_error (see tab.evaluate()).
|
|
749
|
+
const result = (await this.#request("browser.eval", {
|
|
750
|
+
script: serializeEvalWithEnvelope(script, []),
|
|
751
|
+
})) as CmuxEvalResult;
|
|
752
|
+
return unwrapEvalEnvelope<TResult>(result.value, "elementHandle.evaluate()");
|
|
728
753
|
}
|
|
729
754
|
|
|
730
755
|
async pageContent(): Promise<string> {
|
|
@@ -1097,6 +1122,10 @@ class CmuxElementHandle {
|
|
|
1097
1122
|
await this.#tab.fill(this.#selector, value);
|
|
1098
1123
|
}
|
|
1099
1124
|
|
|
1125
|
+
async press(key: string): Promise<void> {
|
|
1126
|
+
await this.#tab.press(key, { selector: this.#selector });
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1100
1129
|
async focus(): Promise<void> {
|
|
1101
1130
|
await this.#tab.focus(this.#selector);
|
|
1102
1131
|
}
|
|
@@ -1284,10 +1313,10 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
|
|
|
1284
1313
|
const signal = AbortSignal.any(
|
|
1285
1314
|
opts.signal ? [timeoutSignal, opts.signal, runAc.signal] : [timeoutSignal, runAc.signal],
|
|
1286
1315
|
);
|
|
1287
|
-
const
|
|
1316
|
+
const output = new RunOutput();
|
|
1288
1317
|
const screenshots: ScreenshotResult[] = [];
|
|
1289
1318
|
const runId = crypto.randomUUID();
|
|
1290
|
-
tab.setRunContext({ session: opts.snapshot,
|
|
1319
|
+
tab.setRunContext({ session: opts.snapshot, output, screenshots, signal, timeoutMs: opts.timeoutMs });
|
|
1291
1320
|
const runtime = tab.ensureRuntime(opts.snapshot);
|
|
1292
1321
|
runtime.setCwd(opts.snapshot.cwd);
|
|
1293
1322
|
const runTab = bindBrowserRunFacade(tab, signal);
|
|
@@ -1298,7 +1327,8 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
|
|
|
1298
1327
|
assert: (cond: unknown, text?: string): void => {
|
|
1299
1328
|
if (!cond) throw new ToolError(text ?? "Assertion failed");
|
|
1300
1329
|
},
|
|
1301
|
-
wait: (
|
|
1330
|
+
wait: (msOrPredicate: number | (() => unknown), opts?: WaitPredicateOptions): Promise<unknown> =>
|
|
1331
|
+
waitForBrowserRun(msOrPredicate, signal, opts),
|
|
1302
1332
|
});
|
|
1303
1333
|
|
|
1304
1334
|
const { promise: cancelRejection, reject } = Promise.withResolvers<never>();
|
|
@@ -1316,11 +1346,12 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
|
|
|
1316
1346
|
const hooks: RuntimeHooks = {
|
|
1317
1347
|
onText: chunk => {
|
|
1318
1348
|
throwIfAborted(signal);
|
|
1349
|
+
output.pushText(chunk);
|
|
1319
1350
|
logger.debug(chunk.replace(/\n$/, ""));
|
|
1320
1351
|
},
|
|
1321
|
-
onDisplay:
|
|
1352
|
+
onDisplay: displayed => {
|
|
1322
1353
|
throwIfAborted(signal);
|
|
1323
|
-
pushDisplay(
|
|
1354
|
+
output.pushDisplay(displayed);
|
|
1324
1355
|
},
|
|
1325
1356
|
callTool: (name, args) => {
|
|
1326
1357
|
throwIfAborted(signal);
|
|
@@ -1333,7 +1364,7 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
|
|
|
1333
1364
|
runtime.run(opts.code, `cmux-run-${runId}.js`, hooks, { runId, cwd: opts.snapshot.cwd }),
|
|
1334
1365
|
cancelRejection,
|
|
1335
1366
|
]);
|
|
1336
|
-
return { displays, returnValue: cloneSafe(returnValue), screenshots };
|
|
1367
|
+
return { displays: output.finish(), returnValue: cloneSafe(returnValue), screenshots };
|
|
1337
1368
|
} finally {
|
|
1338
1369
|
signal.removeEventListener("abort", onAbort);
|
|
1339
1370
|
runAc.abort(new ToolAbortError("Browser run ended"));
|
|
@@ -1341,38 +1372,6 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
|
|
|
1341
1372
|
}
|
|
1342
1373
|
}
|
|
1343
1374
|
|
|
1344
|
-
function pushDisplay(displays: RunResultOk["displays"], output: JsDisplayOutput): void {
|
|
1345
|
-
if (output.type === "image") {
|
|
1346
|
-
displays.push({ type: "image", data: output.data, mimeType: output.mimeType });
|
|
1347
|
-
return;
|
|
1348
|
-
}
|
|
1349
|
-
if (output.type === "json") {
|
|
1350
|
-
displays.push({ type: "text", text: safeJsonStringify(output.data) });
|
|
1351
|
-
return;
|
|
1352
|
-
}
|
|
1353
|
-
displays.push({ type: "text", text: safeJsonStringify(output.event) });
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
function safeJsonStringify(value: unknown): string {
|
|
1357
|
-
try {
|
|
1358
|
-
return JSON.stringify(value, null, 2);
|
|
1359
|
-
} catch {
|
|
1360
|
-
return String(value);
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
function cloneSafe(value: unknown): unknown {
|
|
1365
|
-
if (value === undefined) return undefined;
|
|
1366
|
-
try {
|
|
1367
|
-
structuredClone(value);
|
|
1368
|
-
return value;
|
|
1369
|
-
} catch {}
|
|
1370
|
-
try {
|
|
1371
|
-
return JSON.parse(JSON.stringify(value)) as unknown;
|
|
1372
|
-
} catch {}
|
|
1373
|
-
return String(value);
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
1375
|
function numberFrom(value: unknown, fallback: number): number {
|
|
1377
1376
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
1378
1377
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ToolError } from "../../tool-errors";
|
|
1
2
|
import type { Observation, ObservationEntry } from "../tab-protocol";
|
|
2
3
|
|
|
3
4
|
export interface CmuxKind {
|
|
@@ -120,6 +121,55 @@ export function serializeEval(fn: string | ((...args: unknown[]) => unknown), ar
|
|
|
120
121
|
return `(${fn.toString()})(${args.map(arg => JSON.stringify(arg)).join(",")})`;
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Like {@link serializeEval}, but wraps the expression in a page-side
|
|
126
|
+
* try/catch envelope so a throwing script surfaces its message + stack
|
|
127
|
+
* instead of the daemon's opaque `js_error: A JavaScript exception occurred`,
|
|
128
|
+
* and a Promise return (which the daemon cannot serialize) is flagged
|
|
129
|
+
* explicitly rather than failing as "unsupported type".
|
|
130
|
+
*
|
|
131
|
+
* String scripts run through indirect eval to keep global-scope semantics;
|
|
132
|
+
* function sources are already expressions and are invoked directly.
|
|
133
|
+
* `undefined` results come back as `null` (JSON cannot carry `undefined`).
|
|
134
|
+
* Decode with {@link unwrapEvalEnvelope}.
|
|
135
|
+
*/
|
|
136
|
+
export function serializeEvalWithEnvelope(fn: string | ((...args: unknown[]) => unknown), args: unknown[]): string {
|
|
137
|
+
const inner = serializeEval(fn, args);
|
|
138
|
+
const expr = typeof fn === "string" ? `(0, eval)(${JSON.stringify(inner)})` : inner;
|
|
139
|
+
return `(() => {
|
|
140
|
+
try {
|
|
141
|
+
const __v = (${expr});
|
|
142
|
+
if (__v && typeof __v.then === "function") return { __jeopiPromise: true };
|
|
143
|
+
return { __jeopiOk: __v === undefined ? null : __v };
|
|
144
|
+
} catch (e) {
|
|
145
|
+
return { __jeopiErr: (e && (e.stack || e.message)) || String(e) };
|
|
146
|
+
}
|
|
147
|
+
})()`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Decode a {@link serializeEvalWithEnvelope} result: rethrow page-side
|
|
152
|
+
* exceptions as rich {@link ToolError}s, reject unserializable Promise
|
|
153
|
+
* returns with an actionable message, and pass through values from daemons
|
|
154
|
+
* that did not run the wrapper.
|
|
155
|
+
*/
|
|
156
|
+
export function unwrapEvalEnvelope<TResult>(value: unknown, label: string): TResult {
|
|
157
|
+
if (value && typeof value === "object") {
|
|
158
|
+
if ("__jeopiErr" in value && typeof value.__jeopiErr === "string") {
|
|
159
|
+
throw new ToolError(`${label} threw a JavaScript exception:\n${value.__jeopiErr}`);
|
|
160
|
+
}
|
|
161
|
+
if ("__jeopiPromise" in value && value.__jeopiPromise === true) {
|
|
162
|
+
throw new ToolError(
|
|
163
|
+
`${label} returned a Promise, but this surface evaluates synchronously and cannot await it — return a plain value (poll with waitForFunction for async state instead)`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if ("__jeopiOk" in value) {
|
|
167
|
+
return value.__jeopiOk as TResult;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return value as TResult;
|
|
171
|
+
}
|
|
172
|
+
|
|
123
173
|
export function mapWaitUntil(waitUntil: string | undefined): "interactive" | "complete" {
|
|
124
174
|
return waitUntil === "domcontentloaded" ? "interactive" : "complete";
|
|
125
175
|
}
|
|
@@ -99,12 +99,16 @@ async function loadBrowsers(): Promise<typeof BrowsersNs> {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
|
-
*
|
|
103
|
-
* Skipped when a system Chromium (NixOS)
|
|
104
|
-
* The browser is cached under
|
|
102
|
+
* Resolve the Chromium executable puppeteer will launch, lazily downloading it
|
|
103
|
+
* on first use via @puppeteer/browsers. Skipped when a system Chromium (NixOS)
|
|
104
|
+
* or PUPPETEER_EXECUTABLE_PATH is set. The browser is cached under
|
|
105
|
+
* ~/.jeopi/puppeteer (getPuppeteerDir). Returns undefined when platform
|
|
106
|
+
* detection fails (puppeteer default resolution takes over). Exported so
|
|
107
|
+
* real-browser tests can probe launchability and skip on hosts missing
|
|
108
|
+
* Chrome's system libraries.
|
|
105
109
|
*/
|
|
106
110
|
let chromiumExecutablePromise: Promise<string | undefined> | undefined;
|
|
107
|
-
async function ensureChromiumExecutable(): Promise<string | undefined> {
|
|
111
|
+
export async function ensureChromiumExecutable(): Promise<string | undefined> {
|
|
108
112
|
const sysChrome = resolveSystemChromium();
|
|
109
113
|
if (sysChrome) return sysChrome;
|
|
110
114
|
const envPath = process.env.PUPPETEER_EXECUTABLE_PATH;
|