# Claude Code — Special Systems: Buddy, Memory, Keybindings, Skills, Voice, Plugins & More --- ## Table of Contents 1. [Buddy (Companion/Tamagotchi) System](#1-buddy-companiontagotchi-system) 2. [Memory Directory (memdir) System](#2-memory-directory-memdir-system) 3. [Keybindings System](#3-keybindings-system) 4. [Skills System](#4-skills-system) 5. [Voice System](#5-voice-system) 6. [Plugins System](#6-plugins-system) 7. [Output Styles](#7-output-styles) 8. [Hooks Schema](#8-hooks-schema) 9. [Native-TypeScript Ports (native-ts/)](#9-native-typescript-ports-native-ts) 10. [MoreRight Hook (moreright/)](#10-moreright-hook-moreright) 11. [Migrations](#11-migrations) 12. [Core Type Definitions (types/)](#12-core-type-definitions-types) 13. [Remote Session System (remote/)](#13-remote-session-system-remote) --- ## 1. Buddy (Companion/Tamagotchi) System ### 1.1 System Overview The Buddy system is a virtual companion ("tamagotchi") that appears as an ASCII art sprite beside the user's prompt input. Each user receives a deterministic companion whose appearance (species, eyes, hat, rarity, shiny status, stats) is derived from their user ID via a seeded PRNG — not stored. The companion's "soul" (name and personality) is generated by the AI and persisted in config. The feature is gated behind a `BUDDY` feature flag and was planned to launch April 1–7, 2026 as a "teaser window." **Source directory:** `src/buddy/` **Files:** - `buddy/types.ts` — Type definitions, species/eyes/hats/stats constants, rarity weights - `buddy/companion.ts` — PRNG, rolling, and companion reconstruction logic - `buddy/sprites.ts` — ASCII art sprite frames for all 18 species - `buddy/prompt.ts` — Companion introduction text injection into system prompt - `buddy/useBuddyNotification.tsx` — React hook for startup teaser notification - `buddy/CompanionSprite.tsx` — React component rendering the animated sprite + speech bubble --- ### 1.2 Architecture The system splits companion data into two parts: - **Bones** (`CompanionBones`): Deterministic visual/stat data derived from `hash(userId + SALT)`. Never stored; always recomputed on read. This prevents users from editing their config to claim a legendary rarity. - **Soul** (`CompanionSoul`): AI-generated name and personality, persisted to `config.companion` as `StoredCompanion`. - **Full Companion** (`Companion = CompanionBones & CompanionSoul & { hatchedAt: number }`): assembled at read time by `getCompanion()`. The rolling process uses a single pass through a seeded PRNG (`mulberry32`) to deterministically select: 1. Rarity tier (weighted random) 2. Species 3. Eye style 4. Hat (none if common) 5. Shiny flag (1% probability) 6. Stats (one peak stat, one dump stat, rest scattered; floor scales with rarity) 7. `inspirationSeed` (passed to AI for soul generation) --- ### 1.3 Species Encoding (Canary Bypass) **Critical detail:** Species name strings are encoded as `String.fromCharCode(...)` literals to avoid triggering a build-time string scan that checks for model codenames (`excluded-strings.txt`). One species name collides with an internal model codename canary. All 18 species use this encoding uniformly. ```typescript // In buddy/types.ts: const c = String.fromCharCode export const duck = c(0x64,0x75,0x63,0x6b) as 'duck' // ... all 18 species encoded this way ``` --- ### 1.4 PRNG: Mulberry32 **Algorithm:** ```typescript function mulberry32(seed: number): () => number { let a = seed >>> 0 return function () { a |= 0 a = (a + 0x6d2b79f5) | 0 let t = Math.imul(a ^ (a >>> 15), 1 | a) t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t return ((t ^ (t >>> 14)) >>> 0) / 4294967296 } } ``` - Tiny 32-bit PRNG with good statistical distribution - Seeded from `hashString(userId + SALT)` where `SALT = 'friend-2026-401'` - `hashString` uses Bun's native hash when available, falls back to FNV-1a (32-bit) **Caching:** The `roll()` function memoizes the last result keyed on `userId + SALT`, since it is called from three hot paths: the 500ms sprite tick, per-keystroke PromptInput rendering, and the per-turn observer. --- ### 1.5 Data Structures #### `Rarity` ```typescript export const RARITIES = ['common', 'uncommon', 'rare', 'epic', 'legendary'] as const export type Rarity = (typeof RARITIES)[number] ``` #### `RARITY_WEIGHTS` ```typescript export const RARITY_WEIGHTS = { common: 60, // 60% uncommon: 25, // 25% rare: 10, // 10% epic: 4, // 4% legendary: 1, // 1% } as const ``` #### `RARITY_FLOOR` (stat minimums per rarity) ```typescript const RARITY_FLOOR: Record = { common: 5, uncommon: 15, rare: 25, epic: 35, legendary: 50, } ``` #### `RARITY_STARS` (UI display) ```typescript export const RARITY_STARS = { common: '★', uncommon: '★★', rare: '★★★', epic: '★★★★', legendary: '★★★★★', } ``` #### `RARITY_COLORS` (maps to Theme keys) ```typescript export const RARITY_COLORS = { common: 'inactive', uncommon: 'success', rare: 'permission', epic: 'autoAccept', legendary: 'warning', } ``` #### `Species` (18 total) ```typescript export const SPECIES = [ duck, goose, blob, cat, dragon, octopus, owl, penguin, turtle, snail, ghost, axolotl, capybara, cactus, robot, rabbit, mushroom, chonk, ] as const export type Species = (typeof SPECIES)[number] ``` #### `Eye` (6 styles) ```typescript export const EYES = ['·', '✦', '×', '◉', '@', '°'] as const ``` #### `Hat` (8 types) ```typescript export const HATS = [ 'none', 'crown', 'tophat', 'propeller', 'halo', 'wizard', 'beanie', 'tinyduck', ] as const ``` #### `StatName` (5 stats) ```typescript export const STAT_NAMES = ['DEBUGGING', 'PATIENCE', 'CHAOS', 'WISDOM', 'SNARK'] as const ``` #### `CompanionBones` (deterministic, never stored) ```typescript export type CompanionBones = { rarity: Rarity species: Species eye: Eye hat: Hat shiny: boolean stats: Record // 1–100 per stat } ``` #### `CompanionSoul` (AI-generated, stored) ```typescript export type CompanionSoul = { name: string personality: string } ``` #### `StoredCompanion` (what persists in config) ```typescript export type StoredCompanion = CompanionSoul & { hatchedAt: number } ``` #### `Companion` (runtime assembled) ```typescript export type Companion = CompanionBones & CompanionSoul & { hatchedAt: number } ``` #### `Roll` (output of `rollFrom`) ```typescript export type Roll = { bones: CompanionBones inspirationSeed: number // Passed to AI for deterministic soul generation } ``` --- ### 1.6 Stat Rolling Algorithm ```typescript function rollStats(rng, rarity): Record { const floor = RARITY_FLOOR[rarity] // 5–50 depending on rarity const peak = pick(rng, STAT_NAMES) // one stat gets +50 bonus let dump = pick(rng, STAT_NAMES) // different stat, penalized while (dump === peak) dump = pick(rng, STAT_NAMES) for (name of STAT_NAMES) { if (name === peak) stats[name] = min(100, floor + 50 + rng()*30) // 55–130, capped 100 else if (name === dump) stats[name] = max(1, floor - 10 + rng()*15) // 1–20 (at common) else stats[name] = floor + rng()*40 // spread } } ``` --- ### 1.7 Sprite System Each species has **3 animation frames** of ASCII art, each 5 lines tall, 12 chars wide. The `{E}` placeholder in frame templates is substituted with the companion's `eye` character. **Frame structure:** - Line 0: Hat slot (blank if no hat, or ambient detail like `~` for smoke, `*` for sparks) - Lines 1–4: Body art **Hat rendering:** Hats are placed on line 0 only if it is blank. If all frames have a blank line 0, the blank line is stripped (saves terminal row when no hat). **Hat ASCII art (in `HAT_LINES`):** | Hat | ASCII | |-----|-------| | none | (empty) | | crown | ` \^^^/ ` | | tophat | ` [___] ` | | propeller | ` -+- ` | | halo | ` ( ) ` | | wizard | ` /^\ ` | | beanie | ` (___) ` | | tinyduck | ` ,> ` | **Idle animation sequence:** ```typescript const IDLE_SEQUENCE = [0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 2, 0, 0, 0] // -1 = blink on frame 0 (special) // TICK_MS = 500ms per step ``` **Pet animation:** After `/buddy pet`, hearts float upward for 5 ticks (~2.5 seconds): ```typescript const PET_HEARTS = [ ` ${H} ${H} `, ` ${H} ${H} ${H} `, ` ${H} ${H} ${H} `, `${H} ${H} ${H} `, '· · · ' ] ``` --- ### 1.8 All Exports #### `buddy/companion.ts` | Export | Type | Description | |--------|------|-------------| | `Roll` | type | `{ bones: CompanionBones; inspirationSeed: number }` | | `roll(userId: string)` | `() => Roll` | Deterministic roll (memoized per userId+SALT) | | `rollWithSeed(seed: string)` | `() => Roll` | Roll from arbitrary seed string | | `companionUserId()` | `() => string` | Returns oauthAccount UUID or userID or 'anon' | | `getCompanion()` | `() => Companion \| undefined` | Assembles companion from config + roll | #### `buddy/sprites.ts` | Export | Type | Description | |--------|------|-------------| | `renderSprite(bones, frame?)` | `(CompanionBones, number?) => string[]` | Returns array of lines for the sprite | | `spriteFrameCount(species)` | `(Species) => number` | Number of frames for a species (all are 3) | | `renderFace(bones)` | `(CompanionBones) => string` | Short face string for inline display | #### `buddy/prompt.ts` | Export | Type | Description | |--------|------|-------------| | `companionIntroText(name, species)` | `(string, string) => string` | System prompt text for companion introduction | | `getCompanionIntroAttachment(messages?)` | `(Message[]?) => Attachment[]` | Returns intro attachment if not yet injected | #### `buddy/useBuddyNotification.tsx` | Export | Type | Description | |--------|------|-------------| | `isBuddyTeaserWindow()` | `() => boolean` | True if local date is April 1–7, 2026 | | `isBuddyLive()` | `() => boolean` | True if local date is April 2026 or later | | `useBuddyNotification()` | `() => void` | React hook: shows rainbow `/buddy` startup hint | | `findBuddyTriggerPositions(text)` | `(string) => Array<{start,end}>` | Find `/buddy` occurrences in text | --- ### 1.9 Configuration - `config.companion` — Stores `StoredCompanion` (name, personality, hatchedAt) - `config.companionMuted` — When true, suppresses companion intro injection - Feature flag: `feature('BUDDY')` — gates all buddy functionality - SALT constant: `'friend-2026-401'` — mixed into hash to prevent replay attacks --- ## 2. Memory Directory (memdir) System ### 2.1 System Overview The memory system provides Claude with persistent, file-based memory across sessions. Memory is stored as markdown files with YAML frontmatter in a per-project directory. It encompasses: 1. **Auto memory** (`~/.claude/projects//memory/`) — per-user, per-project 2. **Team memory** (`/team/`) — shared across contributors (feature-gated) 3. **Memory scanning** — reads frontmatter headers to build a manifest without loading full content 4. **Relevance selection** — uses a Sonnet model call to pick the most relevant files (up to 5) for a given query 5. **Freshness warnings** — age-based staleness caveats injected as `` tags **Source directory:** `src/memdir/` --- ### 2.2 Memory Directory Structure ``` ~/.claude/ projects/ / memory/ MEMORY.md -- entrypoint index (always loaded) .md -- individual memory files with frontmatter team/ -- team-shared memories (TEAMMEM feature) MEMORY.md .md logs/ -- KAIROS mode: append-only daily logs YYYY/ MM/ YYYY-MM-DD.md ``` --- ### 2.3 Memory Types Taxonomy All memory falls into four types (stored in frontmatter `type:` field): | Type | Scope (combined mode) | Description | |------|----------------------|-------------| | `user` | Always private | User's role, goals, knowledge, preferences | | `feedback` | Private (default) or team for project-wide conventions | How-to-approach-work guidance from corrections and confirmations | | `project` | Strongly bias team | Ongoing work, goals, bugs, incidents not derivable from code | | `reference` | Usually team | Pointers to external systems (Linear, Grafana, Slack) | **What NOT to save:** - Code patterns, architecture, file structure (derivable by reading code) - Git history, recent changes (use `git log`) - Debugging solutions/fix recipes (the fix is in the code) - Anything in CLAUDE.md - Ephemeral task details, in-progress work --- ### 2.4 Memory Frontmatter Format ```markdown --- name: {{memory name}} description: {{one-line description — used to decide relevance in future conversations, so be specific}} type: {{user, feedback, project, reference}} --- {{memory content}} ``` For `feedback` and `project` types, the body should include: - Rule/fact as the lead - `**Why:**` line with the reason - `**How to apply:**` line with when/where it kicks in --- ### 2.5 Architecture: Data Flow ``` User query arrives │ ▼ scanMemoryFiles(memoryDir) -- reads frontmatter of all .md files │ returns MemoryHeader[] ▼ filterOut(alreadySurfaced) -- skip files shown in prior turns │ ▼ selectRelevantMemories(query, ...) -- sideQuery to Sonnet with manifest │ returns up to 5 filenames ▼ Return RelevantMemory[] (path + mtimeMs) │ ▼ Caller injects file contents -- reads full file content Caller adds freshness warnings -- memoryFreshnessNote(mtimeMs) ``` --- ### 2.6 Memory Scanning (`memoryScan.ts`) **`scanMemoryFiles(memoryDir, signal)`** - `readdir(memoryDir, { recursive: true })` to find all `.md` files - Excludes `MEMORY.md` (entrypoint — already in system prompt) - Reads only first `FRONTMATTER_MAX_LINES = 30` lines of each file - Parses frontmatter for `description` and `type` - Sorts newest-first by `mtimeMs` - Capped at `MAX_MEMORY_FILES = 200` - Returns `MemoryHeader[]` **`MemoryHeader` type:** ```typescript export type MemoryHeader = { filename: string filePath: string mtimeMs: number description: string | null type: MemoryType | undefined } ``` **`formatMemoryManifest(memories)`** Formats headers as text manifest for the selector prompt: ``` - [user] user_role.md (2026-01-15T12:00:00Z): user is a senior Go engineer focused on observability - [feedback] no_mocks.md (2026-01-10T09:30:00Z): integration tests must use real database ``` --- ### 2.7 Relevance Selection (`findRelevantMemories.ts`) **System prompt used for selection:** ``` You are selecting memories that will be useful to Claude Code as it processes a user's query. Return a list of filenames for the memories that will clearly be useful (up to 5). Only include memories you are certain will be helpful. Be selective and discerning. - If unsure, do not include it. - If no memories are clearly useful, return an empty list. - If recently-used tools are provided, do not select reference docs for those tools (skip API usage docs, DO keep warnings/gotchas about active tools). ``` **`sideQuery` parameters:** - Model: `getDefaultSonnetModel()` - Max tokens: 256 - Output format: JSON schema `{ selected_memories: string[] }` - Query source: `'memdir_relevance'` **`findRelevantMemories` signature:** ```typescript export async function findRelevantMemories( query: string, memoryDir: string, signal: AbortSignal, recentTools: readonly string[] = [], alreadySurfaced: ReadonlySet = new Set(), ): Promise export type RelevantMemory = { path: string mtimeMs: number } ``` --- ### 2.8 Memory Freshness System (`memoryAge.ts`) All functions are pure, accepting `mtimeMs` timestamps: | Export | Signature | Description | |--------|-----------|-------------| | `memoryAgeDays(mtimeMs)` | `(number) => number` | Floor-rounded days since mtime, clamped to ≥0 | | `memoryAge(mtimeMs)` | `(number) => string` | Human-readable: 'today', 'yesterday', 'N days ago' | | `memoryFreshnessText(mtimeMs)` | `(number) => string` | Staleness caveat text ('' for ≤1 day, warns for older) | | `memoryFreshnessNote(mtimeMs)` | `(number) => string` | `` wrapped freshness text | **Staleness warning text (for memories >1 day old):** ``` This memory is N days old. Memories are point-in-time observations, not live state — claims about code behavior or file:line citations may be outdated. Verify against current code before asserting as fact. ``` --- ### 2.9 Memory Path Resolution (`paths.ts`) **`isAutoMemoryEnabled()`** — Priority chain: 1. `CLAUDE_CODE_DISABLE_AUTO_MEMORY` env var (truthy → OFF, falsy-defined → ON) 2. `CLAUDE_CODE_SIMPLE` (--bare mode) → OFF 3. `CLAUDE_CODE_REMOTE` without `CLAUDE_CODE_REMOTE_MEMORY_DIR` → OFF 4. `settings.autoMemoryEnabled` (project-level opt-out) 5. Default: **enabled** **`getAutoMemPath()`** — Resolution order (memoized by `getProjectRoot()`): 1. `CLAUDE_COWORK_MEMORY_PATH_OVERRIDE` env var (Cowork spaces) 2. `autoMemoryDirectory` in settings (policy > flag > local > user sources; supports `~/` expansion) 3. `/projects//memory/` - `memoryBase` = `CLAUDE_CODE_REMOTE_MEMORY_DIR` or `~/.claude` **Security validations in `validateMemoryPath()`:** - Rejects relative paths (not `isAbsolute`) - Rejects root/near-root (length < 3) - Rejects Windows drive-root (`C:`) - Rejects UNC paths (`\\server\share`, `//`) - Rejects null bytes - Normalizes NFC **`getAutoMemDailyLogPath(date?)`** — KAIROS mode daily log path: ``` /logs/YYYY/MM/YYYY-MM-DD.md ``` **All path exports:** | Export | Description | |--------|-------------| | `isAutoMemoryEnabled()` | Check if auto-memory is on | | `isExtractModeActive()` | Whether background memory extraction agent will run | | `getMemoryBaseDir()` | Base dir: env override or `~/.claude` | | `getAutoMemPath()` | Full memory directory path (memoized) | | `getAutoMemDailyLogPath(date?)` | Daily log path for KAIROS mode | | `getAutoMemEntrypoint()` | `MEMORY.md` path inside auto-mem dir | | `isAutoMemPath(absolutePath)` | Check if path is inside auto-memory dir | | `hasAutoMemPathOverride()` | Check if Cowork env override is active | --- ### 2.10 Team Memory Paths (`teamMemPaths.ts`) Team memory lives at `/team/`. Heavy path traversal protection: **`PathTraversalError`** — Custom error class thrown on injection attempts. **`sanitizePathKey(key)`** — Rejects: - Null bytes - URL-encoded traversals (e.g. `%2e%2e%2f`) - Unicode normalization attacks (fullwidth `../` → `../`) - Backslashes - Absolute paths **`validateTeamMemWritePath(filePath)`** — Two-pass validation: 1. `path.resolve()` to eliminate `..` segments, check string containment 2. `realpathDeepestExisting()` to follow symlinks and verify real containment **`realpathDeepestExisting(absolutePath)`** — Walks up directory tree until `realpath()` succeeds, handles dangling symlinks (detects via `lstat`) and symlink loops (ELOOP). **All exports:** | Export | Description | |--------|-------------| | `PathTraversalError` | Error class for traversal attempts | | `isTeamMemoryEnabled()` | GrowthBook gate `tengu_herring_clock` + auto-memory enabled | | `getTeamMemPath()` | `/team/` | | `getTeamMemEntrypoint()` | `/team/MEMORY.md` | | `isTeamMemPath(filePath)` | String-level containment check | | `isTeamMemFile(filePath)` | Team enabled + path contained | | `validateTeamMemWritePath(filePath)` | Full symlink-safe write validation | | `validateTeamMemKey(relativeKey)` | Sanitize relative key + validate write path | --- ### 2.11 Memory Prompt Building (`memdir.ts`) **Constants:** ```typescript export const ENTRYPOINT_NAME = 'MEMORY.md' export const MAX_ENTRYPOINT_LINES = 200 export const MAX_ENTRYPOINT_BYTES = 25_000 ``` **`truncateEntrypointContent(raw)`** — Line-truncates first, then byte-truncates at last newline before cap. Appends warning message naming which cap(s) fired. **`buildMemoryLines(displayName, memoryDir, extraGuidelines?, skipIndex?)`** — Builds behavioral instructions without MEMORY.md content. Used in system prompt. **`buildMemoryPrompt({ displayName, memoryDir, extraGuidelines? })`** — Same as `buildMemoryLines` but includes MEMORY.md content. Used by agent memory. **`buildSearchingPastContextSection(autoMemDir)`** — Conditionally adds a "Searching past context" section with grep commands (gated on `tengu_coral_fern` GrowthBook feature). **`ensureMemoryDirExists(memoryDir)`** — Idempotent mkdir (recursive); logs non-EEXIST errors without throwing. **`loadMemoryPrompt()`** — Top-level dispatcher: - KAIROS + kairosActive → `buildAssistantDailyLogPrompt()` - TEAMMEM + team enabled → `buildCombinedMemoryPrompt()` - Auto enabled → `buildMemoryLines()` joined - Otherwise → `null` --- ### 2.12 Memory Type Constants (`memoryTypes.ts`) All exports used in system prompt construction: | Export | Type | Description | |--------|------|-------------| | `MEMORY_TYPES` | `readonly string[]` | `['user', 'feedback', 'project', 'reference']` | | `MemoryType` | type | Union of the four type strings | | `parseMemoryType(raw)` | `(unknown) => MemoryType \| undefined` | Parses frontmatter value | | `TYPES_SECTION_COMBINED` | `readonly string[]` | Prompt section for dual-directory mode | | `TYPES_SECTION_INDIVIDUAL` | `readonly string[]` | Prompt section for single-directory mode | | `WHAT_NOT_TO_SAVE_SECTION` | `readonly string[]` | Exclusion list for memory content | | `MEMORY_DRIFT_CAVEAT` | `string` | Single bullet warning about memory staleness | | `WHEN_TO_ACCESS_SECTION` | `readonly string[]` | When to read memories | | `TRUSTING_RECALL_SECTION` | `readonly string[]` | Verification guidance before recommending from memory | | `MEMORY_FRONTMATTER_EXAMPLE` | `readonly string[]` | Example frontmatter block for the prompt | --- ### 2.13 Team Memory Prompts (`teamMemPrompts.ts`) **`buildCombinedMemoryPrompt(extraGuidelines?, skipIndex?)`** — Builds the combined prompt when both auto and team memory are enabled. Includes: - Both directory paths - Two-scope explanation (private vs team) - Combined `TYPES_SECTION_COMBINED` with `` tags - Dual MEMORY.md index instructions --- ## 3. Keybindings System ### 3.1 System Overview The keybindings system provides a fully configurable keyboard shortcut layer for Claude Code. Users can customize bindings via `~/.claude/keybindings.json` (gated on `tengu_keybinding_customization_release` GrowthBook feature). The system supports: - Single-keystroke bindings - Multi-keystroke chords (e.g., `ctrl+x ctrl+k`) - Context-scoped bindings (Chat, Global, Confirmation, etc.) - Null-unbinding (set action to `null` to disable a default) - Command bindings (`command:help` executes slash commands) - Hot-reload via `chokidar` file watcher **Source directory:** `src/keybindings/` --- ### 3.2 Architecture ``` DEFAULT_BINDINGS (KeybindingBlock[]) │ ▼ parseBindings() ParsedBinding[] <── default bindings parsed at startup │ ▼ merge with user bindings (user appended after, so user wins) ParsedBinding[] <── merged bindings (last-wins for same key+context) │ ▼ KeybindingProvider (React context) │ ├── resolveKeyWithChordState() ◄── called on every keypress │ ├── chord_started → pending state │ ├── chord_cancelled → reset │ ├── match → invoke action │ ├── unbound → swallow event │ └── none → pass through │ └── useKeybinding(action, context, handler) ◄── component hook ``` --- ### 3.3 Keybinding Contexts 18 contexts defined in `KEYBINDING_CONTEXTS`: | Context | Activation | |---------|------------| | `Global` | Active everywhere, regardless of focus | | `Chat` | When the chat input is focused | | `Autocomplete` | When autocomplete menu is visible | | `Confirmation` | When a confirmation/permission dialog is shown | | `Help` | When the help overlay is open | | `Transcript` | When viewing the transcript | | `HistorySearch` | When searching command history (ctrl+r) | | `Task` | When a task/agent is running in the foreground | | `ThemePicker` | When the theme picker is open | | `Settings` | When the settings menu is open | | `Tabs` | When tab navigation is active | | `Attachments` | When navigating image attachments in a select dialog | | `Footer` | When footer indicators are focused | | `MessageSelector` | When the message selector (rewind) is open | | `DiffDialog` | When the diff dialog is open | | `ModelPicker` | When the model picker is open | | `Select` | When a select/list component is focused | | `Plugin` | When the plugin dialog is open | --- ### 3.4 Default Bindings (`defaultBindings.ts`) **Platform-specific keys:** - `IMAGE_PASTE_KEY`: `alt+v` (Windows), `ctrl+v` (others) - `MODE_CYCLE_KEY`: `meta+m` (Windows without VT mode), `shift+tab` (others) - VT mode support check: Node ≥22.17.0 or ≥24.2.0, Bun ≥1.2.23 **Complete default bindings by context:** **Global context:** | Key | Action | |-----|--------| | `ctrl+c` | `app:interrupt` (non-rebindable) | | `ctrl+d` | `app:exit` (non-rebindable) | | `ctrl+l` | `app:redraw` | | `ctrl+t` | `app:toggleTodos` | | `ctrl+o` | `app:toggleTranscript` | | `ctrl+shift+b` | `app:toggleBrief` (KAIROS/KAIROS_BRIEF only) | | `ctrl+shift+o` | `app:toggleTeammatePreview` | | `ctrl+r` | `history:search` | | `ctrl+shift+f` / `cmd+shift+f` | `app:globalSearch` (QUICK_SEARCH feature) | | `ctrl+shift+p` / `cmd+shift+p` | `app:quickOpen` (QUICK_SEARCH feature) | | `meta+j` | `app:toggleTerminal` (TERMINAL_PANEL feature) | **Chat context:** | Key | Action | |-----|--------| | `escape` | `chat:cancel` | | `ctrl+x ctrl+k` | `chat:killAgents` (chord) | | `shift+tab` / `meta+m` | `chat:cycleMode` | | `meta+p` | `chat:modelPicker` | | `meta+o` | `chat:fastMode` | | `meta+t` | `chat:thinkingToggle` | | `enter` | `chat:submit` | | `up` | `history:previous` | | `down` | `history:next` | | `ctrl+_` / `ctrl+shift+-` | `chat:undo` | | `ctrl+x ctrl+e` / `ctrl+g` | `chat:externalEditor` | | `ctrl+s` | `chat:stash` | | `ctrl+v` / `alt+v` | `chat:imagePaste` | | `shift+up` | `chat:messageActions` (MESSAGE_ACTIONS feature) | | `space` | `voice:pushToTalk` (VOICE_MODE feature) | **Autocomplete:** | Key | Action | |-----|--------| | `tab` | `autocomplete:accept` | | `escape` | `autocomplete:dismiss` | | `up` / `down` | `autocomplete:previous` / `autocomplete:next` | **Confirmation:** | Key | Action | |-----|--------| | `y` / `enter` | `confirm:yes` | | `n` / `escape` | `confirm:no` | | `up` / `down` | `confirm:previous` / `confirm:next` | | `tab` | `confirm:nextField` | | `space` | `confirm:toggle` | | `shift+tab` | `confirm:cycleMode` | | `ctrl+e` | `confirm:toggleExplanation` | | `ctrl+d` | `permission:toggleDebug` | **Scroll:** | Key | Action | |-----|--------| | `pageup` / `pagedown` | `scroll:pageUp` / `scroll:pageDown` | | `wheelup` / `wheeldown` | `scroll:lineUp` / `scroll:lineDown` | | `ctrl+home` / `ctrl+end` | `scroll:top` / `scroll:bottom` | | `ctrl+shift+c` / `cmd+c` | `selection:copy` | **Task:** | Key | Action | |-----|--------| | `ctrl+b` | `task:background` | --- ### 3.5 Parser (`parser.ts`) **`parseKeystroke(input: string): ParsedKeystroke`** Splits on `+`, recognizes modifiers (case-insensitive aliases): - `ctrl` / `control` - `alt` / `opt` / `option` - `shift` - `meta` - `cmd` / `command` / `super` / `win` - Special keys: `esc`→`escape`, `return`→`enter`, `space`→` `, `↑↓←→` **`parseChord(input: string): Chord`** Splits space-separated steps into `ParsedKeystroke[]`. Special case: lone space `" "` is the space key, not a separator. **`keystrokeToString(ks: ParsedKeystroke): string`** Canonical string representation (for internal use). **`keystrokeToDisplayString(ks, platform?): string`** Platform-appropriate display: `opt` on macOS, `alt` elsewhere; `cmd` on macOS, `super` elsewhere. **`parseBindings(blocks: KeybindingBlock[]): ParsedBinding[]`** Converts `KeybindingBlock[]` (raw config) to flat `ParsedBinding[]`. --- ### 3.6 Matching (`match.ts`) **`getKeyName(input: string, key: Key): string | null`** Maps Ink's boolean key flags to string names (`key.escape` → `'escape'`, `key.upArrow` → `'up'`, single char → lowercased, etc.). **`matchesKeystroke(input, key, target): boolean`** - Extracts key name and modifiers from Ink's Key object - **Quirk:** `key.meta = true` when escape is pressed in Ink (legacy terminal behavior). Ignored for escape key matching itself. - Alt and meta are treated as aliases in Ink (terminal limitation): `target.alt || target.meta` matched against `key.meta`. - Super/cmd is distinct — only arrives via kitty keyboard protocol. **`matchesBinding(input, key, binding): boolean`** Single-keystroke binding match only (chord length must be 1). --- ### 3.7 Resolver (`resolver.ts`) **`resolveKey(input, key, activeContexts, bindings): ResolveResult`** Pure function for single-keystroke resolution: - Last matching binding wins (for user overrides) - Returns `{ type: 'match', action }`, `{ type: 'none' }`, or `{ type: 'unbound' }` **`resolveKeyWithChordState(input, key, activeContexts, bindings, pending): ChordResolveResult`** Multi-keystroke chord resolution with state: - Cancels on escape when in a chord - Groups chord candidates by `chordToString(chord)` key to handle null-unbinding properly (a null override on a chord prevents its prefix from entering chord-wait) - Returns additional types: `{ type: 'chord_started', pending }`, `{ type: 'chord_cancelled' }` **`ChordResolveResult`:** ```typescript type ChordResolveResult = | { type: 'match'; action: string } | { type: 'none' } | { type: 'unbound' } | { type: 'chord_started'; pending: ParsedKeystroke[] } | { type: 'chord_cancelled' } ``` **`getBindingDisplayText(action, context, bindings): string | undefined`** Searches bindings in reverse order (last wins) for the given action+context. **`keystrokesEqual(a, b): boolean`** Compares with alt/meta collapsed into one logical modifier. --- ### 3.8 Type Definitions (`types.ts` — inferred) ```typescript type KeybindingContextName = (typeof KEYBINDING_CONTEXTS)[number] type ParsedKeystroke = { key: string ctrl: boolean alt: boolean shift: boolean meta: boolean super: boolean } type Chord = ParsedKeystroke[] type ParsedBinding = { chord: Chord action: string | null // null = unbound context: KeybindingContextName } type KeybindingBlock = { context: string bindings: Record } ``` --- ### 3.9 Schema (`schema.ts`) Zod v4 schemas for `keybindings.json` validation: **`KeybindingBlockSchema`** — Validates `{ context, bindings: { key: action | null } }`. Bindings accept: - Known action strings from `KEYBINDING_ACTIONS` - `command:` prefix bindings (`/^command:[a-zA-Z0-9:\-_]+$/`) - `null` (to unbind) **`KeybindingsSchema`** — Wraps with optional `$schema` and `$docs` metadata: ```json { "$schema": "https://www.schemastore.org/claude-code-keybindings.json", "$docs": "https://code.claude.com/docs/en/keybindings", "bindings": [...] } ``` **Complete `KEYBINDING_ACTIONS` list (80 actions):** Categories: `app:*`, `history:*`, `chat:*`, `autocomplete:*`, `confirm:*`, `tabs:*`, `transcript:*`, `historySearch:*`, `task:*`, `theme:*`, `help:*`, `attachments:*`, `footer:*`, `messageSelector:*`, `messageActions:*`, `diff:*`, `modelPicker:*`, `select:*`, `plugin:*`, `permission:*`, `settings:*`, `voice:*` --- ### 3.10 Validation (`validate.ts`) **`KeybindingWarningType`:** `'parse_error' | 'duplicate' | 'reserved' | 'invalid_context' | 'invalid_action'` **`KeybindingWarning`:** ```typescript type KeybindingWarning = { type: KeybindingWarningType severity: 'error' | 'warning' message: string key?: string context?: string action?: string suggestion?: string } ``` **Validation checks:** 1. `validateUserConfig(blocks)` — validates block structure (context string, bindings object) 2. `checkDuplicates(blocks)` — within-context duplicate keys (normalized comparison) 3. `checkReservedShortcuts(bindings)` — against `NON_REBINDABLE` and platform `TERMINAL_RESERVED`/`MACOS_RESERVED` 4. `checkDuplicateKeysInJson(jsonString)` — raw JSON string scan (JSON.parse silently drops earlier duplicates) 5. Special case: `voice:pushToTalk` with bare alphabetic key warns (prints into input during warmup) 6. `command:` bindings must be in `Chat` context --- ### 3.11 Reserved Shortcuts (`reservedShortcuts.ts`) **`NON_REBINDABLE`** (error severity): - `ctrl+c` — interrupt/exit (hardcoded double-press logic) - `ctrl+d` — exit (hardcoded) - `ctrl+m` — identical to Enter in terminals (both send CR) **`TERMINAL_RESERVED`:** - `ctrl+z` — Unix SIGTSTP (warning) - `ctrl+\` — terminal SIGQUIT (error) **`MACOS_RESERVED`** (macOS only, all errors): - `cmd+c/v/x/q/w/tab/space` Note: `ctrl+s` (XOFF) and `ctrl+q` (XON) are intentionally NOT in reserved list — modern terminals disable flow control and Claude Code uses `ctrl+s` for stash. --- ### 3.12 User Bindings Loader (`loadUserBindings.ts`) **File location:** `~/.claude/keybindings.json` **`isKeybindingCustomizationEnabled()`** — GrowthBook gate: `tengu_keybinding_customization_release`. **File watching:** Uses `chokidar` with: - `stabilityThreshold: 500ms` — waits for write to stabilize - `pollInterval: 200ms` - `atomic: true` - Watches parent directory, not just the file (handles file creation) **`loadKeybindings()` / `loadKeybindingsSyncWithWarnings()`** — Load+merge+validate. Returns `{ bindings: ParsedBinding[], warnings: KeybindingWarning[] }`. **All exports:** | Export | Description | |--------|-------------| | `isKeybindingCustomizationEnabled()` | GrowthBook gate check | | `getKeybindingsPath()` | `~/.claude/keybindings.json` | | `loadKeybindings()` | Async load (used in watcher) | | `loadKeybindingsSync()` | Sync load (React useState initializer) | | `loadKeybindingsSyncWithWarnings()` | Sync with warnings | | `initializeKeybindingWatcher()` | Set up chokidar watcher | | `disposeKeybindingWatcher()` | Tear down watcher | | `subscribeToKeybindingChanges` | Subscribe to reload events | | `getCachedKeybindingWarnings()` | Current cached warnings | | `resetKeybindingLoaderForTesting()` | Test reset | --- ### 3.13 Template Generator (`template.ts`) **`generateKeybindingsTemplate()`** — Generates a well-documented template JSON file with all default bindings. Filters out `NON_REBINDABLE` shortcuts to avoid `/doctor` warnings. Includes `$schema` and `$docs` metadata. --- ### 3.14 Shortcut Display (`shortcutFormat.ts`) **`getShortcutDisplay(action, context, fallback)`** — Non-React shortcut display helper. Logs `tengu_keybinding_fallback_used` analytics event (once per action+context) when the binding is not found. Falls back to hardcoded string during migration. --- ## 4. Skills System ### 4.1 System Overview Skills are Claude's slash commands that execute AI prompts or local logic. They come from multiple sources: - **Bundled skills** — compiled into the binary, available to all users - **Disk-based skills** — markdown files in `.claude/skills/` directories - **Plugin skills** — provided by installed plugins - **MCP skills** — generated from MCP server tool definitions **Source directory:** `src/skills/` --- ### 4.2 Bundled Skills Registry (`bundledSkills.ts`) **`BundledSkillDefinition`** type: ```typescript type BundledSkillDefinition = { name: string description: string aliases?: string[] whenToUse?: string argumentHint?: string allowedTools?: string[] model?: string disableModelInvocation?: boolean userInvocable?: boolean isEnabled?: () => boolean hooks?: HooksSettings context?: 'inline' | 'fork' // fork = run as sub-agent agent?: string files?: Record // reference files extracted to disk on first use getPromptForCommand: (args: string, context: ToolUseContext) => Promise } ``` **File extraction (`files` field):** When a skill has `files`, they are extracted to a per-process nonce directory on first invocation using `O_NOFOLLOW | O_EXCL | O_CREAT` flags (mode 0o600, directories mode 0o700). A `Base directory for this skill: ` prefix is prepended to the skill prompt so the model can read these files. **Registry API:** | Export | Description | |--------|-------------| | `registerBundledSkill(definition)` | Register a bundled skill at startup | | `getBundledSkills()` | Get copy of all registered bundled skills | | `clearBundledSkills()` | Clear registry (for testing) | | `getBundledSkillExtractDir(skillName)` | Deterministic extract directory | --- ### 4.3 Bundled Skill Initialization (`bundled/index.ts`) **`initBundledSkills()`** registers the following skills: | Skill | Feature Gate | Description | |-------|-------------|-------------| | `updateConfig` | — | Update Claude Code configuration | | `keybindings` | — | Manage keyboard shortcuts | | `verify` | — | Verify work / run checks | | `debug` | — | Debug assistance | | `loremIpsum` | — | Lorem ipsum text generation | | `skillify` | — | Create new skills | | `remember` | — | Explicitly save to memory | | `simplify` | — | Simplify code/text | | `batch` | — | Batch operations | | `stuck` | — | Help when stuck | | `dream` | `KAIROS \| KAIROS_DREAM` | Nightly memory distillation | | `hunter` | `REVIEW_ARTIFACT` | Code review | | `loop` | `AGENT_TRIGGERS` | Recurring agent triggers | | `scheduleRemoteAgents` | `AGENT_TRIGGERS_REMOTE` | Schedule remote agent runs | | `claudeApi` | `BUILDING_CLAUDE_APPS` | Claude API reference skill | | `claudeInChrome` | `shouldAutoEnableClaudeInChrome()` | Claude in Chrome integration | | `runSkillGenerator` | `RUN_SKILL_GENERATOR` | Skill generation tool | --- ### 4.4 Skills Directory Loader (`loadSkillsDir.ts`) **`LoadedFrom` type:** ```typescript type LoadedFrom = | 'commands_DEPRECATED' | 'skills' | 'plugin' | 'managed' | 'bundled' | 'mcp' ``` **`getSkillsPath(source, dir)`** — Returns the config directory path for a given source and subdirectory (`skills` or `commands`). The loader handles: - Frontmatter parsing (`name`, `description`, `argument-hint`, `when-to-use`, `allowed-tools`, `model`, `disableNonInteractive`, `hooks`, `context`, `agent`, `effort`, `paths`) - Shell execution in prompts (backtick commands in frontmatter) - Gitignore integration - Argument substitution (`$ARGUMENTS`, named args `$ARG_NAME`) - `isRestrictedToPluginOnly()` policy check - Multi-level discovery: project dirs up to home + user config --- ## 5. Voice System ### 5.1 System Overview Voice mode enables hold-to-talk interaction with Claude Code using a push-to-talk model. Voice requires: 1. OAuth authentication (uses `voice_stream` endpoint on claude.ai) 2. GrowthBook feature flag `VOICE_MODE` 3. Kill-switch gate: `tengu_amber_quartz_disabled` (negative flag) **Source:** `src/voice/voiceModeEnabled.ts` --- ### 5.2 API | Export | Description | |--------|-------------| | `isVoiceGrowthBookEnabled()` | `feature('VOICE_MODE') && !tengu_amber_quartz_disabled` | | `hasVoiceAuth()` | OAuth provider + valid access token exists | | `isVoiceModeEnabled()` | Full check: `hasVoiceAuth() && isVoiceGrowthBookEnabled()` | **Why OAuth required:** - Voice uses `voice_stream` endpoint on claude.ai - Not available with API keys, Bedrock, Vertex, or Foundry - `isAnthropicAuthEnabled()` checks the provider, `getClaudeAIOAuthTokens()` verifies a token actually exists **Kill-switch design:** - Flag default is `false` = not killed - A missing/stale disk cache reads as "not killed" → voice works immediately on fresh installs - Emergency kill: flip `tengu_amber_quartz_disabled` to `true` **Default binding:** `space` → `voice:pushToTalk` (defined in `defaultBindings.ts` under VOICE_MODE feature gate) **Validation warning:** Binding `voice:pushToTalk` to a bare alphabetic key (no modifiers) raises a warning because the key prints into the input during the activation warmup period. Recommended: `space` or a modifier combo like `meta+k`. --- ## 6. Plugins System ### 6.1 System Overview Plugins extend Claude Code with additional skills, hooks, MCP servers, LSP servers, and output styles. There are two plugin categories: 1. **Built-in plugins** — ship with the CLI, appear in `/plugin` UI, user-toggleable 2. **Marketplace plugins** — installed from GitHub repos via `/plugin install` **Source:** `src/plugins/` --- ### 6.2 Built-in Plugin Registry (`builtinPlugins.ts`) Built-in plugins use the ID format `{name}@builtin`. **`BuiltinPluginDefinition`** type (from `types/plugin.ts`): ```typescript type BuiltinPluginDefinition = { name: string description: string version?: string skills?: BundledSkillDefinition[] hooks?: HooksSettings mcpServers?: Record isAvailable?: () => boolean defaultEnabled?: boolean // defaults to true } ``` **`LoadedPlugin`** type: ```typescript type LoadedPlugin = { name: string manifest: PluginManifest path: string source: string repository: string enabled?: boolean isBuiltin?: boolean sha?: string commandsPath?: string commandsPaths?: string[] commandsMetadata?: Record agentsPath?: string agentsPaths?: string[] skillsPath?: string skillsPaths?: string[] outputStylesPath?: string outputStylesPaths?: string[] hooksConfig?: HooksSettings mcpServers?: Record lspServers?: Record settings?: Record } ``` **`getBuiltinPlugins()`** — Returns `{ enabled: LoadedPlugin[], disabled: LoadedPlugin[] }`. Enabled state priority: user setting > `defaultEnabled` > `true`. Plugins with `isAvailable() === false` are omitted entirely. **All exports:** | Export | Description | |--------|-------------| | `BUILTIN_MARKETPLACE_NAME` | `'builtin'` — sentinel marketplace name | | `registerBuiltinPlugin(definition)` | Register at startup | | `isBuiltinPluginId(pluginId)` | Check if ends with `@builtin` | | `getBuiltinPluginDefinition(name)` | Get definition by name | | `getBuiltinPlugins()` | Get enabled/disabled split | | `getBuiltinPluginSkillCommands()` | Get skills from enabled plugins as Commands | | `clearBuiltinPlugins()` | Test reset | --- ### 6.3 Built-in Plugin Initialization (`bundled/index.ts`) ```typescript export function initBuiltinPlugins(): void { // No built-in plugins registered yet — scaffolding for migrating // bundled skills that should be user-toggleable. } ``` The file is scaffolding — as of the code snapshot, no built-in plugins have been registered. The infrastructure is complete for migrating bundled skills into the toggleable plugin system. --- ### 6.4 Plugin Error Types (`types/plugin.ts`) `PluginError` is a large discriminated union with 20+ error variants: | Type | Key fields | |------|-----------| | `path-not-found` | `path`, `component` | | `git-auth-failed` | `gitUrl`, `authType: 'ssh' \| 'https'` | | `git-timeout` | `gitUrl`, `operation: 'clone' \| 'pull'` | | `network-error` | `url`, `details?` | | `manifest-parse-error` | `manifestPath`, `parseError` | | `manifest-validation-error` | `manifestPath`, `validationErrors[]` | | `plugin-not-found` | `pluginId`, `marketplace` | | `marketplace-not-found` | `marketplace`, `availableMarketplaces[]` | | `marketplace-load-failed` | `marketplace`, `reason` | | `mcp-config-invalid` | `serverName`, `validationError` | | `mcp-server-suppressed-duplicate` | `serverName`, `duplicateOf` | | `lsp-config-invalid` | `serverName`, `validationError` | | `lsp-server-start-failed` | `serverName`, `reason` | | `lsp-server-crashed` | `exitCode`, `signal?` | | `lsp-request-timeout` | `method`, `timeoutMs` | | `lsp-request-failed` | `method`, `error` | | `marketplace-blocked-by-policy` | `marketplace`, `blockedByBlocklist?`, `allowedSources[]` | | `dependency-unsatisfied` | `dependency`, `reason: 'not-enabled' \| 'not-found'` | | `plugin-cache-miss` | `installPath` | | `hook-load-failed` | `hookPath`, `reason` | | `component-load-failed` | `component`, `path`, `reason` | | `mcpb-download-failed` | `url`, `reason` | | `mcpb-extract-failed` | `mcpbPath`, `reason` | | `mcpb-invalid-manifest` | `mcpbPath`, `validationError` | | `generic-error` | `error` | **`getPluginErrorMessage(error)`** — Returns a human-readable string for any `PluginError` variant. **`PluginLoadResult`:** ```typescript type PluginLoadResult = { enabled: LoadedPlugin[] disabled: LoadedPlugin[] errors: PluginError[] } ``` --- ## 7. Output Styles ### 7.1 System Overview Output styles are markdown files that define custom formatting instructions for Claude's responses. Loaded from `output-styles/` subdirectories in project `.claude/` and user `~/.claude/` directories. **Source:** `src/outputStyles/loadOutputStylesDir.ts` --- ### 7.2 Configuration - **Location:** `.claude/output-styles/*.md` (project) and `~/.claude/output-styles/*.md` (user) - **File naming:** `filename.md` → style name `filename` - **Frontmatter fields:** - `name`: Override the style name (defaults to filename without `.md`) - `description`: Description shown in output style picker - `keep-coding-instructions`: Boolean — whether to preserve coding-specific instructions (`true`/`false` or boolean) - `force-for-plugin`: Only valid for plugin output styles; ignored (with warning) on regular styles --- ### 7.3 API **`getOutputStyleDirStyles(cwd: string): Promise`** — Memoized async loader. Scans `output-styles` subdirectory across project hierarchy and user config. Returns `OutputStyleConfig[]`. **`OutputStyleConfig`** (from `constants/outputStyles.ts`): ```typescript type OutputStyleConfig = { name: string description: string prompt: string // file content (trimmed) source: string // setting source (userSettings, localSettings, etc.) keepCodingInstructions?: boolean } ``` **`clearOutputStyleCaches()`** — Clears memoized `getOutputStyleDirStyles`, `loadMarkdownFilesForSubdir`, and plugin output style caches. --- ## 8. Hooks Schema ### 8.1 System Overview Hooks execute side effects at specific lifecycle events (PreToolUse, PostToolUse, PostResponse, etc.). The schema defines four hook types in a discriminated union, with conditional execution via `if` conditions. **Source:** `src/schemas/hooks.ts` --- ### 8.2 Hook Types #### `BashCommandHook` (`type: 'command'`) ```typescript { type: 'command' command: string // Shell command to execute if?: string // Permission rule syntax filter (e.g., "Bash(git *)") shell?: 'bash' | 'powershell' timeout?: number // Seconds statusMessage?: string // Spinner message once?: boolean // Run once, then remove async?: boolean // Non-blocking background execution asyncRewake?: boolean // Background + wake model on exit code 2 } ``` #### `PromptHook` (`type: 'prompt'`) ```typescript { type: 'prompt' prompt: string // LLM prompt (use $ARGUMENTS for hook input JSON) if?: string timeout?: number model?: string // Default: small fast model statusMessage?: string once?: boolean } ``` #### `HttpHook` (`type: 'http'`) ```typescript { type: 'http' url: string // POST destination if?: string timeout?: number headers?: Record // May use $VAR_NAME interpolation allowedEnvVars?: string[] // Explicit allowlist for env var interpolation statusMessage?: string once?: boolean } ``` #### `AgentHook` (`type: 'agent'`) ```typescript { type: 'agent' prompt: string // Verification prompt (use $ARGUMENTS for hook input JSON) if?: string timeout?: number // Default: 60 seconds model?: string // Default: Haiku statusMessage?: string once?: boolean } ``` **Note on `AgentHook`:** Must NOT use `.transform()` in the Zod schema — the schema is used in `parseSettingsFile`, and round-tripping a transformed function through `JSON.stringify` silently drops the prompt field. --- ### 8.3 Hook Matcher and Settings Structure ```typescript type HookMatcher = { matcher?: string // e.g., "Write", "Bash(git *)" hooks: HookCommand[] } type HooksSettings = Partial> ``` **`IfConditionSchema`:** Shared `if` field uses permission rule syntax (`Bash(git *)`, `Read(*.ts)`) to filter hook execution before spawning. Evaluated against `tool_name` and `tool_input`. --- ### 8.4 Exports | Export | Description | |--------|-------------| | `HookCommandSchema` | Discriminated union of all 4 hook command types | | `HookMatcherSchema` | `{ matcher?, hooks[] }` | | `HooksSchema` | `Partial>` | | `HookCommand` | Inferred type from schema | | `BashCommandHook` | `Extract` | | `PromptHook` | `Extract` | | `AgentHook` | `Extract` | | `HttpHook` | `Extract` | | `HookMatcher` | Inferred from matcher schema | | `HooksSettings` | `Partial>` | --- ## 9. Native-TypeScript Ports (native-ts/) The `native-ts/` directory contains pure-TypeScript ports of Rust NAPI native modules. These are used when the native modules cannot be loaded (platforms without precompiled binaries, etc.). ### 9.1 Color Diff (`native-ts/color-diff/index.ts`) **Purpose:** Port of `vendor/color-diff-src` — syntax-highlighted word-level diff rendering for the diff view. **Implementation:** Uses `highlight.js` (lazy-loaded to defer ~50MB grammar registration) and the `diff` npm package's `diffArrays`. **Semantic differences from native:** - Syntax highlighting via highlight.js instead of syntect/bat - Scope colors approximate syntect output, but plain identifiers and `=` `:` operators render in default fg (no scope in hljs grammar) - `BAT_THEME` env is a stub (always returns default theme) - Output structure (line numbers, markers, backgrounds, word-diff) is identical **Lazy loading pattern:** ```typescript let cachedHljs: HLJSApi | null = null function hljs(): HLJSApi { if (cachedHljs) return cachedHljs const mod = require('highlight.js') // Handles both ESM default export and CJS module-is-API interop cachedHljs = 'default' in mod && mod.default ? mod.default : mod return cachedHljs! } ``` **Key types:** ```typescript type Hunk = { oldStart: number oldLines: number newStart: number newLines: number lines: string[] } type SyntaxTheme = { ... } ``` --- ### 9.2 File Index (`native-ts/file-index/index.ts`) **Purpose:** Port of `vendor/file-index-src` — high-performance fuzzy file path search, replacing nucleo (Rust). **Algorithm:** Approximates fzf-v2/nucleo scoring with: - Bitmap reject: paths missing any needle letter rejected in O(1) - Greedy-earliest position scan via `indexOf` (SIMD-accelerated in JSC/V8) - Scoring constants: | Constant | Value | Description | |----------|-------|-------------| | `SCORE_MATCH` | 16 | Base score per matched char | | `BONUS_BOUNDARY` | 8 | Bonus for path boundary match (`/ \ - _ . space`) | | `BONUS_CAMEL` | 6 | Bonus for camelCase boundary | | `BONUS_CONSECUTIVE` | 4 | Bonus for consecutive matches | | `BONUS_FIRST_CHAR` | 8 | Bonus for matching first char | | `PENALTY_GAP_START` | 3 | Penalty for starting a gap | | `PENALTY_GAP_EXTENSION` | 1 | Penalty per gap character | **Test file penalty:** Paths containing `"test"` get a 1.05× score penalty (capped at 1.0). **Score semantics:** Lower = better. `score = position_in_results / result_count`, so best match is 0.0. **Smart case:** Lowercase query → case-insensitive; any uppercase → case-sensitive. **`FileIndex` class:** ```typescript class FileIndex { loadFromFileList(fileList: string[]): void loadFromFileListAsync(fileList: string[]): { queryable: Promise; done: Promise } search(query: string, limit: number): SearchResult[] } type SearchResult = { path: string score: number } ``` **Async loading:** `loadFromFileListAsync` yields to event loop every `CHUNK_MS = 4ms` of sync work. Resolves `queryable` after first chunk (partial results immediately searchable). For a 270k-path list: ~5–10ms to first queryable state. **Top-level cache:** Stores 100 most-common top-level path segments for empty-query results. **`TOP_LEVEL_CACHE_LIMIT = 100`**, **`MAX_QUERY_LEN = 64`** --- ### 9.3 Yoga Layout (`native-ts/yoga-layout/`) **Purpose:** Pure-TypeScript port of Meta's Yoga flexbox engine (`yoga-layout/load`), used by Ink's layout engine. **`enums.ts` — Yoga constants as `const` objects:** | Enum | Values | |------|--------| | `Align` | Auto(0), FlexStart(1), Center(2), FlexEnd(3), Stretch(4), Baseline(5), SpaceBetween(6), SpaceAround(7), SpaceEvenly(8) | | `BoxSizing` | BorderBox(0), ContentBox(1) | | `Dimension` | Width(0), Height(1) | | `Direction` | Inherit(0), LTR(1), RTL(2) | | `Display` | Flex(0), None(1), Contents(2) | | `Edge` | Left(0), Top(1), Right(2), Bottom(3), ... | | `Errata` | None(0), StretchFlexBasis(1), All(2147483647) | | `ExperimentalFeature` | WebFlexBasis(0) | | `FlexDirection` | Column(0), ColumnReverse(1), Row(2), RowReverse(3) | | `Gutter` | Column(0), Row(1), All(2) | | `Justify` | FlexStart(0), Center(1), FlexEnd(2), SpaceBetween(3), SpaceAround(4), SpaceEvenly(5) | | `MeasureMode` | Undefined(0), Exactly(1), AtMost(2) | | `NodeType` | Default(0), Text(1) | | `Overflow` | Visible(0), Hidden(1), Scroll(2) | | `PositionType` | Static(0), Relative(1), Absolute(2) | | `Unit` | Undefined(0), Point(1), Percent(2), Auto(3) | | `Wrap` | NoWrap(0), Wrap(1), WrapReverse(2) | **`index.ts` — Subset of Yoga implemented:** - flex-direction (row/column + reverse) - flex-grow / flex-shrink / flex-basis - align-items / align-self (all except baseline in practice) - justify-content (all 6 values) - margin / padding / border / gap - width / height / min / max (point, percent, auto) - position: relative / absolute - display: flex / none / contents - measure functions (text nodes) - margin: auto - flex-wrap + align-content - baseline alignment (spec parity, not used by Ink) **Not implemented:** aspect-ratio, box-sizing: content-box, RTL direction --- ## 10. MoreRight Hook (moreright/) ### 10.1 System Overview `useMoreRight` is a React hook that exists as a **stub for external builds**. The real implementation is internal-only (ant users). The stub returns no-op implementations of the hook interface. **Source:** `src/moreright/useMoreRight.tsx` --- ### 10.2 Interface ```typescript export function useMoreRight(_args: { enabled: boolean setMessages: (action: M[] | ((prev: M[]) => M[])) => void inputValue: string setInputValue: (s: string) => void setToolJSX: (args: M) => void }): { onBeforeQuery: (input: string, all: M[], n: number) => Promise onTurnComplete: (all: M[], aborted: boolean) => Promise render: () => null } ``` **Stub behavior:** - `onBeforeQuery` always returns `true` (allow query) - `onTurnComplete` is a no-op - `render` returns `null` The real internal implementation provides pre-query preprocessing and post-turn processing functionality whose details are redacted from the external build. --- ## 11. Migrations Migrations run at startup to update stored config/settings to current formats. All migrations are idempotent (safe to re-run). ### 11.1 Migration Overview | Migration | Source | Target | Condition | |-----------|--------|--------|-----------| | `migrateAutoUpdatesToSettings` | `globalConfig.autoUpdates: false` | `userSettings.env.DISABLE_AUTOUPDATER: '1'` | Only when user-preference disabled (not native protection) | | `migrateBypassPermissionsAcceptedToSettings` | `globalConfig.bypassPermissionsModeAccepted` | `userSettings.skipDangerousModePermissionPrompt: true` | globalConfig field present | | `migrateEnableAllProjectMcpServersToSettings` | `projectConfig.enableAllProjectMcpServers` etc. | `localSettings` | Any of the 3 fields present | | `migrateFennecToOpus` | `userSettings.model` fennec aliases | Opus 4.6 aliases | ant users only | | `migrateLegacyOpusToCurrent` | Explicit Opus 4.0/4.1 model strings | `'opus'` alias | 1P provider + legacy remap enabled | | `migrateOpusToOpus1m` | `userSettings.model: 'opus'` | `'opus[1m]'` | isOpus1mMergeEnabled() | | `migrateReplBridgeEnabledToRemoteControlAtStartup` | `globalConfig.replBridgeEnabled` | `globalConfig.remoteControlAtStartup` | Old key exists, new key absent | | `migrateSonnet1mToSonnet45` | `userSettings.model: 'sonnet[1m]'` | `'sonnet-4-5-20250929[1m]'` | Once (sonnet1m45MigrationComplete flag) | | `migrateSonnet45ToSonnet46` | Explicit Sonnet 4.5 strings | `'sonnet'` or `'sonnet[1m]'` | 1P + Pro/Max/TeamPremium | | `resetAutoModeOptInForDefaultOffer` | `userSettings.skipAutoPermissionPrompt` | Clear the setting | TRANSCRIPT_CLASSIFIER + specific conditions | | `resetProToOpusDefault` | Default model settings | Sets migration timestamp | 1P Pro users | --- ### 11.2 Model Naming Evolution (revealed by migrations) The migration history reveals the complete model codename/alias evolution: **Opus lineage:** 1. `fennec-latest` — internal codename for early Opus 4.x 2. `claude-opus-4-0` / `claude-opus-4-20250514` — Opus 4.0 release 3. `claude-opus-4-1` / `claude-opus-4-1-20250805` — Opus 4.1 release 4. `opus-4-5-fast` — Opus 4.5 fast variant 5. `opus` — Current Opus 4.6 alias 6. `opus[1m]` — Opus 4.6 with 1M context window **Sonnet lineage:** 1. `sonnet[1m]` — Early Sonnet with 1M context (targeted Sonnet 4.5) 2. `claude-sonnet-4-5-20250929` / `sonnet-4-5-20250929` — Sonnet 4.5 explicit 3. `claude-sonnet-4-5-20250929[1m]` / `sonnet-4-5-20250929[1m]` — Sonnet 4.5 with 1M 4. `sonnet` — Current Sonnet 4.6 alias 5. `sonnet[1m]` — Sonnet 4.6 with 1M context (re-aliased) **Internal codenames:** - `fennec-latest` → Opus 4.x series (pre-release internal name) - `fennec-fast-latest` → Opus fast variant --- ### 11.3 Migration Details #### `migrateAutoUpdatesToSettings` - Only migrates when `globalConfig.autoUpdates === false` AND `autoUpdatesProtectedForNative !== true` - Adds `DISABLE_AUTOUPDATER: '1'` to `userSettings.env` - Sets `process.env.DISABLE_AUTOUPDATER = '1'` immediately - Removes `autoUpdates` and `autoUpdatesProtectedForNative` from globalConfig #### `migrateFennecToOpus` (ant-only) Alias mapping: - `fennec-latest[1m]` → `opus[1m]` - `fennec-latest` → `opus` - `fennec-fast-latest` / `opus-4-5-fast` → `opus[1m]` + `fastMode: true` #### `migrateLegacyOpusToCurrent` (1P only) Migrates: `claude-opus-4-20250514`, `claude-opus-4-1-20250805`, `claude-opus-4-0`, `claude-opus-4-1` → `'opus'` Sets `legacyOpusMigrationTimestamp` for one-time notification. #### `migrateOpusToOpus1m` Migrates `'opus'` → `'opus[1m]'` for eligible Max/Team Premium 1P users. Idempotent: only acts when `userSettings.model === 'opus'` exactly. If the migrated value equals the current default, clears `model` setting instead. #### `migrateSonnet1mToSonnet45` Run once (tracked by `sonnet1m45MigrationComplete` in globalConfig). Also migrates the in-memory `mainLoopModelOverride` if set. #### `migrateSonnet45ToSonnet46` Migrates: - `claude-sonnet-4-5-20250929` / `sonnet-4-5-20250929` → `'sonnet'` - `claude-sonnet-4-5-20250929[1m]` / `sonnet-4-5-20250929[1m]` → `'sonnet[1m]'` Sets `sonnet45To46MigrationTimestamp` for notification (skipped for new users with `numStartups <= 1`). #### `migrateReplBridgeEnabledToRemoteControlAtStartup` Renames `replBridgeEnabled` (internal implementation detail) to `remoteControlAtStartup` (user-facing key). Only acts when old key exists and new key is absent. #### `resetAutoModeOptInForDefaultOffer` One-shot (guarded by `hasResetAutoModeOptInForDefaultOffer` flag). Clears `skipAutoPermissionPrompt` for users who accepted the old 2-option dialog but don't have auto as their default mode. Re-surfaces the new dialog with the "make it my default" option. Only runs when `getAutoModeEnabledState() === 'enabled'` (not 'opt-in'). #### `resetProToOpusDefault` Sets migration timestamp for Pro subscribers on 1P with no custom model setting (UI notification that Opus is now the default). For all other users, simply marks migration complete. --- ## 12. Core Type Definitions (types/) ### 12.1 Branded ID Types (`types/ids.ts`) Prevents mixing SessionId and AgentId at compile time: ```typescript type SessionId = string & { readonly __brand: 'SessionId' } type AgentId = string & { readonly __brand: 'AgentId' } ``` **AgentId format:** `a` + optional `