subagent-cli 0.3.0 → 0.3.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # sa — Morph Code
1
+ # mc — Morph Code
2
2
 
3
- A fork of Claude Code with multi-provider LLM support, WarpGrep semantic code search, and subagent orchestration. Runs from source with Bun.
3
+ A compiled fork of Claude Code with WarpGrep semantic code search, multi-provider LLM support, and subagent orchestration. Ships as a single native binary per platform.
4
4
 
5
5
  ## Install
6
6
 
@@ -11,96 +11,103 @@ curl -fsSL subagents.com/install | bash
11
11
  Or with npm:
12
12
 
13
13
  ```bash
14
- npm install -g subagent-cli
14
+ npm install -g @morphllm/morphcode
15
15
  ```
16
16
 
17
- Requires [Bun](https://bun.sh) runtime.
18
-
19
17
  ## Usage
20
18
 
21
19
  ```bash
22
20
  # Interactive mode
23
- sa
21
+ mc
24
22
 
25
23
  # Single prompt
26
- sa -p "refactor the auth module"
24
+ mc -p "refactor the auth module"
27
25
 
28
26
  # Print mode (non-interactive, for scripts)
29
- sa -p "explain this codebase" --print
27
+ mc -p "explain this codebase" --print
30
28
 
31
29
  # Specify model
32
- sa --model claude-sonnet-4-6
33
- sa --model opus
30
+ mc --model claude-sonnet-4-6
31
+ mc --model opus
34
32
 
35
33
  # List available agents
36
- sa agents
34
+ mc agents
37
35
  ```
38
36
 
39
37
  ## What's different from Claude Code
40
38
 
41
- **Multi-provider LLM** — Uses [pi-ai](https://github.com/badlogic/pi-mono) under the hood. Set `PI_ENGINE=on` to route through OpenAI, Google, Mistral, or any supported provider instead of Anthropic.
39
+ **WarpGrep agent** — Semantic code search powered by Morph. The `warpgrep` subagent understands code structure, not just text patterns. Always registered alongside Explore and Plan.
42
40
 
43
- **WarpGrep agent** — Semantic code search powered by Morph. When `MORPH_API_KEY` is set, a `warpgrep` subagent becomes available that understands code structure, not just text patterns.
41
+ **Compiled binary** — Ships as a native Mach-O / ELF binary (like Claude Code itself). Starts instantly, no runtime dependencies.
44
42
 
45
- **Explore & Plan agents** — Always enabled. The Explore agent prefers WarpGrep over regex grep when available.
43
+ **Multi-provider LLM** — Set `PI_ENGINE=on` to route through OpenAI, Google, Mistral, or any provider supported by pi-ai.
46
44
 
47
- **Subagent tools** — WarpGrep and FastApply (smart code editing) are registered as first-class tools, available to all agents.
45
+ **Explore & Plan agents** — Always enabled. The Explore agent prefers WarpGrep over regex grep when available.
48
46
 
49
47
  ## Agents
50
48
 
51
49
  ```
52
- $ sa agents
50
+ $ mc agents
53
51
 
54
- 9 active agents
52
+ 6 active agents
55
53
 
56
54
  Built-in agents:
57
- Explore · haiku — Fast codebase exploration (uses WarpGrep when available)
58
- Plan · inherit — Architecture and implementation planning
59
- warpgrep · haiku — Semantic code search via Morph
60
- general-purpose · inherit — Research, code search, multi-step tasks
61
- claude-code-guide · haiku — Questions about features and usage
62
- statusline-setup · sonnet — Configure status line
55
+ Explore · haiku — Fast codebase exploration
56
+ Plan · inherit — Architecture and implementation planning
57
+ warpgrep · haiku — Semantic code search via Morph
58
+ general-purpose · inherit — Research, code search, multi-step tasks
59
+ claude-code-guide · haiku — Questions about features and usage
60
+ statusline-setup · sonnet — Configure status line
63
61
  ```
64
62
 
65
63
  ## Environment variables
66
64
 
67
65
  | Variable | Required | Purpose |
68
66
  |----------|----------|---------|
69
- | `ANTHROPIC_API_KEY` | Yes (or OAuth) | Anthropic API access |
70
- | `MORPH_API_KEY` | No | Enables WarpGrep, FastApply, and warpgrep agent |
67
+ | `ANTHROPIC_API_KEY` | Yes (or OAuth via `mc auth login`) | Anthropic API access |
68
+ | `MORPH_API_KEY` | No | Enables WarpGrep and FastApply tools |
71
69
  | `OPENAI_API_KEY` | No | For PI_ENGINE multi-provider mode |
72
70
  | `GOOGLE_API_KEY` | No | For PI_ENGINE multi-provider mode |
73
71
  | `PI_ENGINE` | No | Set to `on` to route LLM calls through pi-ai |
74
72
 
73
+ ## Platform binaries
74
+
75
+ Published to npm as platform-specific packages:
76
+
77
+ | Platform | Package |
78
+ |----------|---------|
79
+ | macOS Apple Silicon | `@morphllm/morphcode-darwin-arm64` |
80
+ | macOS Intel | `@morphllm/morphcode-darwin-x64` |
81
+ | Linux x64 | `@morphllm/morphcode-linux-x64` |
82
+ | Linux arm64 | `@morphllm/morphcode-linux-arm64` |
83
+ | Windows x64 | `@morphllm/morphcode-windows-x64` |
84
+
75
85
  ## Development
76
86
 
77
87
  ```bash
78
- # Run from source
79
- cd packages/subagentscccli
88
+ # Run from source (requires Bun)
89
+ cd packages/subagent-cli
80
90
  bun run --preload ./stubs/globals.ts ./src/entrypoints/cli.tsx
81
91
 
82
- # Run tests (88 tests)
92
+ # Build native binary
93
+ bun build --compile _entry.ts --outfile dist/mc --target bun
94
+
95
+ # Run tests
83
96
  npx vitest --run
84
97
 
85
- # Check agents
98
+ # List agents
86
99
  bun run --preload ./stubs/globals.ts ./src/entrypoints/cli.tsx agents
87
-
88
- # Verify version
89
- bun run --preload ./stubs/globals.ts ./src/entrypoints/cli.tsx --version
90
100
  ```
91
101
 
92
102
  ## Architecture
93
103
 
94
- Built on the Claude Code v2.1.88 source with these modifications:
104
+ Built on Claude Code v2.1.88 source, compiled to a native binary via `bun build --compile`.
95
105
 
96
- - `src/engine/` — Pi-mono integration layer (PiAgentAdapter, MessageMapper, ToolAdapter, ModelResolver, CostMapper, PermissionBridge, SubagentTools)
97
- - `src/query/deps.ts` — LLM call routing (Anthropic SDK default, pi-ai when `PI_ENGINE=on`)
98
- - `src/tools/AgentTool/built-in/warpGrepAgent.ts` — WarpGrep subagent definition
106
+ Key modifications:
107
+ - `src/tools/AgentTool/built-in/warpGrepAgent.ts` — WarpGrep subagent
99
108
  - `src/tools/WarpGrepTool/WarpGrepTool.ts` — WarpGrep as a Claude Code Tool
100
109
  - `src/tools/AgentTool/builtInAgents.ts` — Explore/Plan always enabled, warpgrep registered
101
- - `stubs/` — Runtime stubs for unavailable Anthropic-internal packages
102
- - `bin/sa.js` — Bun launcher entry point
103
-
104
- ## License
105
-
106
- Claude Code source is property of Anthropic. This fork is for internal use.
110
+ - `src/engine/` — Pi-mono integration layer for multi-provider LLM
111
+ - `src/query/deps.ts` — LLM call routing (Anthropic default, pi-ai when `PI_ENGINE=on`)
112
+ - `stubs/` — Build-time stubs for unavailable Anthropic-internal packages
113
+ - `.github/workflows/publish-morphcode.yml` — Multi-platform binary build and npm publish
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "subagent-cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "sa — Morph Code CLI (Claude Code fork with multi-provider LLM, WarpGrep, and subagent support)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,8 +21,7 @@
21
21
  "typecheck": "tsc --noEmit",
22
22
  "test": "vitest --run",
23
23
  "clean": "rm -rf dist",
24
- "start": "bun run --preload ./stubs/globals.ts ./src/entrypoints/cli.tsx",
25
- "postinstall": "node -e \"const f=require(\"fs\"),p=require(\"path\");try{const s=p.join(__dirname,\"node_modules\",\"src\");if(!f.existsSync(s)){f.mkdirSync(p.join(__dirname,\"node_modules\"),{recursive:true});f.symlinkSync(p.join(__dirname,\"src\"),s)}}catch{}\""
24
+ "start": "bun run --preload ./stubs/globals.ts ./src/entrypoints/cli.tsx"
26
25
  },
27
26
  "dependencies": {
28
27
  "@anthropic-ai/sdk": "^0.80.0",
@@ -0,0 +1,12 @@
1
+ import type { Command } from '../../commands.js'
2
+
3
+ const installNotch = {
4
+ type: 'local' as const,
5
+ name: 'install-notch',
6
+ description: 'Install the macOS notch helper (shows agent status with a pixel frog)',
7
+ isEnabled: () => process.platform === 'darwin',
8
+ supportsNonInteractive: false,
9
+ load: () => import('./install-notch.js'),
10
+ } satisfies Command
11
+
12
+ export default installNotch
@@ -0,0 +1,151 @@
1
+ /**
2
+ * /install-notch command
3
+ *
4
+ * Builds and installs the SubagentNotch macOS helper app.
5
+ * The app lives in ~/.claude/helpers/SubagentNotch.app and is launched
6
+ * automatically via SessionStart hooks.
7
+ *
8
+ * Steps:
9
+ * 1. Check we're on macOS
10
+ * 2. Build the Swift app from helpers/notch/
11
+ * 3. Copy the built .app bundle to ~/.claude/helpers/
12
+ * 4. Write ~/.claude/notch.json with the port config
13
+ * 5. Register hooks in project settings
14
+ */
15
+
16
+ import { execSync, execFileSync } from 'child_process'
17
+ import { existsSync, mkdirSync, writeFileSync, cpSync } from 'fs'
18
+ import { homedir } from 'os'
19
+ import { join, resolve, dirname } from 'path'
20
+ import { fileURLToPath } from 'url'
21
+ import type { LocalCommandResult } from '../../commands.js'
22
+
23
+ const __filename = fileURLToPath(import.meta.url)
24
+ const __dirname = dirname(__filename)
25
+
26
+ const NOTCH_PORT = 27182
27
+ const HELPERS_DIR = join(homedir(), '.claude', 'helpers')
28
+ const APP_NAME = 'SubagentNotch'
29
+ const CONFIG_PATH = join(homedir(), '.claude', 'notch.json')
30
+
31
+ export async function call(): Promise<LocalCommandResult> {
32
+ // Step 1: Platform check
33
+ if (process.platform !== 'darwin') {
34
+ return {
35
+ type: 'text',
36
+ value: 'The notch helper is only available on macOS.',
37
+ }
38
+ }
39
+
40
+ // Step 2: Check for Swift toolchain
41
+ try {
42
+ execFileSync('swift', ['--version'], { stdio: 'pipe' })
43
+ } catch {
44
+ return {
45
+ type: 'text',
46
+ value:
47
+ 'Swift is not installed. Install Xcode or Xcode Command Line Tools:\n' +
48
+ ' xcode-select --install',
49
+ }
50
+ }
51
+
52
+ // Step 3: Find the Swift source
53
+ // Look relative to the CLI package root, then fall back to monorepo root
54
+ const candidates = [
55
+ resolve(__dirname, '../../../../helpers/notch'),
56
+ resolve(__dirname, '../../../../../helpers/notch'),
57
+ resolve(__dirname, '../../../../../../helpers/notch'),
58
+ ]
59
+ const sourceDir = candidates.find(d => existsSync(join(d, 'Package.swift')))
60
+
61
+ if (!sourceDir) {
62
+ return {
63
+ type: 'text',
64
+ value:
65
+ 'Could not find helpers/notch source directory.\n' +
66
+ 'Make sure you are running from the subagents monorepo.',
67
+ }
68
+ }
69
+
70
+ // Step 4: Build the Swift app
71
+ try {
72
+ execSync('swift build -c release --quiet', {
73
+ cwd: sourceDir,
74
+ stdio: 'pipe',
75
+ timeout: 120_000, // 2 minute timeout
76
+ })
77
+ } catch (err: unknown) {
78
+ const message =
79
+ err instanceof Error ? err.message : String(err)
80
+ return {
81
+ type: 'text',
82
+ value: `Swift build failed:\n${message}`,
83
+ }
84
+ }
85
+
86
+ // Step 5: Copy binary to ~/.claude/helpers/
87
+ mkdirSync(HELPERS_DIR, { recursive: true })
88
+
89
+ const builtBinary = join(
90
+ sourceDir,
91
+ '.build',
92
+ 'release',
93
+ APP_NAME,
94
+ )
95
+
96
+ if (!existsSync(builtBinary)) {
97
+ return {
98
+ type: 'text',
99
+ value: 'Build succeeded but binary not found. Check the Swift build output.',
100
+ }
101
+ }
102
+
103
+ const destBinary = join(HELPERS_DIR, APP_NAME)
104
+
105
+ try {
106
+ // Kill any existing instance before overwriting
107
+ try {
108
+ execSync(`pkill -f "${APP_NAME}" 2>/dev/null || true`, { stdio: 'pipe' })
109
+ } catch {
110
+ // Ignore — might not be running
111
+ }
112
+
113
+ cpSync(builtBinary, destBinary)
114
+ // Make executable
115
+ execSync(`chmod +x "${destBinary}"`, { stdio: 'pipe' })
116
+ } catch (err: unknown) {
117
+ const message = err instanceof Error ? err.message : String(err)
118
+ return {
119
+ type: 'text',
120
+ value: `Failed to install binary:\n${message}`,
121
+ }
122
+ }
123
+
124
+ // Step 6: Write config
125
+ const config = {
126
+ port: NOTCH_PORT,
127
+ binary: destBinary,
128
+ installedAt: new Date().toISOString(),
129
+ }
130
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2))
131
+
132
+ // Step 7: Remove quarantine attribute (Gatekeeper bypass for local builds)
133
+ try {
134
+ execSync(`xattr -cr "${destBinary}"`, { stdio: 'pipe' })
135
+ } catch {
136
+ // Non-critical — might not have xattr issues
137
+ }
138
+
139
+ return {
140
+ type: 'text',
141
+ value: [
142
+ `Notch helper installed to ${destBinary}`,
143
+ `Listening on port ${NOTCH_PORT}`,
144
+ '',
145
+ 'The frog will appear in your MacBook notch when a session starts.',
146
+ 'Hook events are published automatically to the helper.',
147
+ '',
148
+ 'To uninstall: rm ~/.claude/helpers/SubagentNotch ~/.claude/notch.json',
149
+ ].join('\n'),
150
+ }
151
+ }
package/src/commands.ts CHANGED
@@ -28,6 +28,7 @@ import keybindings from './commands/keybindings/index.js'
28
28
  import login from './commands/login/index.js'
29
29
  import logout from './commands/logout/index.js'
30
30
  import installGitHubApp from './commands/install-github-app/index.js'
31
+ import installNotch from './commands/install-notch/index.js'
31
32
  import installSlackApp from './commands/install-slack-app/index.js'
32
33
  import breakCache from './commands/break-cache/index.js'
33
34
  import mcp from './commands/mcp/index.js'
@@ -283,6 +284,7 @@ const COMMANDS = memoize((): Command[] => [
283
284
  init,
284
285
  keybindings,
285
286
  installGitHubApp,
287
+ installNotch,
286
288
  installSlackApp,
287
289
  mcp,
288
290
  memory,
@@ -4,6 +4,9 @@
4
4
  * This module provides a generic event system that is separate from the
5
5
  * main message stream. Handlers can register to receive events and decide
6
6
  * what to do with them (e.g., convert to SDK messages, log, etc.).
7
+ *
8
+ * Secondary listeners (e.g., notch publisher) can register via
9
+ * addSecondaryHookEventListener() without interfering with the primary handler.
7
10
  */
8
11
 
9
12
  import { HOOK_EVENTS } from 'src/entrypoints/sdk/coreTypes.js'
@@ -57,6 +60,7 @@ export type HookEventHandler = (event: HookExecutionEvent) => void
57
60
  const pendingEvents: HookExecutionEvent[] = []
58
61
  let eventHandler: HookEventHandler | null = null
59
62
  let allHookEventsEnabled = false
63
+ const secondaryListeners: HookEventHandler[] = []
60
64
 
61
65
  export function registerHookEventHandler(
62
66
  handler: HookEventHandler | null,
@@ -78,6 +82,30 @@ function emit(event: HookExecutionEvent): void {
78
82
  pendingEvents.shift()
79
83
  }
80
84
  }
85
+ // Notify secondary listeners (e.g., notch publisher) — fire-and-forget
86
+ for (const listener of secondaryListeners) {
87
+ try {
88
+ listener(event)
89
+ } catch {
90
+ // Secondary listeners must never break the primary event flow
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Register a secondary listener that receives all emitted hook events.
97
+ * Unlike the primary handler, secondary listeners are additive — multiple
98
+ * can coexist. They run fire-and-forget and errors are silently caught.
99
+ * Returns an unsubscribe function.
100
+ */
101
+ export function addSecondaryHookEventListener(
102
+ listener: HookEventHandler,
103
+ ): () => void {
104
+ secondaryListeners.push(listener)
105
+ return () => {
106
+ const idx = secondaryListeners.indexOf(listener)
107
+ if (idx !== -1) secondaryListeners.splice(idx, 1)
108
+ }
81
109
  }
82
110
 
83
111
  function shouldEmit(hookEvent: string): boolean {
@@ -189,4 +217,5 @@ export function clearHookEventState(): void {
189
217
  eventHandler = null
190
218
  pendingEvents.length = 0
191
219
  allHookEventsEnabled = false
220
+ secondaryListeners.length = 0
192
221
  }
@@ -4110,6 +4110,14 @@ export async function executeSessionEndHooks(
4110
4110
  timeoutMs = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
4111
4111
  } = options || {}
4112
4112
 
4113
+ // Notify the macOS notch helper before running session end hooks
4114
+ try {
4115
+ const { cleanupNotchBridge } = await import('./sessionStart.js')
4116
+ cleanupNotchBridge()
4117
+ } catch {
4118
+ // Non-critical — notch cleanup failure must never block session end
4119
+ }
4120
+
4113
4121
  const hookInput: SessionEndHookInput = {
4114
4122
  ...createBaseHookInput(undefined),
4115
4123
  hook_event_name: 'SessionEnd',
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Notch Bridge
3
+ *
4
+ * Connects the CLI's hook event system to the SubagentNotch macOS helper.
5
+ * Call initNotchBridge() once at session start. It registers a secondary
6
+ * hook event listener that translates hook events into notch events and
7
+ * publishes them over HTTP.
8
+ *
9
+ * This module is a thin adapter between two systems:
10
+ * - Input: HookExecutionEvent from hookEvents.ts
11
+ * - Output: AgentEvent JSON to the notch helper's HTTP server
12
+ */
13
+
14
+ import { addSecondaryHookEventListener } from './hooks/hookEvents.js'
15
+ import type { HookExecutionEvent } from './hooks/hookEvents.js'
16
+ import {
17
+ isNotchConfigured,
18
+ publishNotchEvent,
19
+ setNotchSessionId,
20
+ } from './notchPublisher.js'
21
+
22
+ /**
23
+ * Map from hook event names to notch event names.
24
+ * SessionStart/SessionEnd are handled manually (not via the listener)
25
+ * to avoid duplicate events.
26
+ */
27
+ const EVENT_MAP: Record<string, string> = {
28
+ PreToolUse: 'tool_start',
29
+ PostToolUse: 'tool_end',
30
+ SubagentStart: 'subagent_start',
31
+ SubagentStop: 'subagent_stop',
32
+ TaskCreated: 'task_created',
33
+ TaskCompleted: 'task_completed',
34
+ Stop: 'stop',
35
+ PostToolUseFailure: 'error',
36
+ }
37
+
38
+ /** Hook events we forward on 'started' (beginning of action). */
39
+ const FORWARD_ON_STARTED = new Set(['PreToolUse', 'SubagentStart'])
40
+
41
+ /**
42
+ * Initialize the notch bridge for a session.
43
+ * No-ops on non-macOS or when the notch helper isn't installed.
44
+ * Returns an unsubscribe function that sends session_end and cleans up.
45
+ */
46
+ export function initNotchBridge(sessionId: string): () => void {
47
+ if (!isNotchConfigured()) return () => {}
48
+
49
+ setNotchSessionId(sessionId)
50
+
51
+ // Notify the notch helper that a session started
52
+ publishNotchEvent({ event: 'session_start' })
53
+
54
+ // Listen to hook events and forward relevant ones to the notch helper
55
+ const unsubscribe = addSecondaryHookEventListener(
56
+ (event: HookExecutionEvent) => {
57
+ const notchEvent = EVENT_MAP[event.hookEvent]
58
+ if (!notchEvent) return
59
+
60
+ // Forward 'started' for beginning-of-action events (tool/subagent start),
61
+ // 'response' for end-of-action events (tool end, task complete, stop, error)
62
+ const wantedType = FORWARD_ON_STARTED.has(event.hookEvent)
63
+ ? 'started'
64
+ : 'response'
65
+ if (event.type !== wantedType) return
66
+
67
+ publishNotchEvent({
68
+ event: notchEvent,
69
+ toolName: event.hookName,
70
+ })
71
+ },
72
+ )
73
+
74
+ return () => {
75
+ publishNotchEvent({ event: 'session_end' })
76
+ unsubscribe()
77
+ }
78
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Notch Event Publisher
3
+ *
4
+ * Publishes hook lifecycle events to the SubagentNotch macOS helper app
5
+ * via HTTP POST to a local port. Fire-and-forget — never blocks the CLI.
6
+ *
7
+ * The notch helper listens on port 27182 by default. The port is stored
8
+ * in ~/.claude/notch.json after installation.
9
+ */
10
+
11
+ import { readFileSync } from 'fs'
12
+ import http from 'http'
13
+ import { homedir } from 'os'
14
+ import { join } from 'path'
15
+
16
+ // Default port (e ≈ 2.7182...)
17
+ const DEFAULT_PORT = 27182
18
+
19
+ // Config file written by /install-notch
20
+ const CONFIG_PATH = join(homedir(), '.claude', 'notch.json')
21
+
22
+ /** Cached port so we only read the config file once. */
23
+ let cachedPort: number | null = null
24
+ let portResolved = false
25
+
26
+ /** Cached session ID for the current CLI session. */
27
+ let currentSessionId: string | null = null
28
+
29
+ // MARK: - Public API
30
+
31
+ /** Set the session ID for all future events. Called once at session start. */
32
+ export function setNotchSessionId(sessionId: string): void {
33
+ currentSessionId = sessionId
34
+ }
35
+
36
+ /** Check if the notch helper is configured (macOS only). */
37
+ export function isNotchConfigured(): boolean {
38
+ if (process.platform !== 'darwin') return false
39
+ return getPort() !== null
40
+ }
41
+
42
+ /**
43
+ * Publish an event to the notch helper. Fire-and-forget.
44
+ * Safe to call on any platform — silently no-ops on non-macOS.
45
+ */
46
+ export function publishNotchEvent(event: {
47
+ event: string
48
+ agentId?: string
49
+ toolName?: string
50
+ taskSubject?: string
51
+ message?: string
52
+ }): void {
53
+ if (process.platform !== 'darwin') return
54
+
55
+ const port = getPort()
56
+ if (!port) return
57
+
58
+ const payload = JSON.stringify({
59
+ sessionId: currentSessionId ?? 'unknown',
60
+ event: event.event,
61
+ agentId: event.agentId ?? null,
62
+ toolName: event.toolName ?? null,
63
+ taskSubject: event.taskSubject ?? null,
64
+ message: event.message ?? null,
65
+ timestamp: Date.now() / 1000,
66
+ })
67
+
68
+ // Fire-and-forget HTTP POST — errors are silently ignored.
69
+ // We use raw http.request to avoid adding dependencies.
70
+ const req = http.request(
71
+ {
72
+ hostname: '127.0.0.1',
73
+ port,
74
+ path: '/event',
75
+ method: 'POST',
76
+ headers: {
77
+ 'Content-Type': 'application/json',
78
+ 'Content-Length': Buffer.byteLength(payload),
79
+ },
80
+ timeout: 500, // 500ms max — never slow down the CLI
81
+ },
82
+ () => {
83
+ // Response received — we don't care about the body
84
+ },
85
+ )
86
+
87
+ req.on('error', () => {
88
+ // Notch helper not running — that's fine, silently ignore
89
+ })
90
+
91
+ req.write(payload)
92
+ req.end()
93
+ }
94
+
95
+ // MARK: - Config
96
+
97
+ /** Read the port from ~/.claude/notch.json (cached after first read). */
98
+ function getPort(): number | null {
99
+ if (portResolved) return cachedPort
100
+
101
+ portResolved = true
102
+ try {
103
+ const raw = readFileSync(CONFIG_PATH, 'utf-8')
104
+ const config = JSON.parse(raw)
105
+ cachedPort = typeof config.port === 'number' ? config.port : DEFAULT_PORT
106
+ } catch {
107
+ // Config doesn't exist — notch not installed
108
+ cachedPort = null
109
+ }
110
+ return cachedPort
111
+ }
@@ -8,6 +8,7 @@ import { updateWatchPaths } from './hooks/fileChangedWatcher.js'
8
8
  import { shouldAllowManagedHooksOnly } from './hooks/hooksConfigSnapshot.js'
9
9
  import { executeSessionStartHooks, executeSetupHooks } from './hooks.js'
10
10
  import { logError } from './log.js'
11
+ import { initNotchBridge } from './notchBridge.js'
11
12
  import { loadPluginHooks } from './plugins/loadPluginHooks.js'
12
13
 
13
14
  type SessionStartHooksOptions = {
@@ -24,6 +25,7 @@ type SessionStartHooksOptions = {
24
25
  // joined later — rippling a structural return-type change through that
25
26
  // handoff would touch five callsites for what is a print-mode-only value).
26
27
  let pendingInitialUserMessage: string | undefined
28
+ let notchCleanup: (() => void) | null = null
27
29
 
28
30
  export function takeInitialUserMessage(): string | undefined {
29
31
  const v = pendingInitialUserMessage
@@ -31,6 +33,12 @@ export function takeInitialUserMessage(): string | undefined {
31
33
  return v
32
34
  }
33
35
 
36
+ /** Clean up the notch bridge. Called from session end. */
37
+ export function cleanupNotchBridge(): void {
38
+ notchCleanup?.()
39
+ notchCleanup = null
40
+ }
41
+
34
42
  // Note to CLAUDE: do not add ANY "warmup" logic. It is **CRITICAL** that you do not add extra work on startup.
35
43
  export async function processSessionStartHooks(
36
44
  source: 'startup' | 'resume' | 'clear' | 'compact',
@@ -47,6 +55,13 @@ export async function processSessionStartHooks(
47
55
  if (isBareMode()) {
48
56
  return []
49
57
  }
58
+
59
+ // Connect the macOS notch helper (no-ops on non-macOS or if not installed)
60
+ if (sessionId) {
61
+ notchCleanup?.()
62
+ notchCleanup = initNotchBridge(sessionId)
63
+ }
64
+
50
65
  const hookMessages: HookResultMessage[] = []
51
66
  const additionalContexts: string[] = []
52
67
  const allWatchPaths: string[] = []