@cleocode/adapters 2026.10.1 → 2026.10.3

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.
@@ -68,134 +68,12 @@ export function claudeSettingsPath(): string {
68
68
  );
69
69
  }
70
70
 
71
- /** Marker CLEO puts in every hook command it owns. */
72
- const CLEO_HOOK_MARKER = '# cleo-hook';
73
-
74
- /**
75
- * Whether a parsed settings value is a plain JSON object.
76
- *
77
- * @param value - Any parsed JSON value
78
- * @returns `true` for a non-null, non-array object
79
- *
80
- * @example
81
- * ```typescript
82
- * isPlainObject(settings.enabledPlugins);
83
- * ```
84
- */
85
- export function isPlainObject(value: unknown): value is Record<string, unknown> {
86
- return typeof value === 'object' && value !== null && !Array.isArray(value);
87
- }
88
-
89
- /**
90
- * Whether a settings.json hook entry carries a CLEO-owned command.
91
- *
92
- * @param entry - One element of a `hooks.<Event>` array
93
- * @param commandIncludes - Extra substring the CLEO command must contain
94
- * @returns `true` when one of the entry's commands has the `# cleo-hook` marker
95
- *
96
- * @example
97
- * ```typescript
98
- * isCleoHookEntry({ hooks: [{ type: "command", command: "x # cleo-hook" }] }); // true
99
- * ```
100
- */
101
- export function isCleoHookEntry(entry: unknown, commandIncludes?: string): boolean {
102
- if (!isPlainObject(entry) || !Array.isArray(entry.hooks)) return false;
103
- return entry.hooks.some(
104
- (hook) =>
105
- isPlainObject(hook) &&
106
- typeof hook.command === 'string' &&
107
- hook.command.includes(CLEO_HOOK_MARKER) &&
108
- (commandIncludes === undefined || hook.command.includes(commandIncludes)),
109
- );
110
- }
111
-
112
- /**
113
- * Whether a `hooks.<Event>` value holds a CLEO-owned entry.
114
- *
115
- * @param entries - The event's value (non-arrays hold none)
116
- * @param commandIncludes - Extra substring the CLEO command must contain
117
- * @returns `true` when a CLEO entry is present
118
- *
119
- * @example
120
- * ```typescript
121
- * hasCleoHook(hookMap(settings).Stop);
122
- * ```
123
- */
124
- export function hasCleoHook(entries: unknown, commandIncludes?: string): boolean {
125
- return Array.isArray(entries) && entries.some((e) => isCleoHookEntry(e, commandIncludes));
126
- }
127
-
128
- /**
129
- * Return the settings object's `hooks` map, creating an empty one if absent.
130
- *
131
- * @param settings - Parsed settings.json
132
- * @returns The live `hooks` object (mutations apply to `settings`)
133
- * @throws Error when `hooks` exists but is not an object — the caller's write
134
- * is aborted and the file left untouched
135
- *
136
- * @example
137
- * ```typescript
138
- * const hooks = hookMap(settings);
139
- * ```
140
- */
141
- export function hookMap(settings: Record<string, unknown>): Record<string, unknown> {
142
- const existing = settings.hooks;
143
- if (existing === undefined) {
144
- const created: Record<string, unknown> = {};
145
- settings.hooks = created;
146
- return created;
147
- }
148
- if (!isPlainObject(existing)) {
149
- throw new Error('settings.json "hooks" is not an object; not modifying it');
150
- }
151
- return existing;
152
- }
153
-
154
- /**
155
- * Append a hook entry to `hooks[event]`, preserving existing entries.
156
- *
157
- * @param hooks - The settings `hooks` map
158
- * @param event - Native event name (e.g. `Stop`, `PreCompact`)
159
- * @param entry - Entry to append
160
- * @throws Error when `hooks[event]` exists but is not an array
161
- *
162
- * @example
163
- * ```typescript
164
- * appendHookEntry(hookMap(settings), "Stop", { matcher: "", hooks: [] });
165
- * ```
166
- */
167
- export function appendHookEntry(
168
- hooks: Record<string, unknown>,
169
- event: string,
170
- entry: Record<string, unknown>,
171
- ): void {
172
- const existing = hooks[event];
173
- if (existing !== undefined && !Array.isArray(existing)) {
174
- throw new Error(`settings.json "hooks.${event}" is not an array; not modifying it`);
175
- }
176
- hooks[event] = [...(existing ?? []), entry];
177
- }
178
-
179
- /**
180
- * Remove every CLEO-owned entry from every event array in `hooks`.
181
- *
182
- * @param hooks - The settings `hooks` map
183
- * @returns Whether anything was removed
184
- *
185
- * @example
186
- * ```typescript
187
- * const changed = removeCleoHookEntries(hookMap(settings));
188
- * ```
189
- */
190
- export function removeCleoHookEntries(hooks: Record<string, unknown>): boolean {
191
- let changed = false;
192
- for (const [event, entries] of Object.entries(hooks)) {
193
- if (!Array.isArray(entries)) continue;
194
- const kept = entries.filter((e) => !isCleoHookEntry(e));
195
- if (kept.length !== entries.length) {
196
- hooks[event] = kept;
197
- changed = true;
198
- }
199
- }
200
- return changed;
201
- }
71
+ export {
72
+ appendHookEntry,
73
+ CLEO_HOOK_MARKER,
74
+ hasCleoHook,
75
+ hookMap,
76
+ isCleoHookEntry,
77
+ isPlainObject,
78
+ removeCleoHookEntries,
79
+ } from '../shared/hook-config.js';
@@ -3,15 +3,19 @@
3
3
  *
4
4
  * Handles CLEO installation into Codex CLI environments:
5
5
  * - Ensures AGENTS.md has CLEO @-references via CAAMP
6
+ * - Installs the heavy-command `PreToolUse` hook in the project's
7
+ * `.codex/hooks.json` (T12983)
6
8
  *
7
9
  * @task T162
8
10
  * @task T9019
11
+ * @task T12983
9
12
  * @epic T134
10
13
  */
11
14
 
12
15
  import { join } from 'node:path';
13
16
  import { ensureProviderInstructionFile } from '@cleocode/caamp';
14
17
  import type { AdapterInstallProvider, InstallOptions, InstallResult } from '@cleocode/contracts';
18
+ import { isUserHomeDir, syncJsonHeavyCommandHook } from '../shared/heavy-command-hook-install.js';
15
19
  import { getCleoTemplatesTildePath } from '../shared/paths.js';
16
20
 
17
21
  /**
@@ -20,11 +24,14 @@ import { getCleoTemplatesTildePath } from '../shared/paths.js';
20
24
  * Manages CLEO's integration with Codex CLI by:
21
25
  * 1. Ensuring AGENTS.md contains @-references to CLEO instruction files
22
26
  * (delegated to CAAMP's canonical {@link ensureProviderInstructionFile}).
27
+ * 2. Installing the heavy-command `PreToolUse` hook (matcher `Bash`) in the
28
+ * project's `.codex/hooks.json`, when `options.heavyCommandHook` is set.
29
+ * Codex asks the user to review and trust a new project hook (`/hooks`)
30
+ * before it runs it.
23
31
  *
24
32
  * @remarks
25
33
  * Installation is idempotent — running install multiple times on the same
26
- * project produces the same result. Only AGENTS.md is managed; Codex CLI
27
- * does not have an MCP or plugin registration mechanism.
34
+ * project produces the same result.
28
35
  *
29
36
  * @task T162
30
37
  * @epic T134
@@ -53,8 +60,25 @@ export class CodexInstallProvider implements AdapterInstallProvider {
53
60
  details.instructionFile = join(projectDir, result.instructFile);
54
61
  }
55
62
 
63
+ // T12983: route heavy shell commands through `cleo run`.
64
+ let success = true;
65
+ if (options.heavyCommandHook !== undefined && isUserHomeDir(projectDir)) {
66
+ details.heavyCommandHook = 'skipped';
67
+ } else if (options.heavyCommandHook !== undefined) {
68
+ try {
69
+ details.heavyCommandHook = await syncJsonHeavyCommandHook(
70
+ join(projectDir, '.codex', 'hooks.json'),
71
+ 'codex',
72
+ options.heavyCommandHook,
73
+ );
74
+ } catch (err) {
75
+ details.settingsErrors = [err instanceof Error ? err.message : String(err)];
76
+ success = false;
77
+ }
78
+ }
79
+
56
80
  return {
57
- success: true,
81
+ success,
58
82
  installedAt,
59
83
  instructionFileUpdated,
60
84
  details,
@@ -27,6 +27,21 @@ import { getCleoTemplatesTildePath } from '../shared/paths.js';
27
27
  * project produces the same result. Only AGENTS.md is managed; Kimi does
28
28
  * not have an MCP or plugin registration mechanism.
29
29
  *
30
+ * The heavy-command hook (T12983) is NOT installed for Kimi: Kimi reads
31
+ * `[[hooks]]` only from the user-global `~/.kimi/config.toml` (no project
32
+ * config, and CLEO installs this hook per project only), and its
33
+ * `PreToolUse` answer cannot rewrite the command. `cleo hook heavy-command
34
+ * --provider kimi` speaks Kimi's protocol (deny with the governed command as
35
+ * the reason) for a manual entry:
36
+ *
37
+ * ```toml
38
+ * [[hooks]]
39
+ * event = "PreToolUse"
40
+ * matcher = "Shell"
41
+ * command = "cleo hook heavy-command --provider kimi"
42
+ * timeout = 20
43
+ * ```
44
+ *
30
45
  * @task T163
31
46
  * @epic T134
32
47
  */
@@ -4,16 +4,22 @@
4
4
  * Handles CLEO installation into OpenCode environments:
5
5
  * - Ensures AGENTS.md has CLEO @-references via CAAMP
6
6
  * - Installs PreCompact hook shell shims + a JS plugin wrapper (T1013)
7
+ * - Installs the heavy-command `tool.execute.before` plugin (T12983)
7
8
  *
8
9
  * @task T5240
9
10
  * @task T1013
10
11
  * @task T9019
12
+ * @task T12983
11
13
  */
12
14
 
13
15
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
14
16
  import { join } from 'node:path';
15
17
  import { ensureProviderInstructionFile } from '@cleocode/caamp';
16
18
  import type { AdapterInstallProvider, InstallOptions, InstallResult } from '@cleocode/contracts';
19
+ import {
20
+ isUserHomeDir,
21
+ syncOpencodeHeavyCommandPlugin,
22
+ } from '../shared/heavy-command-hook-install.js';
17
23
  import {
18
24
  type InstallHookTemplatesResult,
19
25
  installProviderHookTemplates,
@@ -69,8 +75,25 @@ export class OpenCodeInstallProvider implements AdapterInstallProvider {
69
75
  details.hookTemplates = hookResult;
70
76
  }
71
77
 
78
+ // Step 3 (T12983): `.opencode/plugins/cleo-heavy-command.js` routes heavy
79
+ // shell commands through `cleo run`.
80
+ let success = true;
81
+ if (options.heavyCommandHook !== undefined && isUserHomeDir(projectDir)) {
82
+ details.heavyCommandHook = 'skipped';
83
+ } else if (options.heavyCommandHook !== undefined) {
84
+ try {
85
+ details.heavyCommandHook = syncOpencodeHeavyCommandPlugin(
86
+ projectDir,
87
+ options.heavyCommandHook,
88
+ );
89
+ } catch (err) {
90
+ details.settingsErrors = [err instanceof Error ? err.message : String(err)];
91
+ success = false;
92
+ }
93
+ }
94
+
72
95
  return {
73
- success: true,
96
+ success,
74
97
  installedAt,
75
98
  instructionFileUpdated,
76
99
  details,