Auto-create missing cwd for claude_local and codex_local
This commit is contained in:
@@ -14,7 +14,7 @@ The `claude_local` adapter runs Anthropic's Claude Code CLI locally. It supports
|
|||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|-------|------|----------|-------------|
|
|-------|------|----------|-------------|
|
||||||
| `cwd` | string | Yes | Working directory for the agent process |
|
| `cwd` | string | Yes | Working directory for the agent process (absolute path; created automatically if missing when permissions allow) |
|
||||||
| `model` | string | No | Claude model to use (e.g. `claude-opus-4-6`) |
|
| `model` | string | No | Claude model to use (e.g. `claude-opus-4-6`) |
|
||||||
| `promptTemplate` | string | No | Prompt used for all runs |
|
| `promptTemplate` | string | No | Prompt used for all runs |
|
||||||
| `env` | object | No | Environment variables (supports secret refs) |
|
| `env` | object | No | Environment variables (supports secret refs) |
|
||||||
@@ -52,5 +52,5 @@ The adapter creates a temporary directory with symlinks to Paperclip skills and
|
|||||||
Use the "Test Environment" button in the UI to validate the adapter config. It checks:
|
Use the "Test Environment" button in the UI to validate the adapter config. It checks:
|
||||||
|
|
||||||
- Claude CLI is installed and accessible
|
- Claude CLI is installed and accessible
|
||||||
- Working directory exists and is valid
|
- Working directory is absolute and available (auto-created if missing and permitted)
|
||||||
- API key is configured (warning if missing)
|
- API key is configured (warning if missing)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ The `codex_local` adapter runs OpenAI's Codex CLI locally. It supports session p
|
|||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|-------|------|----------|-------------|
|
|-------|------|----------|-------------|
|
||||||
| `cwd` | string | Yes | Working directory for the agent process |
|
| `cwd` | string | Yes | Working directory for the agent process (absolute path; created automatically if missing when permissions allow) |
|
||||||
| `model` | string | No | Model to use |
|
| `model` | string | No | Model to use |
|
||||||
| `promptTemplate` | string | No | Prompt used for all runs |
|
| `promptTemplate` | string | No | Prompt used for all runs |
|
||||||
| `env` | object | No | Environment variables (supports secret refs) |
|
| `env` | object | No | Environment variables (supports secret refs) |
|
||||||
@@ -35,5 +35,5 @@ The adapter symlinks Paperclip skills into the global Codex skills directory (`~
|
|||||||
The environment test checks:
|
The environment test checks:
|
||||||
|
|
||||||
- Codex CLI is installed and accessible
|
- Codex CLI is installed and accessible
|
||||||
- Working directory exists and is valid
|
- Working directory is absolute and available (auto-created if missing and permitted)
|
||||||
- API key is configured
|
- API key is configured
|
||||||
|
|||||||
@@ -113,20 +113,40 @@ export function ensurePathInEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|||||||
return { ...env, PATH: defaultPathForPlatform() };
|
return { ...env, PATH: defaultPathForPlatform() };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureAbsoluteDirectory(cwd: string) {
|
export async function ensureAbsoluteDirectory(
|
||||||
|
cwd: string,
|
||||||
|
opts: { createIfMissing?: boolean } = {},
|
||||||
|
) {
|
||||||
if (!path.isAbsolute(cwd)) {
|
if (!path.isAbsolute(cwd)) {
|
||||||
throw new Error(`Working directory must be an absolute path: "${cwd}"`);
|
throw new Error(`Working directory must be an absolute path: "${cwd}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let stats;
|
const assertDirectory = async () => {
|
||||||
|
const stats = await fs.stat(cwd);
|
||||||
|
if (!stats.isDirectory()) {
|
||||||
|
throw new Error(`Working directory is not a directory: "${cwd}"`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
stats = await fs.stat(cwd);
|
await assertDirectory();
|
||||||
} catch {
|
return;
|
||||||
throw new Error(`Working directory does not exist: "${cwd}"`);
|
} catch (err) {
|
||||||
|
const code = (err as NodeJS.ErrnoException).code;
|
||||||
|
if (!opts.createIfMissing || code !== "ENOENT") {
|
||||||
|
if (code === "ENOENT") {
|
||||||
|
throw new Error(`Working directory does not exist: "${cwd}"`);
|
||||||
|
}
|
||||||
|
throw err instanceof Error ? err : new Error(String(err));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stats.isDirectory()) {
|
try {
|
||||||
throw new Error(`Working directory is not a directory: "${cwd}"`);
|
await fs.mkdir(cwd, { recursive: true });
|
||||||
|
await assertDirectory();
|
||||||
|
} catch (err) {
|
||||||
|
const reason = err instanceof Error ? err.message : String(err);
|
||||||
|
throw new Error(`Could not create working directory "${cwd}": ${reason}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const agentConfigurationDoc = `# claude_local agent configuration
|
|||||||
Adapter: claude_local
|
Adapter: claude_local
|
||||||
|
|
||||||
Core fields:
|
Core fields:
|
||||||
- cwd (string, optional): default absolute working directory fallback for the agent process
|
- cwd (string, optional): default absolute working directory fallback for the agent process (created if missing when possible)
|
||||||
- instructionsFilePath (string, optional): absolute path to a markdown instructions file injected at runtime
|
- instructionsFilePath (string, optional): absolute path to a markdown instructions file injected at runtime
|
||||||
- model (string, optional): Claude model id
|
- model (string, optional): Claude model id
|
||||||
- effort (string, optional): reasoning effort passed via --effort (low|medium|high)
|
- effort (string, optional): reasoning effort passed via --effort (low|medium|high)
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<Cl
|
|||||||
const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd.length > 0;
|
const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd.length > 0;
|
||||||
const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd;
|
const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd;
|
||||||
const cwd = effectiveWorkspaceCwd || configuredCwd || process.cwd();
|
const cwd = effectiveWorkspaceCwd || configuredCwd || process.cwd();
|
||||||
await ensureAbsoluteDirectory(cwd);
|
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
||||||
|
|
||||||
const envConfig = parseObject(config.env);
|
const envConfig = parseObject(config.env);
|
||||||
const hasExplicitApiKey =
|
const hasExplicitApiKey =
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export async function testEnvironment(
|
|||||||
const cwd = asString(config.cwd, process.cwd());
|
const cwd = asString(config.cwd, process.cwd());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ensureAbsoluteDirectory(cwd);
|
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
||||||
checks.push({
|
checks.push({
|
||||||
code: "claude_cwd_valid",
|
code: "claude_cwd_valid",
|
||||||
level: "info",
|
level: "info",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const agentConfigurationDoc = `# codex_local agent configuration
|
|||||||
Adapter: codex_local
|
Adapter: codex_local
|
||||||
|
|
||||||
Core fields:
|
Core fields:
|
||||||
- cwd (string, optional): default absolute working directory fallback for the agent process
|
- cwd (string, optional): default absolute working directory fallback for the agent process (created if missing when possible)
|
||||||
- instructionsFilePath (string, optional): absolute path to a markdown instructions file prepended to stdin prompt at runtime
|
- instructionsFilePath (string, optional): absolute path to a markdown instructions file prepended to stdin prompt at runtime
|
||||||
- model (string, optional): Codex model id
|
- model (string, optional): Codex model id
|
||||||
- modelReasoningEffort (string, optional): reasoning effort override (minimal|low|medium|high) passed via -c model_reasoning_effort=...
|
- modelReasoningEffort (string, optional): reasoning effort override (minimal|low|medium|high) passed via -c model_reasoning_effort=...
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd.length > 0;
|
const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd.length > 0;
|
||||||
const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd;
|
const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd;
|
||||||
const cwd = effectiveWorkspaceCwd || configuredCwd || process.cwd();
|
const cwd = effectiveWorkspaceCwd || configuredCwd || process.cwd();
|
||||||
await ensureAbsoluteDirectory(cwd);
|
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
||||||
await ensureCodexSkillsInjected(onLog);
|
await ensureCodexSkillsInjected(onLog);
|
||||||
const envConfig = parseObject(config.env);
|
const envConfig = parseObject(config.env);
|
||||||
const hasExplicitApiKey =
|
const hasExplicitApiKey =
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export async function testEnvironment(
|
|||||||
const cwd = asString(config.cwd, process.cwd());
|
const cwd = asString(config.cwd, process.cwd());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ensureAbsoluteDirectory(cwd);
|
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
||||||
checks.push({
|
checks.push({
|
||||||
code: "codex_cwd_valid",
|
code: "codex_cwd_valid",
|
||||||
level: "info",
|
level: "info",
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
import { testEnvironment } from "@paperclipai/adapter-claude-local/server";
|
import { testEnvironment } from "@paperclipai/adapter-claude-local/server";
|
||||||
|
|
||||||
const ORIGINAL_ANTHROPIC = process.env.ANTHROPIC_API_KEY;
|
const ORIGINAL_ANTHROPIC = process.env.ANTHROPIC_API_KEY;
|
||||||
@@ -60,4 +63,29 @@ describe("claude_local environment diagnostics", () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(result.checks.some((check) => check.level === "error")).toBe(false);
|
expect(result.checks.some((check) => check.level === "error")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("creates a missing working directory when cwd is absolute", async () => {
|
||||||
|
const cwd = path.join(
|
||||||
|
os.tmpdir(),
|
||||||
|
`paperclip-claude-local-cwd-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||||
|
"workspace",
|
||||||
|
);
|
||||||
|
|
||||||
|
await fs.rm(path.dirname(cwd), { recursive: true, force: true });
|
||||||
|
|
||||||
|
const result = await testEnvironment({
|
||||||
|
companyId: "company-1",
|
||||||
|
adapterType: "claude_local",
|
||||||
|
config: {
|
||||||
|
command: process.execPath,
|
||||||
|
cwd,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.checks.some((check) => check.code === "claude_cwd_valid")).toBe(true);
|
||||||
|
expect(result.checks.some((check) => check.level === "error")).toBe(false);
|
||||||
|
const stats = await fs.stat(cwd);
|
||||||
|
expect(stats.isDirectory()).toBe(true);
|
||||||
|
await fs.rm(path.dirname(cwd), { recursive: true, force: true });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
32
server/src/__tests__/codex-local-adapter-environment.test.ts
Normal file
32
server/src/__tests__/codex-local-adapter-environment.test.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { testEnvironment } from "@paperclipai/adapter-codex-local/server";
|
||||||
|
|
||||||
|
describe("codex_local environment diagnostics", () => {
|
||||||
|
it("creates a missing working directory when cwd is absolute", async () => {
|
||||||
|
const cwd = path.join(
|
||||||
|
os.tmpdir(),
|
||||||
|
`paperclip-codex-local-cwd-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||||
|
"workspace",
|
||||||
|
);
|
||||||
|
|
||||||
|
await fs.rm(path.dirname(cwd), { recursive: true, force: true });
|
||||||
|
|
||||||
|
const result = await testEnvironment({
|
||||||
|
companyId: "company-1",
|
||||||
|
adapterType: "codex_local",
|
||||||
|
config: {
|
||||||
|
command: process.execPath,
|
||||||
|
cwd,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.checks.some((check) => check.code === "codex_cwd_valid")).toBe(true);
|
||||||
|
expect(result.checks.some((check) => check.level === "error")).toBe(false);
|
||||||
|
const stats = await fs.stat(cwd);
|
||||||
|
expect(stats.isDirectory()).toBe(true);
|
||||||
|
await fs.rm(path.dirname(cwd), { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user