@cleocode/adapters 2026.10.6 → 2026.10.7
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/heavy-command-hook.js +979 -0
- package/dist/heavy-command-hook.js.map +6 -0
- package/dist/index.js +179 -9
- package/dist/index.js.map +1 -1
- package/dist/providers/shared/heavy-command-hook-delivery.d.ts.map +1 -1
- package/dist/providers/shared/heavy-command-hook-install.d.ts +23 -0
- package/dist/providers/shared/heavy-command-hook-install.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/providers/shared/__tests__/heavy-command-hook-delivery.test.ts +16 -0
- package/src/providers/shared/__tests__/heavy-command-hook-install.test.ts +40 -0
- package/src/providers/shared/heavy-command-hook-delivery.ts +23 -0
- package/src/providers/shared/heavy-command-hook-install.ts +85 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"heavy-command-hook-delivery.d.ts","sourceRoot":"","sources":["../../../src/providers/shared/heavy-command-hook-delivery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAOH,OAAO,KAAK,EACV,oBAAoB,EACpB,wBAAwB,EACxB,iBAAiB,EACjB,oBAAoB,EACpB,wBAAwB,EACxB,wBAAwB,EACxB,mBAAmB,EACpB,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"heavy-command-hook-delivery.d.ts","sourceRoot":"","sources":["../../../src/providers/shared/heavy-command-hook-delivery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAOH,OAAO,KAAK,EACV,oBAAoB,EACpB,wBAAwB,EACxB,iBAAiB,EACjB,oBAAoB,EACpB,wBAAwB,EACxB,wBAAwB,EACxB,mBAAmB,EACpB,MAAM,qBAAqB,CAAC;AA2B7B,8DAA8D;AAC9D,eAAO,MAAM,6BAA6B,EAAE,SAAS,wBAAwB,EAK3E,CAAC;AAEH,gEAAgE;AAChE,eAAO,MAAM,sBAAsB,yCAAyC,CAAC;AAE7E,KAAK,GAAG,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;AAgBxD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,wBAAwB,EAClC,UAAU,EAAE,MAAM,EAClB,GAAG,GAAE,GAAiB,GACrB,MAAM,CAWR;AAED,uEAAuE;AACvE,MAAM,WAAW,kBAAkB;IACjC,iEAAiE;IACjE,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,8DAA8D;IAC9D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,wBAAwB,EAClC,UAAU,EAAE,MAAM,EAClB,GAAG,GAAE,GAAiB,GACrB,kBAAkB,CAgDpB;AAsFD;;;;;;;;;;GAUG;AACH,wBAAsB,4BAA4B,CAChD,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,oBAAoB,EAC1B,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,SAAS,wBAAwB,EAAE,CAAC,CAwB9C;AA2MD;;;;;;;;GAQG;AACH,wBAAgB,+BAA+B,CAC7C,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,oBAAoB,EAC1B,OAAO,GAAE,wBAA6B,GACrC,SAAS,mBAAmB,EAAE,CAchC;AAwCD;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,wBAA6B,GACrC,iBAAiB,CA6CnB;AAED,+DAA+D;AAC/D,eAAO,MAAM,oBAAoB,EAAE,oBAIjC,CAAC"}
|
|
@@ -145,6 +145,29 @@ export declare function isHeavyHookObject(hook: unknown): boolean;
|
|
|
145
145
|
* @returns what changed.
|
|
146
146
|
*/
|
|
147
147
|
export declare function syncJsonHeavyCommandHook(configPath: string, provider: 'claude-code' | 'codex', mode: HeavyCommandHookMode): Promise<HeavyHookSyncResult>;
|
|
148
|
+
/** The command CLEO's ask-enforce Stop hook runs (T13420); also its marker. */
|
|
149
|
+
export declare const ASK_ENFORCE_HOOK_ID = "cleo hook ask-enforce";
|
|
150
|
+
/**
|
|
151
|
+
* CLEO's ask-enforce Stop hook object (T13420). The script prints the hook's
|
|
152
|
+
* answer only when it is a JSON object and always exits 0, so a machine
|
|
153
|
+
* without `cleo`, or an older `cleo` that lacks the subcommand, never blocks.
|
|
154
|
+
*
|
|
155
|
+
* @param provider - `claude-code` or `codex` (Codex runs hooks through the login shell).
|
|
156
|
+
*/
|
|
157
|
+
export declare function askEnforceHookObject(provider: 'claude-code' | 'codex'): Record<string, unknown>;
|
|
158
|
+
/** Whether one hook object is CLEO's ask-enforce Stop hook. */
|
|
159
|
+
export declare function isAskEnforceHookObject(hook: unknown): boolean;
|
|
160
|
+
/**
|
|
161
|
+
* Add, refresh or (`install: false`) remove CLEO's ask-enforce Stop hook in a
|
|
162
|
+
* JSON hook config (`hooks.Stop`), through CAAMP's locked atomic writer. Only
|
|
163
|
+
* CLEO's own hook object is touched; a malformed file throws untouched.
|
|
164
|
+
*
|
|
165
|
+
* @param configPath - the config file (created when missing, unless removing).
|
|
166
|
+
* @param provider - `claude-code` or `codex`.
|
|
167
|
+
* @param install - `false` removes the hook.
|
|
168
|
+
* @returns what changed.
|
|
169
|
+
*/
|
|
170
|
+
export declare function syncJsonAskEnforceHook(configPath: string, provider: 'claude-code' | 'codex', install: boolean): Promise<HeavyHookSyncResult>;
|
|
148
171
|
/** Claude Code's per-machine settings file, relative to the project. */
|
|
149
172
|
export declare const CLAUDE_LOCAL_SETTINGS = ".claude/settings.local.json";
|
|
150
173
|
/** First line of the `info/exclude` block CLEO adds for `settings.local.json` (and alone may remove). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"heavy-command-hook-install.d.ts","sourceRoot":"","sources":["../../../src/providers/shared/heavy-command-hook-install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAgBH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAGhE,oEAAoE;AACpE,eAAO,MAAM,qBAAqB,4BAA4B,CAAC;AAE/D,iFAAiF;AACjF,eAAO,MAAM,8BAA8B,KAAK,CAAC;AAEjD,kDAAkD;AAClD,eAAO,MAAM,6BAA6B,0BAA0B,CAAC;AAErE;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EA+B5C,CAAC;AAEH;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;AAehG;;;;;GAKG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,iDAAiD;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;;OAGG;gBACS,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAKzC;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAa/D;AASD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,GAAE,MAAkB,GAAG,OAAO,CAEnF;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,QAOjC,CAAC;AAEH,mFAAmF;AACnF,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AAEjE;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,GAAE,MAAgD,GACpD,MAAM,CAkBR;AAqDD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,GAAG,MAAM,CAIjF;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMjG;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEhG;AAED,4DAA4D;AAC5D,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAOxD;
|
|
1
|
+
{"version":3,"file":"heavy-command-hook-install.d.ts","sourceRoot":"","sources":["../../../src/providers/shared/heavy-command-hook-install.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/adapters",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.7",
|
|
4
4
|
"description": "Unified provider adapters for CLEO (Claude Code, OpenCode, Cursor)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"@ai-sdk/anthropic": "^3.0.69",
|
|
20
20
|
"@ai-sdk/openai": "^2.0.53",
|
|
21
21
|
"ai": "^6.0.168",
|
|
22
|
-
"@cleocode/
|
|
23
|
-
"@cleocode/
|
|
24
|
-
"@cleocode/
|
|
22
|
+
"@cleocode/contracts": "2026.10.7",
|
|
23
|
+
"@cleocode/paths": "2026.10.7",
|
|
24
|
+
"@cleocode/caamp": "2026.10.7"
|
|
25
25
|
},
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"engines": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "^24.3.0",
|
|
36
36
|
"vitest": "^4.1.4",
|
|
37
|
-
"@cleocode/playbooks": "2026.10.
|
|
37
|
+
"@cleocode/playbooks": "2026.10.7"
|
|
38
38
|
},
|
|
39
39
|
"repository": {
|
|
40
40
|
"type": "git",
|
|
@@ -96,6 +96,22 @@ describe('syncProjectHeavyCommandHooks', () => {
|
|
|
96
96
|
});
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
it('installs the ask-enforce Stop hook beside it for Claude Code and Codex (T13420)', async () => {
|
|
100
|
+
useAll();
|
|
101
|
+
await syncProjectHeavyCommandHooks(project, 'rewrite', { env });
|
|
102
|
+
const stop = (file: string) =>
|
|
103
|
+
JSON.stringify(JSON.parse(readFileSync(file, 'utf-8')).hooks.Stop ?? []);
|
|
104
|
+
const claude = join(project, '.claude', 'settings.local.json');
|
|
105
|
+
const codex = join(project, '.codex', 'hooks.json');
|
|
106
|
+
expect(stop(claude)).toContain('cleo hook ask-enforce --provider claude-code');
|
|
107
|
+
expect(stop(codex)).toContain('cleo hook ask-enforce --provider codex');
|
|
108
|
+
await syncProjectHeavyCommandHooks(project, 'rewrite', {
|
|
109
|
+
env: { ...env, CLEO_ASK_ENFORCE: 'off' },
|
|
110
|
+
});
|
|
111
|
+
expect(stop(claude)).not.toContain('ask-enforce');
|
|
112
|
+
expect(stop(codex)).not.toContain('ask-enforce');
|
|
113
|
+
});
|
|
114
|
+
|
|
99
115
|
it('a .codex FILE blocks only Codex, with the reason and remedy; the others install', async () => {
|
|
100
116
|
useAll();
|
|
101
117
|
writeFileSync(join(project, '.codex'), '');
|
|
@@ -46,6 +46,8 @@ import { CodexInstallProvider } from '../../codex/install.js';
|
|
|
46
46
|
import { CursorInstallProvider } from '../../cursor/install.js';
|
|
47
47
|
import { OpenCodeInstallProvider } from '../../opencode/install.js';
|
|
48
48
|
import {
|
|
49
|
+
ASK_ENFORCE_HOOK_ID,
|
|
50
|
+
askEnforceHookObject,
|
|
49
51
|
clearOlderCleoMarkers,
|
|
50
52
|
excludeLocalSettingsFromGit,
|
|
51
53
|
HEAVY_COMMAND_HOOK_TIMEOUT_SEC,
|
|
@@ -57,6 +59,7 @@ import {
|
|
|
57
59
|
OLDER_CLEO_HOOK_ANSWER,
|
|
58
60
|
OLDER_CLEO_MARKER_PREFIX,
|
|
59
61
|
OPENCODE_HEAVY_COMMAND_PLUGIN,
|
|
62
|
+
syncJsonAskEnforceHook,
|
|
60
63
|
syncJsonHeavyCommandHook,
|
|
61
64
|
syncOpencodeHeavyCommandPlugin,
|
|
62
65
|
} from '../heavy-command-hook-install.js';
|
|
@@ -327,6 +330,43 @@ describe.each(SHELLS)('heavyCommandHookCommand under %s', (shell) => {
|
|
|
327
330
|
});
|
|
328
331
|
});
|
|
329
332
|
|
|
333
|
+
describe('syncJsonAskEnforceHook (T13420)', () => {
|
|
334
|
+
const read = (p: string) => JSON.parse(readFileSync(p, 'utf-8'));
|
|
335
|
+
|
|
336
|
+
it('adds one Stop entry beside the heavy-command hook, then is idempotent', async () => {
|
|
337
|
+
const path = join(dir, 'project', '.claude', 'settings.local.json');
|
|
338
|
+
await syncJsonHeavyCommandHook(path, 'claude-code', 'rewrite');
|
|
339
|
+
expect(await syncJsonAskEnforceHook(path, 'claude-code', true)).toBe('installed');
|
|
340
|
+
expect(read(path).hooks).toEqual({
|
|
341
|
+
PreToolUse: [heavyCommandHookEntry('claude-code')],
|
|
342
|
+
Stop: [{ hooks: [askEnforceHookObject('claude-code')] }],
|
|
343
|
+
});
|
|
344
|
+
expect(await syncJsonAskEnforceHook(path, 'claude-code', true)).toBe('unchanged');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('keeps user Stop hooks, refreshes a stale CLEO one, and removes only its own', async () => {
|
|
348
|
+
const path = join(dir, 'hooks.json');
|
|
349
|
+
const user = { hooks: [{ type: 'command', command: './notify.sh' }] };
|
|
350
|
+
const stale = { hooks: [{ type: 'command', command: `${ASK_ENFORCE_HOOK_ID} # cleo-hook` }] };
|
|
351
|
+
writeFileSync(path, JSON.stringify({ hooks: { Stop: [user, stale] } }));
|
|
352
|
+
expect(await syncJsonAskEnforceHook(path, 'codex', true)).toBe('updated');
|
|
353
|
+
expect(read(path).hooks.Stop).toEqual([user, { hooks: [askEnforceHookObject('codex')] }]);
|
|
354
|
+
expect(await syncJsonAskEnforceHook(path, 'codex', false)).toBe('removed');
|
|
355
|
+
expect(read(path).hooks.Stop).toEqual([user]);
|
|
356
|
+
expect(await syncJsonAskEnforceHook(join(dir, 'missing.json'), 'codex', false)).toBe(
|
|
357
|
+
'unchanged',
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('runs a fail-open script: exits 0 and prints only a JSON answer', () => {
|
|
362
|
+
const claude = String(askEnforceHookObject('claude-code').command);
|
|
363
|
+
expect(claude).toContain('command -v cleo >/dev/null 2>&1 || exit 0');
|
|
364
|
+
expect(claude).toContain(`${ASK_ENFORCE_HOOK_ID} --provider claude-code 2>/dev/null`);
|
|
365
|
+
expect(claude.endsWith('exit 0 # cleo-hook')).toBe(true);
|
|
366
|
+
expect(String(askEnforceHookObject('codex').command)).toMatch(/^\/bin\/sh -c '/);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
330
370
|
describe('syncJsonHeavyCommandHook', () => {
|
|
331
371
|
const read = (p: string) => JSON.parse(readFileSync(p, 'utf-8'));
|
|
332
372
|
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
opencodeHeavyCommandPluginSource,
|
|
57
57
|
syncClaudeCodeHeavyCommandHook,
|
|
58
58
|
syncCodexHeavyCommandHook,
|
|
59
|
+
syncJsonAskEnforceHook,
|
|
59
60
|
syncOpencodeHeavyCommandPlugin,
|
|
60
61
|
trackedFileModified,
|
|
61
62
|
} from './heavy-command-hook-install.js';
|
|
@@ -191,6 +192,27 @@ function errorText(err: unknown): string {
|
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
/** Sync one provider; never throws. */
|
|
195
|
+
/**
|
|
196
|
+
* Install (or remove) CLEO's ask-enforce Stop hook (T13420) in the same JSON
|
|
197
|
+
* config as the heavy-command hook, after that hook synced. It rides the
|
|
198
|
+
* heavy-command delivery: removed when that hook's mode is `off` or when
|
|
199
|
+
* `CLEO_ASK_ENFORCE=off`. Never fails the heavy-command outcome.
|
|
200
|
+
* shortcut: no separate outcome or doctor row yet; add one when a config key replaces the env switch.
|
|
201
|
+
*/
|
|
202
|
+
async function syncAskEnforceHook(
|
|
203
|
+
provider: 'claude-code' | 'codex',
|
|
204
|
+
target: string,
|
|
205
|
+
mode: HeavyCommandHookMode,
|
|
206
|
+
env: Env,
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
const install = mode !== 'off' && env.CLEO_ASK_ENFORCE?.trim().toLowerCase() !== 'off';
|
|
209
|
+
try {
|
|
210
|
+
await syncJsonAskEnforceHook(target, provider, install);
|
|
211
|
+
} catch {
|
|
212
|
+
// Fail open: a config the Stop hook cannot be written to keeps the heavy-command result.
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
194
216
|
async function syncProvider(
|
|
195
217
|
provider: HeavyCommandHookProvider,
|
|
196
218
|
projectDir: string,
|
|
@@ -210,6 +232,7 @@ async function syncProvider(
|
|
|
210
232
|
: provider === 'codex'
|
|
211
233
|
? await syncCodexHeavyCommandHook(projectDir, mode)
|
|
212
234
|
: syncOpencodeHeavyCommandPlugin(projectDir, mode);
|
|
235
|
+
if (provider !== 'opencode') await syncAskEnforceHook(provider, target, mode, env);
|
|
213
236
|
return { provider, status: result, target };
|
|
214
237
|
} catch (err) {
|
|
215
238
|
if (err instanceof HeavyHookSharedConfigError) {
|
|
@@ -335,20 +335,24 @@ export function isHeavyHookObject(hook: unknown): boolean {
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
/**
|
|
338
|
-
* Put `desired` in place of CLEO's first
|
|
339
|
-
* other copies, and drop a matcher group only when
|
|
340
|
-
* held. `desired: null` removes CLEO's hook everywhere.
|
|
338
|
+
* Put `desired` in place of CLEO's first hook object of one kind under
|
|
339
|
+
* `hooks.<event>`, drop any other copies, and drop a matcher group only when
|
|
340
|
+
* CLEO's hook was all it held. `desired: null` removes CLEO's hook everywhere.
|
|
341
341
|
*
|
|
342
|
+
* @param event - the hook event (`PreToolUse` for heavy-command, `Stop` for ask-enforce).
|
|
343
|
+
* @param isMine - recognises CLEO's hook object of this kind.
|
|
342
344
|
* @returns whether the groups changed, and whether CLEO's hook was found.
|
|
343
345
|
*/
|
|
344
346
|
function placeHeavyHook(
|
|
345
347
|
hooks: Record<string, unknown>,
|
|
346
348
|
desired: Record<string, unknown> | null,
|
|
349
|
+
event = 'PreToolUse',
|
|
350
|
+
isMine: (hook: unknown) => boolean = isHeavyHookObject,
|
|
347
351
|
): { readonly changed: boolean; readonly found: boolean } {
|
|
348
|
-
const groups = hooks
|
|
352
|
+
const groups = hooks[event];
|
|
349
353
|
if (groups === undefined) return { changed: false, found: false };
|
|
350
354
|
if (!Array.isArray(groups)) {
|
|
351
|
-
throw new Error(
|
|
355
|
+
throw new Error(`hooks "${event}" is not an array; not modifying it`);
|
|
352
356
|
}
|
|
353
357
|
let changed = false;
|
|
354
358
|
let found = false;
|
|
@@ -361,7 +365,7 @@ function placeHeavyHook(
|
|
|
361
365
|
const kept: unknown[] = [];
|
|
362
366
|
let groupChanged = false;
|
|
363
367
|
for (const hook of group.hooks) {
|
|
364
|
-
if (!
|
|
368
|
+
if (!isMine(hook)) {
|
|
365
369
|
kept.push(hook);
|
|
366
370
|
} else if (desired !== null && !found) {
|
|
367
371
|
found = true;
|
|
@@ -378,7 +382,7 @@ function placeHeavyHook(
|
|
|
378
382
|
changed = true;
|
|
379
383
|
if (kept.length > 0) next.push({ ...group, hooks: kept });
|
|
380
384
|
}
|
|
381
|
-
if (changed) hooks
|
|
385
|
+
if (changed) hooks[event] = next;
|
|
382
386
|
return { changed, found };
|
|
383
387
|
}
|
|
384
388
|
|
|
@@ -426,6 +430,80 @@ export async function syncJsonHeavyCommandHook(
|
|
|
426
430
|
return result;
|
|
427
431
|
}
|
|
428
432
|
|
|
433
|
+
/** The command CLEO's ask-enforce Stop hook runs (T13420); also its marker. */
|
|
434
|
+
export const ASK_ENFORCE_HOOK_ID = 'cleo hook ask-enforce';
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* CLEO's ask-enforce Stop hook object (T13420). The script prints the hook's
|
|
438
|
+
* answer only when it is a JSON object and always exits 0, so a machine
|
|
439
|
+
* without `cleo`, or an older `cleo` that lacks the subcommand, never blocks.
|
|
440
|
+
*
|
|
441
|
+
* @param provider - `claude-code` or `codex` (Codex runs hooks through the login shell).
|
|
442
|
+
*/
|
|
443
|
+
export function askEnforceHookObject(provider: 'claude-code' | 'codex'): Record<string, unknown> {
|
|
444
|
+
const script = `command -v cleo >/dev/null 2>&1 || exit 0; out="$(${ASK_ENFORCE_HOOK_ID} --provider ${provider} 2>/dev/null)"; case "$out" in "{"*) printf '%s\\n' "$out";; esac; exit 0`;
|
|
445
|
+
const line = provider === 'codex' ? `/bin/sh -c ${shellWord(script)}` : script;
|
|
446
|
+
return {
|
|
447
|
+
type: 'command',
|
|
448
|
+
command: `${line} ${CLEO_HOOK_MARKER}`,
|
|
449
|
+
timeout: HEAVY_COMMAND_HOOK_TIMEOUT_SEC,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Whether one hook object is CLEO's ask-enforce Stop hook. */
|
|
454
|
+
export function isAskEnforceHookObject(hook: unknown): boolean {
|
|
455
|
+
return (
|
|
456
|
+
isPlainObject(hook) &&
|
|
457
|
+
typeof hook.command === 'string' &&
|
|
458
|
+
hook.command.includes(CLEO_HOOK_MARKER) &&
|
|
459
|
+
hook.command.includes(ASK_ENFORCE_HOOK_ID)
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Add, refresh or (`install: false`) remove CLEO's ask-enforce Stop hook in a
|
|
465
|
+
* JSON hook config (`hooks.Stop`), through CAAMP's locked atomic writer. Only
|
|
466
|
+
* CLEO's own hook object is touched; a malformed file throws untouched.
|
|
467
|
+
*
|
|
468
|
+
* @param configPath - the config file (created when missing, unless removing).
|
|
469
|
+
* @param provider - `claude-code` or `codex`.
|
|
470
|
+
* @param install - `false` removes the hook.
|
|
471
|
+
* @returns what changed.
|
|
472
|
+
*/
|
|
473
|
+
export async function syncJsonAskEnforceHook(
|
|
474
|
+
configPath: string,
|
|
475
|
+
provider: 'claude-code' | 'codex',
|
|
476
|
+
install: boolean,
|
|
477
|
+
): Promise<HeavyHookSyncResult> {
|
|
478
|
+
const mine = (hook: unknown) => isAskEnforceHookObject(hook);
|
|
479
|
+
if (!install) {
|
|
480
|
+
if (!existsSync(configPath)) return 'unchanged';
|
|
481
|
+
const removed = await updateJsonConfigFile(configPath, (config) => {
|
|
482
|
+
if (config.hooks === undefined) return false;
|
|
483
|
+
return placeHeavyHook(hookMap(config), null, 'Stop', mine).changed;
|
|
484
|
+
});
|
|
485
|
+
return removed ? 'removed' : 'unchanged';
|
|
486
|
+
}
|
|
487
|
+
ensureParentDir(configPath);
|
|
488
|
+
let result: HeavyHookSyncResult = 'unchanged';
|
|
489
|
+
await updateJsonConfigFile(configPath, (config) => {
|
|
490
|
+
const hooks = hookMap(config);
|
|
491
|
+
const placed = placeHeavyHook(hooks, askEnforceHookObject(provider), 'Stop', mine);
|
|
492
|
+
if (placed.found) {
|
|
493
|
+
if (placed.changed) result = 'updated';
|
|
494
|
+
return placed.changed;
|
|
495
|
+
}
|
|
496
|
+
const existing = hooks.Stop;
|
|
497
|
+
hooks.Stop = [
|
|
498
|
+
...(Array.isArray(existing) ? existing : []),
|
|
499
|
+
{ hooks: [askEnforceHookObject(provider)] },
|
|
500
|
+
];
|
|
501
|
+
result = 'installed';
|
|
502
|
+
return true;
|
|
503
|
+
});
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
506
|
+
|
|
429
507
|
/** Claude Code's per-machine settings file, relative to the project. */
|
|
430
508
|
export const CLAUDE_LOCAL_SETTINGS = '.claude/settings.local.json';
|
|
431
509
|
|