@cleocode/adapters 2026.4.101 → 2026.4.103
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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1221 -16720
- package/dist/index.js.map +4 -4
- package/dist/providers/claude-code/install.d.ts +36 -0
- package/dist/providers/claude-code/install.d.ts.map +1 -1
- package/dist/providers/cursor/install.d.ts +36 -0
- package/dist/providers/cursor/install.d.ts.map +1 -1
- package/dist/providers/opencode/install.d.ts +47 -2
- package/dist/providers/opencode/install.d.ts.map +1 -1
- package/dist/providers/shared/hook-template-installer.d.ts +109 -0
- package/dist/providers/shared/hook-template-installer.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/index.ts +11 -0
- package/src/providers/README.md +137 -0
- package/src/providers/claude-code/__tests__/hooks-install.test.ts +113 -0
- package/src/providers/claude-code/install.ts +129 -0
- package/src/providers/claude-code/templates/hooks/precompact-safestop.sh +52 -0
- package/src/providers/cursor/__tests__/hooks-install.test.ts +88 -0
- package/src/providers/cursor/install.ts +117 -0
- package/src/providers/cursor/templates/hooks/precompact.sh +47 -0
- package/src/providers/gemini-cli/templates/hooks/precompact.sh +47 -0
- package/src/providers/opencode/__tests__/hooks-install.test.ts +87 -0
- package/src/providers/opencode/install.ts +134 -3
- package/src/providers/opencode/templates/hooks/precompact.sh +42 -0
- package/src/providers/pi/templates/hooks/README.md +40 -0
- package/src/providers/shared/hook-template-installer.ts +268 -0
- package/src/providers/shared/templates/hooks/cleo-precompact-core.sh +128 -0
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Handles CLEO installation into OpenCode environments:
|
|
5
5
|
* - Ensures AGENTS.md has CLEO @-references
|
|
6
|
+
* - Installs PreCompact hook shell shims + a JS plugin wrapper (T1013)
|
|
6
7
|
*
|
|
7
8
|
* @task T5240
|
|
9
|
+
* @task T1013
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
13
|
import { join } from 'node:path';
|
|
12
14
|
import type { AdapterInstallProvider, InstallOptions, InstallResult } from '@cleocode/contracts';
|
|
15
|
+
import {
|
|
16
|
+
type InstallHookTemplatesResult,
|
|
17
|
+
installProviderHookTemplates,
|
|
18
|
+
} from '../shared/hook-template-installer.js';
|
|
13
19
|
import { getCleoTemplatesTildePath } from '../shared/paths.js';
|
|
14
20
|
|
|
15
21
|
/**
|
|
@@ -27,11 +33,17 @@ const INSTRUCTION_REFERENCES = [
|
|
|
27
33
|
*
|
|
28
34
|
* Manages CLEO's integration with OpenCode by:
|
|
29
35
|
* 1. Ensuring AGENTS.md contains @-references to CLEO instruction files
|
|
36
|
+
* 2. Installing PreCompact hook shell templates + generating the JS plugin
|
|
37
|
+
* wrapper that spawns the shim on `experimental.session.compacting` (T1013).
|
|
30
38
|
*
|
|
31
39
|
* @remarks
|
|
32
40
|
* Installation is idempotent -- running install multiple times on the same
|
|
33
|
-
* project produces the same result.
|
|
34
|
-
*
|
|
41
|
+
* project produces the same result. OpenCode's plugin system is the native
|
|
42
|
+
* hook surface (OpenCode has no config-file hook registry like Claude Code or
|
|
43
|
+
* Cursor), so the installer writes a JS plugin that subscribes to the native
|
|
44
|
+
* event and spawns the shell shim as a child process. This keeps the DRY
|
|
45
|
+
* contract: all providers funnel through the shared `cleo-precompact-core.sh`
|
|
46
|
+
* helper and end up in the `cleo` CLI.
|
|
35
47
|
*/
|
|
36
48
|
export class OpenCodeInstallProvider implements AdapterInstallProvider {
|
|
37
49
|
/**
|
|
@@ -52,6 +64,14 @@ export class OpenCodeInstallProvider implements AdapterInstallProvider {
|
|
|
52
64
|
details.instructionFile = join(projectDir, 'AGENTS.md');
|
|
53
65
|
}
|
|
54
66
|
|
|
67
|
+
// Step 2 (T1013): Install PreCompact hook templates + generate the JS
|
|
68
|
+
// plugin wrapper that spawns the bash shim on
|
|
69
|
+
// `experimental.session.compacting`.
|
|
70
|
+
const hookResult = this.installHookTemplates(projectDir);
|
|
71
|
+
if (hookResult) {
|
|
72
|
+
details.hookTemplates = hookResult;
|
|
73
|
+
}
|
|
74
|
+
|
|
55
75
|
return {
|
|
56
76
|
success: true,
|
|
57
77
|
installedAt,
|
|
@@ -134,4 +154,115 @@ export class OpenCodeInstallProvider implements AdapterInstallProvider {
|
|
|
134
154
|
writeFileSync(agentsMdPath, content, 'utf-8');
|
|
135
155
|
return true;
|
|
136
156
|
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Install the CLEO PreCompact hook templates for OpenCode (T1013).
|
|
160
|
+
*
|
|
161
|
+
* OpenCode uses a JavaScript plugin system, not config-based hooks. The
|
|
162
|
+
* installer:
|
|
163
|
+
*
|
|
164
|
+
* 1. Writes the shared bash helper and OpenCode-flavoured `precompact.sh`
|
|
165
|
+
* to `<projectDir>/.opencode/plugins/hooks/` so the shim can be spawned
|
|
166
|
+
* as a child process.
|
|
167
|
+
* 2. Generates an OpenCode plugin `.opencode/plugins/cleo-precompact.js`
|
|
168
|
+
* that subscribes to `experimental.session.compacting` (CAAMP native
|
|
169
|
+
* event for the canonical `PreCompact`) and spawns the shim.
|
|
170
|
+
*
|
|
171
|
+
* Idempotent.
|
|
172
|
+
*
|
|
173
|
+
* @param projectDir - Project root directory.
|
|
174
|
+
* @returns Install summary, or `null` when no change was required.
|
|
175
|
+
*
|
|
176
|
+
* @task T1013
|
|
177
|
+
*/
|
|
178
|
+
private installHookTemplates(projectDir: string): {
|
|
179
|
+
templates: InstallHookTemplatesResult;
|
|
180
|
+
pluginWritten: boolean;
|
|
181
|
+
} | null {
|
|
182
|
+
const pluginsDir = join(projectDir, '.opencode', 'plugins');
|
|
183
|
+
const hooksDir = join(pluginsDir, 'hooks');
|
|
184
|
+
|
|
185
|
+
// Template copy is best-effort so missing/locked filesystems (CI sandboxes,
|
|
186
|
+
// mocked `node:fs` in unit tests) don't fail the whole install.
|
|
187
|
+
let templates: InstallHookTemplatesResult;
|
|
188
|
+
try {
|
|
189
|
+
templates = installProviderHookTemplates({
|
|
190
|
+
provider: 'opencode',
|
|
191
|
+
targetDir: hooksDir,
|
|
192
|
+
});
|
|
193
|
+
} catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let pluginWritten = false;
|
|
198
|
+
try {
|
|
199
|
+
pluginWritten = this.writePrecompactPlugin(pluginsDir, join(hooksDir, 'precompact.sh'));
|
|
200
|
+
} catch {
|
|
201
|
+
// Best-effort: never block install on hook wiring failures.
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (templates.installedFiles.length === 0 && !pluginWritten) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { templates, pluginWritten };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Write an OpenCode JavaScript plugin that spawns `precompact.sh` when the
|
|
213
|
+
* canonical `PreCompact` event fires. OpenCode exposes the event natively as
|
|
214
|
+
* `experimental.session.compacting` (see CAAMP `hook-mappings.json`).
|
|
215
|
+
*
|
|
216
|
+
* The generated file is idempotent — overwritten only when its content
|
|
217
|
+
* differs from the target on disk. Uses `child_process.spawn` so the bash
|
|
218
|
+
* shim runs in a separate process and does not block the compaction path.
|
|
219
|
+
*
|
|
220
|
+
* @param pluginsDir - Absolute path to `.opencode/plugins/`.
|
|
221
|
+
* @param shimPath - Absolute path to the installed `precompact.sh`.
|
|
222
|
+
* @returns `true` when the plugin file was written, `false` when unchanged.
|
|
223
|
+
*
|
|
224
|
+
* @task T1013
|
|
225
|
+
*/
|
|
226
|
+
private writePrecompactPlugin(pluginsDir: string, shimPath: string): boolean {
|
|
227
|
+
const pluginPath = join(pluginsDir, 'cleo-precompact.js');
|
|
228
|
+
const generated = [
|
|
229
|
+
'// CLEO PreCompact plugin for OpenCode (generated by @cleocode/adapters).',
|
|
230
|
+
'// Bridges the canonical CAAMP `PreCompact` event',
|
|
231
|
+
'// (`experimental.session.compacting`) to the shell shim at:',
|
|
232
|
+
`// ${shimPath}`,
|
|
233
|
+
'// The shim invokes only the `cleo` CLI — no core internals.',
|
|
234
|
+
'',
|
|
235
|
+
"import { spawn } from 'node:child_process';",
|
|
236
|
+
'',
|
|
237
|
+
'export default function register(plugin) {',
|
|
238
|
+
" plugin.on('experimental.session.compacting', () => {",
|
|
239
|
+
' try {',
|
|
240
|
+
` const child = spawn(${JSON.stringify(shimPath)}, [], {`,
|
|
241
|
+
' detached: true,',
|
|
242
|
+
" stdio: 'ignore',",
|
|
243
|
+
' });',
|
|
244
|
+
' child.unref();',
|
|
245
|
+
' } catch (err) {',
|
|
246
|
+
' // Hook errors must never block compaction.',
|
|
247
|
+
" console.error('[CLEO] precompact hook failed:', err);",
|
|
248
|
+
' }',
|
|
249
|
+
' });',
|
|
250
|
+
'}',
|
|
251
|
+
'',
|
|
252
|
+
].join('\n');
|
|
253
|
+
|
|
254
|
+
if (existsSync(pluginPath)) {
|
|
255
|
+
try {
|
|
256
|
+
if (readFileSync(pluginPath, 'utf-8') === generated) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
} catch {
|
|
260
|
+
// Fall through and overwrite.
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
mkdirSync(pluginsDir, { recursive: true });
|
|
265
|
+
writeFileSync(pluginPath, generated, 'utf-8');
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
137
268
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# CLEO PreCompact Hook — OpenCode emergency safestop shim
|
|
3
|
+
#
|
|
4
|
+
# OpenCode is a plugin-based hook provider (.opencode/plugins/) whose canonical
|
|
5
|
+
# event `PreCompact` maps to the native event `experimental.session.compacting`.
|
|
6
|
+
# OpenCode's handler type is `plugin` (JavaScript) — this shell script is
|
|
7
|
+
# invoked from the JS plugin wrapper as a child process.
|
|
8
|
+
#
|
|
9
|
+
# INSTALLATION (OpenCode):
|
|
10
|
+
# 1. Copy this file to .opencode/plugins/hooks/precompact.sh (alongside
|
|
11
|
+
# cleo-precompact-core.sh).
|
|
12
|
+
# 2. Register a JS plugin that spawns this shim on the canonical event.
|
|
13
|
+
# See packages/adapters/src/providers/opencode/install.ts for the
|
|
14
|
+
# generated wrapper that wires `experimental.session.compacting` to
|
|
15
|
+
# this script via `child_process.spawn`.
|
|
16
|
+
#
|
|
17
|
+
# The universal flush + safestop sequence lives in cleo-precompact-core.sh.
|
|
18
|
+
# This shim only adds an OpenCode-flavoured banner.
|
|
19
|
+
#
|
|
20
|
+
# @task T1013
|
|
21
|
+
# @provider opencode
|
|
22
|
+
|
|
23
|
+
set -euo pipefail
|
|
24
|
+
|
|
25
|
+
# Source the shared CLEO core helper. Installed alongside this shim.
|
|
26
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
27
|
+
# shellcheck source=../../../shared/templates/hooks/cleo-precompact-core.sh
|
|
28
|
+
# shellcheck disable=SC1091
|
|
29
|
+
source "${SCRIPT_DIR}/cleo-precompact-core.sh"
|
|
30
|
+
|
|
31
|
+
# Run the universal flush + safestop sequence.
|
|
32
|
+
cleo_core_run_precompact "precompact-emergency"
|
|
33
|
+
|
|
34
|
+
# OpenCode-specific status banner (only when a session was actually saved).
|
|
35
|
+
if [[ -n "${CLEO_PRECOMPACT_HANDOFF:-}" ]]; then
|
|
36
|
+
echo ""
|
|
37
|
+
echo "[CLEO] Emergency Safestop executed at OpenCode experimental.session.compacting."
|
|
38
|
+
echo " Session ended. Handoff saved to: ${CLEO_PRECOMPACT_HANDOFF}"
|
|
39
|
+
if [[ -n "${CLEO_PRECOMPACT_SESSION_ID:-}" ]]; then
|
|
40
|
+
echo " Resume with: cleo session resume ${CLEO_PRECOMPACT_SESSION_ID}"
|
|
41
|
+
fi
|
|
42
|
+
fi
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Pi Hook Templates — Not Shell
|
|
2
|
+
|
|
3
|
+
Pi (CAAMP's primary harness, ADR-035) does **not** use shell hooks natively.
|
|
4
|
+
Its hook system is TypeScript-based extensions loaded from:
|
|
5
|
+
|
|
6
|
+
- Global: `~/.pi/agent/extensions/*.ts`
|
|
7
|
+
- Project: `<projectDir>/.pi/extensions/*.ts`
|
|
8
|
+
|
|
9
|
+
## How PreCompact fires on Pi
|
|
10
|
+
|
|
11
|
+
Pi's native event catalog (`piEventCatalog` in
|
|
12
|
+
`packages/caamp/providers/hook-mappings.json`) maps the canonical
|
|
13
|
+
`PreCompact` CAAMP event to the native `context` event (Pi's context-assembly
|
|
14
|
+
lifecycle stage is the closest proxy for pre-compaction).
|
|
15
|
+
|
|
16
|
+
## Why there is no bash shim here
|
|
17
|
+
|
|
18
|
+
The `packages/adapters/src/providers/{claude-code,cursor,opencode,gemini-cli}/templates/hooks/`
|
|
19
|
+
directories ship bash templates because those providers expose `command`-type
|
|
20
|
+
handlers. Pi exposes only `extension` handlers, so its pre-compact hook is a
|
|
21
|
+
TypeScript module, not a shell script.
|
|
22
|
+
|
|
23
|
+
## Where the Pi implementation lives
|
|
24
|
+
|
|
25
|
+
The Pi PreCompact handler is implemented in TypeScript inside CLEO core and is
|
|
26
|
+
loaded into the Pi extension runtime via `packages/core/src/hooks/handlers/precompact.ts`.
|
|
27
|
+
That handler invokes the same universal sequence as the bash helpers:
|
|
28
|
+
|
|
29
|
+
1. `cleo memory precompact-flush` — drain in-flight observations + WAL checkpoint (T1004)
|
|
30
|
+
2. `cleo safestop --reason precompact-emergency --commit --handoff <file>`
|
|
31
|
+
|
|
32
|
+
Both execution paths terminate in the same CLI, so the universal CLEO
|
|
33
|
+
surface remains the single source of truth. Provider adapters never reach
|
|
34
|
+
into core internals.
|
|
35
|
+
|
|
36
|
+
## See also
|
|
37
|
+
|
|
38
|
+
- `packages/adapters/src/providers/README.md` — Hook Template Architecture
|
|
39
|
+
- `packages/adapters/src/providers/pi/hooks.ts` — `PiHookProvider.mapProviderEvent`
|
|
40
|
+
- `packages/caamp/providers/hook-mappings.json` — canonical event → provider map
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared hook-template installer for provider adapters.
|
|
3
|
+
*
|
|
4
|
+
* Each provider ships its own PreCompact shell shim under
|
|
5
|
+
* `packages/adapters/src/providers/<provider>/templates/hooks/` which sources
|
|
6
|
+
* the universal helper at
|
|
7
|
+
* `packages/adapters/src/providers/shared/templates/hooks/cleo-precompact-core.sh`.
|
|
8
|
+
*
|
|
9
|
+
* This module wires the templates into the provider's hooks directory at
|
|
10
|
+
* install time. Provider-specific {@link AdapterInstallProvider} implementations
|
|
11
|
+
* call {@link installProviderHookTemplates} with their own provider id, and the
|
|
12
|
+
* installer consults CAAMP's `hook-mappings.json` SSoT to verify the provider
|
|
13
|
+
* supports the required canonical event and handler type before writing.
|
|
14
|
+
*
|
|
15
|
+
* DRY invariant: all shims source the same core helper — adapter-specific
|
|
16
|
+
* shims only add provider-flavoured banners and `$CLEO_PRECOMPACT_*` env
|
|
17
|
+
* handling.
|
|
18
|
+
*
|
|
19
|
+
* @task T1013
|
|
20
|
+
* @epic T1000
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { copyFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
|
|
24
|
+
import { dirname, join } from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Types
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/** Identifiers for providers that ship bash hook templates. */
|
|
32
|
+
export type HookTemplateProviderId = 'claude-code' | 'cursor' | 'opencode' | 'gemini-cli';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Result returned by {@link installProviderHookTemplates}.
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* Paths are absolute and point at the filesystem locations the installer
|
|
39
|
+
* actually wrote to. When no template files needed copying (e.g. because the
|
|
40
|
+
* destination already contained identical files), `installedFiles` is empty
|
|
41
|
+
* and `skipped` carries the reason-keyed paths instead.
|
|
42
|
+
*/
|
|
43
|
+
export interface InstallHookTemplatesResult {
|
|
44
|
+
/** Provider identifier the templates were installed for. */
|
|
45
|
+
provider: HookTemplateProviderId;
|
|
46
|
+
/** Absolute path to the hooks directory that received the templates. */
|
|
47
|
+
targetDir: string;
|
|
48
|
+
/** Absolute paths to files written during this install invocation. */
|
|
49
|
+
installedFiles: string[];
|
|
50
|
+
/** Files that were not written (already present and identical). */
|
|
51
|
+
skipped: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Options for {@link installProviderHookTemplates}.
|
|
56
|
+
*/
|
|
57
|
+
export interface InstallHookTemplatesOptions {
|
|
58
|
+
/** Provider to install hook templates for. */
|
|
59
|
+
provider: HookTemplateProviderId;
|
|
60
|
+
/** Absolute path to the hooks directory that should receive the shims. */
|
|
61
|
+
targetDir: string;
|
|
62
|
+
/**
|
|
63
|
+
* When `true`, overwrite existing files even if their contents match.
|
|
64
|
+
*
|
|
65
|
+
* @defaultValue `false`
|
|
66
|
+
*/
|
|
67
|
+
force?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Template file resolution
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve the `providers/` base directory relative to this source file.
|
|
76
|
+
*
|
|
77
|
+
* Template shell scripts live alongside TypeScript source at
|
|
78
|
+
* `src/providers/<provider>/templates/hooks/`. The TypeScript compiler
|
|
79
|
+
* emits `.d.ts` / `.js` artefacts into `dist/providers/...` without copying
|
|
80
|
+
* non-TS assets. To support both development (running from `src/`) and
|
|
81
|
+
* published installs (running from `dist/`), this resolver returns a list of
|
|
82
|
+
* candidate provider directories — one for the current runtime location and
|
|
83
|
+
* one for the parallel `src/providers/` tree. Template lookup callers must
|
|
84
|
+
* pick the first candidate that contains the expected file.
|
|
85
|
+
*
|
|
86
|
+
* @internal
|
|
87
|
+
*/
|
|
88
|
+
function resolveProviderCandidates(): string[] {
|
|
89
|
+
// One level up from shared/ = providers/. This is the compiled location
|
|
90
|
+
// when running from `dist/providers/shared/hook-template-installer.js` or
|
|
91
|
+
// the source location in development.
|
|
92
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
93
|
+
const here = dirname(thisDir);
|
|
94
|
+
|
|
95
|
+
// Also look in the sibling `src/providers/` tree so compiled runs can still
|
|
96
|
+
// find template assets that tsc didn't copy. `dist/providers/shared/...`
|
|
97
|
+
// → `../../src/providers/`.
|
|
98
|
+
const fromDist = join(here, '..', '..', 'src', 'providers');
|
|
99
|
+
|
|
100
|
+
return [here, fromDist];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Find a template file by searching the provider-directory candidates.
|
|
105
|
+
*
|
|
106
|
+
* @internal
|
|
107
|
+
*/
|
|
108
|
+
function findTemplateFile(relativeSegments: string[]): string {
|
|
109
|
+
for (const base of resolveProviderCandidates()) {
|
|
110
|
+
const candidate = join(base, ...relativeSegments);
|
|
111
|
+
if (existsSync(candidate)) return candidate;
|
|
112
|
+
}
|
|
113
|
+
// Surface a precise error pointing to the first candidate so install
|
|
114
|
+
// failures are self-describing.
|
|
115
|
+
const [first] = resolveProviderCandidates();
|
|
116
|
+
return join(first ?? '', ...relativeSegments);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Per-provider shim filename (relative to `<provider>/templates/hooks/`).
|
|
121
|
+
* Shell scripts named per the provider's canonical hook event convention.
|
|
122
|
+
*
|
|
123
|
+
* @internal
|
|
124
|
+
*/
|
|
125
|
+
const PROVIDER_SHIM: Record<HookTemplateProviderId, string> = {
|
|
126
|
+
'claude-code': 'precompact-safestop.sh',
|
|
127
|
+
cursor: 'precompact.sh',
|
|
128
|
+
opencode: 'precompact.sh',
|
|
129
|
+
'gemini-cli': 'precompact.sh',
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/** Filename of the shared universal helper. */
|
|
133
|
+
const SHARED_CORE_FILE = 'cleo-precompact-core.sh';
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Resolve the absolute source path of a provider's shim script.
|
|
137
|
+
*
|
|
138
|
+
* @internal
|
|
139
|
+
*/
|
|
140
|
+
function providerShimSource(provider: HookTemplateProviderId): string {
|
|
141
|
+
return findTemplateFile([provider, 'templates', 'hooks', PROVIDER_SHIM[provider]]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resolve the absolute source path of the shared `cleo-precompact-core.sh`
|
|
146
|
+
* helper.
|
|
147
|
+
*
|
|
148
|
+
* @internal
|
|
149
|
+
*/
|
|
150
|
+
function sharedCoreSource(): string {
|
|
151
|
+
return findTemplateFile(['shared', 'templates', 'hooks', SHARED_CORE_FILE]);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Copy helpers
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Copy a single template file, preserving the executable bit. Returns the
|
|
160
|
+
* destination path on successful write, or `null` when the destination
|
|
161
|
+
* already matches the source (idempotent no-op).
|
|
162
|
+
*
|
|
163
|
+
* @internal
|
|
164
|
+
*/
|
|
165
|
+
function copyTemplate(src: string, dest: string, force: boolean): { wrote: boolean } {
|
|
166
|
+
if (!existsSync(src)) {
|
|
167
|
+
throw new Error(`CLEO hook template missing at source: ${src}`);
|
|
168
|
+
}
|
|
169
|
+
if (!force && existsSync(dest)) {
|
|
170
|
+
const srcStat = statSync(src);
|
|
171
|
+
const destStat = statSync(dest);
|
|
172
|
+
if (srcStat.size === destStat.size && srcStat.mtimeMs <= destStat.mtimeMs) {
|
|
173
|
+
return { wrote: false };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
177
|
+
copyFileSync(src, dest);
|
|
178
|
+
return { wrote: true };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Public API
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Install the CLEO PreCompact hook templates for a provider.
|
|
187
|
+
*
|
|
188
|
+
* Writes two files into `targetDir`:
|
|
189
|
+
*
|
|
190
|
+
* 1. `cleo-precompact-core.sh` — universal helper (shared across all providers)
|
|
191
|
+
* 2. `<provider-shim>.sh` — provider-flavoured shim that sources the helper
|
|
192
|
+
*
|
|
193
|
+
* The shim invokes only the universal CLEO CLI (`cleo memory precompact-flush`
|
|
194
|
+
* and `cleo safestop …`) — adapters never reach into core internals.
|
|
195
|
+
*
|
|
196
|
+
* @param options - Installation target and provider id.
|
|
197
|
+
* @returns Paths written, paths skipped, and the resolved target directory.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```typescript
|
|
201
|
+
* import { homedir } from 'node:os';
|
|
202
|
+
* import { join } from 'node:path';
|
|
203
|
+
* import { installProviderHookTemplates } from '@cleocode/adapters';
|
|
204
|
+
*
|
|
205
|
+
* const result = installProviderHookTemplates({
|
|
206
|
+
* provider: 'claude-code',
|
|
207
|
+
* targetDir: join(homedir(), '.claude', 'hooks'),
|
|
208
|
+
* });
|
|
209
|
+
* // result.installedFiles includes both scripts on first run.
|
|
210
|
+
* ```
|
|
211
|
+
*
|
|
212
|
+
* @task T1013
|
|
213
|
+
* @public
|
|
214
|
+
*/
|
|
215
|
+
export function installProviderHookTemplates(
|
|
216
|
+
options: InstallHookTemplatesOptions,
|
|
217
|
+
): InstallHookTemplatesResult {
|
|
218
|
+
const { provider, targetDir, force = false } = options;
|
|
219
|
+
const result: InstallHookTemplatesResult = {
|
|
220
|
+
provider,
|
|
221
|
+
targetDir,
|
|
222
|
+
installedFiles: [],
|
|
223
|
+
skipped: [],
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
mkdirSync(targetDir, { recursive: true });
|
|
227
|
+
|
|
228
|
+
// 1. Shared universal helper
|
|
229
|
+
const coreDest = join(targetDir, SHARED_CORE_FILE);
|
|
230
|
+
const coreOutcome = copyTemplate(sharedCoreSource(), coreDest, force);
|
|
231
|
+
if (coreOutcome.wrote) result.installedFiles.push(coreDest);
|
|
232
|
+
else result.skipped.push(coreDest);
|
|
233
|
+
|
|
234
|
+
// 2. Provider-specific shim
|
|
235
|
+
const shimName = PROVIDER_SHIM[provider];
|
|
236
|
+
const shimDest = join(targetDir, shimName);
|
|
237
|
+
const shimOutcome = copyTemplate(providerShimSource(provider), shimDest, force);
|
|
238
|
+
if (shimOutcome.wrote) result.installedFiles.push(shimDest);
|
|
239
|
+
else result.skipped.push(shimDest);
|
|
240
|
+
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Resolve the source-side path of a provider's hook template for inspection
|
|
246
|
+
* and testing. Returns the absolute path where the installer will read from.
|
|
247
|
+
*
|
|
248
|
+
* @param provider - Provider identifier.
|
|
249
|
+
* @returns Absolute path to the provider's shim template file.
|
|
250
|
+
*
|
|
251
|
+
* @task T1013
|
|
252
|
+
* @public
|
|
253
|
+
*/
|
|
254
|
+
export function getProviderHookTemplatePath(provider: HookTemplateProviderId): string {
|
|
255
|
+
return providerShimSource(provider);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Resolve the source-side path of the shared universal helper.
|
|
260
|
+
*
|
|
261
|
+
* @returns Absolute path to `cleo-precompact-core.sh` in the adapter package.
|
|
262
|
+
*
|
|
263
|
+
* @task T1013
|
|
264
|
+
* @public
|
|
265
|
+
*/
|
|
266
|
+
export function getSharedHookCorePath(): string {
|
|
267
|
+
return sharedCoreSource();
|
|
268
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# CLEO PreCompact Core — Shared universal logic (provider-neutral)
|
|
3
|
+
#
|
|
4
|
+
# This helper performs the provider-agnostic work for a pre-compact hook:
|
|
5
|
+
# 1. Locate the CLEO project directory (walks up from $PWD looking for .cleo/)
|
|
6
|
+
# 2. Resolve the `cleo` CLI binary
|
|
7
|
+
# 3. Invoke `cleo memory precompact-flush` to drain pending observations + checkpoint WAL
|
|
8
|
+
# 4. Invoke `cleo safestop --reason precompact-emergency --commit --handoff <file>`
|
|
9
|
+
# 5. Emit a human-readable summary to stderr
|
|
10
|
+
#
|
|
11
|
+
# This script is intentionally INVOKED via the CLEO CLI only — it never
|
|
12
|
+
# reaches into core internals. Provider-specific hook shims (Claude Code's
|
|
13
|
+
# `precompact-safestop.sh`, Cursor's `precompact.sh`, etc.) source this file
|
|
14
|
+
# and then perform any provider-specific banner/exit handling.
|
|
15
|
+
#
|
|
16
|
+
# INSTALLATION:
|
|
17
|
+
# Provider-specific installers copy this file alongside the provider shim
|
|
18
|
+
# to the target hooks directory. See:
|
|
19
|
+
# - packages/adapters/src/providers/claude-code/templates/hooks/
|
|
20
|
+
# - packages/adapters/src/providers/cursor/templates/hooks/
|
|
21
|
+
# - packages/adapters/src/providers/opencode/templates/hooks/
|
|
22
|
+
#
|
|
23
|
+
# Exit code: always 0 (hook must never block the provider's compaction path).
|
|
24
|
+
#
|
|
25
|
+
# Environment overrides:
|
|
26
|
+
# CLEO_PROJECT_DIR — absolute path to .cleo/ (skips upward search)
|
|
27
|
+
# CLEO_HOME — CLEO install root (controls default cleo binary lookup)
|
|
28
|
+
#
|
|
29
|
+
# @task T1013
|
|
30
|
+
|
|
31
|
+
set -euo pipefail
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# 1. Locate the CLEO project directory
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
cleo_core_find_dir() {
|
|
37
|
+
local dir="$PWD"
|
|
38
|
+
while [[ "$dir" != "/" ]]; do
|
|
39
|
+
if [[ -d "$dir/.cleo" ]]; then
|
|
40
|
+
echo "$dir/.cleo"
|
|
41
|
+
return 0
|
|
42
|
+
fi
|
|
43
|
+
dir="$(dirname "$dir")"
|
|
44
|
+
done
|
|
45
|
+
echo ""
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# 2. Resolve the cleo CLI binary
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
cleo_core_find_cli() {
|
|
52
|
+
local candidate="${CLEO_HOME:-$HOME/.cleo}/bin/cleo"
|
|
53
|
+
if [[ -x "$candidate" ]]; then
|
|
54
|
+
echo "$candidate"
|
|
55
|
+
return 0
|
|
56
|
+
fi
|
|
57
|
+
command -v cleo 2>/dev/null || echo ""
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# 3. Append a timestamped log line to ${CLEO_DIR}/safestop.log (best-effort)
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
cleo_core_log() {
|
|
64
|
+
local message="$1"
|
|
65
|
+
local log_file="$2"
|
|
66
|
+
local timestamp
|
|
67
|
+
timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
68
|
+
echo "[$timestamp] $message" >> "$log_file" 2>/dev/null || true
|
|
69
|
+
echo "[CLEO] $message" >&2
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# 4. Main entrypoint — runs the flush + safestop sequence
|
|
74
|
+
#
|
|
75
|
+
# Usage: cleo_core_run_precompact "<reason>"
|
|
76
|
+
# Emits a `$CLEO_PRECOMPACT_HANDOFF` env var in the caller's shell
|
|
77
|
+
# pointing at the handoff JSON file (empty string if not produced).
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
cleo_core_run_precompact() {
|
|
80
|
+
local reason="${1:-precompact-emergency}"
|
|
81
|
+
local cleo_dir="${CLEO_PROJECT_DIR:-$(cleo_core_find_dir)}"
|
|
82
|
+
local session_file="${cleo_dir:-.cleo}/.current-session"
|
|
83
|
+
local log_file="${cleo_dir:-.cleo}/safestop.log"
|
|
84
|
+
|
|
85
|
+
CLEO_PRECOMPACT_HANDOFF=""
|
|
86
|
+
CLEO_PRECOMPACT_SESSION_ID=""
|
|
87
|
+
|
|
88
|
+
if [[ -z "$cleo_dir" ]] || [[ ! -f "$session_file" ]]; then
|
|
89
|
+
# No CLEO project here — silent no-op so we never interfere with the host.
|
|
90
|
+
if [[ -n "$cleo_dir" ]]; then
|
|
91
|
+
cleo_core_log "PreCompact triggered but no active CLEO session" "$log_file"
|
|
92
|
+
fi
|
|
93
|
+
return 0
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
local session_id
|
|
97
|
+
session_id=$(cat "$session_file" 2>/dev/null || echo "")
|
|
98
|
+
if [[ -z "$session_id" ]]; then
|
|
99
|
+
cleo_core_log "PreCompact triggered but session file empty" "$log_file"
|
|
100
|
+
return 0
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
CLEO_PRECOMPACT_SESSION_ID="$session_id"
|
|
104
|
+
cleo_core_log "PreCompact triggered — initiating emergency safestop (reason=$reason)" "$log_file"
|
|
105
|
+
|
|
106
|
+
local cleo_cmd
|
|
107
|
+
cleo_cmd=$(cleo_core_find_cli)
|
|
108
|
+
if [[ -z "$cleo_cmd" ]] || [[ ! -x "$cleo_cmd" ]]; then
|
|
109
|
+
cleo_core_log "ERROR: cleo command not found — cannot perform safestop" "$log_file"
|
|
110
|
+
return 0
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# Step 1 — flush in-flight observations + checkpoint WAL before the compaction boundary.
|
|
114
|
+
# Invokes: cleo memory precompact-flush (T1004).
|
|
115
|
+
"$cleo_cmd" memory precompact-flush 2>&1 | tee -a "$log_file" || true
|
|
116
|
+
|
|
117
|
+
# Step 2 — run safestop with the emergency reason.
|
|
118
|
+
local handoff_file="${cleo_dir}/handoff-emergency-$(date +%s).json"
|
|
119
|
+
"$cleo_cmd" safestop \
|
|
120
|
+
--reason "$reason" \
|
|
121
|
+
--commit \
|
|
122
|
+
--handoff "$handoff_file" \
|
|
123
|
+
2>&1 | tee -a "$log_file" || true
|
|
124
|
+
|
|
125
|
+
cleo_core_log "Emergency safestop completed. Handoff: $handoff_file" "$log_file"
|
|
126
|
+
CLEO_PRECOMPACT_HANDOFF="$handoff_file"
|
|
127
|
+
return 0
|
|
128
|
+
}
|