Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/agent-profile-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ provider: copilot
Unsupported or custom providers are rejected. DevSpace maps providers to their
native integration:

- `codex`: Codex SDK
- `codex`: the user's Codex CLI through `codex app-server`
- `claude`: Claude Code SDK
- `opencode`: OpenCode SDK
- `pi`: Pi RPC mode
Expand Down Expand Up @@ -102,7 +102,7 @@ thinking: xhigh
DevSpace passes this through to providers that expose a matching control:

- `claude`: SDK effort with adaptive thinking.
- `codex`: SDK model reasoning effort.
- `codex`: App Server turn reasoning effort.
- `pi`: `--thinking`.
- `opencode`: model variant.
- `cursor` and `copilot`: ACP thought-level config when supported.
Expand Down
4 changes: 4 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ terminal.
Starter profile templates are available under `examples/agents/`. Copy or adapt
them into one of the active profile directories before use.

Codex subagents use the user's installed `codex` CLI and its existing login,
configuration, and `CODEX_HOME`. Set `CODEX_COMMAND` to an explicit executable
when DevSpace should use a non-default Codex installation.

Legacy project paths such as `.pi/skills` can be added through `DEVSPACE_SKILL_PATHS` when needed.

Example:
Expand Down
135 changes: 0 additions & 135 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"dev": "node scripts/dev-server.mjs",
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
"start": "node dist/cli.js serve",
"test": "tsx src/config.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/local-agent-manager.test.ts && tsx src/local-agent-control.test.ts && tsx src/local-agent-runtime-pool.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
"test": "tsx src/config.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-codex/runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/local-agent-manager.test.ts && tsx src/local-agent-control.test.ts && tsx src/local-agent-runtime-pool.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"keywords": [],
Expand All @@ -41,7 +41,6 @@
"@earendil-works/pi-coding-agent": "^0.80.3",
"@modelcontextprotocol/ext-apps": "^1.7.2",
"@modelcontextprotocol/sdk": "^1.29.0",
"@openai/codex-sdk": "^0.142.5",
"@opencode-ai/sdk": "^1.17.13",
"@pierre/diffs": "^1.2.5",
"better-sqlite3": "^12.10.0",
Expand Down
15 changes: 8 additions & 7 deletions src/local-agent-adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@ import { Readable, Writable } from "node:stream";
import type { EffortLevel } from "@anthropic-ai/claude-agent-sdk";
import type { LocalAgentProvider } from "./local-agent-profiles.js";
import { removeDevspaceNodeModulesBinFromPath } from "./local-agent-path.js";
import {
createCodexSdkLocalAgentRuntime,
type LocalAgentRunInput,
type LocalAgentRunResult,
} from "./local-agent-runtime.js";
import type { LocalAgentRunInput, LocalAgentRunResult } from "./local-agent-runtime.js";
import type { HarnessDriver, HarnessRuntime } from "./local-agent-runtime-pool.js";
import { createCodexHarnessDriver } from "./local-agent-codex/runtime.js";

export interface LocalAgentAdapter {
readonly provider: LocalAgentProvider;
Expand Down Expand Up @@ -49,8 +46,12 @@ class CodexLocalAgentAdapter implements LocalAgentAdapter {
readonly provider = "codex" as const;

async run(input: LocalAgentRunInput): Promise<LocalAgentRunResult> {
const runtime = await createCodexSdkLocalAgentRuntime();
return runtime.run(input);
const runtime = await createCodexHarnessDriver().createRuntime(input);
try {
return await runtime.run(input);
} finally {
await runtime.close();
}
}
}

Expand Down
45 changes: 44 additions & 1 deletion src/local-agent-availability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,51 @@ import {
formatLocalAgentProviderAvailabilitySummary,
getLocalAgentProviderAvailabilitySnapshot,
} from "./local-agent-availability.js";
import { buildCodexProcessLaunch } from "./local-agent-codex/command.js";

assert.equal(checkLocalAgentProviderAvailability("codex").available, true);
{
const availability = checkLocalAgentProviderAvailability("codex", {
...process.env,
CODEX_COMMAND: "/definitely/missing/devspace-codex",
});
assert.equal(availability.available, false);
assert.match(availability.reason ?? "", /executable not found/);
}

{
const native = buildCodexProcessLaunch({
executable: "C:\\Program Files\\Codex\\codex.exe",
env: { ComSpec: "C:\\Windows\\System32\\cmd.exe" },
runtimeKey: "native",
}, ["app-server"], "win32");
assert.deepEqual(native, {
executable: "C:\\Program Files\\Codex\\codex.exe",
args: ["app-server"],
});

const shim = buildCodexProcessLaunch({
executable: "C:\\Users\\me\\App Data\\npm\\codex.cmd",
env: { ComSpec: "C:\\Windows\\System32\\cmd.exe" },
runtimeKey: "shim",
}, ["app-server", "--help"], "win32");
assert.deepEqual(shim, {
executable: "C:\\Windows\\System32\\cmd.exe",
args: [
"/d",
"/s",
"/c",
'""C:\\Users\\me\\App Data\\npm\\codex.cmd" app-server --help"',
],
});
assert.throws(
() => buildCodexProcessLaunch({
executable: "C:\\Users\\me&bad\\codex.cmd",
env: { ComSpec: "cmd.exe" },
runtimeKey: "unsafe",
}, ["app-server"], "win32"),
/cannot be launched safely/,
);
}

{
const availability = checkLocalAgentProviderAvailability("pi", {
Expand Down
56 changes: 12 additions & 44 deletions src/local-agent-availability.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { spawnSync } from "node:child_process";
import { delimiter, resolve } from "node:path";
import { removeDevspaceNodeModulesBinFromPath } from "./local-agent-path.js";
import {
removeDevspaceNodeModulesBinFromPath,
resolveLocalAgentExecutable,
} from "./local-agent-path.js";
import { checkCodexAppServerAvailability } from "./local-agent-codex/command.js";
import {
LOCAL_AGENT_PROVIDERS,
type LocalAgentProvider,
Expand All @@ -24,7 +26,7 @@ export function checkLocalAgentProviderAvailability(
): LocalAgentProviderAvailability {
switch (provider) {
case "codex":
return packageAvailability(provider, "@openai/codex-sdk");
return codexAvailability(env);
case "claude":
return packageAvailability(provider, "@anthropic-ai/claude-agent-sdk");
case "opencode":
Expand Down Expand Up @@ -87,7 +89,7 @@ function commandAvailability(
command: string,
options: { env?: NodeJS.ProcessEnv } = {},
): LocalAgentProviderAvailability {
const executable = resolveCommand(command, options.env);
const executable = resolveLocalAgentExecutable(command, options.env);
if (!executable) {
return {
name: provider,
Expand All @@ -99,45 +101,11 @@ function commandAvailability(
return { name: provider, available: true };
}

function resolveCommand(command: string, env: NodeJS.ProcessEnv = process.env): string | undefined {
const commandHasPath = command.includes("/") || command.includes("\\");
if (commandHasPath) return executableExists(command, env) ? command : undefined;

for (const candidate of candidateCommandPaths(command, env)) {
if (executableExists(candidate, env)) return candidate;
}
return undefined;
}

function candidateCommandPaths(command: string, env: NodeJS.ProcessEnv): string[] {
const path = env.PATH;
if (!path) return [];
const extensions = process.platform === "win32"
? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
.split(";")
.filter(Boolean)
: [""];
const candidates: string[] = [];
for (const directory of path.split(delimiter)) {
if (!directory) continue;
for (const extension of extensions) {
candidates.push(resolve(directory, `${command}${extension}`));
}
}
return candidates;
}

function executableExists(command: string, env: NodeJS.ProcessEnv): boolean {
const result = spawnSync(command, ["--version"], {
encoding: "utf8",
env,
windowsHide: true,
timeout: 5_000,
});
const code = typeof result.error === "object" && result.error && "code" in result.error
? result.error.code
: undefined;
return code !== "ENOENT";
function codexAvailability(env: NodeJS.ProcessEnv): LocalAgentProviderAvailability {
const result = checkCodexAppServerAvailability(env);
return result.available
? { name: "codex", available: true }
: { name: "codex", available: false, reason: result.reason };
}

function piAvailabilityEnvironment(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
Expand Down
Loading
Loading