@cleocode/adapters 2026.9.20 → 2026.9.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cant-context.d.ts +7 -5
- package/dist/cant-context.d.ts.map +1 -1
- package/dist/index.js +505 -330
- package/dist/index.js.map +3 -3
- package/dist/providers/claude-code/adapter.d.ts.map +1 -1
- package/dist/providers/claude-code/spawn.d.ts +2 -2
- package/dist/providers/claude-code/spawn.d.ts.map +1 -1
- package/dist/providers/claude-sdk/spawn.d.ts +1 -1
- package/dist/providers/codex/adapter.d.ts.map +1 -1
- package/dist/providers/codex/spawn.d.ts +1 -1
- package/dist/providers/codex/spawn.d.ts.map +1 -1
- package/dist/providers/gemini-cli/adapter.d.ts.map +1 -1
- package/dist/providers/gemini-cli/spawn.d.ts +1 -1
- package/dist/providers/gemini-cli/spawn.d.ts.map +1 -1
- package/dist/providers/kimi/adapter.d.ts.map +1 -1
- package/dist/providers/kimi/spawn.d.ts.map +1 -1
- package/dist/providers/opencode/adapter.d.ts.map +1 -1
- package/dist/providers/opencode/spawn.d.ts +3 -2
- package/dist/providers/opencode/spawn.d.ts.map +1 -1
- package/dist/providers/pi/adapter.d.ts +1 -1
- package/dist/providers/pi/adapter.d.ts.map +1 -1
- package/dist/providers/pi/spawn.d.ts +3 -3
- package/dist/providers/pi/spawn.d.ts.map +1 -1
- package/dist/providers/shared/agent-spawn-wrapper.d.ts +5 -0
- package/dist/providers/shared/agent-spawn-wrapper.d.ts.map +1 -1
- package/dist/providers/shared/cli-spawn.d.ts +28 -0
- package/dist/providers/shared/cli-spawn.d.ts.map +1 -0
- package/dist/providers/shared/credentials.d.ts +2 -2
- package/dist/providers/shared/credentials.d.ts.map +1 -1
- package/dist/providers/shared/prompt-file.d.ts +32 -0
- package/dist/providers/shared/prompt-file.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/__tests__/agent-spawn-wrapper-win32.test.ts +42 -0
- package/src/__tests__/cant-context.test.ts +89 -37
- package/src/__tests__/cli-spawn-win32.test.ts +105 -0
- package/src/__tests__/spawn-prompt-file.test.ts +37 -0
- package/src/cant-context.ts +11 -15
- package/src/providers/claude-code/adapter.ts +6 -11
- package/src/providers/claude-code/spawn.ts +11 -25
- package/src/providers/claude-sdk/spawn.ts +1 -1
- package/src/providers/codex/adapter.ts +6 -11
- package/src/providers/codex/spawn.ts +17 -31
- package/src/providers/gemini-cli/adapter.ts +6 -11
- package/src/providers/gemini-cli/spawn.ts +13 -18
- package/src/providers/kimi/adapter.ts +6 -11
- package/src/providers/kimi/spawn.ts +3 -11
- package/src/providers/opencode/__tests__/spawn-prompt-delivery.test.ts +123 -0
- package/src/providers/opencode/adapter.ts +6 -11
- package/src/providers/opencode/spawn.ts +23 -25
- package/src/providers/pi/adapter.ts +8 -18
- package/src/providers/pi/spawn.ts +15 -33
- package/src/providers/shared/__tests__/credentials-cleo-home.test.ts +42 -0
- package/src/providers/shared/agent-spawn-wrapper.ts +22 -2
- package/src/providers/shared/cli-spawn.ts +41 -0
- package/src/providers/shared/credentials.ts +6 -5
- package/src/providers/shared/prompt-file.ts +44 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T12618 — provider CLIs are spawned through their resolved path on win32,
|
|
3
|
+
* with npm `.cmd` shims routed through cmd.exe. Node refuses to spawn a
|
|
4
|
+
* `.cmd` directly and its bare-name lookup never finds one, so before this
|
|
5
|
+
* every adapter spawn failed on Windows.
|
|
6
|
+
*
|
|
7
|
+
* Platform and PATH are injected; the real resolver runs with a stubbed
|
|
8
|
+
* filesystem predicate so the test is host-independent.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ChildProcess } from 'node:child_process';
|
|
12
|
+
import { EventEmitter } from 'node:events';
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
14
|
+
|
|
15
|
+
const PRESENT = new Set(['C:\\npm\\claude.cmd', 'C:\\npm\\codex.cmd', 'C:\\bin\\pi.exe']);
|
|
16
|
+
|
|
17
|
+
vi.mock('@cleocode/paths', async (importOriginal) => {
|
|
18
|
+
const actual = await importOriginal<typeof import('@cleocode/paths')>();
|
|
19
|
+
return {
|
|
20
|
+
...actual,
|
|
21
|
+
resolveSpawnInvocation: (
|
|
22
|
+
command: string,
|
|
23
|
+
args: readonly string[],
|
|
24
|
+
opts: import('@cleocode/paths').ExecPathOptions = {},
|
|
25
|
+
) =>
|
|
26
|
+
actual.resolveSpawnInvocation(command, args, {
|
|
27
|
+
...opts,
|
|
28
|
+
isExecutable: (p) => PRESENT.has(p),
|
|
29
|
+
}),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
vi.mock('node:child_process', { spy: true });
|
|
33
|
+
|
|
34
|
+
import { spawn } from 'node:child_process';
|
|
35
|
+
import {
|
|
36
|
+
_forceSystemdRunAvailable,
|
|
37
|
+
buildAgentSpawnArgs,
|
|
38
|
+
} from '../providers/shared/agent-spawn-wrapper.js';
|
|
39
|
+
import { spawnCli } from '../providers/shared/cli-spawn.js';
|
|
40
|
+
|
|
41
|
+
const realPlatform = process.platform;
|
|
42
|
+
function setPlatform(value: NodeJS.Platform): void {
|
|
43
|
+
Object.defineProperty(process, 'platform', { value, configurable: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
vi.stubEnv('PATH', 'C:\\npm;C:\\bin');
|
|
48
|
+
vi.stubEnv('PATHEXT', '.COM;.EXE;.BAT;.CMD');
|
|
49
|
+
vi.stubEnv('ComSpec', 'C:\\Windows\\system32\\cmd.exe');
|
|
50
|
+
vi.mocked(spawn).mockReturnValue(new EventEmitter() as ChildProcess);
|
|
51
|
+
});
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
setPlatform(realPlatform);
|
|
54
|
+
vi.unstubAllEnvs();
|
|
55
|
+
vi.mocked(spawn).mockReset();
|
|
56
|
+
_forceSystemdRunAvailable(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('buildAgentSpawnArgs on win32 (claude-code)', () => {
|
|
60
|
+
it('routes the claude.cmd shim through cmd.exe with verbatim quoting', () => {
|
|
61
|
+
setPlatform('win32');
|
|
62
|
+
_forceSystemdRunAvailable(false);
|
|
63
|
+
const built = buildAgentSpawnArgs('claude', ['--print', 'C:\\Temp\\p q.txt'], 'T1');
|
|
64
|
+
expect(built.command).toBe('C:\\Windows\\system32\\cmd.exe');
|
|
65
|
+
expect(built.windowsVerbatimArguments).toBe(true);
|
|
66
|
+
expect(built.args.slice(0, 3)).toEqual(['/d', '/s', '/c']);
|
|
67
|
+
expect(built.args[3]).toContain('C:\\npm\\claude.cmd');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('POSIX keeps the sh/ulimit wrapper and never sets the verbatim flag', () => {
|
|
71
|
+
setPlatform('linux');
|
|
72
|
+
_forceSystemdRunAvailable(false);
|
|
73
|
+
const built = buildAgentSpawnArgs('claude', ['--print'], 'T1');
|
|
74
|
+
expect(built.command).toBe('sh');
|
|
75
|
+
expect(built.windowsVerbatimArguments).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('spawnCli (codex, gemini, opencode, pi)', () => {
|
|
80
|
+
it('.cmd on win32: spawns cmd.exe with windowsVerbatimArguments', () => {
|
|
81
|
+
setPlatform('win32');
|
|
82
|
+
spawnCli('codex', ['--full-auto', 'a & b'], { detached: true, stdio: 'ignore' });
|
|
83
|
+
const [file, args, opts] = vi.mocked(spawn).mock.calls[0] ?? [];
|
|
84
|
+
expect(file).toBe('C:\\Windows\\system32\\cmd.exe');
|
|
85
|
+
expect(args?.[3]).toContain('C:\\npm\\codex.cmd');
|
|
86
|
+
expect(args?.[3]).not.toMatch(/[^^]&/);
|
|
87
|
+
expect(opts).toMatchObject({ detached: true, windowsVerbatimArguments: true });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('.exe on win32: spawns the absolute path directly', () => {
|
|
91
|
+
setPlatform('win32');
|
|
92
|
+
spawnCli('pi', ['prompt.txt'], { stdio: 'ignore' });
|
|
93
|
+
expect(vi.mocked(spawn).mock.calls[0]).toEqual([
|
|
94
|
+
'C:\\bin\\pi.exe',
|
|
95
|
+
['prompt.txt'],
|
|
96
|
+
{ stdio: 'ignore' },
|
|
97
|
+
]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('POSIX: unchanged bare-name spawn', () => {
|
|
101
|
+
setPlatform('darwin');
|
|
102
|
+
spawnCli('codex', ['x'], { stdio: 'ignore' });
|
|
103
|
+
expect(vi.mocked(spawn).mock.calls[0]).toEqual(['codex', ['x'], { stdio: 'ignore' }]);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T12606 — spawn adapters write prompts under os.tmpdir(), never `/tmp`.
|
|
3
|
+
*
|
|
4
|
+
* The claude-code, codex and pi adapters wrote `/tmp/<provider>-spawn-<id>.txt`.
|
|
5
|
+
* Windows has no `/tmp`, so every spawn failed there; macOS ignored `$TMPDIR`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { dirname, relative } from 'node:path';
|
|
11
|
+
import { describe, expect, it } from 'vitest';
|
|
12
|
+
import { removeSpawnPromptFile, writeSpawnPromptFile } from '../providers/shared/prompt-file.js';
|
|
13
|
+
|
|
14
|
+
describe('writeSpawnPromptFile (T12606)', () => {
|
|
15
|
+
it('writes under a fresh mkdtemp dir in os.tmpdir() and removes the dir', async () => {
|
|
16
|
+
const file = await writeSpawnPromptFile('claude-spawn', 'hello');
|
|
17
|
+
try {
|
|
18
|
+
expect(readFileSync(file, 'utf-8')).toBe('hello');
|
|
19
|
+
const rel = relative(realpathSync(tmpdir()), realpathSync(dirname(file)));
|
|
20
|
+
expect(rel.startsWith('cleo-claude-spawn-')).toBe(true);
|
|
21
|
+
// The dir's parent IS os.tmpdir() (not a hard-coded /tmp), whatever TMPDIR is.
|
|
22
|
+
expect(realpathSync(dirname(dirname(file)))).toBe(realpathSync(tmpdir()));
|
|
23
|
+
} finally {
|
|
24
|
+
await removeSpawnPromptFile(file);
|
|
25
|
+
}
|
|
26
|
+
expect(existsSync(dirname(file))).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('gives concurrent spawns distinct directories', async () => {
|
|
30
|
+
const [a, b] = await Promise.all([
|
|
31
|
+
writeSpawnPromptFile('codex-spawn', 'a'),
|
|
32
|
+
writeSpawnPromptFile('codex-spawn', 'b'),
|
|
33
|
+
]);
|
|
34
|
+
expect(dirname(a)).not.toBe(dirname(b));
|
|
35
|
+
await Promise.all([removeSpawnPromptFile(a), removeSpawnPromptFile(b)]);
|
|
36
|
+
});
|
|
37
|
+
});
|
package/src/cant-context.ts
CHANGED
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
|
|
24
24
|
import { execFile } from 'node:child_process';
|
|
25
25
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
26
|
-
import { homedir } from 'node:os';
|
|
27
26
|
import { basename, join } from 'node:path';
|
|
28
27
|
import { promisify } from 'node:util';
|
|
28
|
+
import { getCleoHome, getCleoPlatformPaths } from '@cleocode/paths';
|
|
29
29
|
|
|
30
30
|
const execFileAsync = promisify(execFile);
|
|
31
31
|
|
|
@@ -175,10 +175,12 @@ export function discoverCantFiles(dir: string): string[] {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
/**
|
|
178
|
-
* Resolve
|
|
178
|
+
* Resolve the platform paths for the 3-tier CANT hierarchy.
|
|
179
179
|
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
180
|
+
* The global tier lives under `getCleoHome()` and the user tier under the
|
|
181
|
+
* CLEO config dir, both from `@cleocode/paths` — on Linux that is
|
|
182
|
+
* `$XDG_DATA_HOME/cleo/cant` and `$XDG_CONFIG_HOME/cleo/cant`; on macOS
|
|
183
|
+
* `~/Library/Application Support/cleo/cant` and `~/Library/Preferences/cleo/cant`.
|
|
182
184
|
*
|
|
183
185
|
* @param projectDir - The project root directory (for the project tier).
|
|
184
186
|
* @returns An object with `global`, `user`, and `project` CANT directory paths.
|
|
@@ -188,13 +190,9 @@ export function resolveThreeTierPaths(projectDir: string): {
|
|
|
188
190
|
user: string;
|
|
189
191
|
project: string;
|
|
190
192
|
} {
|
|
191
|
-
const home = homedir();
|
|
192
|
-
const xdgData = process.env['XDG_DATA_HOME'] ?? join(home, '.local', 'share');
|
|
193
|
-
const xdgConfig = process.env['XDG_CONFIG_HOME'] ?? join(home, '.config');
|
|
194
|
-
|
|
195
193
|
return {
|
|
196
|
-
global: join(
|
|
197
|
-
user: join(
|
|
194
|
+
global: join(getCleoHome(), 'cant'),
|
|
195
|
+
user: join(getCleoPlatformPaths().config, 'cant'),
|
|
198
196
|
project: join(projectDir, '.cleo', 'cant'),
|
|
199
197
|
};
|
|
200
198
|
}
|
|
@@ -377,11 +375,11 @@ async function fetchMentalModelInjection(agentName: string, projectRoot: string)
|
|
|
377
375
|
// ---------------------------------------------------------------------------
|
|
378
376
|
|
|
379
377
|
/**
|
|
380
|
-
* Read CLEOOS-IDENTITY.md from the project or global
|
|
378
|
+
* Read CLEOOS-IDENTITY.md from the project or the global CLEO home.
|
|
381
379
|
*
|
|
382
380
|
* Search order:
|
|
383
381
|
* 1. `<projectDir>/.cleo/CLEOOS-IDENTITY.md` (project-level, deployed by init)
|
|
384
|
-
* 2.
|
|
382
|
+
* 2. `<getCleoHome()>/CLEOOS-IDENTITY.md` (global)
|
|
385
383
|
*
|
|
386
384
|
* @param projectDir - The project root directory.
|
|
387
385
|
* @returns The identity file content, or null if not found.
|
|
@@ -397,9 +395,7 @@ export function readIdentityFile(projectDir: string): string | null {
|
|
|
397
395
|
}
|
|
398
396
|
}
|
|
399
397
|
|
|
400
|
-
const
|
|
401
|
-
const xdgData = process.env['XDG_DATA_HOME'] ?? join(home, '.local', 'share');
|
|
402
|
-
const globalPath = join(xdgData, 'cleo', 'CLEOOS-IDENTITY.md');
|
|
398
|
+
const globalPath = join(getCleoHome(), 'CLEOOS-IDENTITY.md');
|
|
403
399
|
if (existsSync(globalPath)) {
|
|
404
400
|
try {
|
|
405
401
|
const content = readFileSync(globalPath, 'utf-8');
|
|
@@ -7,17 +7,16 @@
|
|
|
7
7
|
* @task T5240
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { exec } from 'node:child_process';
|
|
11
10
|
import { existsSync } from 'node:fs';
|
|
12
11
|
import { homedir } from 'node:os';
|
|
13
12
|
import { join } from 'node:path';
|
|
14
|
-
import { promisify } from 'node:util';
|
|
15
13
|
import type {
|
|
16
14
|
AdapterCapabilities,
|
|
17
15
|
AdapterHealthStatus,
|
|
18
16
|
AdapterSpawnProvider,
|
|
19
17
|
CLEOProviderAdapter,
|
|
20
18
|
} from '@cleocode/contracts';
|
|
19
|
+
import { findOnPath } from '@cleocode/paths';
|
|
21
20
|
import { ClaudeSDKSpawnProvider } from '../claude-sdk/spawn.js';
|
|
22
21
|
import { ClaudeCodeContextMonitorProvider } from './context-monitor.js';
|
|
23
22
|
import { ClaudeCodeHookProvider } from './hooks.js';
|
|
@@ -27,8 +26,6 @@ import { ClaudeCodeSpawnProvider } from './spawn.js';
|
|
|
27
26
|
import { ClaudeCodeTaskSyncProvider } from './task-sync.js';
|
|
28
27
|
import { ClaudeCodeTransportProvider } from './transport.js';
|
|
29
28
|
|
|
30
|
-
const execAsync = promisify(exec);
|
|
31
|
-
|
|
32
29
|
/**
|
|
33
30
|
* CLEO provider adapter for Anthropic Claude Code CLI.
|
|
34
31
|
*
|
|
@@ -202,13 +199,11 @@ export class ClaudeCodeAdapter implements CLEOProviderAdapter {
|
|
|
202
199
|
|
|
203
200
|
// Check Claude CLI availability
|
|
204
201
|
let cliAvailable = false;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
details.cliAvailable = false;
|
|
211
|
-
}
|
|
202
|
+
// In-process PATH/PATHEXT lookup: there is no `which` on Windows (T12604).
|
|
203
|
+
const resolvedCli = findOnPath('claude');
|
|
204
|
+
cliAvailable = resolvedCli !== null;
|
|
205
|
+
if (resolvedCli) details.cliPath = resolvedCli;
|
|
206
|
+
else details.cliAvailable = false;
|
|
212
207
|
|
|
213
208
|
// Check for Claude Code config directory
|
|
214
209
|
const claudeConfigDir = join(homedir(), '.claude');
|
|
@@ -13,9 +13,7 @@
|
|
|
13
13
|
* @task T11998 — per-session scope/pgid suite containment
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
17
|
-
import { unlink, writeFile } from 'node:fs/promises';
|
|
18
|
-
import { promisify } from 'node:util';
|
|
16
|
+
import { spawn as nodeSpawn } from 'node:child_process';
|
|
19
17
|
import type {
|
|
20
18
|
AdapterSpawnProvider,
|
|
21
19
|
AgentSuiteOwnership,
|
|
@@ -23,11 +21,11 @@ import type {
|
|
|
23
21
|
SpawnResult,
|
|
24
22
|
} from '@cleocode/contracts';
|
|
25
23
|
import { getErrorMessage } from '@cleocode/contracts';
|
|
24
|
+
import { findOnPath } from '@cleocode/paths';
|
|
26
25
|
import { buildAgentSpawnArgs } from '../shared/agent-spawn-wrapper.js';
|
|
26
|
+
import { removeSpawnPromptFile, writeSpawnPromptFile } from '../shared/prompt-file.js';
|
|
27
27
|
import { reapAgentSuite } from './suite-reaper.js';
|
|
28
28
|
|
|
29
|
-
const execAsync = promisify(exec);
|
|
30
|
-
|
|
31
29
|
/** Internal tracking entry for a spawned process. */
|
|
32
30
|
interface TrackedProcess {
|
|
33
31
|
pid: number;
|
|
@@ -48,7 +46,7 @@ interface TrackedProcess {
|
|
|
48
46
|
* @remarks
|
|
49
47
|
* The provider uses `--allow-insecure --no-upgrade-check` flags to
|
|
50
48
|
* ensure the Claude CLI starts without interactive prompts. Prompts are
|
|
51
|
-
* written to temporary files under
|
|
49
|
+
* written to temporary files under a private temp directory (`os.tmpdir()`) and cleaned up after the
|
|
52
50
|
* child process exits. Processes are tracked by instance ID in an
|
|
53
51
|
* in-memory map and verified via `kill(pid, 0)` liveness checks.
|
|
54
52
|
*/
|
|
@@ -59,15 +57,10 @@ export class ClaudeCodeSpawnProvider implements AdapterSpawnProvider {
|
|
|
59
57
|
/**
|
|
60
58
|
* Check if the Claude CLI is available in PATH.
|
|
61
59
|
*
|
|
62
|
-
* @returns true if `claude` is found
|
|
60
|
+
* @returns true if `claude` is found on PATH (PATHEXT-aware on Windows)
|
|
63
61
|
*/
|
|
64
62
|
async canSpawn(): Promise<boolean> {
|
|
65
|
-
|
|
66
|
-
await execAsync('which claude');
|
|
67
|
-
return true;
|
|
68
|
-
} catch {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
63
|
+
return findOnPath('claude') !== null;
|
|
71
64
|
}
|
|
72
65
|
|
|
73
66
|
/**
|
|
@@ -103,8 +96,7 @@ export class ClaudeCodeSpawnProvider implements AdapterSpawnProvider {
|
|
|
103
96
|
// CANT enrichment unavailable — use raw prompt
|
|
104
97
|
}
|
|
105
98
|
|
|
106
|
-
tmpFile =
|
|
107
|
-
await writeFile(tmpFile, enrichedPrompt, 'utf-8');
|
|
99
|
+
tmpFile = await writeSpawnPromptFile('claude-spawn', enrichedPrompt);
|
|
108
100
|
|
|
109
101
|
// --print: non-interactive batch mode (process prompt, output response, exit)
|
|
110
102
|
// --dangerously-skip-permissions: allow all tool calls without human approval
|
|
@@ -131,6 +123,8 @@ export class ClaudeCodeSpawnProvider implements AdapterSpawnProvider {
|
|
|
131
123
|
const spawnOpts: Parameters<typeof nodeSpawn>[2] = {
|
|
132
124
|
detached: !isSystemd,
|
|
133
125
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
126
|
+
// win32 `.cmd` shim routed through cmd.exe: keep its quoting (T12618).
|
|
127
|
+
...(spawnBuild.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
134
128
|
};
|
|
135
129
|
|
|
136
130
|
if (context.workingDirectory) {
|
|
@@ -177,11 +171,7 @@ export class ClaudeCodeSpawnProvider implements AdapterSpawnProvider {
|
|
|
177
171
|
const capturedTmpFile = tmpFile;
|
|
178
172
|
child.on('exit', async () => {
|
|
179
173
|
this.processMap.delete(instanceId);
|
|
180
|
-
|
|
181
|
-
await unlink(capturedTmpFile);
|
|
182
|
-
} catch {
|
|
183
|
-
// Ignore cleanup errors
|
|
184
|
-
}
|
|
174
|
+
await removeSpawnPromptFile(capturedTmpFile);
|
|
185
175
|
});
|
|
186
176
|
|
|
187
177
|
return {
|
|
@@ -199,11 +189,7 @@ export class ClaudeCodeSpawnProvider implements AdapterSpawnProvider {
|
|
|
199
189
|
console.error(`[ClaudeCodeSpawnProvider] Failed to spawn: ${getErrorMessage(error)}`);
|
|
200
190
|
|
|
201
191
|
if (tmpFile) {
|
|
202
|
-
|
|
203
|
-
await unlink(tmpFile);
|
|
204
|
-
} catch {
|
|
205
|
-
// Ignore cleanup errors
|
|
206
|
-
}
|
|
192
|
+
await removeSpawnPromptFile(tmpFile);
|
|
207
193
|
}
|
|
208
194
|
|
|
209
195
|
return {
|
|
@@ -56,7 +56,7 @@ export class ClaudeSDKSpawnProvider implements AdapterSpawnProvider {
|
|
|
56
56
|
*
|
|
57
57
|
* Uses 3-tier key resolution so the provider works with:
|
|
58
58
|
* - `ANTHROPIC_API_KEY` environment variable (explicit)
|
|
59
|
-
* -
|
|
59
|
+
* - `<getCleoHome()>/anthropic-key` (user-stored via cleo config)
|
|
60
60
|
* - Claude Code OAuth token (zero-config for Claude Code users)
|
|
61
61
|
*
|
|
62
62
|
* @returns `true` when any Anthropic credential is available
|
|
@@ -8,21 +8,18 @@
|
|
|
8
8
|
* @epic T134
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { exec } from 'node:child_process';
|
|
12
11
|
import { existsSync } from 'node:fs';
|
|
13
12
|
import { homedir } from 'node:os';
|
|
14
13
|
import { join } from 'node:path';
|
|
15
|
-
import { promisify } from 'node:util';
|
|
16
14
|
import type {
|
|
17
15
|
AdapterCapabilities,
|
|
18
16
|
AdapterHealthStatus,
|
|
19
17
|
CLEOProviderAdapter,
|
|
20
18
|
} from '@cleocode/contracts';
|
|
19
|
+
import { findOnPath } from '@cleocode/paths';
|
|
21
20
|
import { CodexHookProvider } from './hooks.js';
|
|
22
21
|
import { CodexInstallProvider } from './install.js';
|
|
23
22
|
|
|
24
|
-
const execAsync = promisify(exec);
|
|
25
|
-
|
|
26
23
|
/**
|
|
27
24
|
* CLEO provider adapter for OpenAI Codex CLI.
|
|
28
25
|
*
|
|
@@ -124,13 +121,11 @@ export class CodexAdapter implements CLEOProviderAdapter {
|
|
|
124
121
|
|
|
125
122
|
// Check Codex CLI availability
|
|
126
123
|
let cliAvailable = false;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
details.cliAvailable = false;
|
|
133
|
-
}
|
|
124
|
+
// In-process PATH/PATHEXT lookup: there is no `which` on Windows (T12604).
|
|
125
|
+
const resolvedCli = findOnPath('codex');
|
|
126
|
+
cliAvailable = resolvedCli !== null;
|
|
127
|
+
if (resolvedCli) details.cliPath = resolvedCli;
|
|
128
|
+
else details.cliAvailable = false;
|
|
134
129
|
|
|
135
130
|
// Check for Codex CLI config directory
|
|
136
131
|
const codexConfigDir = join(homedir(), '.codex');
|
|
@@ -24,13 +24,12 @@
|
|
|
24
24
|
* @task T648
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import {
|
|
28
|
-
import { unlink, writeFile } from 'node:fs/promises';
|
|
29
|
-
import { promisify } from 'node:util';
|
|
27
|
+
import type { SpawnOptions } from 'node:child_process';
|
|
30
28
|
import type { AdapterSpawnProvider, SpawnContext, SpawnResult } from '@cleocode/contracts';
|
|
31
29
|
import { getErrorMessage } from '@cleocode/contracts';
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
import { findOnPath } from '@cleocode/paths';
|
|
31
|
+
import { spawnCli } from '../shared/cli-spawn.js';
|
|
32
|
+
import { removeSpawnPromptFile, writeSpawnPromptFile } from '../shared/prompt-file.js';
|
|
34
33
|
|
|
35
34
|
/** Internal tracking entry for a spawned process. */
|
|
36
35
|
interface TrackedProcess {
|
|
@@ -63,20 +62,16 @@ export class CodexSpawnProvider implements AdapterSpawnProvider {
|
|
|
63
62
|
/**
|
|
64
63
|
* Check if the Codex CLI is available in PATH.
|
|
65
64
|
*
|
|
66
|
-
* @returns `true` if `codex` is found
|
|
65
|
+
* @returns `true` if `codex` is found on PATH (PATHEXT-aware on Windows)
|
|
67
66
|
*/
|
|
68
67
|
async canSpawn(): Promise<boolean> {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
'Docs: https://github.com/openai/codex',
|
|
77
|
-
);
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
68
|
+
if (findOnPath('codex') !== null) return true;
|
|
69
|
+
console.warn(
|
|
70
|
+
'[CodexSpawnProvider] codex CLI not found. ' +
|
|
71
|
+
'Install: npm install -g @openai/codex ' +
|
|
72
|
+
'Docs: https://github.com/openai/codex',
|
|
73
|
+
);
|
|
74
|
+
return false;
|
|
80
75
|
}
|
|
81
76
|
|
|
82
77
|
/**
|
|
@@ -108,12 +103,11 @@ export class CodexSpawnProvider implements AdapterSpawnProvider {
|
|
|
108
103
|
// CANT enrichment unavailable — use raw prompt
|
|
109
104
|
}
|
|
110
105
|
|
|
111
|
-
tmpFile =
|
|
112
|
-
await writeFile(tmpFile, enrichedPrompt, 'utf-8');
|
|
106
|
+
tmpFile = await writeSpawnPromptFile('codex-spawn', enrichedPrompt);
|
|
113
107
|
|
|
114
108
|
// --full-auto: non-interactive batch mode (auto-approve all actions)
|
|
115
109
|
const args = ['--full-auto', tmpFile];
|
|
116
|
-
const spawnOpts:
|
|
110
|
+
const spawnOpts: SpawnOptions = {
|
|
117
111
|
detached: true,
|
|
118
112
|
stdio: 'ignore',
|
|
119
113
|
};
|
|
@@ -122,7 +116,7 @@ export class CodexSpawnProvider implements AdapterSpawnProvider {
|
|
|
122
116
|
spawnOpts.cwd = context.workingDirectory;
|
|
123
117
|
}
|
|
124
118
|
|
|
125
|
-
const child =
|
|
119
|
+
const child = spawnCli('codex', args, spawnOpts);
|
|
126
120
|
child.unref();
|
|
127
121
|
|
|
128
122
|
if (child.pid) {
|
|
@@ -136,11 +130,7 @@ export class CodexSpawnProvider implements AdapterSpawnProvider {
|
|
|
136
130
|
const capturedTmpFile = tmpFile;
|
|
137
131
|
child.on('exit', async () => {
|
|
138
132
|
this.processMap.delete(instanceId);
|
|
139
|
-
|
|
140
|
-
await unlink(capturedTmpFile);
|
|
141
|
-
} catch {
|
|
142
|
-
// Ignore cleanup errors
|
|
143
|
-
}
|
|
133
|
+
await removeSpawnPromptFile(capturedTmpFile);
|
|
144
134
|
});
|
|
145
135
|
|
|
146
136
|
return {
|
|
@@ -154,11 +144,7 @@ export class CodexSpawnProvider implements AdapterSpawnProvider {
|
|
|
154
144
|
console.error(`[CodexSpawnProvider] Failed to spawn: ${getErrorMessage(error)}`);
|
|
155
145
|
|
|
156
146
|
if (tmpFile) {
|
|
157
|
-
|
|
158
|
-
await unlink(tmpFile);
|
|
159
|
-
} catch {
|
|
160
|
-
// Ignore cleanup errors
|
|
161
|
-
}
|
|
147
|
+
await removeSpawnPromptFile(tmpFile);
|
|
162
148
|
}
|
|
163
149
|
|
|
164
150
|
return {
|
|
@@ -8,21 +8,18 @@
|
|
|
8
8
|
* @epic T134
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { exec } from 'node:child_process';
|
|
12
11
|
import { existsSync } from 'node:fs';
|
|
13
12
|
import { homedir } from 'node:os';
|
|
14
13
|
import { join } from 'node:path';
|
|
15
|
-
import { promisify } from 'node:util';
|
|
16
14
|
import type {
|
|
17
15
|
AdapterCapabilities,
|
|
18
16
|
AdapterHealthStatus,
|
|
19
17
|
CLEOProviderAdapter,
|
|
20
18
|
} from '@cleocode/contracts';
|
|
19
|
+
import { findOnPath } from '@cleocode/paths';
|
|
21
20
|
import { GeminiCliHookProvider } from './hooks.js';
|
|
22
21
|
import { GeminiCliInstallProvider } from './install.js';
|
|
23
22
|
|
|
24
|
-
const execAsync = promisify(exec);
|
|
25
|
-
|
|
26
23
|
/**
|
|
27
24
|
* CLEO provider adapter for Google Gemini CLI.
|
|
28
25
|
*
|
|
@@ -136,13 +133,11 @@ export class GeminiCliAdapter implements CLEOProviderAdapter {
|
|
|
136
133
|
|
|
137
134
|
// Check Gemini CLI availability
|
|
138
135
|
let cliAvailable = false;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
details.cliAvailable = false;
|
|
145
|
-
}
|
|
136
|
+
// In-process PATH/PATHEXT lookup: there is no `which` on Windows (T12604).
|
|
137
|
+
const resolvedCli = findOnPath('gemini');
|
|
138
|
+
cliAvailable = resolvedCli !== null;
|
|
139
|
+
if (resolvedCli) details.cliPath = resolvedCli;
|
|
140
|
+
else details.cliAvailable = false;
|
|
146
141
|
|
|
147
142
|
// Check for Gemini CLI config directory
|
|
148
143
|
const geminiConfigDir = join(homedir(), '.gemini');
|
|
@@ -25,12 +25,11 @@
|
|
|
25
25
|
* @task T648
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import {
|
|
29
|
-
import { promisify } from 'node:util';
|
|
28
|
+
import type { SpawnOptions } from 'node:child_process';
|
|
30
29
|
import type { AdapterSpawnProvider, SpawnContext, SpawnResult } from '@cleocode/contracts';
|
|
31
30
|
import { getErrorMessage } from '@cleocode/contracts';
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
import { findOnPath } from '@cleocode/paths';
|
|
32
|
+
import { spawnCli } from '../shared/cli-spawn.js';
|
|
34
33
|
|
|
35
34
|
/** Default Gemini model for subagent spawns. */
|
|
36
35
|
const DEFAULT_MODEL = 'gemini-2.5-pro';
|
|
@@ -66,20 +65,16 @@ export class GeminiCliSpawnProvider implements AdapterSpawnProvider {
|
|
|
66
65
|
/**
|
|
67
66
|
* Check if the Gemini CLI is available in PATH.
|
|
68
67
|
*
|
|
69
|
-
* @returns `true` if `gemini` is found
|
|
68
|
+
* @returns `true` if `gemini` is found on PATH (PATHEXT-aware on Windows)
|
|
70
69
|
*/
|
|
71
70
|
async canSpawn(): Promise<boolean> {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
'Docs: https://github.com/google-gemini/gemini-cli',
|
|
80
|
-
);
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
71
|
+
if (findOnPath('gemini') !== null) return true;
|
|
72
|
+
console.warn(
|
|
73
|
+
'[GeminiCliSpawnProvider] gemini CLI not found. ' +
|
|
74
|
+
'Install: npm install -g @google/gemini-cli ' +
|
|
75
|
+
'Docs: https://github.com/google-gemini/gemini-cli',
|
|
76
|
+
);
|
|
77
|
+
return false;
|
|
83
78
|
}
|
|
84
79
|
|
|
85
80
|
/**
|
|
@@ -116,7 +111,7 @@ export class GeminiCliSpawnProvider implements AdapterSpawnProvider {
|
|
|
116
111
|
// --model: select the Gemini model variant
|
|
117
112
|
// Prompt is supplied via stdin (pipe)
|
|
118
113
|
const args = ['--yolo', '--model', model];
|
|
119
|
-
const spawnOpts:
|
|
114
|
+
const spawnOpts: SpawnOptions = {
|
|
120
115
|
detached: true,
|
|
121
116
|
stdio: ['pipe', 'ignore', 'ignore'],
|
|
122
117
|
};
|
|
@@ -125,7 +120,7 @@ export class GeminiCliSpawnProvider implements AdapterSpawnProvider {
|
|
|
125
120
|
spawnOpts.cwd = context.workingDirectory;
|
|
126
121
|
}
|
|
127
122
|
|
|
128
|
-
const child =
|
|
123
|
+
const child = spawnCli('gemini', args, spawnOpts);
|
|
129
124
|
|
|
130
125
|
// Write the prompt to stdin then close so the CLI receives it.
|
|
131
126
|
if (child.stdin) {
|
|
@@ -9,21 +9,18 @@
|
|
|
9
9
|
* @epic T134
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { exec } from 'node:child_process';
|
|
13
12
|
import { existsSync } from 'node:fs';
|
|
14
13
|
import { homedir } from 'node:os';
|
|
15
14
|
import { join } from 'node:path';
|
|
16
|
-
import { promisify } from 'node:util';
|
|
17
15
|
import type {
|
|
18
16
|
AdapterCapabilities,
|
|
19
17
|
AdapterHealthStatus,
|
|
20
18
|
CLEOProviderAdapter,
|
|
21
19
|
} from '@cleocode/contracts';
|
|
20
|
+
import { findOnPath } from '@cleocode/paths';
|
|
22
21
|
import { KimiHookProvider } from './hooks.js';
|
|
23
22
|
import { KimiInstallProvider } from './install.js';
|
|
24
23
|
|
|
25
|
-
const execAsync = promisify(exec);
|
|
26
|
-
|
|
27
24
|
/**
|
|
28
25
|
* CLEO provider adapter for Moonshot AI Kimi.
|
|
29
26
|
*
|
|
@@ -123,13 +120,11 @@ export class KimiAdapter implements CLEOProviderAdapter {
|
|
|
123
120
|
|
|
124
121
|
// Check Kimi CLI availability
|
|
125
122
|
let cliAvailable = false;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
details.cliAvailable = false;
|
|
132
|
-
}
|
|
123
|
+
// In-process PATH/PATHEXT lookup: there is no `which` on Windows (T12604).
|
|
124
|
+
const resolvedCli = findOnPath('kimi');
|
|
125
|
+
cliAvailable = resolvedCli !== null;
|
|
126
|
+
if (resolvedCli) details.cliPath = resolvedCli;
|
|
127
|
+
else details.cliAvailable = false;
|
|
133
128
|
|
|
134
129
|
// Check for Kimi config directory
|
|
135
130
|
const kimiConfigDir = join(homedir(), '.kimi');
|