Feishu interactive cards support at most 5 table components.
When the agent response contains more than 5 markdown tables,
buildReplyContent now falls back to post (md tag) format instead
of card format, preventing API error 11310 (card table number
over limit).
Add countMarkdownTables helper and corresponding tests.
Implement Discord FileSender so cc-connect can deliver generated files through bot replies and slash-command interactions.
Co-authored-by: Claude Code <noreply@anthropic.com>
Add a new integration test suite that verifies real agent (Claude Code, Codex,
Cursor, Gemini, OpenCode) interactions with a mock platform. Tests are gated
by //go:build integration tag so they're excluded from normal CI.
core/engine.go:
- Add public ReceiveMessage(p, msg) method to allow test callers to
inject messages into the engine
tests/integration/agent_integration_test.go:
- mockPlatform implementing core.Platform interface for test verification
- agentPool for reusing agent instances across tests
- Session tests: new session, /list, /switch, /stop, /new context clear, /history
- Agent tests: claude code, codex, cursor, gemini (skip if quota), opencode (skip if no gitlab auth)
- Command tests: /shell (skip if admin_from not set), /provider list
- i18n: /lang zh language switch
- Concurrency: session isolation, long text chunking, empty message handling
docs/plans/2026-03-24-integration-tests.md:
- Test plan listing all implemented and planned integration test cases
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add new CLI command `cc-connect cron info <id>` that displays full JSON
details of a scheduled task. This helps users inspect all configuration
fields of a cron job that aren't visible in the compact `cron list` output.
Changes:
- Add `/cron/info` API endpoint in core/api.go
- Add `runCronInfo` CLI handler in cmd/cc-connect/cron.go
- Update help text to include the new command
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: require explicit session key when sending attachments with multiple sessions
When cc-connect send --image or --file is called without an explicit
--session flag and there are multiple active sessions, the code would
arbitrarily pick the first session from a map (non-deterministic order).
This could cause attachments to be sent to the wrong chat.
Now returns an error when multiple sessions exist and no session key is
specified for attachment sends. Also adds debug logging to Telegram SendImage
to aid future diagnosis.
Fixes#242
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: clear AgentSessionID on old session in /new command
When /new is invoked, the old session's AgentSessionID was not being
cleared. This could cause the old session to be resumed on the next
message if GetOrCreateActive returned it instead of the newly created
session.
The fix clears the old session's AgentSessionID and clears its history
before creating a new session, similar to how cmdSwitch handles session
transitions.
Fixes#238
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Dev Claude <dev@cc-connect.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(codex): avoid closing events channel before readLoop exits
Close() previously closed cs.events after an 8s wait even when wg.Wait
had not finished, while readLoop could still send in flushPendingAsText
(turn.completed), causing "panic: send on closed channel" after /stop
then /new. Defer channel close until readLoop completes.
Fixes https://github.com/chenhg5/cc-connect/issues/281
* fix(slack): pass non-image file uploads to agent as FileAttachment
- Map PDF/text and other non-audio/non-image Slack shares to core.FileAttachment
so the engine persists them and has_files is true for the agent.
- Parse files from app_mention raw inner JSON (slack-go omits Files on AppMentionEvent).
- Allow app_mention with only attachments and empty stripped text.
Fixes#292
Made-with: Cursor
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(codex): avoid closing events channel before readLoop exits
Close() previously closed cs.events after an 8s wait even when wg.Wait
had not finished, while readLoop could still send in flushPendingAsText
(turn.completed), causing "panic: send on closed channel" after /stop
then /new. Defer channel close until readLoop completes.
Fixes https://github.com/chenhg5/cc-connect/issues/281
* fix(engine): recycle live agent when Session clears AgentSessionID (/new)
getOrCreateInteractiveStateWith reused any alive process when wantID was empty
because the condition treated empty wantID as always compatible. After /new the
Session has no resume ID but the old Claude process still reported the previous
session — messages kept going to stale context.
Recycle when the live process has a concrete ID but the Session expects none or
a different one.
Fixes#238
Made-with: Cursor
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(codex): avoid closing events channel before readLoop exits
Close() previously closed cs.events after an 8s wait even when wg.Wait
had not finished, while readLoop could still send in flushPendingAsText
(turn.completed), causing "panic: send on closed channel" after /stop
then /new. Defer channel close until readLoop completes.
Fixes https://github.com/chenhg5/cc-connect/issues/281
* fix(session): never persist ContinueSession as agent_session_id (#255)
- Reject storing the __continue__ sentinel in SetAgentSessionID; normalize in SetAgentInfo.
- Let CompareAndSetAgentSessionID overwrite a mistaken persisted sentinel with a real ID.
- Strip sentinel when loading session JSON and when saving snapshots.
- Simplify engine post-start writeback to a single CompareAndSet path.
Fixes#255
Made-with: Cursor
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(codex): avoid closing events channel before readLoop exits
Close() previously closed cs.events after an 8s wait even when wg.Wait
had not finished, while readLoop could still send in flushPendingAsText
(turn.completed), causing "panic: send on closed channel" after /stop
then /new. Defer channel close until readLoop completes.
Fixes https://github.com/chenhg5/cc-connect/issues/281
* fix(discord): run slash commands when defer interaction fails (#258)
If InteractionRespond(deferred) errors (e.g. unknown interaction), still invoke
the engine and reply via channel messages, with thread-aware replyContext for
thread channels.
Fixes#258
Made-with: Cursor
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(codex): avoid closing events channel before readLoop exits
Close() previously closed cs.events after an 8s wait even when wg.Wait
had not finished, while readLoop could still send in flushPendingAsText
(turn.completed), causing "panic: send on closed channel" after /stop
then /new. Defer channel close until readLoop completes.
Fixes https://github.com/chenhg5/cc-connect/issues/281
* fix(telegram): UTF-8-safe bot command descriptions and table rows (#119)
- Truncate BotCommand.Description by runes (Telegram 256-char limit); byte
slicing broke CJK and caused API errors.
- Make truncateForLog rune-aware for debug prefixes.
- Parse markdown table rows without byte-slicing the outer pipes.
Fixes#119
Made-with: Cursor
---------
Co-authored-by: Claude <noreply@anthropic.com>
WeCom may include @robot_id in message content. Core permission matching
uses exact string comparison, so replies like "允许 @bot" failed to match.
Strip ASCII and fullwidth @ mentions for configured bot/aibot/agent IDs
before forwarding to the engine.
Fixes#98
Made-with: Cursor
Co-authored-by: Claude <noreply@anthropic.com>
LaunchAgent used KeepAlive=true, so launchd restarted cc-connect after
every process exit, including graceful SIGTERM shutdown. That looked like
"stop then immediate restart" and could make tooling that waits for the
Unix API socket to disappear hang if the service respawned quickly.
Use KeepAlive with SuccessfulExit=true so only unclean exits are restarted,
matching systemd Restart=on-failure behavior.
Existing installs: re-run `cc-connect daemon install --force` to refresh
the plist.
Fixes#153
Made-with: Cursor
Co-authored-by: Claude <noreply@anthropic.com>
When cc-connect send --image or --file is called without an explicit
--session flag and there are multiple active sessions, the code would
arbitrarily pick the first session from a map (non-deterministic order).
This could cause attachments to be sent to the wrong chat.
Now returns an error when multiple sessions exist and no session key is
specified for attachment sends. Also adds debug logging to Telegram SendImage
to aid future diagnosis.
Fixes#242
Co-authored-by: Dev Claude <dev@cc-connect.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Avoids race condition when cron and regular messages share the same
interactiveKey — the shared state.replyCtx was being overwritten by
concurrent goroutines, causing cron responses to be delivered to the
wrong session.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Close() previously closed cs.events after an 8s wait even when wg.Wait
had not finished, while readLoop could still send in flushPendingAsText
(turn.completed), causing "panic: send on closed channel" after /stop
then /new. Defer channel close until readLoop completes.
Fixes https://github.com/chenhg5/cc-connect/issues/281
* feat(agent): add ACP stdio JSON-RPC adapter (MVP)
Implement agent/acp as an ACP client over newline-delimited JSON-RPC: initialize,
session/new or session/load, session/prompt, session/update mapping to core.Event,
and session/request_permission wired to RespondPermission.
Includes plugin_agent_acp.go (!no_acp), Makefile ALL_AGENTS entry, config example,
unit tests for transport and mapping, and AgentDoctorInfo for doctor.
Made-with: Cursor
* fix(acp): Cursor CLI compatibility — authenticate + cursor/* RPC ack
- Optional auth_method (e.g. cursor_login) triggers JSON-RPC authenticate after initialize
- initialize includes minimal clientCapabilities (fs/terminal) per Cursor ACP docs
- Acknowledge cursor/* server requests with empty result to avoid blocking tool flows
- Add integration test against ~/.local/bin/agent when present
- Document auth_method and args in config.example.toml
* fix(telegram): use Request for sendChatAction typing indicator
Telegram returns result=true for sendChatAction; BotAPI.Send unmarshals
result as Message and fails. Request only checks ok and avoids that decode.
* fix(core): surface EventToolResult in IM; extend ACP tool/thinking mapping
- Engine: handle EventToolResult in interactive loop (was silently dropped);
compress and relay paths append tool output too. Add MsgToolResult i18n.
- ACP: stream tool_call_update for in_progress/pending; optional reasoning_*
sessionUpdate kinds -> EventThinking; fallback hook for unknown kinds.
* fix(core): run agent Send concurrently with interactive event loop
Agents that block inside Send (e.g. ACP session/prompt) can emit
EventPermissionRequest while blocked; the engine must consume Events()
in parallel or permission prompts never reach the platform.
- processInteractiveMessageWith: async Send + pass sendDone into processInteractiveEvents
- processInteractiveEvents: select on sendDone for errors and sync at turn boundaries
- drainPendingMessages: same async Send pattern as the main path
- Add TestProcessInteractiveEvents_PermissionWhileSendBlocked regression test
* docs(acp): document OpenClaw and Copilot CLI; add display_name option
- config.example.toml: Cursor, OpenClaw (`openclaw acp`), Copilot (`copilot --acp --stdio`) examples with upstream doc links
- acp agent: optional display_name for doctor (AgentDoctorInfo)
- agent_test.go: cover default and custom display_name
* fix(core): remove ineffectual pendingSend assignments (ineffassign)
pendingSend was set to nil immediately before return or before
reassignment to nextSend; golangci-lint flags these as ineffectual.
* test(core): fix race in PermissionWhileSendBlocked under -race
The test goroutine polled stubInlineButtonPlatform.buttonRows while
processInteractiveEvents wrote them from another goroutine. Use a
permSignalInlinePlatform wrapper that signals on perm:allow via a channel
instead of racy field reads.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(discord): add mode and reasoning button controls
Use Discord buttons for /mode and /reasoning, and apply Claude Code mode changes to the current session when supported.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(discord): clean up mode labels and log button fallback errors
Remove the extra mode marker symbol, show the current mode with text instead, and include the underlying error when permission button fallbacks trigger.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
- Add file_path validation to catch empty paths
- Add HTTP status code check (resp.StatusCode != 200)
- Prevents silent failures when file downloads fail
Fixes issue where Telegram images were not being delivered to Claude Code
due to missing error handling for non-200 HTTP responses.
* feat(cron): add `cron edit` command to modify job fields
Add new CLI command `cc-connect cron edit <id> <field> <value>` that
modifies individual fields of a scheduled task.
Features:
- Supports all editable fields: project, session_key, cron_expr, prompt,
exec, work_dir, description, enabled, silent, mute, session_mode, timeout_mins
- Type validation: bool fields (enabled, mute) and int fields (timeout_mins)
- Read-only fields protected: id, created_at cannot be modified
- Cron expression validation before applying
- Automatic rescheduling when cron_expr or enabled changes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(cron): add fallback reflection for string fields + comprehensive help
- Add reflection-based fallback for unknown string fields in updateJobField
- Add printCronEditUsage() with full field list, types, and examples
- Add --help/-h flag support to runCronEdit
- Update error messages to reference --help
The reflection fallback allows editing new string fields without code changes,
while type validation for known fields (bool, int) is preserved.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(cron): protect last_run and last_error from editing
Add last_run and last_error to read-only fields list.
These are system-maintained fields that should not be manually edited.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- core: interactive /dir card (nav/act) on CardSender platforms; dirApply refactor
- core+i18n: card strings for dir history UI
- config: projects.show_context_indicator (hide [ctx: ~N%] suffix)
- feishu/lark: reply_to_trigger=false posts via Create instead of Reply
- codex: skip flush when ctx done; sync.Once on session Close
- dingtalk: stream runner reconnect loop; cancel on Stop
- go.mod: mark rsc.io/qr as direct require
Add bilingual tables after "What's New" inspired by OpenClaw China,
with legend (✅/🧪/⚠️/❌) and Weixin column marked beta-only.
Include QQ NapCat footnote and pointers to platform setup guides.
* feat: add session resilience with --continue, resume recovery, and context indicator
First connection after engine startup uses --continue to pick up the most
recent CLI session, bridging direct CLI usage and cc-connect. If resume
fails (e.g. prompt too long), automatically falls back to a fresh session.
Appends [ctx: ~XX%] to agent responses using SDK token counts, with
fallback to agent self-reported context percentage when SDK values are
unreliable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: --continue logic bug — respect fresh session intent after first connection
Remove the startSessionID == "" condition that forced --continue even
when users explicitly requested fresh sessions (e.g. via /new). Now only
the first connection after engine startup uses --continue; subsequent
connections respect the stored session ID or "" for fresh.
Add TestFreshSessionRespectedAfterFirstConnection to verify.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(npm): correct pre-release version comparison
The isNewerOrEqual function was comparing pre-release strings
lexicographically (e.g., "beta.2" > "beta.10" due to string comparison),
causing incorrect downgrade detection. Now parses pre-release into
id and numeric parts for proper numeric comparison.
Fixes: beta version upgrade being treated as outdated and downgrading to stable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore(npm): bump version to 1.2.2-beta.4
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make hasConnectedOnce per-workspace so idle-reaped workspaces resume with --continue
hasConnectedOnce was engine-global, so after the first connection set it
to true, any workspace that got idle-reaped and later recreated would
skip --continue and spawn a fresh session — losing all prior context.
Move the flag into workspaceState so each workspace independently tracks
its first connection. When a workspace is reaped and GetOrCreate makes a
new one, the flag starts false again, enabling --continue on reconnection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: fork session on resume/continue to prevent context interleaving
When cc-connect picks up a session via --continue or --resume, add
--fork-session so the platform conversation gets its own independent
context branch. This prevents history interleaving when the user is
also working in the same session from the CLI.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add two focused tests to prevent regression:
1. TestOpencodeSessionEntry_Unmarshal - verifies OpenCode's JSON output
with int64 timestamps can be unmarshaled (prevents the type mismatch
error: cannot unmarshal number into ... of type string)
2. TestNewOpencodeSession_ContinueSessionTreatedAsFresh - verifies the
__continue__ sentinel is not passed as literal session ID to CLI
(regression test for PR #249)
Co-authored-by: Liujing <liujing@LiujingdeMacBook-Pro.local>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Restore persisted /dir overrides on startup, apply them to session and heartbeat wiring, and add /dir reset plus docs so work directory changes survive restart without editing config.toml.
Made-with: Cursor
Co-authored-by: KRATSZ <KRATSZ@users.noreply.github.com>
ilink returns HTTP 200 with business errors in the response body; parse
ret/errcode/errmsg and return an error so the engine logs platform send
failures (e.g. quota). Add sendMessageResp and a small JSON unmarshal test.
- Skip empty Send/Reply text to avoid empty sendMessage item_list
- Reject sendText with no items; cap ilink JSON read at 64MiB
- Dedup key adds create_time_ms and client_id; CDN encrypt_query_param dedupe per message
- Extra raw voice segments attach as voice_N.silk files when Audio already set
Previously, responses were sent as standalone messages unless
reply_in_thread was explicitly enabled in config. Now Send,
SendPreviewStart, and SendAudio always thread replies to the
original user message when messageID is available, making
conversations easier to follow.
Co-authored-by: Kevin Wang Sheng <kevinWangSheng@users.noreply.github.com>
CompareAndSetAgentSessionID only fills an empty ID; after restart the
stored sentinel __continue__ would block writing the actual session id
from the running agent. Handle that case explicitly.
* feat(discord): support @everyone/@here as bot mention via respond_to_everyone config
When multiple cc-connect bots share a Discord channel, users had to
@mention each bot individually. With respond_to_everyone = true, the bot
treats @everyone/@here as a direct mention, allowing broadcast questions
to all opted-in bots at once.
Closes#132
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: rename config to respond_to_at_everyone_and_here
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(discord): strip @everyone/@here from message content
The pi agent treated the raw "@everyone cat or dog?" as a file path,
causing "File not found" errors. Now @everyone and @here are stripped
from the content before passing to the agent.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add DirHistory module to track recently used directories per project
- /dir now shows history list with indices for quick navigation
- Support /dir <number> to jump to history entry
- Support /dir - to return to previous directory
- History stored in data_dir/dir_history.json
- Initial work_dir from config is automatically added to history
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: ignore ContinueSession sentinel in non-Claude agents
The ContinueSession ("__continue__") sentinel was introduced in df5cc64
for the --continue bridge, but only the claudecode agent was updated to
handle it. All other agents (pi, gemini, codex, cursor, qoder, opencode,
iflow) stored the literal "__continue__" as their session/resume ID,
causing CLI failures like `No session found matching '__continue__'` and
persistent "(empty response)" replies.
The fix adds a `resumeID != core.ContinueSession` guard to each agent's
session constructor so the sentinel is treated as "" (fresh session).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(codex): remove --cd from exec resume args
The codex CLI's `exec resume` subcommand does not support --cd, causing
"unexpected argument '--cd'" errors on every resumed session. The process
working directory is already set via cmd.Dir, so --cd is unnecessary.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>