cc-connect Documentation

Bridge local AI coding agents (Claude Code, Cursor, Gemini CLI, Codex) to messaging platforms (Feishu/Lark, DingTalk, Slack, Telegram, Discord, LINE, WeChat...

14,747 stars Go Go 1 file ยท ~21,399 tokens
RAW Doc

File: README.md

Want to appear here? Contact: [email protected] | WeChat: mongorz

Sponsors

Thanks to Kimi for sponsoring this project! Kimi K3 is Moonshot AI's most capable model and the world's first open 3T-class model. With 2.8 trillion parameters, native vision, and a 1-million-token context window, K3 delivers frontier performance across long-horizon coding, knowledge work, and reasoning.

With cc-connect, you can bring Kimi CLI from your local machine into Feishu/Lark, DingTalk, Telegram, Slack, Discord, WeCom, and other instant messaging tools. Wherever you are, you can continue working on local projects through chat and ask Kimi to inspect or modify code, troubleshoot issues, run commands, and handle automation tasks.

**cc-connect already supports Kimi CLI. Try the Kimi Code subscription, or use the API through the Kimi Open Platform (ไธญๆ–‡็ซ™ | Global).


Thanks to APINEBULA for sponsoring this project! APINEBULA, an enterprise-grade AI aggregation platform under Galaxy Video Bureau, leverages extensive platform resources to provide developers, teams, and enterprises with stable, cost-effective access to large language model APIs. The platform integrates leading, full-powered models like Claude, GPT, and Geminiโ€”allowing you to connect to the world's top AI models through a single API, with prices starting as low as 10% of the original cost. Designed for AI programming, Agent development, and business system integration, APINEBULA supports enterprise-grade high concurrency, formal contracts, corporate bank transfers, and invoicing services. APINEBULA provides special discounts for our software users: register using this link and enter the "ccconnect" promo code during first recharge to get 10% off.
Thanks to Qiniu Cloud AI for sponsoring this project! Qiniu Cloud (HK:02567) is an enterprise-grade LLM MaaS platform offering one-stop access to 150+ leading global models with full protocol compatibility across all major providers. It covers text, image, audio, video, and file processing across all modalities, serving over 1.69 million enterprise and developer users worldwide. Exclusive offer for cc-connect users: enterprise customers get 12 million free tokens, and refer friends to earn up to billions of tokens in bonus.
Thanks to Fenno.ai for sponsoring this project! Fenno.ai is a stable, efficient API relay service currently providing Codex relay with full OpenAI and Anthropic protocol compatibility. It flexibly integrates with mainstream coding tools like Codex, Claude Code, and OpenCode, reliably supporting enterprise-level demands of hundreds of billions of tokens per day. Fenno.ai supports domestic and overseas entity-to-entity settlement and invoicing. Exclusive offer for cc-connect users: subscribe via this link for an unbeatable Coding Plan at ยฅ9.9 / $150 credit, and earn up to 20% bonus for each friend you refer.
Thanks to AIGoCode for sponsoring this project! AIGoCode is an all-in-one platform that integrates Claude Code, Codex, and the latest Gemini models, providing you with stable, efficient, and highly cost-effective AI coding services. The platform offers flexible subscription plans, zero risk of account suspension, direct access with no VPN required, and lightning-fast responses. AIGoCode has prepared a special benefit for cc-connect users: if you register via this link, you'll receive an extra 10% bonus credit on your first top-up!
Thanks to DMXAPI for sponsoring this project! DMXAPI provides global large model API services to 200+ enterprise users. One API key for all global models. Features include: instant invoicing, unlimited concurrency, starting from $0.15, 24/7 technical support. GPT/Claude/Gemini all at 32% off, domestic models 20-50% off, Claude Code exclusive models at 66% off! Register via this link.
Thanks to APIKEY.FUN for sponsoring this project! APIKEY.FUN is a professional enterprise-grade AI relay service, dedicated to providing stable, efficient, and cost-effective AI model API access for enterprises and individual developers. The platform supports Claude, OpenAI, Gemini and other mainstream models, with prices as low as 7% of official rates. Register via this link to enjoy an exclusive permanent 5% discount on all top-ups!
Thanks to Shengsuanyun for sponsoring this project! Shengsuanyun is a super factory dedicated to serving AI Native Teams, an industrial-grade AI task parallel execution platform, and a model marketplace that aggregates and supplies computing power from domestic and international LLM and image/video multimedia models such as Claude, Chatgpt, and Gemini. It guarantees no reverse engineering or data manipulation, boasts a 99.7% SLA availability across the entire site, and its monitoring interface is consistently green. Furthermore, it offers an enterprise-grade customized gateway for refined cost and access control, featuring intelligent routing, security protection, and BYOK enterprise-provided key hosting. The platform is billed on a pay-as-you-go basis and with a tokens plan (coming soon), and invoices are available. New users who register using this link will receive 10 yuan in model power and a 10% bonus on their first deposit.
Thanks to VisionCoder for supporting this project. VisionCoder Developer Platform is a reliable and efficient API relay service provider, offering access to mainstream AI models such as Claude Code, Codex, and Gemini. It helps developers and teams integrate AI capabilities more easily and improve productivity. Additionally, VisionCoder now offers retail channels for Claude Max 200 and GPT Pro 200 premium accounts, providing users with instant access to top-tier AI computing power and features.
Thanks to RunAPI for sponsoring this project! RunAPI is an efficient and stable API platformโ€”an alternative to OpenRouter. A single API Key gives you access to 150+ leading models, including OpenAI, Claude, Gemini, DeepSeek, Grok, and more, at prices as low as 10% of the original (up to 90% off), with exceptional stability. It's seamlessly compatible with tools like Claude Code, OpenClaw, and others. RunAPI offers an exclusive perk for cc-connect users: register and contact an administrator to claim ยฅ7 in free credit.
Thanks to CaMeL for sponsoring this project! In-depth cooperation with major research institutes and supercomputing centers, self-developed high-stability high-efficiency cache scheduling solution. Exclusive for cc-connect users: New registration authentication immediately receive $10 credit upon registration. Register via this link.
Thanks to Unity2.ai for sponsoring this project! Unity2.ai is a high-performance AI model API relay platform for individual developers, teams, and enterprises. Long-term service for top domestic enterprises, daily traffic exceeding 30 billion tokens, supporting up to 5000 RPM high concurrency. Offers balance billing, first-top-up bonus, combo subscriptions, enterprise invoicing, and dedicated integration. Register via this link to claim $2 credit, join the official group for an additional $10 credit โ€” up to $12 in free credits!
Thanks to Ergou API (ไบŒ็‹— API) for sponsoring this project! Ergou API is an LLM API relay offering industry-low 0.1x-0.2x rates across the board, with 100% native endpoints for Claude, GPT, Gemini and other top-tier LLMs. Backed by premium IPLC routes plus dual residential ISP redundancy, Ergou API delivers stable, low-latency access nationwide. Developers and studios are welcome to sign up.
Thanks to AnyRoute.io for sponsoring this project! AnyRoute.io is a reliable, stable, and efficient API relay platform integrating the latest Claude Code and Codex models. Transparent pricing with rates as low as 93% off official prices (just 0.7x), supports invoicing and enterprise-grade high-concurrency usage. Register via this link to get started.
Thanks to aicanapi.com for sponsoring this project! Aican API provides high-performance, low-latency, high-concurrency API services for enterprises and developers. Claude Code models at up to 84% off, other models at 80% off official price. Doubao Seedance 2 real-person generation service with queue-free access for faster responses. Choose Aican API for simpler, more efficient, and more cost-effective enterprise-grade AI services. Register via this link to get started.
Thanks to Pateway for sponsoring this project! PatewayAI is a premium API relay service for serious AI developers, offering 100% official direct access to Claude and Codex models โ€” no reverse engineering, no quality degradation. Transparent billing with token-level verification. Enterprise-grade concurrency, formal contracts and invoicing available. Register via this link to get $3 free trial credit, up to 40% off on top-ups, and referral rewards up to $150!
Thanks to 10็‚นAI for sponsoring this project! 10dian-AI Enterprise Platform is an AI API gateway for developers and enterprises, aggregating GPT, Claude, Gemini, DeepSeek and more. Optimized for production environments with stable high-concurrency operation, avoiding interface jitter and timeout issues. Affordable pricing, stable uptime, official guarantee. Register via this link to get ยฅ5 free credit!
Thanks to SiliconFlow for supporting this project! SiliconFlow is a high-performance AI infrastructure and model API platform, providing fast and reliable access to language, speech, image, and video models in one place. With pay-as-you-go billing, broad multimodal model support, high-speed inference, and enterprise-grade stability, SiliconFlow helps developers and teams build and scale AI applications more efficiently. Register via this link and complete real-name verification to receive ยฅ20 in bonus credit!
Thanks to ไผ˜ไบ‘ๆ™บ็ฎ— for sponsoring this project! ไผ˜ไบ‘ๆ™บ็ฎ— (UCloud AI Cloud Platform) provides stable and comprehensive domestic and international model APIs with just one key. Featuring high-value Coding Plan packages (monthly or per-use), plus stable official relay for overseas models. Supports Claude Code, Codex, and API calls. Enterprise features include high concurrency, 7x24 technical support, and self-service invoicing. Register via this link to receive ยฅ5 free platform credit!
Thanks to DragonCode for supporting this project. DragonCode has prepared a special benefit for cc-connect users: register via this link to get started.
Thanks to Code0 for sponsoring this project! Code0 is an AI model aggregation API relay service for Chinese developers, compatible with OpenAI / Anthropic / Gemini protocols. One key for all mainstream models, stable support for Claude Code, Codex, Gemini CLI, cc-connect and more. Fixed exchange rate: ยฅ1.5 CNY = $1 USD API credit, transparent pricing, domestic direct connection, ready to use. Register via this link.
Thanks to claudeapi.com for sponsoring this project! claudeapi is a high-quality direct Claude connection service for mid-to-high-end users. It is fully integrated with Anthropic's official first-party Keys and AWS Bedrock official channels โ€” no reverse engineering, no intelligence degradation, no stitching. It fully preserves the official capabilities, long context, and tool-calling performance of Opus / Sonnet / Haiku. Designed specifically for Claude Code power users, Agent developers, and enterprise teams, it focuses on out-of-the-box usability and enterprise-grade stability. Invoicing and team onboarding are supported. Register via this link.

๐Ÿ†• Whatโ€™s New in v1.3.3

First stable of the 1.3.3 series โ€” stabilizes beta.1 โ†’ beta.5 (โ‰ˆ 235 PRs since v1.3.2) plus 7 post-beta fixes. Highlights:

  • New agents โ€” Devin CLI, Google Antigravity (agy), GitHub Copilot CLI as first-class agents (#672, #1123, #865). Hardened Cursor / OpenCode / Qoder / Kimi / Pi coverage.
  • Platform expansion โ€” QQ (OneBot) file send & receive (#323), QQ Bot inline keyboards (#1131), WeCom SendFile in WebSocket (#1199), Feishu audio + video native media (#1202), Slack Assistant API (#844), MAX webhook delivery (#818), DingTalk @mentions / richText / image / file inbound (#1188, #828, #1357), broader Weibo DM, WPS Xiezuo (้‡‘ๅฑฑๅไฝœ).
  • Long-running turn hardening โ€” new max_turn_time_mins wall-clock cap with soft-stop + force-kill + auto-resume so a long bash / test command can no longer lock a session indefinitely (#1091).
  • New core commands โ€” /timer (one-shot delayed task), /cancel (interrupt current turn), /ps (replaces /btw, kept as alias), cron add --silent, agent-driven TTS.
  • Multi-user / permissions โ€” reply-to-unauthorized-IM-senders option, @Bot/permit โ‰ก /permit keyword matching, Bridge requires token when enabled.
  • Provider ecosystem โ€” NekoCode, VisionCoder, AIHubMix, MiniMax M3 presets; Claude Code 1M-context Opus + append_system_prompt + PermissionRequest hooks; Codex request_user_input app-server events; configurable shell + shell profile for exec.
  • Observability โ€” blackbox testing framework (P0/P1/P2 + config-switch matrix), CUJ test framework, provider-resume regression suite for codex/opencode/kimi, Pi context-usage reporter in reply footer.

โš ๏ธ Behavior changes (action may be required): Telegram/Discord progress_style defaults to compact (set legacy to revert); QQ Bot default intents now include INTERACTION_CREATE (custom values must include 1<<26); DingTalk msgtype=file inbound now reaches the agent; engine permission keywords are @mention-tolerant; reset_on_idle_mins defaults to 30 min; Bridge with no token configured refuses to start. See changelogs/v1.3.3.md for the full themed summary.

No breaking changes. Coming from a v1.3.3-beta.*, this is a small fix-only upgrade.

๐Ÿงฉ Platform feature snapshot

High-level view of what each built-in platform can do in cc-connect.

Legend

Symbol Meaning
โœ… Works in stable cc-connect with typical configuration
โš ๏ธ Partial, needs extra config (e.g. speech / ASR), or limited by the vendor app or API
โŒ Not supported or not applicable in practice

โ€  QQ (NapCat / OneBot) โ€” unofficial self-hosted bridge; behaviour depends on your NapCat / network setup.

Capability Feishu WPS Xiezuo DingTalk Telegram Slack Discord LINE WeCom Weibo Weixin(personal) QQโ€  QQ Bot Matrix
Text & slash commands โœ… โœ… โœ… โœ… โœ… โœ… โœ… โœ… โœ… โœ… โœ… โœ… โœ…
Markdown / cards โœ… โœ… โœ… โœ… โœ… โœ… โš ๏ธ โš ๏ธ โŒ โœ… โœ… โœ… โš ๏ธ
Streaming / chunked replies โœ… โœ… โœ… โœ… โœ… โœ… โœ… โœ… โœ… โœ… โœ… โœ… โœ…
Images & files โœ… โŒ โœ… โœ… โœ… โœ… โš ๏ธ โœ… โŒ โœ… โœ… โœ… โœ…
Voice / STT / TTS โš ๏ธ โŒ โš ๏ธ โœ… โš ๏ธ โš ๏ธ โŒ โš ๏ธ โŒ โœ… โš ๏ธ โš ๏ธ โŒ
Private (DM) โœ… โœ… โœ… โœ… โœ… โœ… โœ… โœ… โœ… โœ… โœ… โœ… โœ…
Group / channel โœ… โœ… โœ… โœ… โœ… โœ… โš ๏ธ โœ… โŒ โœ… โœ… โœ… โœ…

WeCom: Webhook mode needs a public URL; long-connection / WS style setups often do not.
Voice row: many platforms need [speech] / TTS providers enabled in config.toml; values are a best-effort summary.
Per-platform setup: Platform setup guides below.

โœจ Why cc-connect?

๐Ÿค– Universal Agent Support

10+ AI Agents โ€” Claude Code, Codex, Cursor Agent, Kimi CLI, Qoder CLI, Gemini CLI, OpenCode, iFlow CLI, Pi, Devin, Copilot โ€” plus any agent that supports the Agent Client Protocol (ACP). Use whichever fits your workflow, or all of them at once.

๐Ÿ“ฑ Platform Flexibility

13 Chat Platforms โ€” Feishu, WPS Xiezuo, DingTalk, Slack, Telegram, Discord, WeChat Work, Weibo, LINE, QQ, QQ Bot (Official), Matrix, plus Weixin (personal ilink) for personal WeChat. Most platforms need zero public IP.

๐Ÿ”„ Multi-Agent Orchestration

Multi-Bot Relay โ€” Bind multiple bots in a group chat and let them communicate with each other. Ask Claude, get insights from Gemini โ€” all in one conversation.

๐ŸŽฎ Complete Chat Control

Full Control from Chat โ€” Switch models (/model), tune reasoning (/reasoning), change permission modes (/mode), manage sessions, all via slash commands.

Directory Switching in Chat โ€” Change where the next session starts with /dir (and /cd as a compatibility alias), plus quick history jump via /dir <number> / /dir -.

๐Ÿง  Persistent Memory

Agent Memory โ€” Read and write agent instruction files (/memory) without touching the terminal.

โฐ Intelligent Scheduling

Scheduled Tasks โ€” Set up cron jobs in natural language. "Every day at 6am, summarize GitHub trending" just works.

๐ŸŽค Multimodal Support

Voice & Images โ€” Send voice messages or screenshots; cc-connect handles STT/TTS and multimodal forwarding.

๐Ÿ“ฆ Multi-Project Architecture

Multi-Project โ€” One process, multiple projects, each with its own agent + platform combo.

๐ŸŒ Multilingual Interface

5 Languages โ€” Native support for English, Chinese (Simplified & Traditional), Japanese, and Spanish. Built-in i18n ensures everyone feels at home.

๐Ÿ“‹ Prerequisites

Install in this exact order โ€” cc-connect is a bridge for local AI coding agents, so the agent CLI must be installed and authenticated before cc-connect starts. Skipping ahead will cause cc-connect to exit with claudecode: claude CLI not found in PATH (or similar for your chosen agent), and the Web UI on :9820 will never come up.

1๏ธโƒฃ Install your AI Agent CLI

Pick the agent you want to bridge. You need at least one.

bash
# Claude Code (most common)
brew install --cask claude-code            # macOS / Linux Homebrew
# or
npm install @anthropic-ai/claude-code   # any platform via npm

# OpenAI Codex
npm install @openai/codex

# Google Gemini CLI
npm install @google/gemini-cli

# iFlow CLI
npm install @iflow-ai/iflow-cli

# Qoder CLI
curl -fsSL https://qoder.com/install | bash

For Cursor Agent and OpenCode, follow the official install pages:

Verify the binary is on your PATH:

bash
claude --version       # or: codex / gemini / opencode / qodercli / cursor-agent ...

2๏ธโƒฃ Authenticate the agent

Each agent has its own login flow โ€” run the agent once interactively so it stores credentials in your home directory:

bash
claude login           # opens a browser to authenticate
# or
codex login            # /gemini / opencode auth โ€” see the agent's docs

If you skip this step, cc-connect will still start, but the agent will reject every prompt with an auth error.

3๏ธโƒฃ Install cc-connect

bash
# npm (any platform)
npm install cc-connect

# Homebrew (macOS / Linux)
brew install cc-connect

# Or download a binary from https://github.com/chenhg5/cc-connect/releases

4๏ธโƒฃ Start cc-connect and open the Web UI

bash
cc-connect             # starts the service; first run auto-creates ~/.cc-connect/config.toml

On first launch, cc-connect prints something like:

text
Web admin:  http://localhost:9820

Open that URL in your browser. If 9820 is already in use, pass --web-port 9821 or set web_port in config.toml.

Note: cc-connect web only opens the browser and the config UI โ€” it does not start the service. You still need cc-connect running in another terminal.

5๏ธโƒฃ Configure platform bot tokens in the Web UI

In the Web UI, create a project, then add at least one platform (Feishu / Telegram / Discord / Slack / DingTalk / WeChat Work / QQ / LINE / Weixin) and paste the bot token from that platform's developer console. Save and cc-connect will hot-reload.

That's it โ€” send a message to your bot and cc-connect will relay it to your local agent.


๐Ÿš€ Quick Start

The easiest way โ€” Send this to Claude Code or any AI coding agent, and it will handle the entire installation and configuration for you:

bash
Follow https://raw.githubusercontent.com/chenhg5/cc-connect/refs/heads/main/INSTALL.md to install and configure cc-connect.

๐Ÿ“ฆ Manual Install

Via npm:

bash
npm install cc-connect

Via Homebrew (macOS / Linux):

bash
brew install cc-connect

Download binary from GitHub Releases:

bash
# Linux amd64 - Stable
curl -L -o cc-connect https://github.com/chenhg5/cc-connect/releases/latest/download/cc-connect-linux-amd64
chmod +x cc-connect
sudo mv cc-connect /usr/local/bin/

Build from source (requires Go 1.22+):

bash
git clone https://github.com/chenhg5/cc-connect.git
cd cc-connect
make build

โš™๏ธ Configure

๐Ÿ’ก Tip: Use the Web UI to configure โ€” After installing, run cc-connect web to configure the web admin and open the dashboard in your browser. You can visually create projects, add platforms, manage providers, and chat with your agent โ€” no need to manually edit TOML files. Note: cc-connect web only configures and opens the browser โ€” you still need to run cc-connect separately to start the service.

If you prefer manual configuration:

bash
mkdir -p ~/.cc-connect
cp config.example.toml ~/.cc-connect/config.toml
vim ~/.cc-connect/config.toml

Set admin_from = "alice,bob" in a project to allow those user IDs to run privileged commands such as /dir and /shell.
admin_from must be placed under [[projects]] (not under [projects.platforms.options]). You can use /whoami or /status to get your current User ID.
When a user runs /dir reset, cc-connect restores the configured work_dir and clears the persisted override stored under data_dir/projects/.state.json.

โ–ถ๏ธ Run

bash
./cc-connect

๐Ÿ”„ Upgrade

bash
# npm
npm install cc-connect

# Homebrew
brew upgrade cc-connect

# Binary self-update
cc-connect update           # Stable
cc-connect update --pre     # Include pre-releases

๐Ÿ“Š Support Matrix

Component Type Status
Agent Claude Code โœ… Supported
Agent Codex (OpenAI) โœ… Supported
Agent Cursor Agent โœ… Supported
Agent Gemini CLI (Google) โœ… Supported
Agent Qoder CLI โœ… Supported
Agent OpenCode (Crush) โœ… Supported
Agent iFlow CLI โœ… Supported
Agent Kimi CLI (Moonshot) โœ… Supported
Agent Pi (Cursor Background Agent) โœ… Supported
Agent Copilot (GitHub) โœ… Supported
Agent ACP (Agent Client Protocol) โœ… Any ACP-compatible agent
Agent Devin (Cognition) โœ… Supported (via ACP)
Agent Goose (Block) ๐Ÿ”œ Planned
Agent Aider ๐Ÿ”œ Planned
Platform Feishu (Lark) โœ… WebSocket โ€” no public IP needed
Platform DingTalk โœ… Stream โ€” no public IP needed
Platform WPS Xiezuo โœ… WebSocket โ€” no public IP needed
Platform Telegram โœ… Long Polling โ€” no public IP needed
Platform Slack โœ… Socket Mode โ€” no public IP needed
Platform Discord โœ… Gateway โ€” no public IP needed
Platform Weibo โœ… WebSocket โ€” no public IP needed
Platform LINE โœ… Webhook โ€” public URL required
Platform WeChat Work โœ… WebSocket / Webhook
Platform Weixin (personal, ilink) โœ…โ€” HTTP long polling โ€” no public IP needed
Platform QQ (NapCat/OneBot) โœ… WebSocket
Platform QQ Bot (Official) โœ… WebSocket โ€” no public IP needed
Platform Matrix โœ… Long Polling (/sync) โ€” no public IP needed

๐Ÿ“– Platform Setup Guides

Platform Guide Connection Public IP?
Feishu (Lark) docs/feishu.md WebSocket No
DingTalk docs/dingtalk.md Stream No
WPS Xiezuo docs/wps-xiezuo.md WebSocket No
Telegram docs/telegram.md Long Polling No
Slack docs/slack.md Socket Mode No
Google Chat docs/googlechat.md Cloud Pub/Sub No
Discord docs/discord.md Gateway No
Weibo docs/weibo.md WebSocket No
WeChat Work docs/wecom.md WebSocket / Webhook No (WS) / Yes (Webhook)
Weixin (personal) docs/weixin.md HTTP long polling (ilink) No
QQ / QQ Bot docs/qq.md WebSocket No
Matrix docs/matrix.md /sync (Long Polling) No

๐ŸŽฏ Key Features

๐Ÿ’ฌ Session Management

text
/new [name]       Start a new session
/list             List all sessions
/switch       Switch session
/current          Show current session
/dir [path|reset] Show, switch, or reset work directory

Project configs rotate to a fresh session automatically after long inactivity. This prevents "context drift" where stale chat history (failed commands, debugging noise) is repeatedly re-ingested via --continue and starts to dominate the model's attention. The previous session is preserved and remains accessible via /list and /switch.

toml
[[projects]]
reset_on_idle_mins = 30   # default when unset; set to 0 to disable

The default is 30 minutes when unset. Set reset_on_idle_mins = 0 to opt out and always continue the previous session.

๐Ÿ›ก๏ธ OS-User Isolation (`run_as_user`)

On Linux/macOS, a project can spawn its agent under a different Unix
user for OS-level file-system isolation from the supervisor user that
runs cc-connect. Currently supported by Claude Code.

toml
[[projects]]
name = "claude-sandboxed"
run_as_user = "partseeker-coder"
run_as_env = ["PGSSLROOTCERT"]

The target user needs passwordless sudo from the supervisor, no sudo
of its own, read+write on work_dir, and its own ~/.claude/settings.json
with whatever credentials the agent uses. If you authenticate via
claude.ai OAuth, symlink the target user's ~/.claude/.credentials.json
to the supervisor's copy so token refresh stays in sync โ€” see the
environment propagation checklist
for details. See
docs/usage.md
for the full setup.

Before starting cc-connect, audit the setup with:

bash
cc-connect doctor user-isolation

This runs three go/no-go preflight gates and an isolation probe that
reports what the target user can and cannot read. cc-connect refuses to
start if any gate fails or if the probe detects a cross-user leak.


๐Ÿ” Permission Modes

text
/mode             Show available modes
/mode yolo        # Auto-approve all tools
/mode default     # Ask for each tool

๐Ÿ”„ Provider Management

text
/provider list              List providers
/provider switch <name>     Switch API provider at runtime

๐Ÿค– Model Selection

text
/model                      List available models (format: alias - model)
/model switch <alias>       Switch to model by alias

๐Ÿ“‚ Work Directory

text
/dir                         Show current work directory and history
/dir                   Switch to a path (relative or absolute)
/dir <number>                Switch from history
/dir -                       Switch to previous directory
/cd                    Compatibility alias for /dir

โฐ Scheduled Tasks

bash
/cron add 0 6 * * * Summarize GitHub trending

๐Ÿ“Ž Agent Attachment Send-Back

When an agent generates a local screenshot, chart, PDF, bundle, or other file, it can send that attachment back to the current chat.

First release supports:

  • Feishu
  • Telegram

If your agent does not natively inject the system prompt, run this once in chat after upgrading:

text
/bind setup

or:

text
/cron setup

This refreshes the cc-connect instructions in the project memory file so the agent knows how to send attachments back.

You can control this feature globally in config.toml:

toml
attachment_send = "on"  # default: "on"; set to "off" to block image/file send-back

This switch is independent from the agent's /mode. It only controls cc-connect send --image/--file. Voice send-back uses the TTS config instead.

Examples:

bash
cc-connect send --image /absolute/path/to/chart.png
cc-connect send --file /absolute/path/to/report.pdf
cc-connect send --file /absolute/path/to/report.pdf --image /absolute/path/to/chart.png
cc-connect send --tts "Hello from cc-connect"

Notes:

  • Absolute paths are the safest option.
  • --image and --file can both be repeated.
  • --tts sends synthesized speech when the user asks for a voice reply.
  • attachment_send = "off" disables only attachment send-back; ordinary text replies still work.
  • Attachments are capped at 50 MiB by default; configure with max_attachment_size_mb (or CC_MAX_ATTACHMENT_SIZE_MB env, same MiB unit).
  • This command is for generated attachments, not ordinary text replies.

๐Ÿ“– Full documentation: docs/usage.md

๐Ÿ“š Documentation

๐Ÿ‘ฅ Community

โ˜• Support the Project

If cc-connect has been helpful to you, consider buying us a coffee! Your support helps us:

  • ๐Ÿ› ๏ธ Maintain and improve the project
  • ๐Ÿ“š Write better documentation and tutorials
  • ๐Ÿ› Fix bugs and add new features faster
  • โ˜• Keep the developers caffeinated

How to Donate

Buy Me a Coffee: https://buymeacoffee.com/cg33

WeChat Pay / Alipay:

WeChat Pay Alipay

Thank You, Donors! ๐ŸŽ‰

We're grateful to everyone who has supported this project. Leave your GitHub username in the donation message if you'd like to be recognized here!

Avatar GitHub Username Date
@thx0701 2026-04-29

๐Ÿค Commercial Cooperation

We accept the following commercial collaborations:

  • Enterprise Customization: Custom deployment for internal AI tooling (Feishu, DingTalk, WeChat Work, Slack, etc.)
  • Technical Consulting: AI agent integration and architecture design
  • Outsourcing Projects: AI-related system development

Contact: Email: [email protected] | WeChat: mongorz | Telegram | Discord

๐Ÿ™ Contributors

โญ Star History

๐Ÿ“„ License

MIT License


File: docs/images/sponsors/README.md

Sponsors Images

This directory contains sponsor logos for the README sponsor section.

Naming Convention

  • Use lowercase: sponsor-name.png or sponsor-name.jpg
  • Recommended size: 150x50 px (logo), 150x150 px (square)

Adding a Sponsor

  1. Add logo image to this directory
  2. Update README.md and README.zh-CN.md sponsor table
  3. Include sponsor's affiliate link and discount details

Placeholder

Replace your-logo-here.png with actual sponsor logos.


File: docs/plans/2026-03-11-delete-batch.md

Delete Batch Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Add explicit batch deletion support for /delete 1,2,3, /delete 3-7, and /delete 1,3-5,8 without introducing ambiguous whitespace-based parsing.

Architecture: Keep the existing single-delete flow intact for one plain argument, and add a narrow parser that only activates when /delete receives one argument containing comma/range syntax. Resolve list positions from a single session snapshot, deduplicate targets, then execute deletions with a combined reply that reports successes and blocked items.

Tech Stack: Go 1.24, existing core.Engine command handlers, testing package.


Task 1: Add failing command tests

Files:

  • Modify: core/engine_test.go

Step 1: Write the failing test

Add command-level tests for:

  • /delete 1,2,3
  • /delete 3-7
  • /delete 1,3-5,8
  • invalid explicit syntax like /delete 1,3-a,8
  • non-supported whitespace-separated args staying non-batch

Step 2: Run test to verify it fails

Run: go test ./core -run TestCmdDelete
Expected: FAIL because batch parsing does not exist yet.

Task 2: Implement explicit batch delete parsing

Files:

  • Modify: core/engine.go
  • Modify: core/i18n.go

Step 1: Write minimal implementation

Add a helper that:

  • only recognizes one-argument explicit batch syntax
  • parses comma-separated integers and inclusive ranges
  • rejects malformed items
  • deduplicates indices while preserving order

Update cmdDelete to:

  • route explicit batch syntax through the new helper
  • keep existing single-delete behavior for plain one-argument input
  • reject ambiguous multi-argument inputs with usage text
  • aggregate batch results into one reply

Step 2: Run targeted tests

Run: go test ./core -run TestCmdDelete
Expected: PASS

Task 3: Verify no regression in core tests

Files:

  • Test: core/engine_test.go
  • Test: core/i18n_test.go

Step 1: Run broader verification

Run: go test ./core/...
Expected: PASS


File: docs/plans/2026-03-11-feishu-delete-card.md

Feishu Delete Card Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Add a Feishu card-based multi-select delete flow triggered by /delete without changing existing explicit batch delete command semantics.

Architecture: Introduce a per-session delete-mode state keyed by sessionKey, render a dedicated delete-selection card and confirmation card, and route Feishu card act: callbacks through new delete-mode actions. Keep /list unchanged and keep explicit /delete <args> command execution on the existing code path.

Tech Stack: Go 1.24, core.Engine, shared card builder utilities, Feishu interactive cards, Go testing.


Task 1: Add failing tests for delete-mode card flow

Files:

  • Modify: core/engine_test.go

Step 1: Write the failing test

Add tests for:

  • /delete with no args on a card-capable platform renders delete-mode card instead of usage text
  • delete-mode list toggles selected session IDs through card actions
  • confirmation card lists selected sessions
  • submit deletes only selected sessions and clears delete-mode state
  • cancel returns to normal list/current view and clears state

Step 2: Run test to verify it fails

Run: go test ./core -run 'TestCmdDelete|TestDeleteMode'
Expected: FAIL because delete-mode state and card actions do not exist.

Step 3: Write minimal implementation

Add the smallest delete-mode state and rendering hooks required for the first test to pass before expanding behavior.

Step 4: Run test to verify it passes

Run: go test ./core -run 'TestCmdDelete|TestDeleteMode'
Expected: PASS for the implemented slice.

Step 5: Commit

bash
git add core/engine_test.go core/engine.go core/i18n.go
git commit -m "feat: add delete mode card flow scaffolding"

Task 2: Implement delete-mode state and card rendering

Files:

  • Modify: core/engine.go
  • Modify: core/i18n.go
  • Test: core/engine_test.go

Step 1: Write the failing test

Add tests covering:

  • selected rows remain selected across pagination
  • delete-mode footer buttons enable confirmation only when selection exists
  • confirmation card back button preserves selection

Step 2: Run test to verify it fails

Run: go test ./core -run 'TestDeleteMode'
Expected: FAIL on missing state transitions or incorrect card rendering.

Step 3: Write minimal implementation

Implement:

  • delete-mode state structure
  • render function for delete-mode list
  • render function for confirmation/result cards
  • helper to resolve display names from selected session IDs

Step 4: Run test to verify it passes

Run: go test ./core -run 'TestDeleteMode'
Expected: PASS

Step 5: Commit

bash
git add core/engine.go core/engine_test.go core/i18n.go
git commit -m "feat: render delete selection cards"

Task 3: Wire Feishu card actions to delete-mode behavior

Files:

  • Modify: core/engine.go
  • Test: core/engine_test.go
  • Inspect: platform/feishu/feishu.go

Step 1: Write the failing test

Add tests for:

  • act:/delete-mode toggle
  • act:/delete-mode confirm
  • act:/delete-mode back
  • act:/delete-mode submit
  • act:/delete-mode cancel

Step 2: Run test to verify it fails

Run: go test ./core -run 'TestDeleteMode'
Expected: FAIL because action dispatch does not recognize delete-mode actions.

Step 3: Write minimal implementation

Update handleCardNav and executeCardAction to:

  • mutate delete-mode state in place
  • re-render the correct card after each action
  • clear state on submit/cancel

Step 4: Run test to verify it passes

Run: go test ./core -run 'TestDeleteMode'
Expected: PASS

Step 5: Commit

bash
git add core/engine.go core/engine_test.go
git commit -m "feat: wire delete mode card actions"

Task 4: Reuse deletion logic and protect edge cases

Files:

  • Modify: core/engine.go
  • Test: core/engine_test.go

Step 1: Write the failing test

Add tests for:

  • active session in selected set is blocked and reported
  • missing session ID in selected set is reported without aborting the whole batch
  • explicit /delete 1,2,3 and /delete 1,3-5,8 still use command parsing path

Step 2: Run test to verify it fails

Run: go test ./core -run 'TestCmdDelete|TestDeleteMode'
Expected: FAIL on batch execution/reporting edge cases.

Step 3: Write minimal implementation

Refactor deletion helpers so card-mode submit can delete by selected session ID while sharing reply/result formatting with the command path.

Step 4: Run test to verify it passes

Run: go test ./core -run 'TestCmdDelete|TestDeleteMode'
Expected: PASS

Step 5: Commit

bash
git add core/engine.go core/engine_test.go
git commit -m "feat: execute delete mode batch removal"

Task 5: Run broader verification

Files:

  • Test: core/engine_test.go
  • Test: platform/feishu/platform_test.go

Step 1: Run core verification

Run: go test ./core/...
Expected: PASS

Step 2: Run repository verification

Run: go test ./...
Expected: PASS


File: docs/plans/2026-03-11-feishu-delete-card-design.md

Feishu Delete Card Design

Date: 2026-03-11

Goal: Let Feishu users enter /delete with no arguments to open a card-based multi-select delete flow, while keeping explicit delete arguments such as /delete 1,2,3 working as direct command execution.

Scope

  • Only /delete with no arguments activates card selection mode.
  • The normal /list card remains unchanged.
  • Explicit delete arguments continue to bypass the card flow.
  • Card flow is only for card-capable platforms; non-card platforms keep usage text.

Interaction Flow

  1. User sends /delete.
  2. If the platform supports cards, cc-connect renders a delete-mode session list card.
  3. Each session row exposes a single right-side button that toggles selected/unselected state.
  4. The card footer provides:
    • ๅˆ ้™คๅทฒ้€‰
    • ๅ–ๆถˆ
    • pagination controls
  5. ๅˆ ้™คๅทฒ้€‰ opens a confirmation card listing the selected sessions.
  6. The confirmation card provides:
    • ็กฎ่ฎคๅˆ ้™ค
    • ่ฟ”ๅ›ž็ปง็ปญ้€‰ๆ‹ฉ
  7. On confirmation, cc-connect deletes the selected sessions, skips the active session, clears the temporary selection state, and renders a result card.

State Model

Delete-mode state should be tracked per sessionKey, separate from the agent interactive process state. The minimum state needed is:

  • whether delete mode is active
  • current page in delete mode
  • selected session IDs
  • whether the user is currently on the confirmation card

Session IDs, not row numbers, must be the source of truth after selection, so cross-page selection remains stable even if list ordering changes between renders.

Card Actions

  • act:/delete-mode open
  • act:/delete-mode toggle <session-id>
  • act:/delete-mode page <n>
  • act:/delete-mode confirm
  • act:/delete-mode back
  • act:/delete-mode submit
  • act:/delete-mode cancel

All delete-mode actions should update the card in place on Feishu. They should not dispatch as plain user commands.

Error Handling

  • Empty selection cannot proceed to confirmation; the card should stay in delete mode with a hint.
  • Deleting the active session must be reported as blocked, not silently ignored.
  • If a selected session no longer exists, report it in the result card rather than failing the whole batch.
  • Cancel must always clear delete-mode state.

Testing

  • Rendering delete-mode cards with selected/unselected rows
  • In-place toggle behavior and page persistence
  • Confirmation card content
  • Submit path deleting only selected session IDs
  • Active-session protection in card-driven batch delete
  • Explicit /delete 1,3-5,8 continuing to work outside card mode

File: docs/plans/2026-03-12-multi-workspace-design.md

Multi-Workspace Feature Design

Overview

Enable a single cc-connect bot (one Slack token) to serve multiple workspaces, with the channel determining which Claude Code working directory and session to use.

Config

toml
[[projects]]
name = "claude"
mode = "multi-workspace"
base_dir = "~/workspace"

[projects.agent]
type = "claudecode"
permission_mode = "yolo"

[[projects.platforms]]
type = "slack"
bot_token = "xoxb-..."
app_token = "xapp-..."
  • mode = "multi-workspace" enables the feature. Omitting or "single" preserves current behavior.
  • base_dir is the parent directory where workspaces live. Replaces work_dir on the agent.
  • Agent config has no work_dir โ€” resolved per-channel at runtime.

Workspace Resolution Flow

When a message arrives in a channel:

  1. Check bindings โ€” look up workspace_bindings.json for an existing channel-to-workspace mapping.
  2. Convention match โ€” if no binding, check if /<channel-name>/ exists. If yes, auto-bind and confirm:

    "Found ~/workspace/model-profiler matching this channel. Binding workspace and starting session... Ready."

  3. Ask for repo โ€” if no match, reply:

    "No workspace found for this channel. What repo should I clone?"
    User provides URL, bot confirms:
    "I'll clone org/repo to ~/workspace/repo-name and bind to this channel. OK?"

  4. Clone and bind โ€” on confirmation, clone the repo, save the binding, spawn agent subprocess. Explicit feedback throughout:

    "Cloning github.com/org/repo to ~/workspace/repo-name..."
    "Clone complete. Binding workspace to this channel... Ready."

Binding Storage

Persisted in ~/.cc-connect/workspace_bindings.json:

json
{
  "project:claude": {
    "C0AKYKUF75K": {
      "channel_name": "model-profiler",
      "workspace": "/home/leigh/workspace/model-profiler",
      "bound_at": "2026-03-12T10:00:00Z"
    }
  }
}

Agent Subprocess Management

Engine maintains workspaceAgents map[string]*workspaceState keyed by workspace path. Each workspaceState holds the agent subprocess, its SessionManager, and a lastActivity timestamp.

Lifecycle

  1. Spawn on first message โ€” start a Claude Code subprocess with work_dir set to the resolved workspace.
  2. Resume on subsequent messages โ€” reuse the running subprocess with saved session ID.
  3. Idle reap โ€” background goroutine checks lastActivity every minute. Subprocesses idle >15 minutes are stopped. Session ID is preserved so the next message transparently restarts.
  4. Graceful shutdown โ€” on bot shutdown, stop all subprocesses cleanly.

Session Management

Each workspace gets its own SessionManager instance with a separate JSON file (same naming scheme as today: project_hash.json). Named sessions within a workspace work exactly as they do now.

Message Routing Changes

In Engine.handleMessage, the multi-workspace path inserts before the existing flow:

  1. Extract channel ID from the message's session key (slack:channelID:userID).
  2. Resolve workspace โ€” look up binding, convention match, or trigger init flow.
  3. If no workspace resolved (init flow in progress) โ€” handle the init conversation directly, don't forward to any agent.
  4. If workspace resolved โ€” get or spawn the agent subprocess for that workspace, then continue with existing message processing.

interactiveStates gets keyed by workspace+sessionKey (rather than just sessionKey) so the same user in different channels hits different agent processes.

New Commands

  • /workspace โ€” show current channel's bound workspace
  • /workspace init โ€” clone and bind
  • /workspace unbind โ€” remove binding
  • /workspace list โ€” show all bindings

Existing commands (/sessions, /model, etc.) work per-workspace.

Error Handling & Edge Cases

  • Unbound channel, bot mentioned โ€” bot asks for repo URL. No agent forwarding until binding is established.
  • Clone fails (bad URL, auth, disk) โ€” bot reports error and asks user to try again. No partial binding saved.
  • Workspace directory deleted externally โ€” on next message, bot detects missing directory, removes the binding, re-enters init flow: "Workspace ~/workspace/foo no longer exists. What repo should I clone?"
  • Agent subprocess crashes โ€” restart on next message using saved session ID (same as current behavior).
  • Bot in unwanted channel โ€” without binding or matching directory, it just asks for a repo. User can ignore or remove the bot.

Architecture: Approach 1 (Engine-level multiplexing)

The Engine itself handles multi-workspace routing. No new meta-engine or wrapper layers. The multi-workspace logic is gated behind the mode config field, so single-workspace projects are completely unaffected.


File: docs/plans/2026-03-12-multi-workspace-plan.md

Multi-Workspace Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task.

Goal: Enable a single cc-connect bot to serve multiple workspaces, routing messages to different Claude Code sessions based on Slack channel.

Architecture: Engine-level multiplexing. Add workspace resolution to Engine.handleMessage that maps channel โ†’ workspace directory, spawns/resumes per-workspace agent subprocesses, and manages idle reaping. Gated behind mode = "multi-workspace" in ProjectConfig so existing single-workspace projects are unaffected.

Tech Stack: Go, Slack API (conversations.info for channel name resolution), JSON file persistence

Design Doc: docs/plans/2026-03-12-multi-workspace-design.md


Task 1: Add config fields

Files:

  • Modify: config/config.go (ProjectConfig struct ~line 103, validate() ~line 177)
  • Modify: config.example.toml (add multi-workspace example)

Step 1: Add Mode and BaseDir to ProjectConfig

In config/config.go, add two fields to ProjectConfig (after line 105):

go
type ProjectConfig struct {
	Name             string           `toml:"name"`
	Mode             string           `toml:"mode,omitempty"`     // "" or "multi-workspace"
	BaseDir          string           `toml:"base_dir,omitempty"` // parent dir for workspaces
	Agent            AgentConfig      `toml:"agent"`
	Platforms        []PlatformConfig `toml:"platforms"`
	Quiet            *bool            `toml:"quiet,omitempty"`
	DisabledCommands []string         `toml:"disabled_commands,omitempty"`
}

Step 2: Add validation for multi-workspace mode

In config/config.go validate(), after the existing platform checks (~line 196), add:

go
if proj.Mode == "multi-workspace" {
	if proj.BaseDir == "" {
		return fmt.Errorf("project %q: multi-workspace mode requires base_dir", proj.Name)
	}
	if _, ok := proj.Agent.Options["work_dir"]; ok {
		return fmt.Errorf("project %q: multi-workspace mode conflicts with agent work_dir (use base_dir instead)", proj.Name)
	}
}

Step 3: Add example to config.example.toml

Add a commented multi-workspace example section showing the config pattern from the design doc.

Step 4: Run existing tests

Run: go build ./...
Expected: Compiles cleanly

Step 5: Commit

bash
git add config/config.go config.example.toml
git commit -m "feat: add multi-workspace mode and base_dir config fields"

Task 2: Workspace binding persistence

Files:

  • Create: core/workspace_binding.go
  • Create: core/workspace_binding_test.go

Step 1: Write tests for WorkspaceBindingManager

go
package core

import (
	"os"
	"path/filepath"
	"testing"
)

func TestWorkspaceBindingManager_SaveLoad(t *testing.T) {
	dir := t.TempDir()
	storePath := filepath.Join(dir, "bindings.json")

	mgr := NewWorkspaceBindingManager(storePath)
	mgr.Bind("project:claude", "C123", "my-channel", "/home/user/workspace/my-channel")

	b := mgr.Lookup("project:claude", "C123")
	if b == nil {
		t.Fatal("expected binding, got nil")
	}
	if b.ChannelName != "my-channel" {
		t.Errorf("expected channel name 'my-channel', got %q", b.ChannelName)
	}
	if b.Workspace != "/home/user/workspace/my-channel" {
		t.Errorf("expected workspace path, got %q", b.Workspace)
	}

	// Reload from disk
	mgr2 := NewWorkspaceBindingManager(storePath)
	b2 := mgr2.Lookup("project:claude", "C123")
	if b2 == nil {
		t.Fatal("expected binding after reload, got nil")
	}
	if b2.Workspace != "/home/user/workspace/my-channel" {
		t.Errorf("expected workspace path after reload, got %q", b2.Workspace)
	}
}

func TestWorkspaceBindingManager_Unbind(t *testing.T) {
	dir := t.TempDir()
	storePath := filepath.Join(dir, "bindings.json")

	mgr := NewWorkspaceBindingManager(storePath)
	mgr.Bind("project:claude", "C123", "chan", "/path")
	mgr.Unbind("project:claude", "C123")

	if b := mgr.Lookup("project:claude", "C123"); b != nil {
		t.Error("expected nil after unbind")
	}
}

func TestWorkspaceBindingManager_ListByProject(t *testing.T) {
	dir := t.TempDir()
	mgr := NewWorkspaceBindingManager(filepath.Join(dir, "bindings.json"))
	mgr.Bind("project:claude", "C1", "chan1", "/path1")
	mgr.Bind("project:claude", "C2", "chan2", "/path2")
	mgr.Bind("project:other", "C3", "chan3", "/path3")

	list := mgr.ListByProject("project:claude")
	if len(list) != 2 {
		t.Errorf("expected 2 bindings, got %d", len(list))
	}
}

Step 2: Run tests to verify they fail

Run: go test ./core/ -run TestWorkspaceBinding -v
Expected: FAIL (types not defined)

Step 3: Implement WorkspaceBindingManager

text
/* Detailed source-code truncated for AI context efficiency. */

Note: This follows the exact same pattern as core/relay.go RelayManager persistence.

Step 4: Run tests to verify they pass

Run: go test ./core/ -run TestWorkspaceBinding -v
Expected: PASS

Step 5: Commit

bash
git add core/workspace_binding.go core/workspace_binding_test.go
git commit -m "feat: add WorkspaceBindingManager for channel-to-workspace persistence"

Task 3: Workspace state and idle reaper

Files:

  • Create: core/workspace_state.go
  • Create: core/workspace_state_test.go

Step 1: Write tests for workspacePool

go
package core

import (
	"context"
	"testing"
	"time"
)

func TestWorkspacePool_GetOrCreate(t *testing.T) {
	pool := newWorkspacePool(15 * time.Minute)

	state1 := pool.GetOrCreate("/workspace/a")
	state2 := pool.GetOrCreate("/workspace/a")
	state3 := pool.GetOrCreate("/workspace/b")

	if state1 != state2 {
		t.Error("expected same state for same workspace")
	}
	if state1 == state3 {
		t.Error("expected different state for different workspace")
	}
}

func TestWorkspacePool_Touch(t *testing.T) {
	pool := newWorkspacePool(15 * time.Minute)
	state := pool.GetOrCreate("/workspace/a")

	before := state.LastActivity()
	time.Sleep(10 * time.Millisecond)
	state.Touch()
	after := state.LastActivity()

	if !after.After(before) {
		t.Error("expected lastActivity to advance after Touch()")
	}
}

func TestWorkspacePool_ReapIdle(t *testing.T) {
	pool := newWorkspacePool(50 * time.Millisecond)
	pool.GetOrCreate("/workspace/a")

	time.Sleep(100 * time.Millisecond)
	reaped := pool.ReapIdle()

	if len(reaped) != 1 || reaped[0] != "/workspace/a" {
		t.Errorf("expected [/workspace/a] reaped, got %v", reaped)
	}

	if s := pool.Get("/workspace/a"); s != nil {
		t.Error("expected workspace removed after reap")
	}
}

Step 2: Run tests to verify they fail

Run: go test ./core/ -run TestWorkspacePool -v
Expected: FAIL

Step 3: Implement workspacePool and workspaceState

text
/* Detailed source-code truncated for AI context efficiency. */

Step 4: Run tests to verify they pass

Run: go test ./core/ -run TestWorkspacePool -v
Expected: PASS

Step 5: Commit

bash
git add core/workspace_state.go core/workspace_state_test.go
git commit -m "feat: add workspacePool for managing per-workspace agent state"

Task 4: Channel name resolution in Slack platform

Files:

  • Modify: platform/slack/slack.go (~line 25 replyContext, ~line 102 handleEvent)
  • Modify: core/interfaces.go (Platform interface โ€” check if ChannelNameResolver needed)

Step 1: Add ChannelNameResolver interface

In core/interfaces.go, add a new optional interface:

go
// ChannelNameResolver is an optional interface for platforms that can resolve
// channel IDs to human-readable names.
type ChannelNameResolver interface {
	ResolveChannelName(channelID string) (string, error)
}

Step 2: Implement in Slack platform

In platform/slack/slack.go, add a channel name cache and resolver:

go
// Add to Platform struct:
channelNameCache map[string]string
channelCacheMu   sync.RWMutex

// Initialize in New() or Start():
p.channelNameCache = make(map[string]string)

// Add method:
func (p *Platform) ResolveChannelName(channelID string) (string, error) {
	p.channelCacheMu.RLock()
	if name, ok := p.channelNameCache[channelID]; ok {
		p.channelCacheMu.RUnlock()
		return name, nil
	}
	p.channelCacheMu.RUnlock()

	info, err := p.client.GetConversationInfo(&slack.GetConversationInfoInput{
		ChannelID: channelID,
	})
	if err != nil {
		return "", fmt.Errorf("slack: resolve channel name for %s: %w", channelID, err)
	}

	p.channelCacheMu.Lock()
	p.channelNameCache[channelID] = info.Name
	p.channelCacheMu.Unlock()

	return info.Name, nil
}

Step 3: Build to verify compilation

Run: go build ./...
Expected: Compiles cleanly

Step 4: Commit

bash
git add core/interfaces.go platform/slack/slack.go
git commit -m "feat: add ChannelNameResolver interface and Slack implementation"

Task 5: Engine multi-workspace fields and constructor

Files:

  • Modify: core/engine.go (Engine struct ~line 119, NewEngine ~line 200)

Step 1: Add multi-workspace fields to Engine struct

After eventIdleTimeout (~line 168), add:

go
// Multi-workspace mode
multiWorkspace    bool
baseDir           string
workspaceBindings *WorkspaceBindingManager
workspacePool     *workspacePool
initFlows         map[string]*workspaceInitFlow // channelID โ†’ init state
initFlowsMu       sync.Mutex

Add the init flow state struct:

go
type workspaceInitFlow struct {
	state    string // "awaiting_url", "awaiting_confirm"
	repoURL  string
	cloneTo  string
}

Step 2: Add SetMultiWorkspace method

go
func (e *Engine) SetMultiWorkspace(baseDir, bindingStorePath string) {
	e.multiWorkspace = true
	e.baseDir = baseDir
	e.workspaceBindings = NewWorkspaceBindingManager(bindingStorePath)
	e.workspacePool = newWorkspacePool(15 * time.Minute)
	e.initFlows = make(map[string]*workspaceInitFlow)
	go e.runIdleReaper()
}

Step 3: Implement idle reaper goroutine

go
func (e *Engine) runIdleReaper() {
	ticker := time.NewTicker(1 * time.Minute)
	defer ticker.Stop()
	for {
		select {
		case <-e.ctx.Done():
			return
		case <-ticker.C:
			reaped := e.workspacePool.ReapIdle()
			for _, ws := range reaped {
				// Stop interactive states for this workspace
				e.interactiveMu.Lock()
				for key, state := range e.interactiveStates {
					if state.workspaceDir == ws {
						if state.agentSession != nil {
							state.agentSession.Close()
						}
						delete(e.interactiveStates, key)
					}
				}
				e.interactiveMu.Unlock()
				slog.Info("workspace idle-reaped", "workspace", ws)
			}
		}
	}
}

Note: This requires adding workspaceDir string to the interactiveState struct (~line 174).

Step 4: Build to verify compilation

Run: go build ./...
Expected: Compiles cleanly

Step 5: Commit

bash
git add core/engine.go
git commit -m "feat: add multi-workspace fields, SetMultiWorkspace, and idle reaper to Engine"

Task 6: Workspace resolution logic

Files:

  • Modify: core/engine.go

Step 1: Implement resolveWorkspace method

Add this method to Engine. It returns the workspace path or empty string if init flow is needed:

go
// resolveWorkspace resolves a channel to a workspace directory.
// Returns (workspacePath, channelName, error).
// If workspacePath is empty, the init flow should be triggered.
func (e *Engine) resolveWorkspace(p Platform, channelID string) (string, string, error) {
	projectKey := "project:" + e.name

	// Step 1: Check existing binding
	if b := e.workspaceBindings.Lookup(projectKey, channelID); b != nil {
		// Verify workspace directory still exists
		if _, err := os.Stat(b.Workspace); err != nil {
			slog.Warn("bound workspace directory missing, removing binding",
				"workspace", b.Workspace, "channel", channelID)
			e.workspaceBindings.Unbind(projectKey, channelID)
			return "", b.ChannelName, nil
		}
		return b.Workspace, b.ChannelName, nil
	}

	// Step 2: Resolve channel name for convention match
	channelName := ""
	if resolver, ok := p.(ChannelNameResolver); ok {
		name, err := resolver.ResolveChannelName(channelID)
		if err != nil {
			slog.Warn("failed to resolve channel name", "channel", channelID, "err", err)
		} else {
			channelName = name
		}
	}

	if channelName == "" {
		return "", "", nil
	}

	// Step 3: Convention match โ€” check if base_dir/<channel-name> exists
	candidate := filepath.Join(e.baseDir, channelName)
	if info, err := os.Stat(candidate); err == nil && info.IsDir() {
		// Auto-bind with feedback
		e.workspaceBindings.Bind(projectKey, channelID, channelName, candidate)
		slog.Info("workspace auto-bound by convention",
			"channel", channelName, "workspace", candidate)
		return candidate, channelName, nil
	}

	return "", channelName, nil
}

Step 2: Build to verify

Run: go build ./...
Expected: Compiles cleanly

Step 3: Commit

bash
git add core/engine.go
git commit -m "feat: add resolveWorkspace method for channel-to-directory mapping"

Task 7: Init flow conversation handler

Files:

  • Modify: core/engine.go

Step 1: Implement handleInitFlow

This handles the conversational flow when no workspace is bound for a channel:

text
/* Detailed source-code truncated for AI context efficiency. */

Step 2: Build to verify

Run: go build ./...
Expected: Compiles cleanly

Step 3: Commit

bash
git add core/engine.go
git commit -m "feat: add workspace init flow for cloning repos and binding channels"

Task 8: Wire multi-workspace routing into handleMessage

Files:

  • Modify: core/engine.go (handleMessage ~line 574, getOrCreateInteractiveState)

Step 1: Add workspace routing at the top of handleMessage

After the banned words check (~line 611) and before command dispatch (~line 613), insert multi-workspace resolution:

go
// Multi-workspace resolution
var resolvedWorkspace string
if e.multiWorkspace {
	channelID := extractChannelID(msg.SessionKey)
	workspace, channelName, err := e.resolveWorkspace(p, channelID)
	if err != nil {
		slog.Error("workspace resolution failed", "err", err)
		e.reply(p, msg.ReplyCtx, fmt.Sprintf("Workspace resolution error: %v", err))
		return
	}
	if workspace == "" {
		// No workspace โ€” handle init flow (unless it's a /workspace command)
		if !strings.HasPrefix(content, "/workspace") {
			if e.handleWorkspaceInitFlow(p, msg, channelID, channelName) {
				return
			}
		}
		// If init flow didn't consume and no workspace, only /workspace commands work
		if !strings.HasPrefix(content, "/workspace") {
			return
		}
	} else {
		resolvedWorkspace = workspace
		// Touch the workspace pool for idle tracking
		if ws := e.workspacePool.Get(workspace); ws != nil {
			ws.Touch()
		}

		// Auto-bind feedback for convention matches (first message only)
		if ws := e.workspacePool.Get(workspace); ws == nil {
			e.reply(p, msg.ReplyCtx, fmt.Sprintf(
				"Found `%s` matching this channel. Binding workspace and starting session... Ready.", workspace))
		}
	}
}

Step 2: Add extractChannelID helper

go
func extractChannelID(sessionKey string) string {
	// Format: "platform:channelID:userID" or "platform:channelID"
	parts := strings.SplitN(sessionKey, ":", 3)
	if len(parts) >= 2 {
		return parts[1]
	}
	return ""
}

Step 3: Modify getOrCreateInteractiveState for multi-workspace

The existing getOrCreateInteractiveState method creates agent sessions using e.agent. For multi-workspace, it needs to create an agent session with the resolved workspace's work_dir. Find getOrCreateInteractiveState and modify it:

  • Add resolvedWorkspace parameter
  • When e.multiWorkspace is true, use a workspace-scoped key and pass the workspace dir to the agent
  • Key interactiveStates by workspace + ":" + sessionKey instead of just sessionKey
  • Set state.workspaceDir on the interactive state

This requires the Agent interface to support dynamic work dirs. Add a method:

go
// In core/interfaces.go, add optional interface:
type WorkDirSetter interface {
	SetWorkDir(dir string)
}

And in agent/claudecode/claudecode.go:

go
func (a *Agent) SetWorkDir(dir string) {
	a.mu.Lock()
	defer a.mu.Unlock()
	a.workDir = dir
}

func (a *Agent) GetWorkDir() string {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.workDir
}

However, since a single Agent instance is shared, we can't just SetWorkDir โ€” it would race. Instead, for multi-workspace we need per-workspace Agent instances. Store them in the workspaceState:

Add to workspaceState:

go
type workspaceState struct {
	mu           sync.Mutex
	workspace    string
	sessions     *SessionManager
	agent        Agent           // per-workspace agent clone
	lastActivity time.Time
}

Add a method to Engine for creating per-workspace agents:

go
func (e *Engine) getOrCreateWorkspaceAgent(workspace string) (Agent, *SessionManager, error) {
	ws := e.workspacePool.GetOrCreate(workspace)
	ws.mu.Lock()
	defer ws.mu.Unlock()

	if ws.agent != nil {
		return ws.agent, ws.sessions, nil
	}

	// Create agent clone with this workspace's work_dir
	agentOpts := make(map[string]any)
	// Copy relevant options from the original agent if it's a claudecode agent
	if wds, ok := e.agent.(interface{ Options() map[string]any }); ok {
		for k, v := range wds.Options() {
			agentOpts[k] = v
		}
	}
	agentOpts["work_dir"] = workspace

	agent, err := CreateAgent("claudecode", agentOpts)
	if err != nil {
		return nil, nil, fmt.Errorf("create workspace agent: %w", err)
	}

	// Wire providers if original agent has them
	if ps, ok := e.agent.(ProviderSwitcher); ok {
		if ps2, ok := agent.(ProviderSwitcher); ok {
			ps2.SetProviders(ps.GetProviders())
		}
	}

	// Create per-workspace session manager
	sessionFile := filepath.Join(filepath.Dir(e.sessions.storePath),
		fmt.Sprintf("%s_ws_%x.json", e.name, sha256Short(workspace)))
	sessions := NewSessionManager(sessionFile)

	ws.agent = agent
	ws.sessions = sessions
	return agent, sessions, nil
}

func sha256Short(s string) string {
	h := sha256.Sum256([]byte(s))
	return hex.EncodeToString(h[:4])
}

Step 4: Update processInteractiveMessage to use workspace agent

In the multi-workspace path, before calling getOrCreateInteractiveState, swap in the workspace's agent and session manager. The cleanest approach: modify processInteractiveMessage to accept optional overrides, or modify getOrCreateInteractiveState to accept an agent parameter.

Modify getOrCreateInteractiveState signature to optionally accept a workspace agent:

go
func (e *Engine) getOrCreateInteractiveState(sessionKey string, p Platform, replyCtx any, session *Session, agent ...Agent) *interactiveState {
	// ... existing logic, but use agent[0] if provided instead of e.agent
}

Step 5: Build to verify

Run: go build ./...
Expected: Compiles cleanly

Step 6: Commit

bash
git add core/engine.go core/interfaces.go core/workspace_state.go agent/claudecode/claudecode.go
git commit -m "feat: wire multi-workspace routing into handleMessage with per-workspace agents"

Task 9: Workspace commands

Files:

  • Modify: core/engine.go (handleCommand ~line 1320)

Step 1: Add workspace command handler

In the handleCommand switch statement, add cases for workspace commands:

go
case "workspace":
	if !e.multiWorkspace {
		e.reply(p, msg.ReplyCtx, "Workspace commands are only available in multi-workspace mode.")
		return true
	}
	e.handleWorkspaceCommand(p, msg, args)
	return true

Step 2: Implement handleWorkspaceCommand

text
/* Detailed source-code truncated for AI context efficiency. */

Step 3: Build to verify

Run: go build ./...
Expected: Compiles cleanly

Step 4: Commit

bash
git add core/engine.go
git commit -m "feat: add /workspace commands (init, unbind, list, status)"

Task 10: Wire multi-workspace in main.go

Files:

  • Modify: cmd/cc-connect/main.go (~line 139 project setup loop)

Step 1: Add multi-workspace setup after engine creation

After engine := core.NewEngine(...) (~line 195), add:

go
if proj.Mode == "multi-workspace" {
	baseDir := proj.BaseDir
	if strings.HasPrefix(baseDir, "~/") {
		home, _ := os.UserHomeDir()
		baseDir = filepath.Join(home, baseDir[2:])
	}
	// Ensure base dir exists
	if err := os.MkdirAll(baseDir, 0o755); err != nil {
		slog.Error("failed to create base_dir", "path", baseDir, "err", err)
		continue
	}
	bindingStore := filepath.Join(cfg.DataDir, "workspace_bindings.json")
	engine.SetMultiWorkspace(baseDir, bindingStore)
	slog.Info("multi-workspace mode enabled", "project", proj.Name, "base_dir", baseDir)
}

Step 2: Build to verify

Run: go build ./...
Expected: Compiles cleanly

Step 3: Commit

bash
git add cmd/cc-connect/main.go
git commit -m "feat: wire multi-workspace mode setup in main.go"

Task 11: Integration testing

Files:

  • Create: core/multi_workspace_test.go

Step 1: Write integration test for full resolution flow

text
/* Detailed source-code truncated for AI context efficiency. */

Note: The mock types will need to be adapted based on existing test helpers in the codebase. Check core/*_test.go for existing mock patterns.

Step 2: Run tests

Run: go test ./core/ -run TestMultiWorkspace -v
Expected: PASS

Step 3: Run full test suite

Run: go test ./...
Expected: All existing tests still pass

Step 4: Commit

bash
git add core/multi_workspace_test.go
git commit -m "test: add integration tests for multi-workspace resolution"

Task 12: Update config.example.toml and verify build

Files:

  • Modify: config.example.toml

Step 1: Add multi-workspace example section

Find the projects section and add a complete multi-workspace example:

toml
# Multi-workspace mode: single bot, multiple workspaces
# Channel name maps to ~/workspace/<channel-name> automatically.
# Use /workspace init  to clone and bind a new repo.
#
# [[projects]]
# name = "claude"
# mode = "multi-workspace"
# base_dir = "~/workspace"
#
# [projects.agent]
# type = "claudecode"
# permission_mode = "yolo"
#
# [[projects.platforms]]
# type = "slack"
# [projects.platforms.options]
# bot_token = "xoxb-..."
# app_token = "xapp-..."

Step 2: Final build and test

Run: go build ./... && go test ./...
Expected: Clean build and all tests pass

Step 3: Commit

bash
git add config.example.toml
git commit -m "docs: add multi-workspace example to config.example.toml"

File: docs/plans/2026-03-12-usage.md

Usage Command Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Add a built-in /usage command with a generic agent usage-reporting interface, and implement the first provider-backed version for Codex using ChatGPT OAuth quota data.

Architecture: The engine gains a new built-in command that depends only on an optional UsageReporter interface. Agents that implement the interface return a generic UsageReport; the engine formats that into text. The Codex agent reads ChatGPT OAuth credentials from ~/.codex/auth.json, calls the wham/usage endpoint, and maps the response into the generic structure.

Tech Stack: Go, built-in command dispatch in core/engine.go, optional agent interfaces in core/interfaces.go, HTTP/JSON via Go standard library, table-driven tests.


Task 1: Add generic usage-reporting types

Files:

  • Modify: core/interfaces.go

Step 1: Add the failing interface usage surface

Define:

  • UsageReporter
  • UsageReport
  • UsageBucket
  • UsageWindow
  • UsageCredits

The structure should cover provider identity, plan/tier, generic allowed/limit-reached flags, one or more windows, and credits metadata.

Step 2: Run targeted compile check

Run: go test ./core/...
Expected: compile failures in engine/tests until the command layer is wired.

Step 3: Keep the types minimal

Do not add provider-specific fields unless they are broadly useful.

Step 4: Re-run compile check

Run: go test ./core/...
Expected: still failing only because /usage command is not yet fully integrated.

Task 2: Add `/usage` built-in command and output formatter

Files:

  • Modify: core/engine.go
  • Modify: core/i18n.go
  • Test: core/engine_test.go

Step 1: Write/extend failing tests

Add tests for:

  • /usage dispatch to a supporting agent
  • unsupported-agent message
  • successful output formatting with representative report data

Step 2: Run the targeted tests

Run: go test ./core/... -run 'Test.*Usage|TestHandleCommand'
Expected: FAIL because the command is not registered yet.

Step 3: Implement command plumbing

Update:

  • built-in command registration
  • command dispatch switch
  • bot command listing
  • help/i18n text

Add a cmdUsage implementation that:

  • type-asserts UsageReporter
  • fetches usage with timeout
  • formats a concise text response

Step 4: Run the targeted tests again

Run: go test ./core/... -run 'Test.*Usage|TestHandleCommand'
Expected: PASS

Task 3: Implement Codex usage retrieval against ChatGPT OAuth

Files:

  • Modify: agent/codex/codex.go
  • Add or Modify: agent/codex/usage.go
  • Test: agent/codex/usage_test.go

Step 1: Write failing tests for the mapper/fetcher

Cover:

  • successful parsing of a representative wham/usage payload
  • missing auth.json
  • missing token/account fields
  • HTTP error response

Step 2: Run the targeted tests

Run: go test ./agent/codex -run 'Test.*Usage'
Expected: FAIL because the implementation does not exist yet.

Step 3: Implement minimal production code

Add:

  • auth file path resolver
  • auth JSON loader
  • HTTP request builder
  • response DTOs
  • mapping into core.UsageReport

Prefer dependency injection for:

  • auth file path / reader
  • HTTP client / transport

So tests avoid live network and real user files.

Step 4: Run the targeted tests again

Run: go test ./agent/codex -run 'Test.*Usage'
Expected: PASS

Task 4: End-to-end verification and cleanup

Files:

  • Modify: README.md
  • Modify: README.zh-CN.md
  • Modify: CHANGELOG.md

Step 1: Document the new command

Add /usage to command lists and a short explanation that support depends on the current agent.

Step 2: Run focused test suites

Run: go test ./core/... ./agent/codex
Expected: PASS

Step 3: Run broader verification

Run: go test ./...
Expected: PASS, or identify unrelated pre-existing failures clearly.

Step 4: Commit

Run:

bash
git add core/interfaces.go core/engine.go core/i18n.go core/engine_test.go agent/codex/codex.go agent/codex/usage.go agent/codex/usage_test.go README.md README.zh-CN.md CHANGELOG.md docs/plans/2026-03-12-usage-design.md docs/plans/2026-03-12-usage.md
git commit -m "feat: add usage command for codex oauth"

File: docs/plans/2026-03-12-usage-design.md

Usage Command Design

Date: 2026-03-12

Goal: Add a built-in /usage command that reports model/account quota usage, starting with Codex running under ChatGPT OAuth, while keeping the retrieval path generic so other agents can plug in later.

Scope

  • Add a new built-in slash command: /usage.
  • The command is independent from /status and /doctor.
  • Usage retrieval is exposed as an optional agent capability, not hardcoded into the engine for a single vendor.
  • First implementation targets the Codex agent when local ChatGPT OAuth credentials are available in ~/.codex/auth.json.
  • Unsupported agents should return a clear โ€œnot supportedโ€ style message rather than failing the whole command system.

Architecture

Command Layer

core/engine.go will register and dispatch /usage as a normal built-in command. The engine should not know how ChatGPT, Gemini, or any future provider exposes quota data. It only detects whether the current agent implements a usage-reporting interface and formats the response.

Agent Capability Layer

Add a new optional interface in core/interfaces.go, for example:

  • UsageReporter
  • a method such as GetUsage(context.Context) (*UsageReport, error)

The report type should be generic enough to cover multiple providers:

  • provider/agent name
  • subject or account label
  • plan type / tier if available
  • one or more rate-limit buckets/windows
  • optional credits/balance fields
  • raw provider-specific metadata only if needed for debugging

This keeps future integrations local to each agent package.

Codex Implementation

agent/codex will implement the new interface by:

  1. Reading ~/.codex/auth.json
  2. Extracting:
    • tokens.access_token
    • tokens.account_id
  3. Calling:
    • GET https://chatgpt.com/backend-api/wham/usage
  4. Passing headers:
    • Authorization: Bearer <access_token>
    • ChatGPT-Account-Id: <account_id>
    • User-Agent: codex-cli
  5. Mapping the JSON response into the generic usage report

If auth.json is missing, fields are absent, or the HTTP call fails, the agent should return a normal error so /usage can present a concise failure message.

Data Model

The generic report should support at least:

  • identity:
    • provider
    • account_id
    • user/email if available
  • plan:
    • plan_type
  • standard rate limits:
    • allowed
    • limit_reached
    • primary window
    • secondary window
  • review/code-review limits:
    • same window shape when available
  • credits:
    • has_credits
    • unlimited
    • balance

Each window should preserve:

  • used percent
  • total window seconds
  • reset-after seconds
  • reset timestamp if supplied

This is enough for the current ChatGPT OAuth response and still general enough for other providers with multiple quota windows.

Output Format

The first output version should be plain text and compact. Suggested shape:

  • title line with agent/provider
  • plan line
  • standard usage section
  • code review usage section if present
  • credits section if present

For each window:

  • whether requests are currently allowed
  • whether the limit is reached
  • used percent
  • reset timing

Prefer rendering both relative time and absolute time if easy to do consistently. If absolute rendering is added, it should use local time formatting already used by the project, or a simple RFC3339 fallback.

Error Handling

  • Agent does not implement usage reporting:
    • reply with a user-facing โ€œcurrent agent does not support /usageโ€
  • Codex auth file missing:
    • explain that ChatGPT OAuth login data was not found
  • Token/account id missing:
    • explain credentials are incomplete
  • HTTP non-200:
    • include status code in logs; show concise failure text to user
  • JSON decode failure:
    • report provider response parse failure

Do not expose bearer tokens or raw auth file contents in user-visible output or logs.

Testing

Minimum tests should cover:

  • command dispatch recognizes /usage
  • engine returns unsupported message when agent lacks the interface
  • engine formats a successful generic usage report
  • Codex usage fetch maps a representative wham/usage payload correctly
  • Codex usage fetch errors on missing auth file / missing token fields

Network-dependent tests should use injected transport or httptest, not live requests.

Non-Goals

  • No merging into /status or /doctor
  • No card UI in the first version
  • No polling or background caching in the first version
  • No support for non-Codex agents in this change beyond the shared interface

File: docs/plans/2026-03-13-session-resilience-design.md

Session Resilience Design

Date: 2026-03-13
Status: Approved
Branch: feat/multi-workspace

Problem

Multi-workspace mode introduces long-lived, concurrent Claude Code sessions that are reaped on idle and resumed on demand. Several failure modes cause silent context loss ("context rot"):

  1. CWD mismatch โ€” workspace paths that differ by trailing slash, symlink, or relative segment map to different Claude Code session directories, causing resume to silently start a fresh session
  2. Resume failure โ€” when a session's context is too large, --resume fails with "Prompt is too long" and the session becomes permanently broken until manual !new
  3. Invisible context degradation โ€” users have no signal that context is filling up until Claude starts forgetting things
  4. Silent failures โ€” session lifecycle events (spawn, resume, reap, failure) lack diagnostic logging

Design

1. Path Normalization

Helper: normalizeWorkspacePath(path string) string in workspace_state.go

text
filepath.Clean(path) โ†’ filepath.EvalSymlinks(cleanedPath)

If EvalSymlinks fails (path doesn't exist yet), fall back to filepath.Clean only.

Applied at two sites:

  • workspacePool.GetOrCreate(workspace) โ€” normalize the key before map lookup/insert
  • Workspace binding resolution โ€” normalize before the workspace string enters the system

Logging: slog.Debug("workspace path normalized", "original", path, "normalized", result) when normalization changes the input.

2. Resume Failure โ†’ Fresh Session Fallback

Location: getOrCreateInteractiveStateWith() in engine.go

Current behavior: StartSession failure โ†’ state with nil agentSession โ†’ broken until !new.

New behavior:

  1. If StartSession fails AND session.AgentSessionID != "" (resume attempt):
    • Log failure with diagnostics: session ID, error message, cwd
    • Clear session.AgentSessionID and save
    • Retry agent.StartSession(ctx, "") for a fresh session
    • Post platform notification: "Session context was too large to resume โ€” starting fresh. Project context is preserved in CLAUDE.md."
  2. If fresh retry also fails โ†’ fall through to existing nil-state behavior
  3. If original call was already fresh (AgentSessionID == "") โ†’ no retry, fall through as today

Notification: Send via p.Send(ctx, replyCtx, msg) โ€” both are available on the interactiveState being constructed.

3. Context Consumption Indicator

Dual-track approach with logging to compare accuracy over time.

Track A: SDK token counts (accurate, cc-connect-owned)

  • In processInteractiveEvents, parse result events for input_tokens usage
  • Store cumulative input_tokens on the interactiveState (updated each turn)
  • Compute percentage: input_tokens / 200_000 * 100 (model context window)
  • Append [ctx: XX%] to every message relayed to the platform

Track B: Claude self-report (approximate, for comparison)

  • Add to system prompt via --append-system-prompt: instruction to append [ctx: ~XX%] to every response
  • Parse the self-reported value from Claude's output before relaying

Logging

On every turn that has both values:

text
slog.Info("context_usage",
    "sdk_pct", sdkPct,
    "self_reported_pct", selfReportedPct,
    "session_key", sessionKey,
    "input_tokens", inputTokens)

Over time, compare drift to decide whether the system prompt instruction adds value.

Display

Appended to every visible message relayed to the platform:

text
Here's the refactored auth module...
[ctx: 62%]

If no token data available yet (first message), skip the indicator.

4. Diagnostic Logging

Structured slog logging at key lifecycle points:

Event Level Fields
Session spawn Info normalized cwd, session ID (or "new"), model
Session resume Info session ID, JSONL file path, file size
Resume failure Error session ID, error, stderr, cwd, JSONL file size
Fresh fallback Warn original session ID, new session ID, cwd
Idle reap Info session key, workspace path, idle duration, token count at reap
Context per-turn Info session key, input_tokens, sdk_pct, self_reported_pct
Path normalization Debug original path, normalized path (only when changed)

JSONL file size: Resolve via findProjectDir + stat at resume time. Log even if file not found (indicates cwd mismatch).

Non-Goals

  • Proactive compaction โ€” likely to cause more trouble than it's worth; the context indicator gives users agency to compact manually
  • Session summary โ†’ new session pattern โ€” more robust but significantly more implementation work; revisit if resume-with-fallback proves insufficient
  • Disk/memory monitoring โ€” out of scope; can be added as operational tooling later

Implementation Order

  1. Path normalization (prerequisite for everything else being reliable)
  2. Diagnostic logging (needed to verify the other changes work)
  3. Resume failure fallback (highest-value fix)
  4. Context consumption indicator (most complex, benefits from logging already being in place)