1767 lines
63 KiB
Markdown
1767 lines
63 KiB
Markdown
# 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<Rarity, number> = {
|
||
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<StatName, number> // 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<StatName, number> {
|
||
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/<sanitized-git-root>/memory/`) — per-user, per-project
|
||
2. **Team memory** (`<auto-mem-path>/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 `<system-reminder>` tags
|
||
|
||
**Source directory:** `src/memdir/`
|
||
|
||
---
|
||
|
||
### 2.2 Memory Directory Structure
|
||
|
||
```
|
||
~/.claude/
|
||
projects/
|
||
<sanitized-git-root>/
|
||
memory/
|
||
MEMORY.md -- entrypoint index (always loaded)
|
||
<topic-file>.md -- individual memory files with frontmatter
|
||
team/ -- team-shared memories (TEAMMEM feature)
|
||
MEMORY.md
|
||
<topic-file>.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<string> = new Set(),
|
||
): Promise<RelevantMemory[]>
|
||
|
||
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` | `<system-reminder>` 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. `<memoryBase>/projects/<sanitized-git-root>/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:
|
||
```
|
||
<autoMemPath>/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 `<autoMemPath>/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()` | `<autoMemPath>/team/` |
|
||
| `getTeamMemEntrypoint()` | `<autoMemPath>/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 `<scope>` 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<string, string | null>
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 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<string, string> // reference files extracted to disk on first use
|
||
getPromptForCommand: (args: string, context: ToolUseContext) => Promise<ContentBlockParam[]>
|
||
}
|
||
```
|
||
|
||
**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: <dir>` 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<string, McpServerConfig>
|
||
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<string, CommandMetadata>
|
||
agentsPath?: string
|
||
agentsPaths?: string[]
|
||
skillsPath?: string
|
||
skillsPaths?: string[]
|
||
outputStylesPath?: string
|
||
outputStylesPaths?: string[]
|
||
hooksConfig?: HooksSettings
|
||
mcpServers?: Record<string, McpServerConfig>
|
||
lspServers?: Record<string, LspServerConfig>
|
||
settings?: Record<string, unknown>
|
||
}
|
||
```
|
||
|
||
**`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<OutputStyleConfig[]>`** — 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<string, string> // 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<Record<HookEvent, HookMatcher[]>>
|
||
```
|
||
|
||
**`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<Record<HookEvent, HookMatcher[]>>` |
|
||
| `HookCommand` | Inferred type from schema |
|
||
| `BashCommandHook` | `Extract<HookCommand, { type: 'command' }>` |
|
||
| `PromptHook` | `Extract<HookCommand, { type: 'prompt' }>` |
|
||
| `AgentHook` | `Extract<HookCommand, { type: 'agent' }>` |
|
||
| `HttpHook` | `Extract<HookCommand, { type: 'http' }>` |
|
||
| `HookMatcher` | Inferred from matcher schema |
|
||
| `HooksSettings` | `Partial<Record<HookEvent, HookMatcher[]>>` |
|
||
|
||
---
|
||
|
||
## 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<void>; done: Promise<void> }
|
||
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<boolean>
|
||
onTurnComplete: (all: M[], aborted: boolean) => Promise<void>
|
||
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 `<label>-` + 16 hex chars
|
||
Pattern: `/^a(?:.+-)?[0-9a-f]{16}$/`
|
||
|
||
| Export | Description |
|
||
|--------|-------------|
|
||
| `SessionId` | Branded session ID type |
|
||
| `AgentId` | Branded agent ID type |
|
||
| `asSessionId(id)` | Cast string to SessionId |
|
||
| `asAgentId(id)` | Cast string to AgentId |
|
||
| `toAgentId(s)` | Validate + brand as AgentId, or null |
|
||
|
||
---
|
||
|
||
### 12.2 Command Types (`types/command.ts`)
|
||
|
||
**`PromptCommand`** — Skill/command that generates a prompt:
|
||
```typescript
|
||
type PromptCommand = {
|
||
type: 'prompt'
|
||
progressMessage: string
|
||
contentLength: number
|
||
argNames?: string[]
|
||
allowedTools?: string[]
|
||
model?: string
|
||
source: SettingSource | 'builtin' | 'mcp' | 'plugin' | 'bundled'
|
||
pluginInfo?: { pluginManifest: PluginManifest; repository: string }
|
||
disableNonInteractive?: boolean
|
||
hooks?: HooksSettings
|
||
skillRoot?: string
|
||
context?: 'inline' | 'fork'
|
||
agent?: string
|
||
effort?: EffortValue
|
||
paths?: string[] // glob patterns — skill only visible after model touches matching files
|
||
getPromptForCommand(args, context): Promise<ContentBlockParam[]>
|
||
}
|
||
```
|
||
|
||
**Execution contexts:**
|
||
- `'inline'` (default): Skill content expands into current conversation
|
||
- `'fork'`: Skill runs as sub-agent with separate context and token budget
|
||
|
||
---
|
||
|
||
### 12.3 Plugin Types (`types/plugin.ts`)
|
||
|
||
See Section 6.4 for `LoadedPlugin`, `BuiltinPluginDefinition`, `PluginError`, and `PluginLoadResult`.
|
||
|
||
---
|
||
|
||
## 13. Remote Session System (remote/)
|
||
|
||
### 13.1 System Overview
|
||
|
||
The remote session system manages multi-user and remote-connected Claude Code sessions, bridging between the local CLI process and remote session infrastructure.
|
||
|
||
**Source:** `src/remote/`
|
||
|
||
**Files:**
|
||
- `SessionsWebSocket.ts` — WebSocket client for server-sent session events
|
||
- `RemoteSessionManager.ts` — Session state management and reconnection logic
|
||
- `remotePermissionBridge.ts` — Permission request routing for remote sessions
|
||
- `sdkMessageAdapter.ts` — Adapts SDK message format to internal message format
|
||
|
||
---
|
||
|
||
### 13.2 Component Roles
|
||
|
||
**`SessionsWebSocket`** — Maintains a WebSocket connection to the remote session server. Handles:
|
||
- Reconnection with exponential backoff
|
||
- Session event routing
|
||
- Auth token refresh integration
|
||
|
||
**`RemoteSessionManager`** — Top-level orchestrator for remote sessions:
|
||
- Manages session lifecycle (create, attach, detach, resume)
|
||
- Tracks active remote sessions
|
||
- Routes permission requests to appropriate sessions
|
||
- Handles session backgrounding/foregrounding
|
||
|
||
**`remotePermissionBridge`** — Bridges permission requests from remote sessions back to the local UI:
|
||
- Permission requests originating in remote worker sessions are forwarded to the interactive CLI
|
||
- Responses are sent back to the worker via the session infrastructure
|
||
|
||
**`sdkMessageAdapter`** — Converts between SDK wire-format messages and the internal `Message` type used by the UI layer.
|
||
|
||
---
|
||
|
||
*End of spec document.*
|