@cleocode/adapters 2026.10.8 → 2026.10.9

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.
Files changed (32) hide show
  1. package/dist/heavy-command-hook.js +13 -12
  2. package/dist/heavy-command-hook.js.map +1 -1
  3. package/dist/index.js +60 -133
  4. package/dist/index.js.map +2 -2
  5. package/dist/project-hook-delivery.d.ts +6 -0
  6. package/dist/project-hook-delivery.d.ts.map +1 -0
  7. package/dist/project-hook-delivery.js +527 -0
  8. package/dist/project-hook-delivery.js.map +6 -0
  9. package/dist/project-hook-native.d.ts +7 -0
  10. package/dist/project-hook-native.d.ts.map +1 -0
  11. package/dist/project-hook-native.js +84 -0
  12. package/dist/project-hook-native.js.map +6 -0
  13. package/dist/providers/claude-code/hooks.d.ts.map +1 -1
  14. package/dist/providers/codex/hooks.d.ts +5 -4
  15. package/dist/providers/codex/hooks.d.ts.map +1 -1
  16. package/dist/providers/cursor/hooks.d.ts.map +1 -1
  17. package/dist/providers/gemini-cli/hooks.d.ts.map +1 -1
  18. package/dist/providers/opencode/hooks.d.ts.map +1 -1
  19. package/dist/providers/pi/hooks.d.ts.map +1 -1
  20. package/dist/providers/shared/heavy-command-hook-install.d.ts +2 -1
  21. package/dist/providers/shared/heavy-command-hook-install.d.ts.map +1 -1
  22. package/package.json +14 -5
  23. package/src/__tests__/project-hook-delivery.test.ts +319 -0
  24. package/src/project-hook-delivery.ts +532 -0
  25. package/src/project-hook-native.ts +128 -0
  26. package/src/providers/claude-code/hooks.ts +2 -30
  27. package/src/providers/codex/hooks.ts +7 -9
  28. package/src/providers/cursor/hooks.ts +2 -22
  29. package/src/providers/gemini-cli/hooks.ts +2 -12
  30. package/src/providers/opencode/hooks.ts +2 -22
  31. package/src/providers/pi/hooks.ts +2 -26
  32. package/src/providers/shared/heavy-command-hook-install.ts +21 -10
@@ -20,6 +20,7 @@ import { existsSync } from 'node:fs';
20
20
  import { readdir, readFile } from 'node:fs/promises';
21
21
  import { join } from 'node:path';
22
22
  import { updateJsonConfigFile } from '@cleocode/caamp';
23
+ import { PROVIDER_NATIVE_EVENT_MAPS } from '@cleocode/caamp/hooks';
23
24
  import type { AdapterHookProvider } from '@cleocode/contracts';
24
25
  import { excludeLocalSettingsFromGit } from '../shared/heavy-command-hook-install.js';
25
26
  import { appendHookEntry, hookMap, isPlainObject, projectClaudeSettingsPath } from './paths.js';
@@ -114,36 +115,7 @@ const PROVIDER_ID = 'claude-code' as const;
114
115
  * Used as fallback when CAAMP runtime is unavailable, and as the synchronous
115
116
  * implementation of `mapProviderEvent()`.
116
117
  */
117
- const CLAUDE_CODE_EVENT_MAP: Record<string, string> = {
118
- // CAAMP: toNative('SessionStart', 'claude-code') = 'SessionStart'
119
- SessionStart: 'SessionStart',
120
- // CAAMP: toNative('SessionEnd', 'claude-code') = 'SessionEnd'
121
- SessionEnd: 'SessionEnd',
122
- // CAAMP: toNative('PromptSubmit', 'claude-code') = 'UserPromptSubmit'
123
- UserPromptSubmit: 'PromptSubmit',
124
- // CAAMP: toNative('ResponseComplete', 'claude-code') = 'Stop'
125
- Stop: 'ResponseComplete',
126
- // CAAMP: toNative('PreToolUse', 'claude-code') = 'PreToolUse'
127
- PreToolUse: 'PreToolUse',
128
- // CAAMP: toNative('PostToolUse', 'claude-code') = 'PostToolUse'
129
- PostToolUse: 'PostToolUse',
130
- // CAAMP: toNative('PostToolUseFailure','claude-code') = 'PostToolUseFailure'
131
- PostToolUseFailure: 'PostToolUseFailure',
132
- // CAAMP: toNative('PermissionRequest', 'claude-code') = 'PermissionRequest'
133
- PermissionRequest: 'PermissionRequest',
134
- // CAAMP: toNative('SubagentStart', 'claude-code') = 'SubagentStart'
135
- SubagentStart: 'SubagentStart',
136
- // CAAMP: toNative('SubagentStop', 'claude-code') = 'SubagentStop'
137
- SubagentStop: 'SubagentStop',
138
- // CAAMP: toNative('PreCompact', 'claude-code') = 'PreCompact'
139
- PreCompact: 'PreCompact',
140
- // CAAMP: toNative('PostCompact', 'claude-code') = 'PostCompact'
141
- PostCompact: 'PostCompact',
142
- // CAAMP: toNative('Notification', 'claude-code') = 'Notification'
143
- Notification: 'Notification',
144
- // CAAMP: toNative('ConfigChange', 'claude-code') = 'ConfigChange'
145
- ConfigChange: 'ConfigChange',
146
- };
118
+ const CLAUDE_CODE_EVENT_MAP = PROVIDER_NATIVE_EVENT_MAPS['claude-code'] ?? {};
147
119
 
148
120
  /**
149
121
  * Hook provider for Claude Code.
@@ -2,12 +2,13 @@
2
2
  * Codex CLI Hook Provider
3
3
  *
4
4
  * Maps Codex CLI's native hook events to CAAMP canonical hook events.
5
- * Codex CLI supports 3 canonical events through its hook system.
5
+ * Capabilities and fallback maps are generated from the CAAMP registry.
6
6
  *
7
7
  * Codex CLI event mapping:
8
8
  * - SessionStart -> SessionStart
9
9
  * - PromptSubmit -> UserPromptSubmit
10
10
  * - ResponseComplete -> Stop
11
+ * - PreToolUse -> PreToolUse
11
12
  *
12
13
  * @task T162
13
14
  * @epic T134
@@ -15,23 +16,20 @@
15
16
 
16
17
  import { homedir } from 'node:os';
17
18
  import { join } from 'node:path';
19
+ import { PROVIDER_NATIVE_EVENT_MAPS } from '@cleocode/caamp/hooks';
18
20
  import type { AdapterHookProvider } from '@cleocode/contracts';
19
21
  import { readLatestTranscript } from '../shared/transcript-reader.js';
20
22
 
21
23
  /**
22
24
  * Mapping from Codex CLI native event names to CAAMP canonical event names.
23
25
  */
24
- const CODEX_EVENT_MAP: Record<string, string> = {
25
- SessionStart: 'SessionStart',
26
- PromptSubmit: 'UserPromptSubmit',
27
- ResponseComplete: 'Stop',
28
- };
26
+ const CODEX_EVENT_MAP = PROVIDER_NATIVE_EVENT_MAPS['codex'] ?? {};
29
27
 
30
28
  /**
31
29
  * Hook provider for Codex CLI.
32
30
  *
33
31
  * Codex CLI registers hooks via its configuration system at
34
- * ~/.codex/. Hook handlers are shell commands or script paths that
32
+ * the project-local .codex/hooks.json. Hook handlers are shell commands or script paths that
35
33
  * execute when the corresponding event fires.
36
34
  *
37
35
  * Since hooks are registered through the config system (managed by
@@ -39,9 +37,9 @@ const CODEX_EVENT_MAP: Record<string, string> = {
39
37
  * track registration state without performing filesystem operations.
40
38
  *
41
39
  * @remarks
42
- * Codex CLI has a minimal hook surface with only 3 canonical events.
40
+ * Codex supports PreToolUse; version certification remains separate from installation.
43
41
  * Registration state is tracked in-memory because Codex CLI manages
44
- * hooks through its own configuration system at `~/.codex/`.
42
+ * hooks through its own configuration system. Native project and hook trust are separate.
45
43
  *
46
44
  * @task T162
47
45
  * @epic T134
@@ -20,6 +20,7 @@
20
20
  * @epic T134
21
21
  */
22
22
 
23
+ import { PROVIDER_NATIVE_EVENT_MAPS } from '@cleocode/caamp/hooks';
23
24
  import type { AdapterHookProvider } from '@cleocode/contracts';
24
25
 
25
26
  /** CAAMP provider identifier for Cursor. */
@@ -34,28 +35,7 @@ const PROVIDER_ID = 'cursor' as const;
34
35
  *
35
36
  * Cursor uses camelCase names while CAAMP canonical names are PascalCase.
36
37
  */
37
- const CURSOR_EVENT_MAP: Record<string, string> = {
38
- // CAAMP: toNative('SessionStart', 'cursor') = 'sessionStart'
39
- sessionStart: 'SessionStart',
40
- // CAAMP: toNative('SessionEnd', 'cursor') = 'sessionEnd'
41
- sessionEnd: 'SessionEnd',
42
- // CAAMP: toNative('PromptSubmit', 'cursor') = 'beforeSubmitPrompt'
43
- beforeSubmitPrompt: 'PromptSubmit',
44
- // CAAMP: toNative('ResponseComplete', 'cursor') = 'stop'
45
- stop: 'ResponseComplete',
46
- // CAAMP: toNative('PreToolUse', 'cursor') = 'preToolUse'
47
- preToolUse: 'PreToolUse',
48
- // CAAMP: toNative('PostToolUse', 'cursor') = 'postToolUse'
49
- postToolUse: 'PostToolUse',
50
- // CAAMP: toNative('PostToolUseFailure', 'cursor') = 'postToolUseFailure'
51
- postToolUseFailure: 'PostToolUseFailure',
52
- // CAAMP: toNative('SubagentStart', 'cursor') = 'subagentStart'
53
- subagentStart: 'SubagentStart',
54
- // CAAMP: toNative('SubagentStop', 'cursor') = 'subagentStop'
55
- subagentStop: 'SubagentStop',
56
- // CAAMP: toNative('PreCompact', 'cursor') = 'preCompact'
57
- preCompact: 'PreCompact',
58
- };
38
+ const CURSOR_EVENT_MAP = PROVIDER_NATIVE_EVENT_MAPS['cursor'] ?? {};
59
39
 
60
40
  /**
61
41
  * Hook provider for Cursor.
@@ -22,24 +22,14 @@
22
22
 
23
23
  import { homedir } from 'node:os';
24
24
  import { join } from 'node:path';
25
+ import { PROVIDER_NATIVE_EVENT_MAPS } from '@cleocode/caamp/hooks';
25
26
  import type { AdapterHookProvider } from '@cleocode/contracts';
26
27
  import { readLatestTranscript } from '../shared/transcript-reader.js';
27
28
 
28
29
  /**
29
30
  * Mapping from Gemini CLI native event names to CAAMP canonical event names.
30
31
  */
31
- const GEMINI_CLI_EVENT_MAP: Record<string, string> = {
32
- SessionStart: 'SessionStart',
33
- SessionEnd: 'SessionEnd',
34
- PromptSubmit: 'BeforeAgent',
35
- ResponseComplete: 'AfterAgent',
36
- PreToolUse: 'BeforeTool',
37
- PostToolUse: 'AfterTool',
38
- PreModel: 'BeforeModel',
39
- PostModel: 'AfterModel',
40
- PreCompact: 'PreCompress',
41
- Notification: 'Notification',
42
- };
32
+ const GEMINI_CLI_EVENT_MAP = PROVIDER_NATIVE_EVENT_MAPS['gemini-cli'] ?? {};
43
33
 
44
34
  /**
45
35
  * Hook provider for Gemini CLI.
@@ -19,6 +19,7 @@
19
19
  * @epic T134
20
20
  */
21
21
 
22
+ import { PROVIDER_NATIVE_EVENT_MAPS } from '@cleocode/caamp/hooks';
22
23
  import type { AdapterHookProvider } from '@cleocode/contracts';
23
24
 
24
25
  /** CAAMP provider identifier for OpenCode. */
@@ -35,28 +36,7 @@ const PROVIDER_ID = 'opencode' as const;
35
36
  * OpenCode uses dot-delimited and event-prefixed names (e.g. "event:session.created")
36
37
  * while CAAMP canonical names are PascalCase (e.g. "SessionStart").
37
38
  */
38
- const OPENCODE_EVENT_MAP: Record<string, string> = {
39
- // CAAMP: toNative('SessionStart', 'opencode') = 'event:session.created'
40
- 'event:session.created': 'SessionStart',
41
- // CAAMP: toNative('SessionEnd', 'opencode') = 'event:session.deleted'
42
- 'event:session.deleted': 'SessionEnd',
43
- // CAAMP: toNative('PromptSubmit', 'opencode') = 'chat.message'
44
- 'chat.message': 'PromptSubmit',
45
- // CAAMP: toNative('ResponseComplete', 'opencode') = 'event:session.idle'
46
- 'event:session.idle': 'ResponseComplete',
47
- // CAAMP: toNative('PreToolUse', 'opencode') = 'tool.execute.before'
48
- 'tool.execute.before': 'PreToolUse',
49
- // CAAMP: toNative('PostToolUse', 'opencode') = 'tool.execute.after'
50
- 'tool.execute.after': 'PostToolUse',
51
- // CAAMP: toNative('PermissionRequest', 'opencode') = 'permission.ask'
52
- 'permission.ask': 'PermissionRequest',
53
- // CAAMP: toNative('PreModel', 'opencode') = 'chat.params'
54
- 'chat.params': 'PreModel',
55
- // CAAMP: toNative('PreCompact', 'opencode') = 'experimental.session.compacting'
56
- 'experimental.session.compacting': 'PreCompact',
57
- // CAAMP: toNative('PostCompact', 'opencode') = 'event:session.compacted'
58
- 'event:session.compacted': 'PostCompact',
59
- };
39
+ const OPENCODE_EVENT_MAP = PROVIDER_NATIVE_EVENT_MAPS['opencode'] ?? {};
60
40
 
61
41
  /**
62
42
  * Hook provider for OpenCode.
@@ -32,6 +32,7 @@
32
32
  * @task T553
33
33
  */
34
34
 
35
+ import { PROVIDER_NATIVE_EVENT_MAPS } from '@cleocode/caamp/hooks';
35
36
  import type { AdapterHookProvider } from '@cleocode/contracts';
36
37
 
37
38
  /** CAAMP provider identifier for Pi. */
@@ -48,32 +49,7 @@ const PROVIDER_ID = 'pi' as const;
48
49
  * Used as fallback when CAAMP runtime is unavailable, and as the
49
50
  * synchronous implementation of `mapProviderEvent()`.
50
51
  */
51
- const PI_EVENT_MAP: Record<string, string> = {
52
- // piEventCatalog: session_start → SessionStart
53
- session_start: 'SessionStart',
54
- // piEventCatalog: session_shutdown → SessionEnd
55
- session_shutdown: 'SessionEnd',
56
- // piEventCatalog: input → PromptSubmit
57
- input: 'PromptSubmit',
58
- // piEventCatalog: turn_end → Notification (assistant turn complete)
59
- turn_end: 'Notification',
60
- // piEventCatalog: tool_call → PreToolUse
61
- tool_call: 'PreToolUse',
62
- // piEventCatalog: tool_execution_start → PreToolUse (duplicate path)
63
- tool_execution_start: 'PreToolUse',
64
- // piEventCatalog: tool_result → PostToolUse
65
- tool_result: 'PostToolUse',
66
- // piEventCatalog: tool_execution_end → PostToolUse (duplicate path)
67
- tool_execution_end: 'PostToolUse',
68
- // piEventCatalog: before_agent_start → SubagentStart
69
- before_agent_start: 'SubagentStart',
70
- // piEventCatalog: agent_end → SubagentStop
71
- agent_end: 'SubagentStop',
72
- // piEventCatalog: before_provider_request → PreModel
73
- before_provider_request: 'PreModel',
74
- // piEventCatalog: context → PreCompact (context assembly is the pre-compaction proxy for Pi)
75
- context: 'PreCompact',
76
- };
52
+ const PI_EVENT_MAP = PROVIDER_NATIVE_EVENT_MAPS['pi'] ?? {};
77
53
 
78
54
  /**
79
55
  * Hook provider for Pi coding agent.
@@ -526,12 +526,17 @@ export const CODEX_HOOKS_FILE = '.codex/hooks.json';
526
526
  export const OPENCODE_PLUGIN_FILE = `.opencode/plugins/${OPENCODE_HEAVY_COMMAND_PLUGIN}`;
527
527
 
528
528
  /** Run git in `projectDir`; its trimmed stdout, or `null` on any failure. */
529
- function gitOutput(projectDir: string, args: readonly string[]): string | null {
529
+ function gitOutput(
530
+ projectDir: string,
531
+ args: readonly string[],
532
+ env: NodeJS.ProcessEnv = process.env,
533
+ ): string | null {
530
534
  try {
531
535
  return execFileSync('git', ['-C', projectDir, ...args], {
532
536
  encoding: 'utf-8',
533
537
  stdio: ['ignore', 'pipe', 'ignore'],
534
538
  timeout: 5000,
539
+ env,
535
540
  }).trim();
536
541
  } catch {
537
542
  return null;
@@ -539,16 +544,20 @@ function gitOutput(projectDir: string, args: readonly string[]): string | null {
539
544
  }
540
545
 
541
546
  /** The repository's `info/exclude` path for `projectDir`, or `null` outside a work tree. */
542
- function excludeFile(projectDir: string): string | null {
543
- if (gitOutput(projectDir, ['rev-parse', '--is-inside-work-tree']) !== 'true') return null;
544
- const path = gitOutput(projectDir, ['rev-parse', '--git-path', 'info/exclude']);
547
+ function excludeFile(projectDir: string, env: NodeJS.ProcessEnv = process.env): string | null {
548
+ if (gitOutput(projectDir, ['rev-parse', '--is-inside-work-tree'], env) !== 'true') return null;
549
+ const path = gitOutput(projectDir, ['rev-parse', '--git-path', 'info/exclude'], env);
545
550
  if (path === null || path === '') return null;
546
551
  return isAbsolute(path) ? path : join(projectDir, path);
547
552
  }
548
553
 
549
554
  /** The `info/exclude` line for a project file (relative to the repo root). */
550
- function excludeLine(projectDir: string, relPath: string): string {
551
- const prefix = gitOutput(projectDir, ['rev-parse', '--show-prefix']) ?? '';
555
+ function excludeLine(
556
+ projectDir: string,
557
+ relPath: string,
558
+ env: NodeJS.ProcessEnv = process.env,
559
+ ): string {
560
+ const prefix = gitOutput(projectDir, ['rev-parse', '--show-prefix'], env) ?? '';
552
561
  return `/${prefix}${relPath}`;
553
562
  }
554
563
 
@@ -563,18 +572,20 @@ function excludeLine(projectDir: string, relPath: string): string {
563
572
  * @param projectDir - the project root.
564
573
  * @param relPath - the file, relative to the project (forward slashes).
565
574
  * @param marker - the block's first line.
575
+ * @param env - Git discovery environment; existing callers retain the process environment.
566
576
  * @returns whether a block was added.
567
577
  */
568
578
  export function excludeHookFileFromGit(
569
579
  projectDir: string,
570
580
  relPath: string,
571
581
  marker: string = HOOK_FILE_EXCLUDE_MARKER,
582
+ env: NodeJS.ProcessEnv = process.env,
572
583
  ): boolean {
573
- const exclude = excludeFile(projectDir);
584
+ const exclude = excludeFile(projectDir, env);
574
585
  if (exclude === null) return false;
575
- if (gitOutput(projectDir, ['check-ignore', relPath]) !== null) return false;
576
- if (gitOutput(projectDir, ['ls-files', '--error-unmatch', relPath]) !== null) return false;
577
- const line = excludeLine(projectDir, relPath);
586
+ if (gitOutput(projectDir, ['check-ignore', relPath], env) !== null) return false;
587
+ if (gitOutput(projectDir, ['ls-files', '--error-unmatch', relPath], env) !== null) return false;
588
+ const line = excludeLine(projectDir, relPath, env);
578
589
  const text = existsSync(exclude) ? readFileSync(exclude, 'utf-8') : '';
579
590
  if (text.split('\n').includes(line)) return false;
580
591
  const block = `${marker}\n${line}\n`;