@cleocode/adapters 2026.10.1 → 2026.10.2

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.
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Helpers for JSON hook configs shaped like Claude Code's `settings.json`
3
+ * `hooks` map: `{ "<Event>": [{ "matcher": "...", "hooks": [{ "type":
4
+ * "command", "command": "..." }] }] }`. Codex's `hooks.json` uses the same
5
+ * shape. Every command CLEO owns carries the `# cleo-hook` marker so CLEO can
6
+ * find (and remove) its own entries without touching user-authored ones.
7
+ *
8
+ * Moved from `claude-code/paths.ts` (T12385) so the Codex installer can share
9
+ * them (T12983); `claude-code/paths.ts` re-exports them.
10
+ *
11
+ * @task T12385
12
+ * @task T12983
13
+ */
14
+
15
+ /** Marker CLEO puts in every hook command it owns. */
16
+ export const CLEO_HOOK_MARKER = '# cleo-hook';
17
+
18
+ /**
19
+ * Whether a parsed settings value is a plain JSON object.
20
+ *
21
+ * @param value - Any parsed JSON value
22
+ * @returns `true` for a non-null, non-array object
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * isPlainObject(settings.enabledPlugins);
27
+ * ```
28
+ */
29
+ export function isPlainObject(value: unknown): value is Record<string, unknown> {
30
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
31
+ }
32
+
33
+ /**
34
+ * Whether a settings.json hook entry carries a CLEO-owned command.
35
+ *
36
+ * @param entry - One element of a `hooks.<Event>` array
37
+ * @param commandIncludes - Extra substring the CLEO command must contain
38
+ * @returns `true` when one of the entry's commands has the `# cleo-hook` marker
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * isCleoHookEntry({ hooks: [{ type: "command", command: "x # cleo-hook" }] }); // true
43
+ * ```
44
+ */
45
+ export function isCleoHookEntry(entry: unknown, commandIncludes?: string): boolean {
46
+ if (!isPlainObject(entry) || !Array.isArray(entry.hooks)) return false;
47
+ return entry.hooks.some(
48
+ (hook) =>
49
+ isPlainObject(hook) &&
50
+ typeof hook.command === 'string' &&
51
+ hook.command.includes(CLEO_HOOK_MARKER) &&
52
+ (commandIncludes === undefined || hook.command.includes(commandIncludes)),
53
+ );
54
+ }
55
+
56
+ /**
57
+ * Whether a `hooks.<Event>` value holds a CLEO-owned entry.
58
+ *
59
+ * @param entries - The event's value (non-arrays hold none)
60
+ * @param commandIncludes - Extra substring the CLEO command must contain
61
+ * @returns `true` when a CLEO entry is present
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * hasCleoHook(hookMap(settings).Stop);
66
+ * ```
67
+ */
68
+ export function hasCleoHook(entries: unknown, commandIncludes?: string): boolean {
69
+ return Array.isArray(entries) && entries.some((e) => isCleoHookEntry(e, commandIncludes));
70
+ }
71
+
72
+ /**
73
+ * Return the settings object's `hooks` map, creating an empty one if absent.
74
+ *
75
+ * @param settings - Parsed settings.json
76
+ * @returns The live `hooks` object (mutations apply to `settings`)
77
+ * @throws Error when `hooks` exists but is not an object — the caller's write
78
+ * is aborted and the file left untouched
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * const hooks = hookMap(settings);
83
+ * ```
84
+ */
85
+ export function hookMap(settings: Record<string, unknown>): Record<string, unknown> {
86
+ const existing = settings.hooks;
87
+ if (existing === undefined) {
88
+ const created: Record<string, unknown> = {};
89
+ settings.hooks = created;
90
+ return created;
91
+ }
92
+ if (!isPlainObject(existing)) {
93
+ throw new Error('settings.json "hooks" is not an object; not modifying it');
94
+ }
95
+ return existing;
96
+ }
97
+
98
+ /**
99
+ * Append a hook entry to `hooks[event]`, preserving existing entries.
100
+ *
101
+ * @param hooks - The settings `hooks` map
102
+ * @param event - Native event name (e.g. `Stop`, `PreCompact`)
103
+ * @param entry - Entry to append
104
+ * @throws Error when `hooks[event]` exists but is not an array
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * appendHookEntry(hookMap(settings), "Stop", { matcher: "", hooks: [] });
109
+ * ```
110
+ */
111
+ export function appendHookEntry(
112
+ hooks: Record<string, unknown>,
113
+ event: string,
114
+ entry: Record<string, unknown>,
115
+ ): void {
116
+ const existing = hooks[event];
117
+ if (existing !== undefined && !Array.isArray(existing)) {
118
+ throw new Error(`settings.json "hooks.${event}" is not an array; not modifying it`);
119
+ }
120
+ hooks[event] = [...(existing ?? []), entry];
121
+ }
122
+
123
+ /**
124
+ * Remove every CLEO-owned entry from every event array in `hooks`.
125
+ *
126
+ * @param hooks - The settings `hooks` map
127
+ * @returns Whether anything was removed
128
+ *
129
+ * @example
130
+ * ```typescript
131
+ * const changed = removeCleoHookEntries(hookMap(settings));
132
+ * ```
133
+ */
134
+ export function removeCleoHookEntries(hooks: Record<string, unknown>): boolean {
135
+ let changed = false;
136
+ for (const [event, entries] of Object.entries(hooks)) {
137
+ if (!Array.isArray(entries)) continue;
138
+ const kept = entries.filter((e) => !isCleoHookEntry(e));
139
+ if (kept.length !== entries.length) {
140
+ hooks[event] = kept;
141
+ changed = true;
142
+ }
143
+ }
144
+ return changed;
145
+ }