@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
|
@@ -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 {
|
|
@@ -45,6 +45,13 @@ vi.mock('@cleocode/caamp', () => ({
|
|
|
45
45
|
action: 'added',
|
|
46
46
|
providerId: 'cursor',
|
|
47
47
|
}),
|
|
48
|
+
// T12385: Cursor's rule files and hooks.json go through the CAAMP writers.
|
|
49
|
+
ensureProviderRuleFile: vi.fn().mockResolvedValue({
|
|
50
|
+
filePath: '/tmp/test-project/.cursor/rules/cleo.mdc',
|
|
51
|
+
action: 'created',
|
|
52
|
+
providerId: 'cursor',
|
|
53
|
+
}),
|
|
54
|
+
updateJsonConfigFile: vi.fn().mockResolvedValue(true),
|
|
48
55
|
}));
|
|
49
56
|
|
|
50
57
|
describe('CursorAdapter', () => {
|
|
@@ -95,4 +95,40 @@ describe('CursorInstallProvider — PreCompact hook templates', () => {
|
|
|
95
95
|
};
|
|
96
96
|
expect((config.hooks?.preCompact ?? []).length).toBe(1);
|
|
97
97
|
});
|
|
98
|
+
|
|
99
|
+
it('T12385: reports a malformed hooks.json and leaves it byte-identical', async () => {
|
|
100
|
+
const hooksJsonPath = join(projectDir, '.cursor', 'hooks.json');
|
|
101
|
+
mkdirSync(join(projectDir, '.cursor'), { recursive: true });
|
|
102
|
+
const malformed = '{ "hooks": { "preCompact": [ ';
|
|
103
|
+
writeFileSync(hooksJsonPath, malformed, 'utf-8');
|
|
104
|
+
|
|
105
|
+
const result = await new CursorInstallProvider().install({ projectDir });
|
|
106
|
+
|
|
107
|
+
expect(readFileSync(hooksJsonPath, 'utf-8')).toBe(malformed);
|
|
108
|
+
expect(result.success).toBe(false);
|
|
109
|
+
expect(String(result.details?.hooksError)).toMatch(/not a valid JSON object/);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('T12385: writes rule files through the CAAMP writer (managed block, registry references)', async () => {
|
|
113
|
+
const legacyPath = join(projectDir, '.cursorrules');
|
|
114
|
+
// A pre-CAAMP .cursorrules with the old bare reference lines appended.
|
|
115
|
+
writeFileSync(
|
|
116
|
+
legacyPath,
|
|
117
|
+
'# Project Rules\nUse TypeScript.\n@~/.cleo/templates/CLEO-INJECTION.md\n@.cleo/memory-bridge.md\n',
|
|
118
|
+
'utf-8',
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
await new CursorInstallProvider().install({ projectDir });
|
|
122
|
+
|
|
123
|
+
const legacy = readFileSync(legacyPath, 'utf-8');
|
|
124
|
+
expect(legacy).toContain('# Project Rules\nUse TypeScript.');
|
|
125
|
+
expect(legacy).toContain('<!-- CAAMP:START -->');
|
|
126
|
+
expect(legacy.match(/CLEO-INJECTION\.md/g)).toHaveLength(1);
|
|
127
|
+
|
|
128
|
+
const mdc = readFileSync(join(projectDir, '.cursor', 'rules', 'cleo.mdc'), 'utf-8');
|
|
129
|
+
expect(mdc.startsWith('---\n')).toBe(true);
|
|
130
|
+
expect(mdc).toContain('alwaysApply: true');
|
|
131
|
+
expect(mdc).toContain('<!-- CAAMP:START -->');
|
|
132
|
+
expect(mdc).toContain('@.cleo/memory-bridge.md');
|
|
133
|
+
});
|
|
98
134
|
});
|
|
@@ -16,9 +16,14 @@
|
|
|
16
16
|
* @task T1013
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { existsSync,
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
type EnsureProviderRuleFileResult,
|
|
23
|
+
ensureProviderInstructionFile,
|
|
24
|
+
ensureProviderRuleFile,
|
|
25
|
+
updateJsonConfigFile,
|
|
26
|
+
} from '@cleocode/caamp';
|
|
22
27
|
import type { AdapterInstallProvider, InstallOptions, InstallResult } from '@cleocode/contracts';
|
|
23
28
|
import {
|
|
24
29
|
type InstallHookTemplatesResult,
|
|
@@ -26,6 +31,21 @@ import {
|
|
|
26
31
|
} from '../shared/hook-template-installer.js';
|
|
27
32
|
import { getCleoTemplatesTildePath } from '../shared/paths.js';
|
|
28
33
|
|
|
34
|
+
/** MDC frontmatter for the CLEO-owned `.cursor/rules/cleo.mdc` rule file. */
|
|
35
|
+
const CLEO_MDC_FRONTMATTER = [
|
|
36
|
+
'---',
|
|
37
|
+
'description: CLEO task management protocol references',
|
|
38
|
+
'globs: "**/*"',
|
|
39
|
+
'alwaysApply: true',
|
|
40
|
+
'---',
|
|
41
|
+
'',
|
|
42
|
+
].join('\n');
|
|
43
|
+
|
|
44
|
+
/** Whether a parsed JSON value is a plain object. */
|
|
45
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
46
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
29
49
|
/**
|
|
30
50
|
* Install provider for Cursor.
|
|
31
51
|
*
|
|
@@ -57,7 +77,7 @@ export class CursorInstallProvider implements AdapterInstallProvider {
|
|
|
57
77
|
// ensureProviderInstructionFile handles the primary AGENTS.md (registry instructFile);
|
|
58
78
|
// updateInstructionFiles also manages cursor-specific MDC + legacy .cursorrules formats.
|
|
59
79
|
const instructionResult = await ensureProviderInstructionFile('cursor', projectDir, {});
|
|
60
|
-
const cursorFilesUpdated = this.updateInstructionFiles(projectDir);
|
|
80
|
+
const cursorFilesUpdated = await this.updateInstructionFiles(projectDir);
|
|
61
81
|
instructionFileUpdated = instructionResult.action !== 'intact' || cursorFilesUpdated;
|
|
62
82
|
if (instructionFileUpdated) {
|
|
63
83
|
details.instructionFiles = this.getUpdatedFileList(projectDir);
|
|
@@ -65,13 +85,21 @@ export class CursorInstallProvider implements AdapterInstallProvider {
|
|
|
65
85
|
|
|
66
86
|
// Step 2 (T1013): Install PreCompact hook templates + wire the handler
|
|
67
87
|
// command into .cursor/hooks.json's `preCompact` event.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
88
|
+
// T12385: a malformed hooks.json aborts the write and is reported; it is
|
|
89
|
+
// never reset to `{}`.
|
|
90
|
+
let hooksError: string | null = null;
|
|
91
|
+
try {
|
|
92
|
+
const hookResult = await this.installHookTemplates(projectDir);
|
|
93
|
+
if (hookResult) {
|
|
94
|
+
details.hookTemplates = hookResult;
|
|
95
|
+
}
|
|
96
|
+
} catch (err) {
|
|
97
|
+
hooksError = err instanceof Error ? err.message : String(err);
|
|
98
|
+
details.hooksError = hooksError;
|
|
71
99
|
}
|
|
72
100
|
|
|
73
101
|
return {
|
|
74
|
-
success:
|
|
102
|
+
success: hooksError === null,
|
|
75
103
|
installedAt,
|
|
76
104
|
instructionFileUpdated,
|
|
77
105
|
details,
|
|
@@ -127,102 +155,37 @@ export class CursorInstallProvider implements AdapterInstallProvider {
|
|
|
127
155
|
*/
|
|
128
156
|
async ensureInstructionReferences(projectDir: string): Promise<void> {
|
|
129
157
|
await ensureProviderInstructionFile('cursor', projectDir, {});
|
|
130
|
-
this.updateInstructionFiles(projectDir);
|
|
158
|
+
await this.updateInstructionFiles(projectDir);
|
|
131
159
|
}
|
|
132
160
|
|
|
133
161
|
/**
|
|
134
|
-
* Update
|
|
135
|
-
*
|
|
136
|
-
* Handles both legacy (.cursorrules) and modern (.cursor/rules/cleo.mdc) formats.
|
|
162
|
+
* Update Cursor's auxiliary rule files through the CAAMP writer.
|
|
137
163
|
*
|
|
138
|
-
* @
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
// Update legacy .cursorrules if it exists
|
|
144
|
-
if (this.updateLegacyRules(projectDir)) {
|
|
145
|
-
updated = true;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Create/update modern .cursor/rules/cleo.mdc
|
|
149
|
-
if (this.updateModernRules(projectDir)) {
|
|
150
|
-
updated = true;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return updated;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Update legacy .cursorrules file with @-references.
|
|
158
|
-
*
|
|
159
|
-
* References are sourced from the CAAMP provider registry (T1919) rather than
|
|
160
|
-
* a local constant, keeping this adapter in sync with the single source of truth.
|
|
161
|
-
* Only modifies the file if it already exists (does not create it).
|
|
162
|
-
*
|
|
163
|
-
* @returns true if the file was modified
|
|
164
|
-
*/
|
|
165
|
-
private updateLegacyRules(projectDir: string): boolean {
|
|
166
|
-
const rulesPath = join(projectDir, '.cursorrules');
|
|
167
|
-
if (!existsSync(rulesPath)) {
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
let content = readFileSync(rulesPath, 'utf-8');
|
|
172
|
-
const cursorRefs = [
|
|
173
|
-
`@${getCleoTemplatesTildePath()}/CLEO-INJECTION.md`,
|
|
174
|
-
'@.cleo/memory-bridge.md',
|
|
175
|
-
];
|
|
176
|
-
const missingRefs = cursorRefs.filter((ref) => !content.includes(ref));
|
|
177
|
-
|
|
178
|
-
if (missingRefs.length === 0) {
|
|
179
|
-
return false;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const separator = content.endsWith('\n') ? '' : '\n';
|
|
183
|
-
content = content + separator + missingRefs.join('\n') + '\n';
|
|
184
|
-
writeFileSync(rulesPath, content, 'utf-8');
|
|
185
|
-
return true;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Create or update .cursor/rules/cleo.mdc with CLEO references.
|
|
164
|
+
* @remarks
|
|
165
|
+
* ADR-064 / T12385: both files are written by CAAMP's
|
|
166
|
+
* `ensureProviderRuleFile` — locked, atomic, with references taken from the
|
|
167
|
+
* provider registry — instead of raw `writeFileSync` with a local copy of
|
|
168
|
+
* the references.
|
|
190
169
|
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
170
|
+
* - Legacy `.cursorrules` is only updated when it already exists; CLEO's
|
|
171
|
+
* references live in a CAAMP block and user text is preserved.
|
|
172
|
+
* - Modern `.cursor/rules/cleo.mdc` is CLEO-owned: MDC frontmatter followed
|
|
173
|
+
* by the CAAMP block.
|
|
194
174
|
*
|
|
195
|
-
* @returns true if
|
|
175
|
+
* @returns true if any file was created or modified
|
|
196
176
|
*/
|
|
197
|
-
private
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
'
|
|
177
|
+
private async updateInstructionFiles(projectDir: string): Promise<boolean> {
|
|
178
|
+
const results: Array<EnsureProviderRuleFileResult | null> = [
|
|
179
|
+
await ensureProviderRuleFile('cursor', projectDir, {
|
|
180
|
+
relativePath: '.cursorrules',
|
|
181
|
+
onlyIfExists: true,
|
|
182
|
+
}),
|
|
183
|
+
await ensureProviderRuleFile('cursor', projectDir, {
|
|
184
|
+
relativePath: join('.cursor', 'rules', 'cleo.mdc'),
|
|
185
|
+
preamble: CLEO_MDC_FRONTMATTER,
|
|
186
|
+
}),
|
|
204
187
|
];
|
|
205
|
-
|
|
206
|
-
'---',
|
|
207
|
-
'description: CLEO task management protocol references',
|
|
208
|
-
'globs: "**/*"',
|
|
209
|
-
'alwaysApply: true',
|
|
210
|
-
'---',
|
|
211
|
-
'',
|
|
212
|
-
...cursorRefs,
|
|
213
|
-
'',
|
|
214
|
-
].join('\n');
|
|
215
|
-
|
|
216
|
-
if (existsSync(mdcPath)) {
|
|
217
|
-
const existing = readFileSync(mdcPath, 'utf-8');
|
|
218
|
-
if (existing === expectedContent) {
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
mkdirSync(rulesDir, { recursive: true });
|
|
224
|
-
writeFileSync(mdcPath, expectedContent, 'utf-8');
|
|
225
|
-
return true;
|
|
188
|
+
return results.some((r) => r !== null && r.action !== 'intact');
|
|
226
189
|
}
|
|
227
190
|
|
|
228
191
|
/**
|
|
@@ -255,10 +218,10 @@ export class CursorInstallProvider implements AdapterInstallProvider {
|
|
|
255
218
|
*
|
|
256
219
|
* @task T1013
|
|
257
220
|
*/
|
|
258
|
-
private installHookTemplates(projectDir: string): {
|
|
221
|
+
private async installHookTemplates(projectDir: string): Promise<{
|
|
259
222
|
templates: InstallHookTemplatesResult;
|
|
260
223
|
hooksJsonEntryAdded: boolean;
|
|
261
|
-
} | null {
|
|
224
|
+
} | null> {
|
|
262
225
|
const hooksDir = join(projectDir, '.cursor', 'hooks');
|
|
263
226
|
|
|
264
227
|
// Template copy is best-effort so missing/locked filesystems (CI sandboxes,
|
|
@@ -273,7 +236,7 @@ export class CursorInstallProvider implements AdapterInstallProvider {
|
|
|
273
236
|
return null;
|
|
274
237
|
}
|
|
275
238
|
|
|
276
|
-
const hooksJsonEntryAdded = this.registerPreCompactHook(
|
|
239
|
+
const hooksJsonEntryAdded = await this.registerPreCompactHook(
|
|
277
240
|
projectDir,
|
|
278
241
|
join(hooksDir, 'precompact.sh'),
|
|
279
242
|
);
|
|
@@ -298,45 +261,35 @@ export class CursorInstallProvider implements AdapterInstallProvider {
|
|
|
298
261
|
*
|
|
299
262
|
* @task T1013
|
|
300
263
|
*/
|
|
301
|
-
private registerPreCompactHook(projectDir: string, shimPath: string): boolean {
|
|
264
|
+
private async registerPreCompactHook(projectDir: string, shimPath: string): Promise<boolean> {
|
|
302
265
|
const hooksJsonPath = join(projectDir, '.cursor', 'hooks.json');
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
266
|
+
return updateJsonConfigFile(hooksJsonPath, (config) => {
|
|
267
|
+
const hooks = config.hooks ?? {};
|
|
268
|
+
if (!isRecord(hooks)) {
|
|
269
|
+
throw new Error('.cursor/hooks.json "hooks" is not an object; not modifying it');
|
|
270
|
+
}
|
|
271
|
+
const entries = hooks.preCompact ?? [];
|
|
272
|
+
if (!Array.isArray(entries)) {
|
|
273
|
+
throw new Error('.cursor/hooks.json "hooks.preCompact" is not an array; not modifying it');
|
|
310
274
|
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const hooks = (config.hooks as Record<string, unknown[]> | undefined) ?? {};
|
|
314
|
-
const entries = (hooks.preCompact as unknown[] | undefined) ?? [];
|
|
315
|
-
|
|
316
|
-
const alreadyWired = entries.some(
|
|
317
|
-
(entry) =>
|
|
318
|
-
typeof entry === 'object' &&
|
|
319
|
-
entry !== null &&
|
|
320
|
-
typeof (entry as Record<string, unknown>).command === 'string' &&
|
|
321
|
-
((entry as Record<string, unknown>).command as string).includes('# cleo-hook') &&
|
|
322
|
-
((entry as Record<string, unknown>).command as string).includes('precompact.sh'),
|
|
323
|
-
);
|
|
324
275
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
276
|
+
const alreadyWired = entries.some(
|
|
277
|
+
(entry) =>
|
|
278
|
+
isRecord(entry) &&
|
|
279
|
+
typeof entry.command === 'string' &&
|
|
280
|
+
entry.command.includes('# cleo-hook') &&
|
|
281
|
+
entry.command.includes('precompact.sh'),
|
|
282
|
+
);
|
|
283
|
+
if (alreadyWired) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
328
286
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
287
|
+
hooks.preCompact = [
|
|
288
|
+
...entries,
|
|
289
|
+
{ type: 'command', command: `"${shimPath}" # cleo-hook`, timeout: 30 },
|
|
290
|
+
];
|
|
291
|
+
config.hooks = hooks;
|
|
292
|
+
return true;
|
|
333
293
|
});
|
|
334
|
-
|
|
335
|
-
hooks.preCompact = entries;
|
|
336
|
-
config.hooks = hooks;
|
|
337
|
-
|
|
338
|
-
mkdirSync(join(projectDir, '.cursor'), { recursive: true });
|
|
339
|
-
writeFileSync(hooksJsonPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
340
|
-
return true;
|
|
341
294
|
}
|
|
342
295
|
}
|
|
@@ -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');
|