@cleocode/adapters 2026.9.19 → 2026.9.21
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 +748 -594
- package/dist/index.js.map +3 -3
- package/dist/providers/claude-code/adapter.d.ts.map +1 -1
- package/dist/providers/claude-code/hooks.d.ts +18 -2
- package/dist/providers/claude-code/hooks.d.ts.map +1 -1
- package/dist/providers/claude-code/install.d.ts.map +1 -1
- package/dist/providers/claude-code/paths.d.ts +96 -0
- package/dist/providers/claude-code/paths.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/cursor/install.d.ts +11 -22
- package/dist/providers/cursor/install.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/__tests__/adapter.test.ts +1 -0
- package/src/providers/claude-code/__tests__/hooks-install.test.ts +63 -2
- package/src/providers/claude-code/adapter.ts +6 -11
- package/src/providers/claude-code/hooks.ts +60 -89
- package/src/providers/claude-code/install.ts +70 -96
- package/src/providers/claude-code/paths.ts +155 -0
- 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/cursor/__tests__/adapter.test.ts +7 -0
- package/src/providers/cursor/__tests__/hooks-install.test.ts +36 -0
- package/src/providers/cursor/install.ts +86 -133
- 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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Tests cover:
|
|
5
5
|
* - discoverCantFiles: finds .cant files, handles missing dirs
|
|
6
|
-
* - resolveThreeTierPaths:
|
|
6
|
+
* - resolveThreeTierPaths: tiers resolve through @cleocode/paths on every platform
|
|
7
7
|
* - discoverCantFilesMultiTier: 3-tier merge with override semantics
|
|
8
8
|
* - readMemoryBridge: reads file, handles missing/empty
|
|
9
9
|
* - buildMemoryBridgeBlock: wraps content in labeled section
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
17
|
-
import { tmpdir } from 'node:os';
|
|
17
|
+
import { homedir, tmpdir } from 'node:os';
|
|
18
18
|
import { join } from 'node:path';
|
|
19
|
-
import {
|
|
19
|
+
import { getCleoHome } from '@cleocode/paths';
|
|
20
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
20
21
|
|
|
21
22
|
import {
|
|
22
23
|
buildCantEnrichedPrompt,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
buildMentalModelInjection,
|
|
25
26
|
discoverCantFiles,
|
|
26
27
|
discoverCantFilesMultiTier,
|
|
28
|
+
readIdentityFile,
|
|
27
29
|
readMemoryBridge,
|
|
28
30
|
resolveThreeTierPaths,
|
|
29
31
|
} from '../cant-context.js';
|
|
@@ -68,33 +70,81 @@ describe('discoverCantFiles', () => {
|
|
|
68
70
|
// ---------------------------------------------------------------------------
|
|
69
71
|
|
|
70
72
|
describe('resolveThreeTierPaths', () => {
|
|
73
|
+
const PLATFORM = Object.getOwnPropertyDescriptor(process, 'platform');
|
|
74
|
+
|
|
75
|
+
function stubPlatform(platform: NodeJS.Platform): void {
|
|
76
|
+
Object.defineProperty(process, 'platform', { value: platform, configurable: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
vi.stubEnv('CLEO_HOME', undefined);
|
|
81
|
+
vi.stubEnv('CLEO_CONFIG_HOME', undefined);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
if (PLATFORM) Object.defineProperty(process, 'platform', PLATFORM);
|
|
86
|
+
vi.unstubAllEnvs();
|
|
87
|
+
});
|
|
88
|
+
|
|
71
89
|
it('returns project tier pointing to .cleo/cant/', () => {
|
|
72
90
|
const paths = resolveThreeTierPaths('/my/project');
|
|
73
91
|
expect(paths.project).toBe(join('/my/project', '.cleo', 'cant'));
|
|
74
92
|
});
|
|
75
93
|
|
|
76
|
-
it('
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
it('global tier is <getCleoHome()>/cant and user tier is <CLEO config>/cant', () => {
|
|
95
|
+
vi.stubEnv('CLEO_HOME', '/opt/cleo-data');
|
|
96
|
+
vi.stubEnv('CLEO_CONFIG_HOME', '/opt/cleo-config');
|
|
97
|
+
const paths = resolveThreeTierPaths('/my/project');
|
|
98
|
+
expect(paths.global).toBe(join('/opt/cleo-data', 'cant'));
|
|
99
|
+
expect(paths.user).toBe(join('/opt/cleo-config', 'cant'));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('macOS: global and user tiers are under ~/Library, not the XDG dirs (T12602)', () => {
|
|
103
|
+
stubPlatform('darwin');
|
|
104
|
+
vi.stubEnv('XDG_DATA_HOME', '/custom/data');
|
|
105
|
+
vi.stubEnv('XDG_CONFIG_HOME', '/custom/config');
|
|
106
|
+
const paths = resolveThreeTierPaths('/my/project');
|
|
107
|
+
expect(paths.global).toBe(join(getCleoHome(), 'cant'));
|
|
108
|
+
expect(paths.global).toBe(join(homedir(), 'Library', 'Application Support', 'cleo', 'cant'));
|
|
109
|
+
expect(paths.user).toBe(join(homedir(), 'Library', 'Preferences', 'cleo', 'cant'));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('Windows: global tier is under %LOCALAPPDATA% (T12602)', () => {
|
|
113
|
+
stubPlatform('win32');
|
|
114
|
+
vi.stubEnv('LOCALAPPDATA', 'C:\\Users\\me\\AppData\\Local');
|
|
115
|
+
vi.stubEnv('XDG_DATA_HOME', '/custom/data');
|
|
116
|
+
const paths = resolveThreeTierPaths('/my/project');
|
|
117
|
+
expect(paths.global).toBe(join(getCleoHome(), 'cant'));
|
|
118
|
+
expect(paths.global.startsWith('C:\\Users\\me\\AppData\\Local')).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('Linux: honours XDG_DATA_HOME and XDG_CONFIG_HOME', () => {
|
|
122
|
+
stubPlatform('linux');
|
|
123
|
+
vi.stubEnv('XDG_DATA_HOME', '/custom/data');
|
|
124
|
+
vi.stubEnv('XDG_CONFIG_HOME', '/custom/config');
|
|
125
|
+
const paths = resolveThreeTierPaths('/my/project');
|
|
126
|
+
expect(paths.global).toBe(join('/custom/data', 'cleo', 'cant'));
|
|
127
|
+
expect(paths.user).toBe(join('/custom/config', 'cleo', 'cant'));
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// readIdentityFile — global tier (T12602)
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
describe('readIdentityFile', () => {
|
|
136
|
+
afterEach(() => {
|
|
137
|
+
vi.unstubAllEnvs();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('reads the global CLEOOS-IDENTITY.md from getCleoHome(), not $XDG_DATA_HOME/cleo', () => {
|
|
141
|
+
const cleoHome = join(tempDir, 'cleo-home');
|
|
142
|
+
mkdirSync(cleoHome, { recursive: true });
|
|
143
|
+
writeFileSync(join(cleoHome, 'CLEOOS-IDENTITY.md'), 'global identity');
|
|
144
|
+
vi.stubEnv('CLEO_HOME', cleoHome);
|
|
145
|
+
vi.stubEnv('XDG_DATA_HOME', join(tempDir, 'empty-xdg'));
|
|
146
|
+
|
|
147
|
+
expect(readIdentityFile(join(tempDir, 'project'))).toBe('global identity');
|
|
98
148
|
});
|
|
99
149
|
});
|
|
100
150
|
|
|
@@ -103,22 +153,24 @@ describe('resolveThreeTierPaths', () => {
|
|
|
103
153
|
// ---------------------------------------------------------------------------
|
|
104
154
|
|
|
105
155
|
describe('discoverCantFilesMultiTier', () => {
|
|
106
|
-
let origXdgData: string | undefined;
|
|
107
|
-
let origXdgConfig: string | undefined;
|
|
108
|
-
|
|
109
156
|
beforeEach(() => {
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
process.env['XDG_DATA_HOME'] = join(tempDir, 'xdg-data');
|
|
114
|
-
process.env['XDG_CONFIG_HOME'] = join(tempDir, 'xdg-config');
|
|
157
|
+
// Point the global/user tiers at empty temp subdirs on every platform.
|
|
158
|
+
vi.stubEnv('CLEO_HOME', join(tempDir, 'cleo-home'));
|
|
159
|
+
vi.stubEnv('CLEO_CONFIG_HOME', join(tempDir, 'cleo-config'));
|
|
115
160
|
});
|
|
116
161
|
|
|
117
162
|
afterEach(() => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
163
|
+
vi.unstubAllEnvs();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('discovers files from the global tier under getCleoHome() (T12602)', () => {
|
|
167
|
+
const globalCant = join(tempDir, 'cleo-home', 'cant');
|
|
168
|
+
mkdirSync(globalCant, { recursive: true });
|
|
169
|
+
writeFileSync(join(globalCant, 'global.cant'), 'team: global');
|
|
170
|
+
|
|
171
|
+
const result = discoverCantFilesMultiTier(tempDir);
|
|
172
|
+
expect(result.stats.global).toBe(1);
|
|
173
|
+
expect(result.files).toEqual([join(globalCant, 'global.cant')]);
|
|
122
174
|
});
|
|
123
175
|
|
|
124
176
|
it('discovers files from project tier', () => {
|
|
@@ -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');
|
|
@@ -17,10 +17,11 @@
|
|
|
17
17
|
* @epic T1000
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
21
21
|
import { tmpdir } from 'node:os';
|
|
22
22
|
import { join } from 'node:path';
|
|
23
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
23
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
24
|
+
import { ClaudeCodeHookProvider } from '../hooks.js';
|
|
24
25
|
import { ClaudeCodeInstallProvider } from '../install.js';
|
|
25
26
|
|
|
26
27
|
describe('ClaudeCodeInstallProvider — PreCompact hook templates', () => {
|
|
@@ -129,4 +130,64 @@ describe('ClaudeCodeInstallProvider — PreCompact hook templates', () => {
|
|
|
129
130
|
};
|
|
130
131
|
expect((settings.hooks?.PreCompact ?? []).length).toBe(1);
|
|
131
132
|
});
|
|
133
|
+
|
|
134
|
+
// T12385 — a malformed settings.json used to be replaced with an object
|
|
135
|
+
// holding only CLEO's entries (the parse error was caught and the writer
|
|
136
|
+
// "started fresh"). It must now be reported and left byte-identical.
|
|
137
|
+
describe('T12385 — malformed settings.json is never rewritten', () => {
|
|
138
|
+
const malformed = '{\n "permissions": { "allow": ["Bash(ls)"] },\n "model": "opus",\n';
|
|
139
|
+
|
|
140
|
+
it('install reports the parse error and leaves the file byte-identical', async () => {
|
|
141
|
+
const settingsPath = join(fakeHome, '.claude', 'settings.json');
|
|
142
|
+
mkdirSync(join(fakeHome, '.claude'), { recursive: true });
|
|
143
|
+
writeFileSync(settingsPath, malformed, 'utf-8');
|
|
144
|
+
|
|
145
|
+
const result = await new ClaudeCodeInstallProvider().install({ projectDir });
|
|
146
|
+
|
|
147
|
+
expect(readFileSync(settingsPath, 'utf-8')).toBe(malformed);
|
|
148
|
+
expect(result.success).toBe(false);
|
|
149
|
+
const errors = (result.details?.settingsErrors ?? []) as string[];
|
|
150
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
151
|
+
expect(errors[0]).toMatch(/not a valid JSON object/);
|
|
152
|
+
expect(existsSync(`${settingsPath}.lock`)).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('hook registration reports the parse error and leaves the file byte-identical', async () => {
|
|
156
|
+
const settingsPath = join(fakeHome, '.claude', 'settings.json');
|
|
157
|
+
mkdirSync(join(fakeHome, '.claude'), { recursive: true });
|
|
158
|
+
writeFileSync(settingsPath, malformed, 'utf-8');
|
|
159
|
+
const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
160
|
+
|
|
161
|
+
const hooks = new ClaudeCodeHookProvider();
|
|
162
|
+
await hooks.registerNativeHooks(projectDir);
|
|
163
|
+
await hooks.unregisterNativeHooks();
|
|
164
|
+
|
|
165
|
+
stderr.mockRestore();
|
|
166
|
+
expect(readFileSync(settingsPath, 'utf-8')).toBe(malformed);
|
|
167
|
+
expect(hooks.getSettingsError()).toMatch(/not a valid JSON object/);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('preserves unrelated user settings when it does write', async () => {
|
|
171
|
+
const settingsPath = join(fakeHome, '.claude', 'settings.json');
|
|
172
|
+
mkdirSync(join(fakeHome, '.claude'), { recursive: true });
|
|
173
|
+
writeFileSync(
|
|
174
|
+
settingsPath,
|
|
175
|
+
JSON.stringify({ model: 'opus', hooks: { Stop: [{ matcher: 'x', hooks: [] }] } }),
|
|
176
|
+
'utf-8',
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
await new ClaudeCodeHookProvider().registerNativeHooks(projectDir);
|
|
180
|
+
await new ClaudeCodeInstallProvider().install({ projectDir });
|
|
181
|
+
|
|
182
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8')) as {
|
|
183
|
+
model?: string;
|
|
184
|
+
enabledPlugins?: Record<string, boolean>;
|
|
185
|
+
hooks?: Record<string, unknown[]>;
|
|
186
|
+
};
|
|
187
|
+
expect(settings.model).toBe('opus');
|
|
188
|
+
expect(settings.enabledPlugins?.['cleo@cleocode']).toBe(true);
|
|
189
|
+
expect(settings.hooks?.Stop).toHaveLength(2);
|
|
190
|
+
expect(settings.hooks?.PreCompact).toHaveLength(1);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
132
193
|
});
|
|
@@ -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');
|