jeopi-cli 16.4.2 → 16.4.4
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 +74 -0
- package/dist/cli.js +3033 -2991
- package/dist/types/advisor/advise-tool.d.ts +5 -1
- package/dist/types/advisor/config.d.ts +4 -3
- package/dist/types/advisor/runtime.d.ts +15 -0
- 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-registry.d.ts +3 -3
- package/dist/types/config/model-resolver.d.ts +7 -1
- package/dist/types/config/models-config.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +3 -13
- 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/rpc/host-tools.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +27 -7
- package/dist/types/modes/ultrathink.d.ts +1 -1
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +9 -0
- package/dist/types/modes/workflow.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +6 -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/advise-tool.ts +6 -1
- package/src/advisor/config.ts +11 -8
- package/src/advisor/runtime.ts +127 -0
- package/src/autolearn/controller.ts +15 -3
- 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/config-file.ts +30 -12
- package/src/config/model-registry.ts +7 -7
- package/src/config/model-resolver.ts +24 -6
- package/src/config/models-config.ts +1 -1
- package/src/config/settings-schema.ts +3 -15
- 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/legacy-pi-bundled-keys.ts +9 -1
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +30 -3
- 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/chat-transcript-builder.ts +31 -11
- 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 +58 -42
- 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/rpc/host-tools.ts +13 -0
- package/src/modes/rpc/rpc-mode.ts +130 -56
- package/src/modes/ultrathink.ts +6 -5
- package/src/modes/utils/transcript-render-helpers.ts +44 -0
- package/src/modes/utils/ui-helpers.ts +16 -1
- 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 +267 -36
- 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/task/render.ts +51 -44
- package/src/tools/bash-skill-urls.ts +7 -4
- package/src/tools/bash.ts +30 -33
- 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/path-utils.ts +6 -1
- 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 +72 -7
- package/src/web/search/index.ts +14 -6
- package/src/web/search/providers/perplexity.ts +18 -2
- package/dist/types/tools/bash-command-fixup.d.ts +0 -11
- package/src/tools/bash-command-fixup.ts +0 -37
package/src/tools/bash.ts
CHANGED
|
@@ -23,7 +23,6 @@ import { CachedOutputBlock, markFramedBlockComponent, outputBlockContentWidth }
|
|
|
23
23
|
import { getSixelLineMask } from "../utils/sixel";
|
|
24
24
|
import type { ToolSession } from ".";
|
|
25
25
|
import { truncateForPrompt } from "./approval";
|
|
26
|
-
import { applyBashFixups } from "./bash-command-fixup";
|
|
27
26
|
import { type BashInteractiveResult, runInteractiveBashPty } from "./bash-interactive";
|
|
28
27
|
import { checkBashInterception } from "./bash-interceptor";
|
|
29
28
|
import { canUseInteractiveBashPty } from "./bash-pty-selection";
|
|
@@ -163,10 +162,9 @@ type ManagedBashJobCompletion =
|
|
|
163
162
|
|
|
164
163
|
interface ManagedBashJobHandle {
|
|
165
164
|
jobId: string;
|
|
166
|
-
label: string;
|
|
167
165
|
completion: Promise<ManagedBashJobCompletion>;
|
|
168
166
|
getLatestText: () => string;
|
|
169
|
-
|
|
167
|
+
stopUpdates: () => void;
|
|
170
168
|
}
|
|
171
169
|
|
|
172
170
|
function normalizeResultOutput(result: BashResult | BashInteractiveResult): string {
|
|
@@ -297,6 +295,10 @@ function formatExitCodeNotice(exitCode: number): string {
|
|
|
297
295
|
return `Command exited with code ${exitCode}`;
|
|
298
296
|
}
|
|
299
297
|
|
|
298
|
+
function formatBackgroundNotice(jobId: string): string {
|
|
299
|
+
return `Backgrounded as job ${jobId}; result will be delivered automatically.`;
|
|
300
|
+
}
|
|
301
|
+
|
|
300
302
|
/**
|
|
301
303
|
* Strip the trailing occurrence of `notice` (plus a single surrounding newline
|
|
302
304
|
* on each side) so the TUI can echo the value via a styled footer label
|
|
@@ -325,6 +327,11 @@ function stripExitCodeNotice(text: string, exitCode: number | undefined): string
|
|
|
325
327
|
return stripTrailingNotice(text, formatExitCodeNotice(exitCode));
|
|
326
328
|
}
|
|
327
329
|
|
|
330
|
+
function stripBackgroundNotice(text: string, async: BashToolDetails["async"] | undefined): string {
|
|
331
|
+
if (async?.state !== "running") return text;
|
|
332
|
+
return stripTrailingNotice(text, formatBackgroundNotice(async.jobId));
|
|
333
|
+
}
|
|
334
|
+
|
|
328
335
|
/**
|
|
329
336
|
* Bash tool implementation.
|
|
330
337
|
*
|
|
@@ -473,7 +480,6 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
473
480
|
|
|
474
481
|
#buildBackgroundStartResult(
|
|
475
482
|
jobId: string,
|
|
476
|
-
label: string,
|
|
477
483
|
previewText: string,
|
|
478
484
|
timeoutSec: number,
|
|
479
485
|
options: { requestedTimeoutSec?: number; notices?: readonly string[] } = {},
|
|
@@ -493,11 +499,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
493
499
|
if (options.notices?.length) {
|
|
494
500
|
lines.push(...options.notices, "");
|
|
495
501
|
}
|
|
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
|
-
);
|
|
502
|
+
lines.push(formatBackgroundNotice(jobId));
|
|
501
503
|
return {
|
|
502
504
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
503
505
|
details,
|
|
@@ -518,7 +520,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
518
520
|
|
|
519
521
|
resolvedEnv?: Record<string, string>;
|
|
520
522
|
onUpdate?: AgentToolUpdateCallback<BashToolDetails>;
|
|
521
|
-
|
|
523
|
+
forwardUpdates: boolean;
|
|
522
524
|
}): ManagedBashJobHandle {
|
|
523
525
|
const manager = this.session.asyncJobManager;
|
|
524
526
|
if (!manager) {
|
|
@@ -527,7 +529,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
527
529
|
|
|
528
530
|
const label = options.command.length > 120 ? `${options.command.slice(0, 117)}...` : options.command;
|
|
529
531
|
let latestText = "";
|
|
530
|
-
let
|
|
532
|
+
let forwardUpdates = options.forwardUpdates;
|
|
531
533
|
const completion = Promise.withResolvers<ManagedBashJobCompletion>();
|
|
532
534
|
|
|
533
535
|
const jobId = manager.register(
|
|
@@ -583,11 +585,12 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
583
585
|
},
|
|
584
586
|
{
|
|
585
587
|
ownerId: this.session.getAgentId?.() ?? undefined,
|
|
586
|
-
onProgress: async
|
|
588
|
+
onProgress: async text => {
|
|
587
589
|
latestText = text;
|
|
590
|
+
if (!forwardUpdates) return;
|
|
588
591
|
await options.onUpdate?.({
|
|
589
592
|
content: [{ type: "text", text }],
|
|
590
|
-
details:
|
|
593
|
+
details: {},
|
|
591
594
|
});
|
|
592
595
|
},
|
|
593
596
|
},
|
|
@@ -595,11 +598,10 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
595
598
|
|
|
596
599
|
return {
|
|
597
600
|
jobId,
|
|
598
|
-
label,
|
|
599
601
|
completion: completion.promise,
|
|
600
602
|
getLatestText: () => latestText,
|
|
601
|
-
|
|
602
|
-
|
|
603
|
+
stopUpdates: () => {
|
|
604
|
+
forwardUpdates = false;
|
|
603
605
|
},
|
|
604
606
|
};
|
|
605
607
|
}
|
|
@@ -657,16 +659,6 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
657
659
|
let command = rawCommand;
|
|
658
660
|
const env = normalizeBashEnv(rawEnv);
|
|
659
661
|
|
|
660
|
-
// Apply conservative bash fixups (strip trailing `| head|tail` and redundant
|
|
661
|
-
// `2>&1`). The helper is single-line only and refuses anything that could
|
|
662
|
-
// change semantics.
|
|
663
|
-
if (this.session.settings.get("bash.stripTrailingHeadTail")) {
|
|
664
|
-
const fixup = applyBashFixups(command);
|
|
665
|
-
if (fixup.stripped.length > 0) {
|
|
666
|
-
command = fixup.command;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
662
|
// Extract leading `cd <path> && ...` into cwd when the model ignores the cwd parameter.
|
|
671
663
|
// Constrained to a single line so a `&&` that sits on a later line of a multiline
|
|
672
664
|
// script can't pull the entire script into the "cwd" capture.
|
|
@@ -701,6 +693,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
701
693
|
const internalUrlOptions: InternalUrlExpansionOptions = {
|
|
702
694
|
skills: this.session.skills ?? [],
|
|
703
695
|
internalRouter: InternalUrlRouter.instance(),
|
|
696
|
+
cwd: this.session.cwd,
|
|
704
697
|
localOptions: {
|
|
705
698
|
getArtifactsDir: this.session.getArtifactsDir,
|
|
706
699
|
getSessionId: this.session.getSessionId,
|
|
@@ -769,9 +762,9 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
769
762
|
|
|
770
763
|
resolvedEnv,
|
|
771
764
|
onUpdate,
|
|
772
|
-
|
|
765
|
+
forwardUpdates: false,
|
|
773
766
|
});
|
|
774
|
-
return this.#buildBackgroundStartResult(job.jobId,
|
|
767
|
+
return this.#buildBackgroundStartResult(job.jobId, "", timeoutSec, {
|
|
775
768
|
requestedTimeoutSec,
|
|
776
769
|
notices: pendingNotices,
|
|
777
770
|
});
|
|
@@ -807,10 +800,10 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
807
800
|
|
|
808
801
|
resolvedEnv,
|
|
809
802
|
onUpdate,
|
|
810
|
-
startBackgrounded,
|
|
803
|
+
forwardUpdates: !startBackgrounded,
|
|
811
804
|
});
|
|
812
805
|
if (startBackgrounded) {
|
|
813
|
-
return this.#buildBackgroundStartResult(job.jobId,
|
|
806
|
+
return this.#buildBackgroundStartResult(job.jobId, "", timeoutSec, {
|
|
814
807
|
requestedTimeoutSec,
|
|
815
808
|
notices: pendingNotices,
|
|
816
809
|
});
|
|
@@ -830,9 +823,9 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
830
823
|
autoBgManager.cancel(job.jobId);
|
|
831
824
|
throw new ToolAbortError(job.getLatestText() || "Command aborted");
|
|
832
825
|
}
|
|
833
|
-
job.
|
|
826
|
+
job.stopUpdates();
|
|
834
827
|
autoBgManager.resumeDeliveries([job.jobId]);
|
|
835
|
-
return this.#buildBackgroundStartResult(job.jobId, job.
|
|
828
|
+
return this.#buildBackgroundStartResult(job.jobId, job.getLatestText(), timeoutSec, {
|
|
836
829
|
requestedTimeoutSec,
|
|
837
830
|
notices: pendingNotices,
|
|
838
831
|
});
|
|
@@ -1249,7 +1242,8 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1249
1242
|
) {
|
|
1250
1243
|
return cachedLines;
|
|
1251
1244
|
}
|
|
1252
|
-
const
|
|
1245
|
+
const withoutBackground = stripBackgroundNotice(rawOutput, details?.async);
|
|
1246
|
+
const strippedOutput = stripOutputNotice(withoutBackground, details?.meta);
|
|
1253
1247
|
const withoutExit = stripExitCodeNotice(strippedOutput, details?.exitCode);
|
|
1254
1248
|
const withoutWall = stripWallTimeNotice(withoutExit, details?.wallTimeMs);
|
|
1255
1249
|
const rawOutputArtifact = stripRawOutputArtifactNotice(withoutWall);
|
|
@@ -1262,6 +1256,9 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1262
1256
|
const requestedTimeoutSeconds = details?.requestedTimeoutSeconds;
|
|
1263
1257
|
const wallTimeMs = details?.wallTimeMs;
|
|
1264
1258
|
const statsParts: string[] = [];
|
|
1259
|
+
if (details?.async?.state === "running") {
|
|
1260
|
+
statsParts.push(`Backgrounded: ${details.async.jobId}`);
|
|
1261
|
+
}
|
|
1265
1262
|
if (wallTimeMs !== undefined) {
|
|
1266
1263
|
statsParts.push(`Wall: ${formatWallTimeSeconds(wallTimeMs)}s`);
|
|
1267
1264
|
}
|
|
@@ -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;
|
|
@@ -1,11 +1,50 @@
|
|
|
1
1
|
import { untilAborted } from "jeopi-utils";
|
|
2
|
-
import { throwIfAborted } from "../tool-errors";
|
|
2
|
+
import { ToolError, throwIfAborted } from "../tool-errors";
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
/** Options for the predicate form of the run-scoped `wait()` helper. */
|
|
5
|
+
export interface WaitPredicateOptions {
|
|
6
|
+
/** Max time to poll before failing, in ms (default 30_000). */
|
|
7
|
+
timeout?: number;
|
|
8
|
+
/** Poll interval in ms (default 100, floor 10). */
|
|
9
|
+
interval?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Run-scoped `wait()` helper for evaluated browser code, honoring the owning run's
|
|
14
|
+
* cancellation signal.
|
|
15
|
+
*
|
|
16
|
+
* - `wait(ms)` sleeps for `ms` milliseconds.
|
|
17
|
+
* - `wait(fn, { timeout?, interval? })` polls `fn` (sync or async) until it returns a
|
|
18
|
+
* truthy value and resolves with that value; throws a named `ToolError` on timeout
|
|
19
|
+
* instead of stalling into the whole-cell deadline. Predicate errors propagate.
|
|
20
|
+
*/
|
|
21
|
+
export async function waitForBrowserRun(
|
|
22
|
+
msOrPredicate: number | (() => unknown),
|
|
23
|
+
signal: AbortSignal,
|
|
24
|
+
opts?: WaitPredicateOptions,
|
|
25
|
+
): Promise<unknown> {
|
|
8
26
|
throwIfAborted(signal);
|
|
27
|
+
if (typeof msOrPredicate === "number") {
|
|
28
|
+
await untilAborted(signal, async () => await Bun.sleep(msOrPredicate));
|
|
29
|
+
throwIfAborted(signal);
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
if (typeof msOrPredicate !== "function") {
|
|
33
|
+
throw new ToolError("wait(...) expects milliseconds (number) or a predicate function to poll");
|
|
34
|
+
}
|
|
35
|
+
const timeout =
|
|
36
|
+
opts?.timeout !== undefined && Number.isFinite(opts.timeout) && opts.timeout > 0 ? opts.timeout : 30_000;
|
|
37
|
+
const interval = Math.max(opts?.interval ?? 100, 10);
|
|
38
|
+
const deadline = Date.now() + timeout;
|
|
39
|
+
for (;;) {
|
|
40
|
+
const value = await untilAborted(signal, async () => await msOrPredicate());
|
|
41
|
+
throwIfAborted(signal);
|
|
42
|
+
if (value) return value;
|
|
43
|
+
if (Date.now() + interval > deadline) {
|
|
44
|
+
throw new ToolError(`wait(predicate) timed out after ${timeout}ms — predicate never returned truthy`);
|
|
45
|
+
}
|
|
46
|
+
await untilAborted(signal, async () => await Bun.sleep(interval));
|
|
47
|
+
}
|
|
9
48
|
}
|
|
10
49
|
|
|
11
50
|
/** Binds a long-lived browser facade to one evaluated run's abort signal. */
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ImageContent, TextContent } from "jeopi-ai";
|
|
2
|
+
import type { JsDisplayOutput } from "../../eval/js/shared/types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Accumulates a browser run's result entries: explicit `display()` payloads,
|
|
6
|
+
* screenshot captions/images, and buffered stream text (`console.*`, `print`,
|
|
7
|
+
* `display()` of strings/primitives — `JsRuntime.displayValue` emits those via
|
|
8
|
+
* `onText`). Stream text is buffered and flushed as one entry before the next
|
|
9
|
+
* display/screenshot (and on `finish()`) so it reaches the tool result in
|
|
10
|
+
* order instead of vanishing into the debug log.
|
|
11
|
+
*/
|
|
12
|
+
export class RunOutput {
|
|
13
|
+
readonly #displays: Array<TextContent | ImageContent> = [];
|
|
14
|
+
#textBuffer = "";
|
|
15
|
+
|
|
16
|
+
/** Buffer a stream-text chunk; it joins the entries at the next push or on finish(). */
|
|
17
|
+
pushText(chunk: string): void {
|
|
18
|
+
this.#textBuffer += chunk;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Append a `display()` payload (image/json/status), flushing buffered text first. */
|
|
22
|
+
pushDisplay(output: JsDisplayOutput): void {
|
|
23
|
+
if (output.type === "image") {
|
|
24
|
+
this.push({ type: "image", data: output.data, mimeType: output.mimeType });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (output.type === "json") {
|
|
28
|
+
this.push({ type: "text", text: safeJsonStringify(output.data) });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// status — surface as compact JSON so helper side effects (read/write/env) appear in
|
|
32
|
+
// the cell result alongside explicit display() output.
|
|
33
|
+
this.push({ type: "text", text: safeJsonStringify(output.event) });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Append a pre-built entry (e.g. a screenshot caption/image), flushing buffered text first. */
|
|
37
|
+
push(entry: TextContent | ImageContent): void {
|
|
38
|
+
this.#flush();
|
|
39
|
+
this.#displays.push(entry);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Flush any remaining stream text and return the ordered entries. */
|
|
43
|
+
finish(): Array<TextContent | ImageContent> {
|
|
44
|
+
this.#flush();
|
|
45
|
+
return this.#displays;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#flush(): void {
|
|
49
|
+
if (!this.#textBuffer) return;
|
|
50
|
+
// Entries are newline-joined at render; drop the stream's trailing newline.
|
|
51
|
+
this.#displays.push({ type: "text", text: this.#textBuffer.replace(/\n$/, "") });
|
|
52
|
+
this.#textBuffer = "";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** JSON.stringify that never throws (cycles/BigInt → String(value)). */
|
|
57
|
+
export function safeJsonStringify(value: unknown): string {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.stringify(value, null, 2);
|
|
60
|
+
} catch {
|
|
61
|
+
return String(value);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Pass a return value across the run boundary: structured-cloneable as-is, else JSON round-trip, else String. */
|
|
66
|
+
export function cloneSafe(value: unknown): unknown {
|
|
67
|
+
if (value === undefined) return undefined;
|
|
68
|
+
try {
|
|
69
|
+
structuredClone(value);
|
|
70
|
+
return value;
|
|
71
|
+
} catch {}
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(JSON.stringify(value)) as unknown;
|
|
74
|
+
} catch {}
|
|
75
|
+
return String(value);
|
|
76
|
+
}
|