Browser skill (#485)
* Add `BrowserTool` * add some logging * go with npm install -g * remove dep changes since the switch to npm install -g * tweak * tweak * tweak * tweak * tweak timeout * tweak * remove logs * skill investigation doc * wip * wip * tweak * lock agent-browser version * tweak * logs * logs * more logs * more debug stuff * try this * try this * try this * fix PATH * try this * tweak * tweak * tweak * update wiki entries * update wiki once again * lint fix --------- Co-authored-by: Colin McDonnell <colinmcd94@gmail.com>
This commit is contained in:
committed by
pullfrog[bot]
parent
6b18b6730b
commit
c0f6f9ef2a
+35
-9
@@ -10,7 +10,7 @@
|
|||||||
* the agent process itself gets full env (needs LLM API keys, PATH, etc.).
|
* the agent process itself gets full env (needs LLM API keys, PATH, etc.).
|
||||||
* security is enforced at the tool layer, not the process layer.
|
* security is enforced at the tool layer, not the process layer.
|
||||||
*/
|
*/
|
||||||
import { execFileSync } from "node:child_process";
|
import { execFileSync, spawnSync } from "node:child_process";
|
||||||
import { mkdirSync } from "node:fs";
|
import { mkdirSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { performance } from "node:perf_hooks";
|
import { performance } from "node:perf_hooks";
|
||||||
@@ -21,16 +21,14 @@ import { log } from "../utils/cli.ts";
|
|||||||
import { installFromNpmTarball } from "../utils/install.ts";
|
import { installFromNpmTarball } from "../utils/install.ts";
|
||||||
import { spawn } from "../utils/subprocess.ts";
|
import { spawn } from "../utils/subprocess.ts";
|
||||||
import { ThinkingTimer } from "../utils/timer.ts";
|
import { ThinkingTimer } from "../utils/timer.ts";
|
||||||
|
import { getDevDependencyVersion } from "../utils/version.ts";
|
||||||
import type { TodoTracker } from "../utils/todoTracking.ts";
|
import type { TodoTracker } from "../utils/todoTracking.ts";
|
||||||
import { type AgentResult, type AgentRunContext, type AgentUsage, agent } from "./shared.ts";
|
import { type AgentResult, type AgentRunContext, type AgentUsage, agent } from "./shared.ts";
|
||||||
|
|
||||||
// pinned CLI version
|
|
||||||
const OPENCODE_CLI_VERSION = "1.1.56";
|
|
||||||
|
|
||||||
async function installOpencodeCli(): Promise<string> {
|
async function installOpencodeCli(): Promise<string> {
|
||||||
return await installFromNpmTarball({
|
return await installFromNpmTarball({
|
||||||
packageName: "opencode-ai",
|
packageName: "opencode-ai",
|
||||||
version: OPENCODE_CLI_VERSION,
|
version: getDevDependencyVersion("opencode-ai"),
|
||||||
executablePath: "bin/opencode",
|
executablePath: "bin/opencode",
|
||||||
installDependencies: true,
|
installDependencies: true,
|
||||||
});
|
});
|
||||||
@@ -55,6 +53,7 @@ function buildSecurityConfig(ctx: AgentRunContext, model: string | undefined): s
|
|||||||
read: "allow",
|
read: "allow",
|
||||||
webfetch: "allow",
|
webfetch: "allow",
|
||||||
external_directory: "deny",
|
external_directory: "deny",
|
||||||
|
skill: "allow",
|
||||||
},
|
},
|
||||||
mcp: {
|
mcp: {
|
||||||
[ghPullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl },
|
[ghPullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl },
|
||||||
@@ -175,6 +174,23 @@ function detectProviderError(text: string): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addSkill(params: { ref: string; skill: string; env: Record<string, string> }): void {
|
||||||
|
const result = spawnSync(
|
||||||
|
"npx",
|
||||||
|
["skills", "add", params.ref, "--skill", params.skill, "-g", "-a", "opencode", "-y"],
|
||||||
|
{
|
||||||
|
env: { ...process.env, ...params.env },
|
||||||
|
stdio: "pipe",
|
||||||
|
timeout: 30_000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (result.status === 0) {
|
||||||
|
log.info(`installed ${params.skill} skill`);
|
||||||
|
} else {
|
||||||
|
log.info(`${params.skill} skill install failed: ${(result.stderr?.toString() || "").trim()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── NDJSON event types ─────────────────────────────────────────────────────────
|
// ── NDJSON event types ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
interface OpenCodeInitEvent {
|
interface OpenCodeInitEvent {
|
||||||
@@ -652,8 +668,19 @@ export const opentoad = agent({
|
|||||||
modelSlug: ctx.payload.model,
|
modelSlug: ctx.payload.model,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tempHome = ctx.tmpdir;
|
const homeEnv = {
|
||||||
mkdirSync(join(tempHome, ".config", "opencode"), { recursive: true });
|
HOME: ctx.tmpdir,
|
||||||
|
XDG_CONFIG_HOME: join(ctx.tmpdir, ".config"),
|
||||||
|
};
|
||||||
|
|
||||||
|
mkdirSync(join(homeEnv.XDG_CONFIG_HOME, "opencode"), { recursive: true });
|
||||||
|
|
||||||
|
const agentBrowserVersion = getDevDependencyVersion("agent-browser");
|
||||||
|
addSkill({
|
||||||
|
ref: `vercel-labs/agent-browser@v${agentBrowserVersion}`,
|
||||||
|
skill: "agent-browser",
|
||||||
|
env: homeEnv,
|
||||||
|
});
|
||||||
|
|
||||||
const args = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
|
const args = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
|
||||||
|
|
||||||
@@ -661,8 +688,7 @@ export const opentoad = agent({
|
|||||||
// security is enforced via OPENCODE_CONFIG_CONTENT (bash: deny) and MCP tool filtering.
|
// security is enforced via OPENCODE_CONFIG_CONTENT (bash: deny) and MCP tool filtering.
|
||||||
const env: Record<string, string | undefined> = {
|
const env: Record<string, string | undefined> = {
|
||||||
...process.env,
|
...process.env,
|
||||||
HOME: tempHome,
|
...homeEnv,
|
||||||
XDG_CONFIG_HOME: join(tempHome, ".config"),
|
|
||||||
OPENCODE_CONFIG_CONTENT: buildSecurityConfig(ctx, model),
|
OPENCODE_CONFIG_CONTENT: buildSecurityConfig(ctx, model),
|
||||||
GOOGLE_GENERATIVE_AI_API_KEY:
|
GOOGLE_GENERATIVE_AI_API_KEY:
|
||||||
process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY,
|
process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY,
|
||||||
|
|||||||
@@ -25743,6 +25743,7 @@ async function apiFetch(options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// utils/retry.ts
|
// utils/retry.ts
|
||||||
|
import { setTimeout as sleep } from "node:timers/promises";
|
||||||
var defaultShouldRetry = (error2) => {
|
var defaultShouldRetry = (error2) => {
|
||||||
if (!(error2 instanceof Error)) return false;
|
if (!(error2 instanceof Error)) return false;
|
||||||
return error2.name === "AbortError" || error2.message.includes("fetch failed") || error2.message.includes("ECONNRESET") || error2.message.includes("ETIMEDOUT");
|
return error2.name === "AbortError" || error2.message.includes("fetch failed") || error2.message.includes("ECONNRESET") || error2.message.includes("ETIMEDOUT");
|
||||||
@@ -25763,7 +25764,7 @@ async function retry(fn, options = {}) {
|
|||||||
}
|
}
|
||||||
const delay = delayMs * attempt;
|
const delay = delayMs * attempt;
|
||||||
log.info(`\xBB ${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms...`);
|
log.info(`\xBB ${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms...`);
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
await sleep(delay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw lastError;
|
throw lastError;
|
||||||
|
|||||||
+7
-1
@@ -1,11 +1,13 @@
|
|||||||
// this must be imported first
|
// this must be imported first
|
||||||
import "./arkConfig.ts";
|
import "./arkConfig.ts";
|
||||||
import { createServer } from "node:net";
|
import { createServer } from "node:net";
|
||||||
|
import { setTimeout as sleep } from "node:timers/promises";
|
||||||
import { FastMCP, type Tool } from "fastmcp";
|
import { FastMCP, type Tool } from "fastmcp";
|
||||||
import type { AgentUsage } from "../agents/index.ts";
|
import type { AgentUsage } from "../agents/index.ts";
|
||||||
import { ghPullfrogMcpName } from "../external.ts";
|
import { ghPullfrogMcpName } from "../external.ts";
|
||||||
import type { Mode } from "../modes.ts";
|
import type { Mode } from "../modes.ts";
|
||||||
import type { PrepResult } from "../prep/index.ts";
|
import type { PrepResult } from "../prep/index.ts";
|
||||||
|
import { closeBrowserDaemon } from "../utils/browser.ts";
|
||||||
import { log } from "../utils/cli.ts";
|
import { log } from "../utils/cli.ts";
|
||||||
import type { OctokitWithPlugins } from "../utils/github.ts";
|
import type { OctokitWithPlugins } from "../utils/github.ts";
|
||||||
import type { ResolvedPayload } from "../utils/payload.ts";
|
import type { ResolvedPayload } from "../utils/payload.ts";
|
||||||
@@ -51,6 +53,8 @@ export type BackgroundProcess = {
|
|||||||
pidPath: string;
|
pidPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type BrowserDaemon = { binDir: string; error?: never } | { binDir?: never; error: string };
|
||||||
|
|
||||||
export type StoredPushDest = {
|
export type StoredPushDest = {
|
||||||
remoteName: string;
|
remoteName: string;
|
||||||
remoteBranch: string;
|
remoteBranch: string;
|
||||||
@@ -70,6 +74,7 @@ export interface ToolState {
|
|||||||
checkoutSha?: string;
|
checkoutSha?: string;
|
||||||
selectedMode?: string;
|
selectedMode?: string;
|
||||||
backgroundProcesses: Map<string, BackgroundProcess>;
|
backgroundProcesses: Map<string, BackgroundProcess>;
|
||||||
|
browserDaemon?: BrowserDaemon | undefined;
|
||||||
review?: {
|
review?: {
|
||||||
id: number;
|
id: number;
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
@@ -319,7 +324,7 @@ async function killBackgroundProcesses(toolState: ToolState): Promise<void> {
|
|||||||
// already dead
|
// already dead
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
await sleep(200);
|
||||||
for (const proc of backgroundProcesses.values()) {
|
for (const proc of backgroundProcesses.values()) {
|
||||||
try {
|
try {
|
||||||
process.kill(-proc.pid, "SIGKILL");
|
process.kill(-proc.pid, "SIGKILL");
|
||||||
@@ -347,6 +352,7 @@ export async function startMcpHttpServer(
|
|||||||
return {
|
return {
|
||||||
url: startResult.url,
|
url: startResult.url,
|
||||||
[Symbol.asyncDispose]: async () => {
|
[Symbol.asyncDispose]: async () => {
|
||||||
|
closeBrowserDaemon(ctx.toolState);
|
||||||
await killBackgroundProcesses(ctx.toolState);
|
await killBackgroundProcesses(ctx.toolState);
|
||||||
await startResult.server.stop();
|
await startResult.server.stop();
|
||||||
},
|
},
|
||||||
|
|||||||
+24
-2
@@ -4,7 +4,9 @@ import { randomUUID } from "node:crypto";
|
|||||||
import { closeSync, openSync, writeFileSync } from "node:fs";
|
import { closeSync, openSync, writeFileSync } from "node:fs";
|
||||||
import { userInfo } from "node:os";
|
import { userInfo } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
import { setTimeout as sleep } from "node:timers/promises";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
|
import { ensureBrowserDaemon } from "../utils/browser.ts";
|
||||||
import { log } from "../utils/log.ts";
|
import { log } from "../utils/log.ts";
|
||||||
import { resolveEnv } from "../utils/secrets.ts";
|
import { resolveEnv } from "../utils/secrets.ts";
|
||||||
import type { ToolContext } from "./server.ts";
|
import type { ToolContext } from "./server.ts";
|
||||||
@@ -115,7 +117,12 @@ function spawnShell(params: SpawnParams): ChildProcess {
|
|||||||
// sudo is only needed for unshare; the actual command should run as the normal user
|
// sudo is only needed for unshare; the actual command should run as the normal user
|
||||||
// to avoid ownership mismatches with files created by the Node.js parent process.
|
// to avoid ownership mismatches with files created by the Node.js parent process.
|
||||||
const username = userInfo().username;
|
const username = userInfo().username;
|
||||||
const escaped = params.command.replace(/'/g, "'\\''");
|
// su -p resets PATH on many Linux systems (ALWAYS_SET_PATH in /etc/login.defs).
|
||||||
|
// restore it from the SANDBOX_PATH env var that survives the su transition.
|
||||||
|
// biome-ignore lint/suspicious/noTemplateCurlyInString: we need to restore the PATH variable
|
||||||
|
const pathRestore = 'export PATH="${SANDBOX_PATH:-$PATH}"; ';
|
||||||
|
const escaped = (pathRestore + params.command).replace(/'/g, "'\\''");
|
||||||
|
envArgs.push(`SANDBOX_PATH=${params.env.PATH ?? ""}`);
|
||||||
return spawn(
|
return spawn(
|
||||||
"sudo",
|
"sudo",
|
||||||
[
|
[
|
||||||
@@ -195,6 +202,21 @@ Do NOT use this tool for git commands — use the dedicated git tools instead.`,
|
|||||||
const cwd = params.working_directory ?? process.cwd();
|
const cwd = params.working_directory ?? process.cwd();
|
||||||
const env = resolveEnv(ctx.payload.shell === "enabled" ? "inherit" : "restricted");
|
const env = resolveEnv(ctx.payload.shell === "enabled" ? "inherit" : "restricted");
|
||||||
|
|
||||||
|
if (params.command.includes("agent-browser")) {
|
||||||
|
const daemonError = ensureBrowserDaemon(ctx.toolState);
|
||||||
|
if (daemonError) {
|
||||||
|
return {
|
||||||
|
output: `browser daemon unavailable: ${daemonError}`,
|
||||||
|
exit_code: 1,
|
||||||
|
timed_out: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const binDir = ctx.toolState.browserDaemon?.binDir;
|
||||||
|
if (binDir) {
|
||||||
|
env.PATH = `${binDir}:${env.PATH ?? ""}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (params.background) {
|
if (params.background) {
|
||||||
const tempDir = getTempDir();
|
const tempDir = getTempDir();
|
||||||
const handle = `bg-${randomUUID().slice(0, 8)}`;
|
const handle = `bg-${randomUUID().slice(0, 8)}`;
|
||||||
@@ -305,7 +327,7 @@ export function KillBackgroundTool(ctx: ToolContext) {
|
|||||||
} catch {
|
} catch {
|
||||||
// already dead
|
// already dead
|
||||||
}
|
}
|
||||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
await sleep(200);
|
||||||
try {
|
try {
|
||||||
process.kill(-proc.pid, "SIGKILL");
|
process.kill(-proc.pid, "SIGKILL");
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
+2
-1
@@ -31,7 +31,6 @@
|
|||||||
"@octokit/plugin-throttling": "^11.0.3",
|
"@octokit/plugin-throttling": "^11.0.3",
|
||||||
"@octokit/rest": "^22.0.0",
|
"@octokit/rest": "^22.0.0",
|
||||||
"@octokit/webhooks-types": "^7.6.1",
|
"@octokit/webhooks-types": "^7.6.1",
|
||||||
"@opencode-ai/sdk": "^1.0.143",
|
|
||||||
"@standard-schema/spec": "1.1.0",
|
"@standard-schema/spec": "1.1.0",
|
||||||
"@toon-format/toon": "^1.0.0",
|
"@toon-format/toon": "^1.0.0",
|
||||||
"ajv": "^8.18.0",
|
"ajv": "^8.18.0",
|
||||||
@@ -47,6 +46,7 @@
|
|||||||
"turndown": "^7.2.0"
|
"turndown": "^7.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"agent-browser": "0.21.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||||
"@types/node": "^24.7.2",
|
"@types/node": "^24.7.2",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
@@ -54,6 +54,7 @@
|
|||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
"esbuild": "^0.25.9",
|
"esbuild": "^0.25.9",
|
||||||
"husky": "^9.0.0",
|
"husky": "^9.0.0",
|
||||||
|
"opencode-ai": "1.1.56",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vitest": "^4.0.17",
|
"vitest": "^4.0.17",
|
||||||
"yaml": "^2.8.2"
|
"yaml": "^2.8.2"
|
||||||
|
|||||||
Generated
+133
-15
@@ -26,9 +26,6 @@ importers:
|
|||||||
'@octokit/webhooks-types':
|
'@octokit/webhooks-types':
|
||||||
specifier: ^7.6.1
|
specifier: ^7.6.1
|
||||||
version: 7.6.1
|
version: 7.6.1
|
||||||
'@opencode-ai/sdk':
|
|
||||||
specifier: ^1.0.143
|
|
||||||
version: 1.0.143
|
|
||||||
'@standard-schema/spec':
|
'@standard-schema/spec':
|
||||||
specifier: 1.1.0
|
specifier: 1.1.0
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
@@ -81,6 +78,9 @@ importers:
|
|||||||
'@types/turndown':
|
'@types/turndown':
|
||||||
specifier: ^5.0.5
|
specifier: ^5.0.5
|
||||||
version: 5.0.6
|
version: 5.0.6
|
||||||
|
agent-browser:
|
||||||
|
specifier: 0.21.0
|
||||||
|
version: 0.21.0
|
||||||
arg:
|
arg:
|
||||||
specifier: ^5.0.2
|
specifier: ^5.0.2
|
||||||
version: 5.0.2
|
version: 5.0.2
|
||||||
@@ -90,12 +90,15 @@ importers:
|
|||||||
husky:
|
husky:
|
||||||
specifier: ^9.0.0
|
specifier: ^9.0.0
|
||||||
version: 9.1.7
|
version: 9.1.7
|
||||||
|
opencode-ai:
|
||||||
|
specifier: 1.1.56
|
||||||
|
version: 1.1.56
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.9.3
|
specifier: ^5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^4.0.17
|
specifier: ^4.0.17
|
||||||
version: 4.0.17(@types/node@24.7.2)(yaml@2.8.2)
|
version: 4.0.17(@types/node@24.7.2)(jiti@2.6.1)(yaml@2.8.2)
|
||||||
yaml:
|
yaml:
|
||||||
specifier: ^2.8.2
|
specifier: ^2.8.2
|
||||||
version: 2.8.2
|
version: 2.8.2
|
||||||
@@ -547,9 +550,6 @@ packages:
|
|||||||
'@octokit/webhooks-types@7.6.1':
|
'@octokit/webhooks-types@7.6.1':
|
||||||
resolution: {integrity: sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw==}
|
resolution: {integrity: sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw==}
|
||||||
|
|
||||||
'@opencode-ai/sdk@1.0.143':
|
|
||||||
resolution: {integrity: sha512-dtmkBfJ7IIAHzL6KCzAlwc9GybfJONVeCsF6ePYySpkuhslDbRkZBJYb5vqGd1H5zdsgjc6JjuvmOf0rPWUL6A==}
|
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.55.1':
|
'@rollup/rollup-android-arm-eabi@4.55.1':
|
||||||
resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==}
|
resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
@@ -753,6 +753,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
agent-browser@0.21.0:
|
||||||
|
resolution: {integrity: sha512-isVHEeb2WL5hLhr4o+zNmcYwmBrldxvrH+FIoRoUmDxyrHr3bhIS6L8BlUMHqT77YtkPq0YSmwoBRrwqeouw9Q==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
ajv-formats@3.0.1:
|
ajv-formats@3.0.1:
|
||||||
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
|
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1209,6 +1213,10 @@ packages:
|
|||||||
isexe@2.0.0:
|
isexe@2.0.0:
|
||||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
|
jiti@2.6.1:
|
||||||
|
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
jose@6.1.3:
|
jose@6.1.3:
|
||||||
resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==}
|
resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==}
|
||||||
|
|
||||||
@@ -1316,6 +1324,65 @@ packages:
|
|||||||
once@1.4.0:
|
once@1.4.0:
|
||||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
|
|
||||||
|
opencode-ai@1.1.56:
|
||||||
|
resolution: {integrity: sha512-OAF0G/1jVXpOrCh++M5gFEZ0bRLiXOnbxSYMFx5TOoD0OhCjHJS1JlARzaLDAx461qBnn+jocI9BBxD0wwFH3Q==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
opencode-darwin-arm64@1.1.56:
|
||||||
|
resolution: {integrity: sha512-0HqvLm7tcYZr4VJgEzi3Wicia5M9yCX66O7Cv470Qu4+GGbCC2sTTmzQu6pCehSleYgkZsiSBNRcFcW/6F7v0Q==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
opencode-darwin-x64-baseline@1.1.56:
|
||||||
|
resolution: {integrity: sha512-Z9QO9cTC9TnlUxTfEtDbvZbRFxc5Je8rs2Ei0cLH46W9gHGhh1fW4oX64tcrlhF5NUDKJzr/qwdoMfmKkeu53A==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
opencode-darwin-x64@1.1.56:
|
||||||
|
resolution: {integrity: sha512-8ZhNd4sFbDviT1OJs42C35Bx2/z6mkcb7uNhPHX3KKm27KOm7cYjFQa1UTF5or3ZpjkjPQpR/cpo7TELvnnmRA==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
opencode-linux-arm64-musl@1.1.56:
|
||||||
|
resolution: {integrity: sha512-f30SmYX4xE2fUsnNl66dDX++8iTMI9PLXz7BRHhgXL4XAdMpUUJbOKIsw3ZUB0KWHwjR8cjkEJQOZxR3HPbrZg==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
opencode-linux-arm64@1.1.56:
|
||||||
|
resolution: {integrity: sha512-wEmIEXiEKghurQYgKY5yFUFOmnT/QIlAbYnVeH73gSqOwlYFUUlowXLDJnF+3OGd6m000qalhCxjExL7qKINWw==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
opencode-linux-x64-baseline-musl@1.1.56:
|
||||||
|
resolution: {integrity: sha512-EC8EGTJCgTZgGIMZdQjOKGXw02+igo0am1Ry6wrPrB0li7XBlYbY6dz8tL3FlknRVMLamtGtb3tXQ6tqbwjX0g==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
opencode-linux-x64-baseline@1.1.56:
|
||||||
|
resolution: {integrity: sha512-jKOvhkNLcn0h6zmKX+hDdkODijSVDxjAGTZp8BPbJCCJVxnKll9dYfDMTqxi3YN6yXp6sBvyaJT1mcpf8knkeA==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
opencode-linux-x64-musl@1.1.56:
|
||||||
|
resolution: {integrity: sha512-9BFYBPgpY2RrUd7/Ul0VNh+6B9l0FmCNRUV4yYmeFGee9ZDgPem5YGicsKAMQqS/5X0jW3ZWje+KBbBQz4RMwQ==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
opencode-linux-x64@1.1.56:
|
||||||
|
resolution: {integrity: sha512-0FP3BzLjn+a9naTtm9hpHibJ+eV1RkI+tItcDQXKwHtvzoBUvytDRP8v6TotEHNtNF0ZLAb3OGurwDIIdtLarg==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
opencode-windows-x64-baseline@1.1.56:
|
||||||
|
resolution: {integrity: sha512-zEokdohfoDjWzwULvlSq0Y/tRoVmz2/6GofYO936buPQf09cBcPqpAgLUjZV+pQI0Atyd05YG3BCO4PQxmXzdw==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
opencode-windows-x64@1.1.56:
|
||||||
|
resolution: {integrity: sha512-zm/oaWT5uGrW3DumKRHiqv2L2pKwrTvhsT4XUSfIPLQn5EvrYNU6bh3WFO4v71ZBjSIKx5Q7rEvTEQvFiFEZFQ==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
package-manager-detector@1.6.0:
|
package-manager-detector@1.6.0:
|
||||||
resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
|
resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
|
||||||
|
|
||||||
@@ -2060,8 +2127,6 @@ snapshots:
|
|||||||
|
|
||||||
'@octokit/webhooks-types@7.6.1': {}
|
'@octokit/webhooks-types@7.6.1': {}
|
||||||
|
|
||||||
'@opencode-ai/sdk@1.0.143': {}
|
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.55.1':
|
'@rollup/rollup-android-arm-eabi@4.55.1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -2182,13 +2247,13 @@ snapshots:
|
|||||||
chai: 6.2.2
|
chai: 6.2.2
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.0.3
|
||||||
|
|
||||||
'@vitest/mocker@4.0.17(vite@7.3.1(@types/node@24.7.2)(yaml@2.8.2))':
|
'@vitest/mocker@4.0.17(vite@7.3.1(@types/node@24.7.2)(jiti@2.6.1)(yaml@2.8.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 4.0.17
|
'@vitest/spy': 4.0.17
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 7.3.1(@types/node@24.7.2)(yaml@2.8.2)
|
vite: 7.3.1(@types/node@24.7.2)(jiti@2.6.1)(yaml@2.8.2)
|
||||||
|
|
||||||
'@vitest/pretty-format@4.0.17':
|
'@vitest/pretty-format@4.0.17':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2222,6 +2287,8 @@ snapshots:
|
|||||||
mime-types: 3.0.2
|
mime-types: 3.0.2
|
||||||
negotiator: 1.0.0
|
negotiator: 1.0.0
|
||||||
|
|
||||||
|
agent-browser@0.21.0: {}
|
||||||
|
|
||||||
ajv-formats@3.0.1(ajv@8.17.1):
|
ajv-formats@3.0.1(ajv@8.17.1):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
ajv: 8.17.1
|
ajv: 8.17.1
|
||||||
@@ -2752,6 +2819,9 @@ snapshots:
|
|||||||
|
|
||||||
isexe@2.0.0: {}
|
isexe@2.0.0: {}
|
||||||
|
|
||||||
|
jiti@2.6.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
jose@6.1.3: {}
|
jose@6.1.3: {}
|
||||||
|
|
||||||
jose@6.2.0: {}
|
jose@6.2.0: {}
|
||||||
@@ -2855,6 +2925,53 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
wrappy: 1.0.2
|
wrappy: 1.0.2
|
||||||
|
|
||||||
|
opencode-ai@1.1.56:
|
||||||
|
optionalDependencies:
|
||||||
|
opencode-darwin-arm64: 1.1.56
|
||||||
|
opencode-darwin-x64: 1.1.56
|
||||||
|
opencode-darwin-x64-baseline: 1.1.56
|
||||||
|
opencode-linux-arm64: 1.1.56
|
||||||
|
opencode-linux-arm64-musl: 1.1.56
|
||||||
|
opencode-linux-x64: 1.1.56
|
||||||
|
opencode-linux-x64-baseline: 1.1.56
|
||||||
|
opencode-linux-x64-baseline-musl: 1.1.56
|
||||||
|
opencode-linux-x64-musl: 1.1.56
|
||||||
|
opencode-windows-x64: 1.1.56
|
||||||
|
opencode-windows-x64-baseline: 1.1.56
|
||||||
|
|
||||||
|
opencode-darwin-arm64@1.1.56:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
opencode-darwin-x64-baseline@1.1.56:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
opencode-darwin-x64@1.1.56:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
opencode-linux-arm64-musl@1.1.56:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
opencode-linux-arm64@1.1.56:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
opencode-linux-x64-baseline-musl@1.1.56:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
opencode-linux-x64-baseline@1.1.56:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
opencode-linux-x64-musl@1.1.56:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
opencode-linux-x64@1.1.56:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
opencode-windows-x64-baseline@1.1.56:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
opencode-windows-x64@1.1.56:
|
||||||
|
optional: true
|
||||||
|
|
||||||
package-manager-detector@1.6.0: {}
|
package-manager-detector@1.6.0: {}
|
||||||
|
|
||||||
parse-ms@4.0.0: {}
|
parse-ms@4.0.0: {}
|
||||||
@@ -3159,7 +3276,7 @@ snapshots:
|
|||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
vite@7.3.1(@types/node@24.7.2)(yaml@2.8.2):
|
vite@7.3.1(@types/node@24.7.2)(jiti@2.6.1)(yaml@2.8.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.2
|
esbuild: 0.27.2
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
@@ -3170,12 +3287,13 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 24.7.2
|
'@types/node': 24.7.2
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
jiti: 2.6.1
|
||||||
yaml: 2.8.2
|
yaml: 2.8.2
|
||||||
|
|
||||||
vitest@4.0.17(@types/node@24.7.2)(yaml@2.8.2):
|
vitest@4.0.17(@types/node@24.7.2)(jiti@2.6.1)(yaml@2.8.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 4.0.17
|
'@vitest/expect': 4.0.17
|
||||||
'@vitest/mocker': 4.0.17(vite@7.3.1(@types/node@24.7.2)(yaml@2.8.2))
|
'@vitest/mocker': 4.0.17(vite@7.3.1(@types/node@24.7.2)(jiti@2.6.1)(yaml@2.8.2))
|
||||||
'@vitest/pretty-format': 4.0.17
|
'@vitest/pretty-format': 4.0.17
|
||||||
'@vitest/runner': 4.0.17
|
'@vitest/runner': 4.0.17
|
||||||
'@vitest/snapshot': 4.0.17
|
'@vitest/snapshot': 4.0.17
|
||||||
@@ -3192,7 +3310,7 @@ snapshots:
|
|||||||
tinyexec: 1.0.2
|
tinyexec: 1.0.2
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.0.3
|
||||||
vite: 7.3.1(@types/node@24.7.2)(yaml@2.8.2)
|
vite: 7.3.1(@types/node@24.7.2)(jiti@2.6.1)(yaml@2.8.2)
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 24.7.2
|
'@types/node': 24.7.2
|
||||||
|
|||||||
@@ -41587,7 +41587,6 @@ var package_default = {
|
|||||||
"@octokit/plugin-throttling": "^11.0.3",
|
"@octokit/plugin-throttling": "^11.0.3",
|
||||||
"@octokit/rest": "^22.0.0",
|
"@octokit/rest": "^22.0.0",
|
||||||
"@octokit/webhooks-types": "^7.6.1",
|
"@octokit/webhooks-types": "^7.6.1",
|
||||||
"@opencode-ai/sdk": "^1.0.143",
|
|
||||||
"@standard-schema/spec": "1.1.0",
|
"@standard-schema/spec": "1.1.0",
|
||||||
"@toon-format/toon": "^1.0.0",
|
"@toon-format/toon": "^1.0.0",
|
||||||
ajv: "^8.18.0",
|
ajv: "^8.18.0",
|
||||||
@@ -41603,6 +41602,7 @@ var package_default = {
|
|||||||
turndown: "^7.2.0"
|
turndown: "^7.2.0"
|
||||||
},
|
},
|
||||||
devDependencies: {
|
devDependencies: {
|
||||||
|
"agent-browser": "0.21.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||||
"@types/node": "^24.7.2",
|
"@types/node": "^24.7.2",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
@@ -41610,6 +41610,7 @@ var package_default = {
|
|||||||
arg: "^5.0.2",
|
arg: "^5.0.2",
|
||||||
esbuild: "^0.25.9",
|
esbuild: "^0.25.9",
|
||||||
husky: "^9.0.0",
|
husky: "^9.0.0",
|
||||||
|
"opencode-ai": "1.1.56",
|
||||||
typescript: "^5.9.3",
|
typescript: "^5.9.3",
|
||||||
vitest: "^4.0.17",
|
vitest: "^4.0.17",
|
||||||
yaml: "^2.8.2"
|
yaml: "^2.8.2"
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { execFileSync, spawnSync } from "node:child_process";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import { dirname } from "node:path";
|
||||||
|
import type { ToolState } from "../mcp/server.ts";
|
||||||
|
import { log } from "./cli.ts";
|
||||||
|
import { filterEnv } from "./secrets.ts";
|
||||||
|
import { getDevDependencyVersion } from "./version.ts";
|
||||||
|
|
||||||
|
// agent-browser already discovers chrome via `which` and the playwright cache as fallbacks,
|
||||||
|
// so this list only needs to cover the GHA ubuntu-latest runner where we know the exact path.
|
||||||
|
const CHROME_PATHS = ["/usr/bin/google-chrome-stable"];
|
||||||
|
|
||||||
|
let systemChromePath: string | undefined;
|
||||||
|
|
||||||
|
function findSystemChromePath(): string | undefined {
|
||||||
|
if (typeof systemChromePath === "string") {
|
||||||
|
// return cached result but normalize to undefined if empty
|
||||||
|
return systemChromePath || undefined;
|
||||||
|
}
|
||||||
|
for (const p of CHROME_PATHS) {
|
||||||
|
if (existsSync(p)) {
|
||||||
|
systemChromePath = p;
|
||||||
|
log.info(`found system chrome: ${p}`);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// set to an empty string to indicate no system chrome found
|
||||||
|
// and to avoid repeated lookups
|
||||||
|
systemChromePath = "";
|
||||||
|
log.info(`no system chrome found (checked: ${CHROME_PATHS.join(", ")})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEnv(): Record<string, string> {
|
||||||
|
const env: Record<string, string> = { ...filterEnv() };
|
||||||
|
const chromePath = findSystemChromePath();
|
||||||
|
if (chromePath) {
|
||||||
|
env.AGENT_BROWSER_EXECUTABLE_PATH = chromePath;
|
||||||
|
}
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ensure the agent-browser daemon is running by issuing a real command.
|
||||||
|
*
|
||||||
|
* agent-browser is stateful — it manages a persistent browser process via a
|
||||||
|
* daemon that communicates over a Unix socket. we start the daemon here,
|
||||||
|
* outside of ShellTool, because ShellTool's child process lifecycle would
|
||||||
|
* kill it between invocations and the daemon must survive across calls.
|
||||||
|
*
|
||||||
|
* despite ShellTool commands running inside unshare-sandboxed namespaces,
|
||||||
|
* they can still reach this daemon because the Unix socket is discoverable
|
||||||
|
* regardless of unshare's PID/mount isolation. starting the daemon in the
|
||||||
|
* host namespace keeps it alive while sandboxed shells come and go.
|
||||||
|
*
|
||||||
|
* agent-browser auto-starts its daemon on the first CLI invocation and
|
||||||
|
* keeps it alive via the socket for subsequent commands.
|
||||||
|
* we run `open about:blank` as the seed command to trigger this.
|
||||||
|
* idempotent — only runs once.
|
||||||
|
*/
|
||||||
|
export function ensureBrowserDaemon(toolState: ToolState): string | undefined {
|
||||||
|
if (toolState.browserDaemon) {
|
||||||
|
return toolState.browserDaemon.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentBrowserVersion = getDevDependencyVersion("agent-browser");
|
||||||
|
log.info(`installing agent-browser@${agentBrowserVersion}...`);
|
||||||
|
const install = spawnSync("npm", ["install", "-g", `agent-browser@${agentBrowserVersion}`], {
|
||||||
|
stdio: "pipe",
|
||||||
|
encoding: "utf-8",
|
||||||
|
});
|
||||||
|
if (install.status !== 0) {
|
||||||
|
const error = `agent-browser install failed: ${(install.stderr || install.stdout || "unknown error").trim()}`;
|
||||||
|
log.error(error);
|
||||||
|
toolState.browserDaemon = { error };
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
log.info("agent-browser installed");
|
||||||
|
|
||||||
|
let binDir: string;
|
||||||
|
try {
|
||||||
|
const binPath = execFileSync("which", ["agent-browser"], { encoding: "utf-8" }).trim();
|
||||||
|
binDir = dirname(binPath);
|
||||||
|
log.info(`agent-browser binary: ${binPath}`);
|
||||||
|
} catch {
|
||||||
|
const error = "agent-browser binary not found in PATH after install";
|
||||||
|
log.error(error);
|
||||||
|
toolState.browserDaemon = { error };
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const env = buildEnv();
|
||||||
|
|
||||||
|
// `open about:blank` triggers daemon auto-start and returns once the daemon + browser are ready
|
||||||
|
log.info("starting browser daemon...");
|
||||||
|
const seed = spawnSync("agent-browser", ["open", "about:blank"], {
|
||||||
|
env,
|
||||||
|
stdio: "pipe",
|
||||||
|
encoding: "utf-8",
|
||||||
|
timeout: 30_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (seed.status !== 0) {
|
||||||
|
const output = (seed.stderr || seed.stdout || "unknown error").trim();
|
||||||
|
const error = `agent-browser open about:blank failed (exit=${seed.status}): ${output}`;
|
||||||
|
log.error(error);
|
||||||
|
toolState.browserDaemon = { error };
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
log.debug(`seed command done (exit=0): ${(seed.stdout || "").trim()}`);
|
||||||
|
|
||||||
|
toolState.browserDaemon = { binDir };
|
||||||
|
log.info("browser daemon ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeBrowserDaemon(toolState: ToolState): void {
|
||||||
|
if (!toolState.browserDaemon?.binDir) {
|
||||||
|
delete toolState.browserDaemon;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delete toolState.browserDaemon;
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info("closing browser daemon...");
|
||||||
|
spawnSync("agent-browser", ["close"], {
|
||||||
|
env: filterEnv(),
|
||||||
|
stdio: "pipe",
|
||||||
|
timeout: 10_000,
|
||||||
|
});
|
||||||
|
log.info("browser daemon closed");
|
||||||
|
} catch {
|
||||||
|
// best-effort
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -4,6 +4,7 @@ import { mkdtemp } from "node:fs/promises";
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { pipeline } from "node:stream/promises";
|
import { pipeline } from "node:stream/promises";
|
||||||
|
import { setTimeout as sleep } from "node:timers/promises";
|
||||||
import { log } from "./cli.ts";
|
import { log } from "./cli.ts";
|
||||||
|
|
||||||
export interface InstallFromNpmTarballParams {
|
export interface InstallFromNpmTarballParams {
|
||||||
@@ -172,7 +173,7 @@ async function fetchWithRetry(
|
|||||||
const waitSeconds = parseInt(retryAfter, 10);
|
const waitSeconds = parseInt(retryAfter, 10);
|
||||||
if (!Number.isNaN(waitSeconds) && waitSeconds > 0) {
|
if (!Number.isNaN(waitSeconds) && waitSeconds > 0) {
|
||||||
log.info(`» rate limited, waiting ${waitSeconds} seconds before retry...`);
|
log.info(`» rate limited, waiting ${waitSeconds} seconds before retry...`);
|
||||||
await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000));
|
await sleep(waitSeconds * 1000);
|
||||||
const retryResponse = await fetch(url, { headers });
|
const retryResponse = await fetch(url, { headers });
|
||||||
if (!retryResponse.ok) {
|
if (!retryResponse.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import { setTimeout as sleep } from "node:timers/promises";
|
||||||
import { log } from "./cli.ts";
|
import { log } from "./cli.ts";
|
||||||
|
|
||||||
export type RetryOptions = {
|
export type RetryOptions = {
|
||||||
@@ -38,7 +39,7 @@ export async function retry<T>(fn: () => Promise<T>, options: RetryOptions = {})
|
|||||||
|
|
||||||
const delay = delayMs * attempt;
|
const delay = delayMs * attempt;
|
||||||
log.info(`» ${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms...`);
|
log.info(`» ${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms...`);
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
await sleep(delay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import semver from "semver";
|
||||||
|
import packageJson from "../package.json" with { type: "json" };
|
||||||
|
|
||||||
|
export function getDevDependencyVersion(name: keyof typeof packageJson.devDependencies): string {
|
||||||
|
const version = packageJson.devDependencies[name];
|
||||||
|
if (!semver.valid(version)) {
|
||||||
|
throw new Error(`dev dependency "${name}" must be a pinned version, got "${version}"`);
|
||||||
|
}
|
||||||
|
return version;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user