Dev #11
@@ -144,6 +144,8 @@ export async function prepareCommandManagedRuntime(input: {
|
||||
preserveAbsentOnRestore?: string[];
|
||||
assets?: CommandManagedRuntimeAsset[];
|
||||
installCommand?: string | null;
|
||||
/** When provided alongside `installCommand`, skip the install if `command -v <detectCommand>` succeeds. */
|
||||
detectCommand?: string | null;
|
||||
}): Promise<PreparedSandboxManagedRuntime> {
|
||||
const timeoutMs = input.spec.timeoutMs && input.spec.timeoutMs > 0 ? input.spec.timeoutMs : 300_000;
|
||||
const workspaceRemoteDir = input.workspaceRemoteDir ?? input.spec.remoteCwd;
|
||||
@@ -164,13 +166,52 @@ export async function prepareCommandManagedRuntime(input: {
|
||||
const shellCommand = preferredShellForSandbox(input.spec.shellCommand);
|
||||
|
||||
if (input.installCommand?.trim()) {
|
||||
const installCommand = input.installCommand.trim();
|
||||
const detectCommand = input.detectCommand?.trim();
|
||||
// Skip the install when the binary is already on PATH. Without this
|
||||
// probe the install runs unconditionally on every execute() call (and
|
||||
// also runs a second time after `ensureAdapterExecutionTargetCommandResolvable`
|
||||
// has already installed it during the resolvability gate).
|
||||
if (detectCommand) {
|
||||
const probe = await input.runner.execute({
|
||||
command: shellCommand,
|
||||
args: ["-lc", `command -v ${shellQuote(detectCommand)} >/dev/null 2>&1`],
|
||||
cwd: workspaceRemoteDir,
|
||||
timeoutMs,
|
||||
});
|
||||
if (!probe.timedOut && (probe.exitCode ?? 1) === 0) {
|
||||
return await prepareSandboxManagedRuntime({
|
||||
spec: runtimeSpec,
|
||||
client,
|
||||
adapterKey: input.adapterKey,
|
||||
workspaceLocalDir: input.workspaceLocalDir,
|
||||
workspaceRemoteDir,
|
||||
workspaceExclude: mergeRuntimeExcludes(input.workspaceExclude),
|
||||
preserveAbsentOnRestore: input.preserveAbsentOnRestore,
|
||||
assets: input.assets,
|
||||
});
|
||||
}
|
||||
}
|
||||
const result = await input.runner.execute({
|
||||
command: shellCommand,
|
||||
args: ["-lc", input.installCommand.trim()],
|
||||
args: ["-lc", installCommand],
|
||||
cwd: workspaceRemoteDir,
|
||||
timeoutMs,
|
||||
});
|
||||
requireSuccessfulResult(result, input.installCommand.trim());
|
||||
// A failed install is not always fatal: the CLI may already be on PATH
|
||||
// from a previous lease, the template image, or another path entry. Log
|
||||
// and continue rather than aborting the agent run; downstream code that
|
||||
// exec's the CLI will surface a clear "command not found" if it is in
|
||||
// fact missing. The test path's `maybeRunSandboxInstallCommand` already
|
||||
// honors this contract — keep them consistent.
|
||||
if (result.timedOut || (result.exitCode ?? 0) !== 0) {
|
||||
const tail = (text: string) =>
|
||||
text.split(/\r?\n/).filter((line) => line.trim().length > 0).slice(-3).join(" | ").slice(0, 480);
|
||||
const reason = result.timedOut ? "timed out" : `exited ${result.exitCode ?? "?"}`;
|
||||
console.warn(
|
||||
`[paperclip] managed-runtime install command ${reason}: ${installCommand} :: ${tail(result.stderr || result.stdout)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return await prepareSandboxManagedRuntime({
|
||||
|
||||
@@ -230,9 +230,10 @@ export async function ensureAdapterExecutionTargetCommandResolvable(
|
||||
target: AdapterExecutionTarget | null | undefined,
|
||||
cwd: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
options: { installCommand?: string | null } = {},
|
||||
) {
|
||||
if (target?.kind === "remote" && target.transport === "sandbox") {
|
||||
await ensureSandboxCommandResolvable(command, target);
|
||||
await ensureSandboxCommandResolvable(command, target, options.installCommand?.trim() || null);
|
||||
return;
|
||||
}
|
||||
await ensureCommandResolvable(command, cwd, env, {
|
||||
@@ -240,17 +241,10 @@ export async function ensureAdapterExecutionTargetCommandResolvable(
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureSandboxCommandResolvable(
|
||||
async function probeSandboxCommandResolvable(
|
||||
command: string,
|
||||
target: AdapterSandboxExecutionTarget,
|
||||
): Promise<void> {
|
||||
// Probe whether the binary is resolvable inside the sandbox. We previously
|
||||
// short-circuited this for sandbox targets, which let the caller report a
|
||||
// success message even when the CLI was missing from the image. Now we run
|
||||
// a real `command -v` through the same runner the hello probe will use, so
|
||||
// the first step honestly reflects whether the binary is on PATH. The
|
||||
// sandbox provider is responsible for sourcing login profiles (e2b mirrors
|
||||
// SSH's buildSshSpawnTarget) so this and the hello probe agree on PATH.
|
||||
): Promise<{ resolved: boolean; timedOut: boolean; stderr: string }> {
|
||||
const runner = requireSandboxRunner(target);
|
||||
const probeScript = `command -v ${shellQuote(command)}`;
|
||||
const result = await runner.execute({
|
||||
@@ -259,14 +253,67 @@ async function ensureSandboxCommandResolvable(
|
||||
cwd: target.remoteCwd,
|
||||
timeoutMs: target.timeoutMs ?? 15_000,
|
||||
});
|
||||
if (result.timedOut) {
|
||||
return {
|
||||
resolved: !result.timedOut && (result.exitCode ?? 1) === 0,
|
||||
timedOut: result.timedOut,
|
||||
stderr: result.stderr.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureSandboxCommandResolvable(
|
||||
command: string,
|
||||
target: AdapterSandboxExecutionTarget,
|
||||
installCommand: string | null,
|
||||
): Promise<void> {
|
||||
// Probe whether the binary is resolvable inside the sandbox. We previously
|
||||
// short-circuited this for sandbox targets, which let the caller report a
|
||||
// success message even when the CLI was missing from the image. Now we run
|
||||
// a real `command -v` through the same runner the hello probe will use, so
|
||||
// the first step honestly reflects whether the binary is on PATH. The
|
||||
// sandbox provider is responsible for sourcing login profiles (e2b mirrors
|
||||
// SSH's buildSshSpawnTarget) so this and the hello probe agree on PATH.
|
||||
let probe = await probeSandboxCommandResolvable(command, target);
|
||||
if (probe.resolved) return;
|
||||
if (probe.timedOut) {
|
||||
throw new Error(`Timed out checking command "${command}" on sandbox target.`);
|
||||
}
|
||||
if ((result.exitCode ?? 1) === 0) return;
|
||||
const stderr = result.stderr.trim();
|
||||
const detail = stderr.length > 0 ? ` (${stderr})` : "";
|
||||
|
||||
// If the caller supplied an install command, attempt the install once via
|
||||
// the sandbox runner (which the sandbox provider wraps in a login shell)
|
||||
// and re-probe before reporting failure. This lets fresh sandbox leases
|
||||
// bring up the CLI before the resolvability gate, mirroring the test path.
|
||||
let installFailureDetail: string | null = null;
|
||||
if (installCommand) {
|
||||
const runner = requireSandboxRunner(target);
|
||||
try {
|
||||
const installResult = await runner.execute({
|
||||
command: "sh",
|
||||
args: ["-lc", installCommand],
|
||||
cwd: target.remoteCwd,
|
||||
timeoutMs: target.timeoutMs ?? 300_000,
|
||||
});
|
||||
if (installResult.timedOut) {
|
||||
installFailureDetail = `install command timed out: ${installCommand}`;
|
||||
} else if ((installResult.exitCode ?? 0) !== 0) {
|
||||
const tail = (text: string) =>
|
||||
text.split(/\r?\n/).filter((line) => line.trim().length > 0).slice(-2).join(" | ").slice(0, 240);
|
||||
const reason = tail(installResult.stderr || installResult.stdout) || `exit ${installResult.exitCode ?? "?"}`;
|
||||
installFailureDetail = `install command exited ${installResult.exitCode ?? "?"}: ${reason}`;
|
||||
}
|
||||
} catch (err) {
|
||||
installFailureDetail = `install command threw: ${err instanceof Error ? err.message : String(err)}`;
|
||||
}
|
||||
probe = await probeSandboxCommandResolvable(command, target);
|
||||
if (probe.resolved) return;
|
||||
if (probe.timedOut) {
|
||||
throw new Error(`Timed out checking command "${command}" on sandbox target.`);
|
||||
}
|
||||
}
|
||||
|
||||
const probeStderr = probe.stderr.length > 0 ? ` probe stderr: ${probe.stderr}` : "";
|
||||
const installDetail = installFailureDetail ? `; ${installFailureDetail}` : "";
|
||||
throw new Error(
|
||||
`Command "${command}" is not installed or not on PATH in the sandbox environment${detail}.`,
|
||||
`Command "${command}" is not installed or not on PATH in the sandbox environment${installDetail}.${probeStderr}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -409,6 +456,111 @@ export async function runAdapterExecutionTargetShellCommand(
|
||||
);
|
||||
}
|
||||
|
||||
export interface AdapterSandboxInstallCommandCheck {
|
||||
code: string;
|
||||
level: "info" | "warn" | "error";
|
||||
message: string;
|
||||
detail?: string;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
// Best-effort run of an adapter-supplied install command on a sandbox target
|
||||
// before the resolvability + hello probe. Returns null for non-sandbox
|
||||
// targets so callers can no-op. Returns a structured check otherwise — never
|
||||
// throws — so the rest of the test still runs and reports the post-install
|
||||
// state honestly. Caller pushes the check into its result array; the test
|
||||
// report shows whether install was attempted and what came back.
|
||||
export async function maybeRunSandboxInstallCommand(input: {
|
||||
runId: string;
|
||||
target: AdapterExecutionTarget | null | undefined;
|
||||
adapterKey: string;
|
||||
installCommand: string;
|
||||
/** When provided, skip the install if `command -v <detectCommand>` succeeds. */
|
||||
detectCommand?: string | null;
|
||||
env?: Record<string, string>;
|
||||
timeoutSec?: number;
|
||||
}): Promise<AdapterSandboxInstallCommandCheck | null> {
|
||||
const { target, adapterKey, installCommand } = input;
|
||||
if (!target || target.kind !== "remote" || target.transport !== "sandbox") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = installCommand.trim();
|
||||
if (trimmed.length === 0) return null;
|
||||
|
||||
const code = `${adapterKey}_install_command_run`;
|
||||
|
||||
// Skip install when the binary is already on PATH. Avoids running
|
||||
// network-dependent installers (e.g. `curl ... | bash`) on every test
|
||||
// probe when the CLI is preinstalled on the lease/template.
|
||||
const detectCommand = input.detectCommand?.trim();
|
||||
if (detectCommand) {
|
||||
try {
|
||||
const probe = await runAdapterExecutionTargetShellCommand(
|
||||
input.runId,
|
||||
target,
|
||||
`command -v ${shellQuote(detectCommand)} >/dev/null 2>&1`,
|
||||
{
|
||||
cwd: target.remoteCwd,
|
||||
env: input.env ?? {},
|
||||
timeoutSec: 30,
|
||||
graceSec: 5,
|
||||
},
|
||||
);
|
||||
if (!probe.timedOut && probe.exitCode === 0) {
|
||||
return {
|
||||
code,
|
||||
level: "info",
|
||||
message: `${detectCommand} already on PATH; skipped install.`,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Fall through to actually running the install — failure to probe
|
||||
// is not a reason to skip the install gate.
|
||||
}
|
||||
}
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await runAdapterExecutionTargetShellCommand(input.runId, target, trimmed, {
|
||||
cwd: target.remoteCwd,
|
||||
env: input.env ?? {},
|
||||
timeoutSec: input.timeoutSec ?? 240,
|
||||
graceSec: 10,
|
||||
});
|
||||
} catch (err) {
|
||||
return {
|
||||
code,
|
||||
level: "warn",
|
||||
message: "Install command threw before completion.",
|
||||
detail: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
}
|
||||
const tail = (text: string) =>
|
||||
text.split(/\r?\n/).filter((line) => line.trim().length > 0).slice(-3).join(" | ").slice(0, 480);
|
||||
if (result.timedOut) {
|
||||
return {
|
||||
code,
|
||||
level: "warn",
|
||||
message: `Install command timed out: ${trimmed}`,
|
||||
detail: tail(result.stderr || result.stdout),
|
||||
};
|
||||
}
|
||||
if ((result.exitCode ?? 1) === 0) {
|
||||
return {
|
||||
code,
|
||||
level: "info",
|
||||
message: `Install command ran: ${trimmed}`,
|
||||
...(tail(result.stdout) ? { detail: tail(result.stdout) } : {}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
code,
|
||||
level: "warn",
|
||||
message: `Install command exited ${result.exitCode}: ${trimmed}`,
|
||||
detail: tail(result.stderr || result.stdout),
|
||||
};
|
||||
}
|
||||
|
||||
export async function readAdapterExecutionTargetHomeDir(
|
||||
runId: string,
|
||||
target: AdapterExecutionTarget | null | undefined,
|
||||
@@ -470,12 +622,43 @@ export async function ensureAdapterExecutionTargetRuntimeCommandInstalled(input:
|
||||
onLog: input.onLog,
|
||||
},
|
||||
);
|
||||
|
||||
// A failed or timed-out install is not necessarily fatal: the CLI may already
|
||||
// be on PATH from a previous lease's install, the template image, or another
|
||||
// path entry. Re-run the detect probe (when one is configured) so a transient
|
||||
// install failure does not abort the agent run when the binary is reachable.
|
||||
const installFailed = result.timedOut || (result.exitCode ?? 0) !== 0;
|
||||
if (!installFailed) {
|
||||
return;
|
||||
}
|
||||
if (detectCommand) {
|
||||
const recheck = await runAdapterExecutionTargetShellCommand(
|
||||
input.runId,
|
||||
input.target,
|
||||
`command -v ${shellQuote(detectCommand)} >/dev/null 2>&1`,
|
||||
{
|
||||
cwd: input.cwd,
|
||||
env: input.env,
|
||||
timeoutSec: input.timeoutSec,
|
||||
graceSec: input.graceSec,
|
||||
},
|
||||
);
|
||||
if (!recheck.timedOut && recheck.exitCode === 0) {
|
||||
if (input.onLog) {
|
||||
const reason = result.timedOut ? "timed out" : `exited ${result.exitCode ?? "?"}`;
|
||||
await input.onLog(
|
||||
"stderr",
|
||||
`[paperclip] Install command ${reason} (${installCommand}) but ${detectCommand} is on PATH; continuing.\n`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.timedOut) {
|
||||
throw new Error(`Timed out while installing the adapter runtime command via: ${installCommand}`);
|
||||
}
|
||||
if ((result.exitCode ?? 0) !== 0) {
|
||||
throw new Error(`Failed to install the adapter runtime command via: ${installCommand}`);
|
||||
}
|
||||
throw new Error(`Failed to install the adapter runtime command via: ${installCommand}`);
|
||||
}
|
||||
|
||||
export async function ensureAdapterExecutionTargetFile(
|
||||
@@ -666,6 +849,8 @@ export async function prepareAdapterExecutionTargetRuntime(input: {
|
||||
preserveAbsentOnRestore?: string[];
|
||||
assets?: AdapterManagedRuntimeAsset[];
|
||||
installCommand?: string | null;
|
||||
/** When provided alongside `installCommand`, skip the install if the binary is already on PATH. */
|
||||
detectCommand?: string | null;
|
||||
}): Promise<PreparedAdapterExecutionTargetRuntime> {
|
||||
const target = input.target ?? { kind: "local" as const };
|
||||
if (target.kind === "local") {
|
||||
@@ -707,6 +892,7 @@ export async function prepareAdapterExecutionTargetRuntime(input: {
|
||||
preserveAbsentOnRestore: input.preserveAbsentOnRestore,
|
||||
assets: input.assets,
|
||||
installCommand: input.installCommand,
|
||||
detectCommand: input.detectCommand,
|
||||
});
|
||||
return {
|
||||
target,
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { AdapterModelProfileDefinition } from "@paperclipai/adapter-utils";
|
||||
export const type = "claude_local";
|
||||
export const label = "Claude Code (local)";
|
||||
|
||||
export const SANDBOX_INSTALL_COMMAND = "npm install -g @anthropic-ai/claude-code";
|
||||
|
||||
export const models = [
|
||||
{ id: "claude-opus-4-7", label: "Claude Opus 4.7" },
|
||||
{ id: "claude-opus-4-6", label: "Claude Opus 4.6" },
|
||||
|
||||
@@ -54,6 +54,7 @@ import { prepareClaudeConfigSeed } from "./claude-config.js";
|
||||
import { resolveClaudeDesiredSkillNames } from "./skills.js";
|
||||
import { isBedrockModelId } from "./models.js";
|
||||
import { prepareClaudePromptBundle } from "./prompt-cache.js";
|
||||
import { SANDBOX_INSTALL_COMMAND } from "../index.js";
|
||||
|
||||
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -261,7 +262,7 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<Cl
|
||||
graceSec,
|
||||
onLog,
|
||||
});
|
||||
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv);
|
||||
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv, { installCommand: SANDBOX_INSTALL_COMMAND });
|
||||
const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv);
|
||||
const loggedEnv = buildInvocationEnvForLogs(env, {
|
||||
runtimeEnv,
|
||||
@@ -430,6 +431,8 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
target: executionTarget,
|
||||
adapterKey: "claude",
|
||||
workspaceLocalDir: cwd,
|
||||
installCommand: SANDBOX_INSTALL_COMMAND,
|
||||
detectCommand: command,
|
||||
assets: [
|
||||
{
|
||||
key: "skills",
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import {
|
||||
ensureAdapterExecutionTargetCommandResolvable,
|
||||
ensureAdapterExecutionTargetDirectory,
|
||||
maybeRunSandboxInstallCommand,
|
||||
runAdapterExecutionTargetProcess,
|
||||
describeAdapterExecutionTarget,
|
||||
resolveAdapterExecutionTargetCwd,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
import path from "node:path";
|
||||
import { detectClaudeLoginRequired, parseClaudeStreamJson } from "./parse.js";
|
||||
import { isBedrockModelId } from "./models.js";
|
||||
import { SANDBOX_INSTALL_COMMAND } from "../index.js";
|
||||
|
||||
function summarizeStatus(checks: AdapterEnvironmentCheck[]): AdapterEnvironmentTestResult["status"] {
|
||||
if (checks.some((check) => check.level === "error")) return "fail";
|
||||
@@ -102,6 +104,15 @@ export async function testEnvironment(
|
||||
if (typeof value === "string") env[key] = value;
|
||||
}
|
||||
const runtimeEnv = ensurePathInEnv({ ...process.env, ...env });
|
||||
const installCheck = await maybeRunSandboxInstallCommand({
|
||||
runId,
|
||||
target,
|
||||
adapterKey: "claude",
|
||||
installCommand: SANDBOX_INSTALL_COMMAND,
|
||||
detectCommand: command,
|
||||
env,
|
||||
});
|
||||
if (installCheck) checks.push(installCheck);
|
||||
try {
|
||||
await ensureAdapterExecutionTargetCommandResolvable(command, target, cwd, runtimeEnv);
|
||||
checks.push({
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { AdapterModelProfileDefinition } from "@paperclipai/adapter-utils";
|
||||
export const type = "codex_local";
|
||||
export const label = "Codex (local)";
|
||||
|
||||
export const SANDBOX_INSTALL_COMMAND = "npm install -g @openai/codex";
|
||||
|
||||
export const DEFAULT_CODEX_LOCAL_MODEL = "gpt-5.3-codex";
|
||||
export const DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX = true;
|
||||
export const CODEX_LOCAL_FAST_MODE_SUPPORTED_MODELS = ["gpt-5.4"] as const;
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
import { pathExists, prepareManagedCodexHome, resolveManagedCodexHomeDir, resolveSharedCodexHomeDir } from "./codex-home.js";
|
||||
import { resolveCodexDesiredSkillNames } from "./skills.js";
|
||||
import { buildCodexExecArgs } from "./codex-args.js";
|
||||
import { SANDBOX_INSTALL_COMMAND } from "../index.js";
|
||||
|
||||
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const CODEX_ROLLOUT_NOISE_RE =
|
||||
@@ -374,6 +375,8 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
target: executionTarget,
|
||||
adapterKey: "codex",
|
||||
workspaceLocalDir: cwd,
|
||||
installCommand: SANDBOX_INSTALL_COMMAND,
|
||||
detectCommand: command,
|
||||
assets: [
|
||||
{
|
||||
key: "home",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import {
|
||||
ensureAdapterExecutionTargetCommandResolvable,
|
||||
ensureAdapterExecutionTargetDirectory,
|
||||
maybeRunSandboxInstallCommand,
|
||||
runAdapterExecutionTargetProcess,
|
||||
describeAdapterExecutionTarget,
|
||||
resolveAdapterExecutionTargetCwd,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import { parseCodexJsonl } from "./parse.js";
|
||||
import { SANDBOX_INSTALL_COMMAND } from "../index.js";
|
||||
import { codexHomeDir, readCodexAuthInfo } from "./quota.js";
|
||||
import { buildCodexExecArgs } from "./codex-args.js";
|
||||
|
||||
@@ -104,6 +106,15 @@ export async function testEnvironment(
|
||||
if (typeof value === "string") env[key] = value;
|
||||
}
|
||||
const runtimeEnv = ensurePathInEnv({ ...process.env, ...env });
|
||||
const installCheck = await maybeRunSandboxInstallCommand({
|
||||
runId,
|
||||
target,
|
||||
adapterKey: "codex",
|
||||
installCommand: SANDBOX_INSTALL_COMMAND,
|
||||
detectCommand: command,
|
||||
env,
|
||||
});
|
||||
if (installCheck) checks.push(installCheck);
|
||||
try {
|
||||
await ensureAdapterExecutionTargetCommandResolvable(command, target, cwd, runtimeEnv);
|
||||
checks.push({
|
||||
|
||||
@@ -3,6 +3,15 @@ import type { AdapterModelProfileDefinition } from "@paperclipai/adapter-utils";
|
||||
export const type = "cursor";
|
||||
export const label = "Cursor CLI (local)";
|
||||
|
||||
// Cursor CLI is not distributed as an npm package — the official install
|
||||
// path is the upstream installer script at cursor.com/install. Other adapters
|
||||
// in this repo prefer `npm install -g <pkg>` which is content-addressed by the
|
||||
// registry; cursor must use `curl | bash` until upstream publishes a registry
|
||||
// artifact. Pinning a commit/version here would require shipping our own
|
||||
// mirror of the installer; revisit if Cursor adds an npm/release-asset
|
||||
// equivalent.
|
||||
export const SANDBOX_INSTALL_COMMAND = "curl https://cursor.com/install -fsS | bash";
|
||||
|
||||
export const DEFAULT_CURSOR_LOCAL_MODEL = "auto";
|
||||
|
||||
const CURSOR_FALLBACK_MODEL_IDS = [
|
||||
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
joinPromptSections,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import { DEFAULT_CURSOR_LOCAL_MODEL } from "../index.js";
|
||||
import { DEFAULT_CURSOR_LOCAL_MODEL, SANDBOX_INSTALL_COMMAND } from "../index.js";
|
||||
import { parseCursorJsonl, isCursorUnknownSessionError } from "./parse.js";
|
||||
import { prepareCursorSandboxCommand } from "./remote-command.js";
|
||||
import { normalizeCursorStreamLine } from "../shared/stream.js";
|
||||
@@ -340,7 +340,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
);
|
||||
const billingType = resolveCursorBillingType(effectiveEnv);
|
||||
const runtimeEnv = ensurePathInEnv(effectiveEnv);
|
||||
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv);
|
||||
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv, { installCommand: SANDBOX_INSTALL_COMMAND });
|
||||
const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv);
|
||||
let loggedEnv = buildInvocationEnvForLogs(env, {
|
||||
runtimeEnv,
|
||||
@@ -370,6 +370,8 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
target: executionTarget,
|
||||
adapterKey: "cursor",
|
||||
workspaceLocalDir: cwd,
|
||||
installCommand: SANDBOX_INSTALL_COMMAND,
|
||||
detectCommand: command,
|
||||
assets: [{
|
||||
key: "skills",
|
||||
localDir: localSkillsDir,
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import {
|
||||
ensureAdapterExecutionTargetCommandResolvable,
|
||||
ensureAdapterExecutionTargetDirectory,
|
||||
maybeRunSandboxInstallCommand,
|
||||
runAdapterExecutionTargetProcess,
|
||||
describeAdapterExecutionTarget,
|
||||
resolveAdapterExecutionTargetCwd,
|
||||
@@ -19,7 +20,7 @@ import {
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { DEFAULT_CURSOR_LOCAL_MODEL } from "../index.js";
|
||||
import { DEFAULT_CURSOR_LOCAL_MODEL, SANDBOX_INSTALL_COMMAND } from "../index.js";
|
||||
import { parseCursorJsonl } from "./parse.js";
|
||||
import { isDefaultCursorCommand, prepareCursorSandboxCommand } from "./remote-command.js";
|
||||
import { hasCursorTrustBypassArg } from "../shared/trust.js";
|
||||
@@ -148,6 +149,15 @@ export async function testEnvironment(
|
||||
command = sandboxCommand.command;
|
||||
env = sandboxCommand.env;
|
||||
const runtimeEnv = ensurePathInEnv({ ...process.env, ...env });
|
||||
const installCheck = await maybeRunSandboxInstallCommand({
|
||||
runId,
|
||||
target,
|
||||
adapterKey: "cursor",
|
||||
installCommand: SANDBOX_INSTALL_COMMAND,
|
||||
detectCommand: command,
|
||||
env,
|
||||
});
|
||||
if (installCheck) checks.push(installCheck);
|
||||
try {
|
||||
await ensureAdapterExecutionTargetCommandResolvable(command, target, cwd, runtimeEnv);
|
||||
checks.push({
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { AdapterModelProfileDefinition } from "@paperclipai/adapter-utils";
|
||||
export const type = "gemini_local";
|
||||
export const label = "Gemini CLI (local)";
|
||||
|
||||
export const SANDBOX_INSTALL_COMMAND = "npm install -g @google/gemini-cli";
|
||||
|
||||
export const DEFAULT_GEMINI_LOCAL_MODEL = "auto";
|
||||
|
||||
export const models = [
|
||||
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
runChildProcess,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import { DEFAULT_GEMINI_LOCAL_MODEL } from "../index.js";
|
||||
import { DEFAULT_GEMINI_LOCAL_MODEL, SANDBOX_INSTALL_COMMAND } from "../index.js";
|
||||
import {
|
||||
describeGeminiFailure,
|
||||
detectGeminiAuthRequired,
|
||||
@@ -292,7 +292,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
graceSec,
|
||||
onLog,
|
||||
});
|
||||
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv);
|
||||
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv, { installCommand: SANDBOX_INSTALL_COMMAND });
|
||||
const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv);
|
||||
let loggedEnv = buildInvocationEnvForLogs(env, {
|
||||
runtimeEnv,
|
||||
@@ -322,6 +322,8 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
target: executionTarget,
|
||||
adapterKey: "gemini",
|
||||
workspaceLocalDir: cwd,
|
||||
installCommand: SANDBOX_INSTALL_COMMAND,
|
||||
detectCommand: command,
|
||||
assets: [{
|
||||
key: "skills",
|
||||
localDir: localSkillsDir,
|
||||
|
||||
@@ -14,12 +14,13 @@ import {
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import {
|
||||
ensureAdapterExecutionTargetCommandResolvable,
|
||||
maybeRunSandboxInstallCommand,
|
||||
ensureAdapterExecutionTargetDirectory,
|
||||
runAdapterExecutionTargetProcess,
|
||||
describeAdapterExecutionTarget,
|
||||
resolveAdapterExecutionTargetCwd,
|
||||
} from "@paperclipai/adapter-utils/execution-target";
|
||||
import { DEFAULT_GEMINI_LOCAL_MODEL } from "../index.js";
|
||||
import { DEFAULT_GEMINI_LOCAL_MODEL, SANDBOX_INSTALL_COMMAND } from "../index.js";
|
||||
import { detectGeminiAuthRequired, detectGeminiQuotaExhausted, parseGeminiJsonl } from "./parse.js";
|
||||
import { firstNonEmptyLine } from "./utils.js";
|
||||
|
||||
@@ -94,6 +95,15 @@ export async function testEnvironment(
|
||||
if (typeof value === "string") env[key] = value;
|
||||
}
|
||||
const runtimeEnv = ensurePathInEnv({ ...process.env, ...env });
|
||||
const installCheck = await maybeRunSandboxInstallCommand({
|
||||
runId,
|
||||
target,
|
||||
adapterKey: "gemini",
|
||||
installCommand: SANDBOX_INSTALL_COMMAND,
|
||||
detectCommand: command,
|
||||
env,
|
||||
});
|
||||
if (installCheck) checks.push(installCheck);
|
||||
try {
|
||||
await ensureAdapterExecutionTargetCommandResolvable(command, target, cwd, runtimeEnv);
|
||||
checks.push({
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { AdapterModelProfileDefinition } from "@paperclipai/adapter-utils";
|
||||
export const type = "opencode_local";
|
||||
export const label = "OpenCode (local)";
|
||||
|
||||
export const SANDBOX_INSTALL_COMMAND = "npm install -g opencode-ai";
|
||||
|
||||
export const DEFAULT_OPENCODE_LOCAL_MODEL = "openai/gpt-5.2-codex";
|
||||
|
||||
export function isValidOpenCodeModelId(value: unknown): value is string {
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
} from "./models.js";
|
||||
import { removeMaintainerOnlySkillSymlinks } from "@paperclipai/adapter-utils/server-utils";
|
||||
import { prepareOpenCodeRuntimeConfig } from "./runtime-config.js";
|
||||
import { SANDBOX_INSTALL_COMMAND } from "../index.js";
|
||||
|
||||
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -316,7 +317,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
graceSec,
|
||||
onLog,
|
||||
});
|
||||
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv);
|
||||
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv, { installCommand: SANDBOX_INSTALL_COMMAND });
|
||||
const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv);
|
||||
let loggedEnv = buildInvocationEnvForLogs(preparedRuntimeConfig.env, {
|
||||
runtimeEnv,
|
||||
@@ -352,6 +353,8 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
target: executionTarget,
|
||||
adapterKey: "opencode",
|
||||
workspaceLocalDir: cwd,
|
||||
installCommand: SANDBOX_INSTALL_COMMAND,
|
||||
detectCommand: command,
|
||||
assets: [
|
||||
{
|
||||
key: "skills",
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import {
|
||||
ensureAdapterExecutionTargetCommandResolvable,
|
||||
maybeRunSandboxInstallCommand,
|
||||
ensureAdapterExecutionTargetDirectory,
|
||||
runAdapterExecutionTargetProcess,
|
||||
describeAdapterExecutionTarget,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
} from "@paperclipai/adapter-utils/execution-target";
|
||||
import { discoverOpenCodeModels, ensureOpenCodeModelConfiguredAndAvailable } from "./models.js";
|
||||
import { parseOpenCodeJsonl } from "./parse.js";
|
||||
import { SANDBOX_INSTALL_COMMAND } from "../index.js";
|
||||
import { prepareOpenCodeRuntimeConfig } from "./runtime-config.js";
|
||||
|
||||
function summarizeStatus(checks: AdapterEnvironmentCheck[]): AdapterEnvironmentTestResult["status"] {
|
||||
@@ -136,6 +138,15 @@ export async function testEnvironment(
|
||||
detail: command,
|
||||
});
|
||||
} else {
|
||||
const installCheck = await maybeRunSandboxInstallCommand({
|
||||
runId,
|
||||
target,
|
||||
adapterKey: "opencode",
|
||||
installCommand: SANDBOX_INSTALL_COMMAND,
|
||||
detectCommand: command,
|
||||
env,
|
||||
});
|
||||
if (installCheck) checks.push(installCheck);
|
||||
try {
|
||||
await ensureAdapterExecutionTargetCommandResolvable(command, target, cwd, runtimeEnv);
|
||||
checks.push({
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { AdapterModelProfileDefinition } from "@paperclipai/adapter-utils";
|
||||
export const type = "pi_local";
|
||||
export const label = "Pi (local)";
|
||||
|
||||
export const SANDBOX_INSTALL_COMMAND = "npm install -g @mariozechner/pi-coding-agent";
|
||||
|
||||
export const models: Array<{ id: string; label: string }> = [];
|
||||
|
||||
export const modelProfiles: AdapterModelProfileDefinition[] = [];
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
import { shellQuote } from "@paperclipai/adapter-utils/ssh";
|
||||
import { isPiUnknownSessionError, parsePiJsonl } from "./parse.js";
|
||||
import { ensurePiModelConfiguredAndAvailable } from "./models.js";
|
||||
import { SANDBOX_INSTALL_COMMAND } from "../index.js";
|
||||
|
||||
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -361,7 +362,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
graceSec,
|
||||
onLog,
|
||||
});
|
||||
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv);
|
||||
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv, { installCommand: SANDBOX_INSTALL_COMMAND });
|
||||
const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv);
|
||||
let loggedEnv = buildInvocationEnvForLogs(env, {
|
||||
runtimeEnv,
|
||||
@@ -400,6 +401,8 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
target: executionTarget,
|
||||
adapterKey: "pi",
|
||||
workspaceLocalDir: cwd,
|
||||
installCommand: SANDBOX_INSTALL_COMMAND,
|
||||
detectCommand: command,
|
||||
assets: [
|
||||
{
|
||||
key: "skills",
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import {
|
||||
ensureAdapterExecutionTargetCommandResolvable,
|
||||
maybeRunSandboxInstallCommand,
|
||||
ensureAdapterExecutionTargetDirectory,
|
||||
runAdapterExecutionTargetProcess,
|
||||
describeAdapterExecutionTarget,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
} from "@paperclipai/adapter-utils/execution-target";
|
||||
import { discoverPiModelsCached } from "./models.js";
|
||||
import { parsePiJsonl } from "./parse.js";
|
||||
import { SANDBOX_INSTALL_COMMAND } from "../index.js";
|
||||
|
||||
function summarizeStatus(checks: AdapterEnvironmentCheck[]): AdapterEnvironmentTestResult["status"] {
|
||||
if (checks.some((check) => check.level === "error")) return "fail";
|
||||
@@ -134,6 +136,15 @@ export async function testEnvironment(
|
||||
detail: command,
|
||||
});
|
||||
} else {
|
||||
const installCheck = await maybeRunSandboxInstallCommand({
|
||||
runId,
|
||||
target,
|
||||
adapterKey: "pi",
|
||||
installCommand: SANDBOX_INSTALL_COMMAND,
|
||||
detectCommand: command,
|
||||
env,
|
||||
});
|
||||
if (installCheck) checks.push(installCheck);
|
||||
try {
|
||||
await ensureAdapterExecutionTargetCommandResolvable(command, target, cwd, runtimeEnv);
|
||||
checks.push({
|
||||
|
||||
Reference in New Issue
Block a user