# cc-linker
> Seamless switching between mobile chat apps and terminal (Claude Code CLI) conversations — like switching devices.
>
> **Currently supports Feishu**, with more chat platforms coming soon.
[](https://www.npmjs.com/package/cc-linker)
[](LICENSE)
**Language:** [中文](README.md) | English
## Why cc-linker?
Have you ever found yourself in these situations:
- **Chat on your phone during commute, continue on terminal at work** — Discuss technical solutions with the Bot via Feishu on the subway, then at the office run `cc-linker list` to find the session and `resume` to pick up right where you left off
- **Quick questions on Feishu, deep debugging on terminal** — Ask a quick API usage question on Feishu, realize you need local debugging, and switch to the same session on terminal to keep coding with Claude
- **Multiple projects, sessions stay organized** — Talk to Claude across `project-a` and `project-b` simultaneously, use `/list` to clearly see each session's directory and status, and switch with one click without getting confused
**cc-linker is the bridge tool that solves these pain points.** It maintains a unified session registry on your machine, allowing mobile chat apps and Claude Code CLI to share the same session state — no matter which side you start a conversation on, you can seamlessly switch to the other.
> **Currently supports Feishu**, with more chat platforms coming soon.
## Features
| Feature | Description |
|---------|-------------|
| Seamless cross-device switching | Chat app sessions resumed on terminal (with context & directory); terminal sessions visible in chat app |
| Streaming card interaction | See Claude's thinking, tool_use summary (≤80 chars), and replies in real-time in your chat app, with a 5s tick showing elapsed time — no more "spinning wait" |
| SDK permission control | Optional SDK mode: approve/deny each tool use via interactive cards before Claude executes |
| Image message support | Feishu images are downloaded automatically and passed to Claude for analysis |
| Directory browsing | Use `/listDir` to browse directories and choose where the next new session should start |
| Unified session management | Auto-scan, incremental sync, no manual session list maintenance needed |
| Multi-model switching | One-click model switch in cards, no config changes required |
| Persistent message queue | File-level message queue survives crashes and restarts |
| 3-step setup | `install → setup → start`, ready in 5 minutes |
## Showcase
### Chat App Experience (Feishu)
> Currently supports Feishu, more platforms coming soon.
Session List
/list to view all sessions |
Processing Instant feedback after sending |
Streaming Feedback thinking + tool_use + reply, 5s tick |
 |
 |
 |
Complete Token / time / turn stats |
Complete (Long Reply) Long text displayed equally well |
Model Switch One-click switch via card buttons |
 |
 |
 |
### Terminal Experience
**View all sessions** — Clean table display, status at a glance:
**Resume session with one click** — Supports prefix matching, auto-switches directory and restores context:
## Quick Start
### 1. Install Bun (Required Runtime)
cc-linker is built on Bun, so **Bun >= 1.0** is a hard prerequisite.
**macOS / Linux:**
```bash
curl -fsSL https://bun.sh/install | bash
```
**macOS (Homebrew):**
```bash
brew tap oven-sh/bun && brew install bun
```
**Verify after install (open a new terminal first):**
```bash
bun --version # should print >= 1.0.0
```
> **Not working?** Your PATH wasn't updated. Manually add `~/.bun/bin` to `PATH`, or simply restart the terminal.
### 2. Install cc-linker
**Option 1: Install globally via npm** (requires Node.js >= 20 to provide `npm`; cc-linker itself still needs Bun to run)
```bash
npm install -g cc-linker@latest
```
**Option 2: Install globally via Bun** (recommended)
```bash
bun add -g cc-linker
```
> **Update tip**: when running `npm install -g cc-linker@latest`, if the daemon is already running, it will automatically call `cc-linker restart` to upgrade to the new version — no manual restart needed.
### 3. One-Step Setup
```bash
cc-linker setup
```
The interactive wizard guides you through:
- Initializing the session registry
- Installing the Claude Code auto-register hook
- Configuring the chat app Bot (currently Feishu only: App ID + App Secret + auto-start)
> **Need terminal-only features?** Run `cc-linker setup --skip-feishu` to skip chat app configuration.
### 4. Start Using
| Scenario | Action |
|----------|--------|
| Send message in chat app (Feishu) | Direct conversation, streaming card updates in real-time |
| View all sessions on terminal | `cc-linker list` |
| Resume a session on terminal | `cc-linker resume ` |
| Switch session in chat app (Feishu) | `/switch ` |
| Choose a directory for the next new session (Feishu) | `/listDir` |
| Create a new session in chat app (Feishu) | `/new [path] [--model ] [-- prompt]` |
## Command Reference
### CLI Commands
```bash
cc-linker list # List all sessions
cc-linker resume # Resume a session on terminal (supports prefix matching)
cc-linker show # Show session details
cc-linker sync # Manually sync sessions on both sides
cc-linker search # Search sessions
cc-linker export # Export session as Markdown/JSON/Text
cc-linker clean # Clean up invalid records
cc-linker status # Check bridge status
```
### Chat App Bot Commands (Feishu)
Send these in a Feishu private chat with the Bot:
| Command | Description |
|---------|-------------|
| `/help` | Show help |
| `/list` | List sessions (with switch/resume button cards) |
| `/listDir` | Browse directories and choose where the next new session should start |
| `/new [path] [--model ] [-- prompt]` | Create a new session immediately, or preselect directory/model first |
| `/switch ` | Switch session |
| `/resume ` | Get terminal resume command |
| `/model [index\|alias\|--clear]` | View, set, or clear the default model |
| `/agents` | View the list of background `claude` sessions on the terminal (grouped by busy / waiting / idle / completed) — see "Agent View Integration" below |
| `/status` | Check status |
| `/stop` | **Hard-kill** the current session's claude process (works for both regular and bg sessions; state is cleared automatically) |
| `/cancel` | **Soft-exit** the Agent View "waiting for reply" state — only clears `pending_agent_reply`; **the bg keeps running**, and the next chat message takes the default path instead of rendezvous |
| `/whoami` | Get your open_id |
> **Note 1**: numeric indexes used by `/switch` and `/resume` come from the most recent `/list` snapshot and expire after 10 minutes by default. Run `/list` again if needed.
>
> **Note 2**: `/new` also supports a "prepare first, create on the next message" flow. `/model` sets the per-user default model until you clear it with `/model --clear`.
>
> **Note 3**: `/stop` vs `/cancel` — `/stop` kills the process (irreversible), `/cancel` only clears the waiting state (bg keeps running). Use `/cancel` to immediately exit "I just clicked [Reply]" mode; use `/stop` to terminate a hung Claude process. The two are independent.
### Bot Runtime Management
| Command | Description |
|---------|-------------|
| `cc-linker start` | Start in foreground (blocks terminal) |
| `cc-linker start --daemon` | Start as background daemon |
| `cc-linker stop` | Stop the background Bot |
| `cc-linker restart` | Restart the Bot service |
| `cc-linker daemon install` | Configure auto-start on boot |
| `cc-linker daemon uninstall` | Remove auto-start on boot |
| `cc-linker daemon status` | Check background service status |
## Agent View Integration
Agent View is cc-linker's "remote session takeover" capability: from Feishu, inspect live status of any background `claude` session running on your terminal, then Peek its log tail, Reply with text, Stop the process, or Attach it back into the main chat flow. The primary data source is `~/.claude/jobs//state.json` (the authoritative state machine maintained by Claude CLI). Requires Claude Code CLI >= 2.1.139 with the daemon running.
> 💡 **Fork auto-continuation** (v2.6+): when a bg session is continued via `claude --resume --fork` to a new TUI (e.g., you switch between multiple TUIs), cc-linker automatically routes your reply to the latest live session. **No manual switching needed** — `/agents` list, [Peek], [Reply], and [Attach] all work against wherever the conversation is actually running. Clicking [Reply] on an old card also auto-jumps to the fork's TUI instead of erroring with "Claude Code process exited with code 1".
### Commands and Button Semantics
| Entry | Behavior |
|-------|----------|
| `/agents` | Fetch all background sessions, group by busy / waiting / idle / completed (completed group capped at 5, overflow shown as "… N more"), send an interactive list card; auto-fallback to plain text if card exceeds 25KB |
| List card `[Peek]` | Resolves the latest assistant output via a 3-tier fallback: Tier 1 reads JSONL directly (via `state.json.linkScanPath` or JsonlIndex) → Tier 2 reads the roster parent JSONL (resume-from scenarios) → Tier 3 falls back to `claude logs`; sends a dedicated peek card |
| List card `[Reply]` | Shown only on waiting sessions: triggers an "✍️ waiting for reply" card, just send text within 5 minutes. The processing card auto-refreshes every 5s (thinking + tool_use + reply). **If the bg runs and asks a new question, just type your reply in chat — you do NOT need to click [Reply] again** (v2.4.x chained reply) |
| List card `[Stop]` | Shown only on busy sessions: first shows a confirmation card (to prevent mis-clicks), then `claude stop ` on confirm |
| List card `[Attach]` | Switches the openId to that session; subsequent plain messages go through the SDK (preserves the user's defaultProvider); also auto-starts the Attached Card live watcher (see below) |
| List card `[Refresh]` | Patches the original card (2s debounce); if messageId mismatches, sends a new card to avoid patching a stale card that was already overwritten |
| List card `[Back to chat]` | Plain text reply, no state change, drops back into the regular message flow |
| Peek card `[❌ Cancel wait]` | Clears the `pending_agent_reply` state (only shown while a Reply wait is active) |
| `/cancel` | Same as above, text command form |
### Configuration
A new `[agent_view]` section in `config.toml` (all keys optional — defaults are tuned for most setups):
```toml
[agent_view]
# Master switch. Set to false to disable /agents and all related card actions
# enabled = true
# /agents list card [Refresh] debounce interval (ms)
# refresh_min_interval_ms = 2000
# How many recent lines of `claude logs` output to fetch in Tier 3 fallback;
# ignored when Tier 1/2 reads JSONL directly
# peek_lines = 30
# Peek card recentOutput byte cap (over the cap is truncated by character;
# avoids Feishu card size limits)
# peek_max_bytes = 2048
# waiting → how long to wait for the user's reply text before auto-cancelling (ms)
# expected_reply_timeout_ms = 300000
# Whether to filter out daemon-spawned sub-agent sessions (source=spare);
# only user-dispatched sessions appear in the list
# background_only = true
# Whether the Stop button needs a confirmation card
# stop_requires_confirm = true
# Minimum Claude CLI version required; below this, Agent View is disabled
# min_claude_version = "2.1.139"
# Minimum gap between two replies on the Reply path (ms), anti-spam
# reply_throttle_ms = 500
# v2.4 GA: inject reply via rendezvous socket submit + state.json polling
# Defaults to true (v2.4 GA flipped the default; previously opt-in).
# Set to false to fall back to the v2.3.5 SDK path
# rendezvous_enabled = true
# v2.4: max time (ms) to wait for state.json to reach a terminal state
# after submitting a reply
# rendezvous_timeout_ms = 60000
```
> **v2.4 GA default behavior**: Reply goes through the rendezvous channel by default, no setup needed. If the bg doesn't support rendezvous (daemon offline, bg truly processing, or socket missing), cc-linker automatically falls back to the legacy SDK path. To force the legacy behavior, explicitly set `rendezvous_enabled = false`.
Corresponding environment variables (take precedence over the config file; `min_claude_version`, `rendezvous_enabled`, `rendezvous_timeout_ms` are config-file only):
| Variable | Field |
|----------|-------|
| `CC_LINKER_AGENT_VIEW_ENABLED` | `enabled` |
| `CC_LINKER_AGENT_VIEW_REFRESH_MIN_INTERVAL_MS` | `refresh_min_interval_ms` |
| `CC_LINKER_AGENT_VIEW_PEEK_LINES` | `peek_lines` |
| `CC_LINKER_AGENT_VIEW_PEEK_MAX_BYTES` | `peek_max_bytes` |
| `CC_LINKER_AGENT_VIEW_EXPECTED_REPLY_TIMEOUT_MS` | `expected_reply_timeout_ms` |
| `CC_LINKER_AGENT_VIEW_BACKGROUND_ONLY` | `background_only` |
| `CC_LINKER_AGENT_VIEW_STOP_REQUIRES_CONFIRM` | `stop_requires_confirm` |
| `CC_LINKER_AGENT_VIEW_REPLY_THROTTLE_MS` | `reply_throttle_ms` |
> **Prerequisite**: a `claude` daemon must be running locally (>= `agent_view.min_claude_version`). `/agents` first runs a `claude --version` version guard, then checks that `~/.claude/daemon/roster.json` exists, then reads `~/.claude/jobs/*/state.json` to build the snapshot. When the version is too old or the daemon is not running, it returns a red error card without polluting the user's main flow.
#### `waiting` State Decision (v2.4.x)
**TL;DR: if the bg is waiting for user input (whether explicitly blocked or "pseudo-busy, real waiting"), it shows up in the `waiting` group with a [Reply] button.** The actual implementation uses the `checkRendezvousEligibility` decision tree, covering three cases:
- `tempo=blocked && needs` — bg explicitly waits for user input
- `state in {running, working} && needs` — CLI 2.1.163 behavior: the worker process is alive but is actually waiting for the user ("pseudo-busy, real waiting")
- `state=blocked` — no needs but still in the `blocked` state (conservative: lets the user actively decide via [Peek])
Sessions in any other busy/idle state do NOT show a `[Reply]` button — either the bg is truly processing (`tempo=active`, no needs), or it's already done / stopped.
### Reply UX Improvements (v2.4.x)
> **What you'll see**: after clicking [Reply], the original "↩️ waiting for reply" card stays in history and a new processing card appears. The processing card has three sections that refresh every 5 seconds:
>
> - **💭 Thinking**: Claude's current thinking blocks (concatenated)
> - **🔧 Current action**: the tool Claude is calling + an input summary (≤80 chars, so you can scan what it's doing)
> - **📝 Reply**: the final text Claude has produced so far
>
> When the bg runs and asks a new question, the processing card transitions seamlessly back to a "waiting for input" card (same card patched, not rebuilt) — just type your reply in chat. **5-second refresh is by design, not a hang** — hit [Refresh] to force update.
#### Implementation details (optional reading)
- **Layered cards**: original "↩️ waiting for reply" card **kept as history**; new processing card is sent (`buildProcessingCard() = buildStreamingCard('', '', 0)`, `card-updater.ts:483-484`). On `new_needs`, the same card is `patchWaitingCard` (`bot.ts:1602-1618`) — NOT `cancel()` (that would render a misleading gray "🛑 Cancelled" card)
- **5s tick elapsed time**: streaming patches throttled at `bot.ts:1484`
- **JSONL rich content**: `readLastAssistantTurn` extracts thinking blocks (joined with `\n`) + tool_use name + input summary (`INPUT_SUMMARY_MAX = 80`)
- **`new_needs` chained reply**: rendezvous returns `new_needs` → `runChatSDK` passes `bgAskedNewQuestion` through to `handleReply` → `handleReply` re-`set()`s `expectedReply`
- **markSent (M1) double-reply guard**: T2 immediately calls `expectedReply.markSent(openId)`, so any subsequent chat message during the 60s wait does NOT take the rendezvous path (`manager.ts:915-917` + `expected-reply-state.ts:130`)
### Live Progress Card
When you `/switch` to a session that is actively processing, the Feishu overview card automatically refreshes every 10 seconds (`LiveProgressWatcher`), showing the latest assistant output, elapsed runtime, and time since last output — until the session finishes or the user sends a new message / switches / manually stops.
```toml
[feishu_bot.live_progress]
# interval_ms = 10000 # poll interval (~133 min max duration = 800 ticks)
# max_ticks = 800
# max_patch_failures = 3 # auto-stop after 3 consecutive Feishu API failures
```
### Attached Card Auto-Refresh
After a successful `[Attach]`, an `AttachedCardWatcher` is created automatically, polling the target session every 10 seconds and patching the card with the latest assistant output and runtime status. The card sets `update_multi: true` to prevent Feishu's concurrent-patch content-revert bug. Stop conditions mirror Live Progress Card (session ends, repeated patch failures, user intervention, bot shutdown, etc.).
### Session Activity Sync (CLI-side Activity Detection)
When the CLI side is actively using a session, sending a message to the same session from Feishu triggers a yellow warning card about concurrent conflict risk. The user can:
- Wait for the CLI to finish
- Click `⚠️ I understand the risk, send anyway` to force-send (auto-forces after 60s of inactivity)
- Send a new message, which automatically overrides the old force-send request
Detection signal priority: activity marker sidecar file > OS process detection (find claude process by cwd + sample child processes CPU) > JSONL mtime double-sampling.
For 100% accurate CLI-side activity reporting, configure `cc-linker activity-hook` as a Claude Code hook (see "Enable CLI-side activity marker" section below).
### BG-Conflict Rejection Card (Safety Guard)
When a user Attaches to a session whose daemon background worker is still running and then sends a message, Feishu shows a three-choice recovery card:
| Button | Behavior |
|--------|----------|
| 🛑 `[Stop bg and send]` | Runs `claude stop` → waits for worker to exit → falls back to the parent session → sends the message |
| 🌿 `[Open new session]` | Creates a brand-new session for the message; original worker is unaffected |
| ❌ `[Cancel]` | Cancels the send; bg worker is unaffected |
This prevents two claude processes from sharing the same cwd and causing file-write conflicts (safety > convenience).
## Feishu Integration (First Supported Platform)
cc-linker's architecture supports multiple chat apps — **Feishu is the first implemented platform**. More IM platforms can be added in the future.
Before configuring the Feishu Bot, you need to create an app and set permissions on the [Feishu Open Platform](https://open.feishu.cn/app).
### Create an App
1. Visit https://open.feishu.cn/app → Create an enterprise self-built app
2. Enable Bot capability under "App Features" → "Bot"
3. Get App ID and App Secret (from Credentials & Basic Info)
### Required Permissions
Go to "Permission Management", search and enable:
| Permission | Purpose |
|------------|---------|
| `im:message` | Read and send messages |
| `im:message:send_as_bot` | Send messages as the app |
| `im:message:readonly` | Get message details (REST active read) |
| `im:message.p2p_msg:readonly` | Receive private messages sent to the Bot (**required for event push**) |
| `im:resource` | Download image resources sent by users |
> **Trap warning**: `im:message.p2p_msg:readonly` is NOT the same as `im:message:readonly`.
> The Feishu `im.message.receive_v1` event subscription page's "required permissions" list **only shows the former**.
> If you only enable `im:message:readonly` (REST active read), the Bot will connect via WebSocket but will **NOT receive any private messages**.
> cc-linker only handles `chat_type === 'p2p'` private messages, so this permission is required.
> Missing it makes `cc-linker setup` hang for 120 seconds trying to capture `open_id`.
### Required Event Subscriptions
Go to "Events & Callbacks", add in the two tabs below:
**Event Subscriptions**:
| Event | Purpose |
|-------|---------|
| `im.message.receive_v1` | Receive messages sent to the Bot |
| `im.chat.member.bot.added_v1` | Triggered when Bot is invited to a group (optional) |
**Callback Subscriptions**:
| Callback | Purpose |
|----------|---------|
| `card.action.trigger` | Receive card button clicks (`/list` session switching, model switching, SDK permission confirmation, etc.) |
> **Important**: Choose **WebSocket** (not HTTP callback) for subscription method.
>
> **Note**: `card.action.trigger` is required for all card interactions. Without it, buttons in `/list`, `/model`, and SDK permission cards will not respond.
### Publish the App
After configuring permissions, go to "Version Management & Release" → Create Version → Publish. **Permissions only take effect after publishing.**
## Configuration
Config file: `~/.cc-linker/config.toml` (optional, defaults used if not created)
```toml
[general]
log_level = "info"
claude_bin = "claude"
[feishu_bot]
# owner_open_id = "ou_xxx"
# default_cwd = "/path/to/workspace"
[stream]
enabled = true
throttle_ms = 1500
show_thinking = true
max_card_bytes = 25000
fallback_to_text = true
[claude]
# Permission mode for Claude Code tool execution
# Since Feishu cannot do terminal-style confirmation, the default is to auto-accept edits
# Available values: acceptEdits / auto / bypassPermissions / default / dontAsk / plan
permission_mode = "acceptEdits"
# Optional allowlist: explicitly allowed tools
# Empty by default, which means cc-linker follows local Claude Code settings
# allowed_tools = []
# Optional denylist: explicitly blocked tools
# Empty by default, which means cc-linker follows local Claude Code settings
# disallowed_tools = []
[sdk]
# Agent SDK mode (supports interactive permission approval cards in Feishu)
# Enabled by default. Set to false to disable it
# enabled = true
# permission_mode = "acceptEdits"
# timeout_ms = 600000
# claude_executable = "claude"
[images]
# Image message handling (enabled by default)
# enabled = true
# max_size_bytes = 10485760
# cleanup_max_age_hours = 24
```
**Note**: SDK mode requires the `claude` CLI to be installed on the system (`npm install -g @anthropic-ai/claude-code`). You can override the executable path with `general.claude_bin` or `sdk.claude_executable`.
> cc-linker defaults to the SDK-bundled `claude` binary (guaranteed version compatibility). If that binary is omitted during install (e.g. `--omit=optional`, `NODE_ENV=production`), cc-linker automatically falls back to `general.claude_bin` (typically system PATH `claude`) and emits a WARN log. If you hit version-incompatibility issues, set `sdk.claude_executable` explicitly to a compatible path.
**Additional note**: if the permission card cannot be delivered, or the user does not respond before timeout, the current implementation automatically denies that tool request.
**Environment Variable Overrides**:
| Variable | Description |
|----------|-------------|
| `CC_LINKER_DIR` | Override the cc-linker data directory (default: `~/.cc-linker`) |
| `CC_LINKER_CONFIG_PATH` | Specify the config file path |
| `CC_LINKER_REGISTRY_PATH` | Specify the registry file path |
| `CC_LINKER_LOG_PATH` | Specify the log file path |
| `CC_LINKER_FEISHU_APP_ID` | Feishu App ID |
| `CC_LINKER_FEISHU_APP_SECRET` | Feishu App Secret |
| `CC_LINKER_FEISHU_OWNER_OPEN_ID` | Restrict to specific user only |
| `CC_LINKER_FEISHU_DEFAULT_CWD` | Default working directory for new sessions |
| `CC_LINKER_STREAM_ENABLED` | Streaming response toggle |
| `CC_LINKER_LOG_LEVEL` | Log level |
| `CC_LINKER_CLAUDE_PERMISSION_MODE` | Claude Code permission mode |
| `CC_LINKER_CLAUDE_ALLOWED_TOOLS` | Allowed tools list (comma-separated) |
| `CC_LINKER_CLAUDE_DISALLOWED_TOOLS` | Disallowed tools list (comma-separated) |
| `CC_LINKER_SDK_ENABLED` | SDK mode with permission control toggle |
| `CC_LINKER_SDK_PERMISSION_MODE` | SDK permission mode |
| `CC_LINKER_SDK_TIMEOUT_MS` | Permission prompt timeout in milliseconds |
| `CC_LINKER_SDK_CLAUDE_EXECUTABLE` | Path to the Claude executable |
| `CC_LINKER_MAX_CONCURRENT_SESSIONS` | Maximum concurrent sessions (default: 5) |
| `CC_LINKER_SESSION_LOCK_TIMEOUT_MS` | Per-session lock timeout in milliseconds |
| `CC_LINKER_MAX_QUEUE_SIZE` | Maximum pending queue size |
| `CC_LINKER_CONFIRM_RISKY_ACTIONS` | Whether to confirm risky actions (true/false) |
| `CC_LINKER_IMAGES_ENABLED` | Enable or disable image handling |
| `CC_LINKER_IMAGES_MAX_SIZE` | Maximum image size in bytes |
| `CC_LINKER_IMAGES_CLEANUP_HOURS` | Image cleanup retention in hours |
## Architecture Overview
```
┌──────────────────────────────────────────────────────┐
│ Claude Code CLI ←→ Registry ←→ Chat App Bot │
│ (session JSONL) (registry.json) (Current: Feishu)│
│ ↑ │
│ SessionStart hook │
└──────────────────────────────────────────────────────┘
```
- **Registry** (`~/.cc-linker/registry.json`): Unified session index with file locking and auto-backup
- **User Mapping** (`~/.cc-linker/user-mapping.json`): Maps Feishu open_id to the current session target with atomic compare-and-swap updates
- **Scanner**: Incrementally scans Claude Code JSONL files to keep registry up-to-date
- **Hook**: Auto-registers new sessions when Claude Code starts
- **Spool Queue**: Persistent message queue, recoverable after crashes and restarts
- **Stream Parser**: Parses Claude `stream-json` output
- **Card Updater**: Streaming card sending with throttling
- **Permission Handler** (SDK mode): Interactive tool approval via Feishu cards
- **Image Processor**: Downloads Feishu images, injects image references into prompts, and cleans up expired files
- **Startup Reconciler**: Repairs inconsistent runtime state on startup, including stuck messages and incomplete session metadata
- **Provider Manager**: Multi-model management with CC Switch integration and manual configuration
- **Activity Detection**: CLI-side session activity detection to prevent concurrent access from Feishu and CLI
## Developer Guide
```bash
git clone https://github.com/yujuntea/cc-linker.git
cd cc-linker
bun install
bun run dev # Dev mode
bun run typecheck # Type check
bun test # Run tests
bun test --coverage # With coverage
```
### Two Build Artifacts
cc-linker supports two distribution forms with different build scripts:
| Artifact | Build Command | Output | Use Case |
|----------|---------------|--------|----------|
| **Standalone binary** | `bun run build` | `dist/cc-linker` | Single-machine use with no extra runtime required |
| **npm package** | `bun run build:npm` | `dist/cli.js` | `npm install -g` for global installation, still requires Bun at runtime |
### Local npm Package Testing
Before publishing, pack and install locally to verify `files` and `bin` entries are correct.
**Method 1: pack + install (closest to real publish)**
```bash
# 1. Build and pack
bun run build:npm # Generate dist/cli.js
npm pack # → cc-linker-x.y.z.tgz
# 2. Install in a clean environment
mkdir -p /tmp/test-cc-linker && cd /tmp/test-cc-linker
npm install /path/to/cc-linker-0.2.0.tgz
npx cc-linker --version # Verify command is available
cc-linker list # Verify functionality
# 3. Especially verify the executable path in plist/service generated by daemon install
cc-linker daemon install # Check ProgramArguments/ExecStart in the generated config
```
**Method 2: bun link (fastest for development iteration)**
```bash
# Create a global symlink; re-run build:npm after code changes for instant effect
bun run build:npm
bun link # or npm link
# Test globally
cc-linker list
cc-linker daemon install
# Unlink
bun unlink cc-linker
```
> ⚠️ `bun link` creates a symlink to the source directory and bypasses the `files` filter. **Always verify with Method 1 before publishing** to avoid missing files in the `files` field.
### Release
```bash
# Standalone binary (local distribution)
bun run build # → dist/cc-linker
# npm publish
npm version minor # or patch / major
npm publish # prepublishOnly triggers build:npm automatically
git push --tags
```
## Detailed Documentation
| Document | Description |
|----------|-------------|
| [Product Design Doc (Chinese)](docs/产品设计文档-自建方案.md) | Product design document |
| [Acceptance Guide](docs/验收指南.md) | Feature acceptance guide |
| [Acceptance Test Report](docs/验收测试报告.md) | Acceptance test results |
| [Product Requirements](docs/Product.md) | Product requirements document |
| [Model Switch Design](docs/model-switch-design.md) | Model switching design |
## License
MIT