* fix(kimi): conditionally pass --print so newer Kimi Code CLI works (#1456)
## Problem
`cc-connect` unconditionally passes `--print --output-format stream-json
--prompt …` to the Kimi CLI. The newer Kimi Code CLI removed the
standalone `--print` flag and now exits immediately with
error: unknown option '--print' (Did you mean --prompt?)
so every Kimi chat is broken for users on the new CLI. Issue #1456.
## Root cause
The two CLIs in the wild have opposing requirements for non-interactive
runs:
| CLI | --print? | --output-format requires |
|----------------|----------|--------------------------|
| legacy kimi-cli| yes | --print |
| new Kimi Code | removed | --prompt |
A fixed arg list cannot satisfy both, and `--print` cannot be emulated
the way #1253 emulates `--quiet` — it is the CLI's own non-interactive
mode toggle, not output formatting.
## Fix
Probe `kimi --help` once when the Agent is constructed and cache the
detected flag set. `buildArgs()` (newly extracted from `Send()` so it is
unit-testable) only emits `--print` when the installed binary still
advertises it. Probe failure / timeout falls back to "no --print", which
matches the direction the Kimi CLI is moving and avoids the hard-failure
mode of the current code.
- `agent/kimi/probe.go` — `parseKimiHelpFlags()` pure parser + bounded
`probeKimiFlags()` (5s timeout, swallows errors).
- `agent/kimi/kimi.go` — Agent caches `kimiFlagSupport`, threads it into
`newKimiSession`; doc comments updated.
- `agent/kimi/session.go` — `kimiSession.flagSupport` + `buildArgs()`
helper; conditional `--print`; permission-mode comment updated to
cover both legacy (`--print` implicit `--yolo`) and modern (`--prompt`
auto-permission) behaviors.
## Tests
- `TestBuildArgs_NoPrintSupportOmitsPrintFlag` — regression test for
#1456; fails on pre-fix code, passes on this branch.
- `TestBuildArgs_PrintSupportIncludesPrintFlag` — legacy CLI branch.
- `TestBuildArgs_PlanMode` / `TestBuildArgs_ResumeSession` — sanity
coverage for other arg paths.
- `TestParseKimiHelpFlags_LegacyAdvertisesPrint` /
`TestParseKimiHelpFlags_ModernHidesPrint` — parser handles both typer
box-drawing and standard click `-p, --prompt` layouts.
- `TestParseKimiHelpFlags_IgnoresPositionalAndShortOnly` — robustness.
- `TestProbeKimiFlags_FallbackOnMissingBinary` — probe failure path
returns zero value (modern-CLI default).
Full suite verified: `go test ./agent/kimi/...` (and `-race`), plus
`go test ./core/ -run TestCUJ`. Pre-existing `agent/acp` integration
test still fails on `origin/main` (requires `cursor agent login`) —
unrelated to this change.
## Non-goals
- Does not touch the cursor agent; Claude Code CLI still supports
`--print` so cursor's hard-coded flag is fine.
- Does not change `--quiet` handling; PR #1253 owns that fix for #806.
- Does not introduce new config schema.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(kimi): wrap deferred Close in func to satisfy errcheck
CI lint failed with 3 (and a 4th caught locally) `defer ks.Close()`
sites that ignore the returned error. Wrap in a closure that
explicitly discards the return value so golangci-lint errcheck is
happy and the tests still run.
No functional change.
---------
Co-authored-by: dev-claudecode <dev-claudecode@cc-connect.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
writeTempAppendPromptFile (the 1% edge-case path for prompts that have
session-specific platform formatting or user append_system_prompt) used
os.CreateTemp, which leaves the file at mode 0600 owned by the
cc-connect process user (often root under systemd). When the agent
was spawned under a different run_as_user, the target user got EACCES
and the agent exited before reading any prompt.
Fix: f.Chmod(0o644) immediately after write, mirroring the shared
ensureSharedSystemPromptFile path (which already writes 0o644 via
writeFileAtomic). The per-spawn content is a superset of the already
shared base prompt, so 0644 is consistent with the shared file.
The shared-file path (ensureSharedSystemPromptFile, used for the
common 99% case where no platform/user append is set) is already 0644
and untouched here. The daemon-mode path resolution fix from #1419
and the v1.3.4 cmdline 8192 fix from #1376 are independent and not
modified.
Test: TestWriteTempAppendPromptFile_ReadableByOtherUser asserts the
on-disk mode is 0o644 (the run_as_user contract) and that an
O_RDONLY open succeeds — same access path the spawned agent uses
for --append-system-prompt-file.
Co-authored-by: dev-claudecode <dev-claudecode@cc-connect.local>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat: add reasonix agent adapter
Adds a new agent adapter for Reasonix, a multi-model coding agent.
The adapter communicates with a running 'reasonix serve' instance via its HTTP API:
- Submits prompts via POST /submit
- Consumes agent events via SSE /events
- Handles tool approval via POST /approve
- Supports mode switching (default/yolo/plan)
- Accumulates incremental reasoning chunks into single events
* fix: handle resp.Body.Close errors (errcheck lint) and fix close-before-status order
* fix: add SSE auto-reconnect with backoff when reasonix serve restarts
* fix: add unit tests, static assertions, error body, reconnect limit
10 unit tests with httptest. All pass -race. P2 fixes: static assertions, error body, max reconnect.
* fix: lint: check error returns in session_test.go (errcheck)
* fix: lint: check remaining fmt.Fprintf error in TestSSEReconnect
* fix: add reasonix to ALL_AGENTS in Makefile, fix CHANGELOG, add doc comments
P1: Added reasonix to ALL_AGENTS in Makefile so make build includes it by default.
P2: Fixed CHANGELOG Unreleased entry, added formatImages doc comment, serve_url normalize comment, normalizeMode auto/force comment.
* chore: retrigger CI
* refactor: centralize cmd/env option parsing into core
- Add core.ParseCmdOpts() to unify cmd/cli_path/command option
parsing across all agents, with deprecation warnings for old keys
- Add core.ParseConfigEnv() to parse [projects.agent.options.env]
from config into []string KEY=VALUE format
- Rename cliBin/command struct fields to cmd consistently
- Add cliExtraArgs support so cmd="binary arg1 arg2" works
- Add configEnv field for static env that persists across SetSessionEnv
- Fix env merge order: configEnv < providerEnv < sessionEnv (was
inconsistent in copilot and opencode agents)
- Update all tests for renamed fields
* fix(claudecode): add backward compat for deprecated cli_args_flag
Users with cli_args_flag in their config.toml will see a deprecation
warning directing them to the new cmd_args_flag key.
* docs(config): update config examples to use cmd instead of deprecated keys
- config.example.toml: cli_path → cmd, command(S) → S(6 occurrences)
- claudecode/claudecode.go: comment cli_path → cmd
- copilot/copilot_test.go: comment cliBin/cli_path → cmd
* test(core): add direct unit tests for ParseCmdOpts and ParseConfigEnv
Address review feedback on PR #1297 (P2). The two helper functions in
core/cmdopts.go are depended on by 13 agent packages; previously their
behavior was only covered indirectly via agent-level New() tests. This
commit adds direct unit tests covering:
- ParseCmdOpts: four-tier priority (cmd / cli_path / command / default),
empty/whitespace boundaries, tokenization of extra args, no-warning
for canonical cmd field, and capture of deprecation warnings.
- ParseConfigEnv: nil / missing key, map[string]string, map[string]any
(TOML parser output), non-string value filtering, unsupported types,
and input non-mutation.
Also adds structured slog attrs (deprecated_key, new_key) to all three
deprecation warnings (cli_path, command, cli_args_flag) so that future
log aggregation can scan deprecated-key usage without code changes
(review feedback P3).
Ref: PR #1297 review.
* fix(codex): use local cmd var (not stale cliBin name) in struct init
Post-rebase fixup: the struct field was renamed cliBin -> cmd in
PR #1297, and the local variable returned by core.ParseCmdOpts is
also named cmd. The rebased struct initializer still referenced
the old name 'cliBin', which no longer exists in scope. Assign
the local cmd variable to the cmd struct field.
- Add readCodexModelCatalog() that reads $CODEX_HOME/config.toml,
resolves model_catalog_json path (expand ~, relative to CODEX_HOME),
and parses the JSON as the primary model list
- Extract shared parseCodexModelsJSON() to deduplicate JSON parsing
logic between readCodexCachedModels and readCodexModelCatalog
- Refactor readCodexCachedModels() to reuse resolveCodexHome(nil)
- Update AvailableModels() priority:
1. model_catalog_json (new, highest)
2. provider config
3. GET /v1/models API
4. models_cache.json
5. hardcoded fallback
- Add tests: TestAvailableModels_UsesModelCatalog, TestReadCodexModelCatalog_NoConfigFile
When /stop is issued against an ACP session, the engine was killing the
entire subprocess via Process.Kill(), destroying the session. For ACP
agents like OpenCode, the subprocess is a long-lived server that should
survive /stop — only the current turn should be cancelled.
This change introduces a clean, opt-in mechanism:
1. Adds AgentSessionCanceller interface in core/interfaces.go with a
single CancelTurn() method. Sessions that implement it signal that
/stop should cancel only the current turn, not kill the process.
2. Adds transport.sendNotification() in agent/acp/rpc.go for sending
JSON-RPC 2.0 notifications (no response expected).
3. Implements CancelTurn() on acpSession: sends an ACP session/cancel
notification to abort the current LLM request while keeping the
process alive for the next user message.
4. Modifies stopInteractiveSessionWithOptions in the engine: when the
agent session implements AgentSessionCanceller, calls CancelTurn()
instead of markStopped + delete + close. Sets eventsNeedResync=true
so stale events from the cancelled turn are drained before the next
turn starts.
The engine falls back to the existing Close() path if CancelTurn()
returns an error, ensuring backward compatibility.
Co-authored-by: SXongin <sxongin@users.noreply.github.com>
* feat(codex): support custom system_prompt / append_system_prompt config
Codex has no native system-prompt CLI flag, so these project options are
synthesized into a preamble and prepended to the first message of each new
session. Resumed sessions are not re-injected (preambleSent is preset for
resume IDs). Covers both the exec and app_server backends.
Adds config.example.toml documentation for the two new options.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(codex): check cs.Close return value to satisfy errcheck
CI lints new lines with golangci-lint --new-from-rev; the freshly added
TestSend_PrependsProjectPromptOnFreshSession used an unchecked defer
cs.Close(). Wrap it in a deferred closure that explicitly ignores the
error, matching the errcheck-clean pattern.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
cc-connect's built-in AgentSystemPrompt is ~9KB, which on its own
exceeds Windows cmd.exe's 8192-byte command-line limit when claude
is spawned with --append-system-prompt <inline content>. The
claude.exe child immediately fails with GBK stderr "命令行太长。"
(command line too long) before any user message reaches the agent.
On v1.3.3 this breaks every Feishu / IM message on Windows hosts.
Pass the merged prompt through Claude Code CLI's
--append-system-prompt-file <path> flag instead of the inline form.
Only the short file path travels on the command line, so the cap is
no longer a function of prompt size.
Common case (99% of users — no platform formatting hints, no
user-configured append_system_prompt):
* On agent construction, write the full AgentSystemPrompt() once to
<data_dir>/agent-prompts/cc-connect-system.md (default
~/.cc-connect/agent-prompts/cc-connect-system.md). claude reads
this file at every spawn; cc-connect never rewrites it unless the
cc-connect version actually changes the prompt content.
* Every spawn reuses the same file, so there is no per-spawn write
and no concurrency race (claude is a strict reader).
Edge case (Slack / Weixin / MAX with FormattingInstructions, or
project-level append_system_prompt):
* Write a per-spawn temp file under <data_dir>/agent-prompts/
holding the merged content, passed via the same flag.
* Cleaned up on session Close to avoid leaking files.
Notes for reviewers:
* claude CLI refuses --append-system-prompt and
--append-system-prompt-file together
("Cannot use both ... Please use only one"), so the merged content
must travel as a single file.
* Only the claudecode agent runtime is affected. Other agents
(codex/opencode/qoder/cursor/gemini/kimi) inject AgentSystemPrompt
via memory files (CLAUDE.md/AGENTS.md/...), which already have no
cmdline cap.
* core/interfaces.go (AgentSystemPrompt content) is unchanged from
v1.3.3; the full prompt is preserved so agent behaviour does not
regress.
Tests:
* TestEnsureSharedSystemPromptFile_WritesOnceAndReuses — verifies the
shared file is written once and skipped on identical content.
* TestEnsureSharedSystemPromptFile_RewritesOnContentChange — verifies
cc-connect upgrades refresh the shared file.
* TestEnsureSharedSystemPromptFile_EmptyDirUsesTempDir — verifies the
fallback when ccDataDir is unset.
* TestWriteTempAppendPromptFile_UniquePerCall — verifies concurrent
edge-case spawns get unique paths.
* Existing claudecode + core suites all pass (one pre-existing flake
in TestCUJ_H2_TwoPlatformsConcurrentNoBleed is unrelated to this
change and still passes when run in isolation).
E2E (Windows, MiniMax-M3 backend, Feishu platform):
* Before fix: every Feishu message produces no reply; claude.exe
fails with "命令行太长。" on stderr.
* After fix: "你好" → MiniMax intro reply; "3 分钟后帮我看看
ipconfig" → timer correctly created in data/timers/jobs.json;
"每天早上 6 点提醒我吃药" → cron correctly created in
data/crons/jobs.json. Agent uses /timer and /cron commands as
instructed by the prompt.
Fixes#1376.
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds `plugin_dir` support to `agent.options` in config.toml so that
cc-connect can pass `--plugin-dir` flags to Claude Code when spawning
sessions, enabling installed plugins (like superpowers, last30days) to
be loaded in IM-bridged sessions.
Supports both single string and array:
plugin_dir = "/path/to/plugins/cache/xyz" # single
plugin_dir = ["/path/a", "/path/b"] # multiple
The option propagates to multi-workspace agents via
WorkspaceAgentOptions().
Closes#1324
sudo -i simulates a login and runs the command from the target user's HOME,
silently overriding cmd.Dir. Under run_as_user the agent therefore ignored its
(multi-workspace) working directory and started in HOME, so it couldn't see the
bound workspace or its CLAUDE.md.
Re-establish the intended cwd inside the spawn: when SpawnOptions.WorkDir is set,
BuildSpawnCommand wraps the command to 'cd "$CC_RUNAS_CHDIR" && exec "$@"'.
The path travels via the preserved env var (not argv) so non-ASCII/space paths
survive sudo's command re-quoting.
Fixes#1312
* fix(claudecode): keep turn running on mid-turn compaction event (fixes#481)
Claude Code's stream-json protocol emits a `type:"result"` event with
`subtype:"compact"` (newer CLI) or `subtype:"compaction"` (older
CLI) when it performs automatic context compaction mid-turn. The
existing handleResult treated every result event as turn completion
(Done=true), so the engine's processInteractiveEvents loop would
return early and drop any subsequent tool calls and assistant
messages for the same turn.
Recognize the compaction subtypes and emit EventResult with
Done=false so the event loop keeps reading from the CLI process. Add
a default case to the readLoop event switch that logs unrecognized
event types at debug level (with the full payload) so future new
event shapes are diagnosable from the log stream.
Rebuild of #483. The original PR also referenced adding diagnostic
logging for unknown event types; that change is included as the new
default case.
- Add isCompactionResult / resultSubtype helpers in session.go.
- Set Done=!isCompaction in handleResult's emitted EventResult.
- Add default case to readLoop's event switch with slog.Debug.
- Add TestHandleResultCompactionSubtypeIsNotTerminal and
TestIsCompactionResult regression tests; tighten existing
TestHandleResultParsesUsage with a Done=true assertion.
* fix(core): gate EventResult case body on event.Done (fixes#481)
The previous PR #1272 fix changed the agent-side handleResult to set
Done=!isCompaction, but processInteractiveEvents in core/engine.go never
read event.Done — every EventResult unconditionally ran
cp.Finalize(Completed), AddHistory, noteUserTurnCompleted, and a final
return. So a mid-turn compaction result still caused the engine loop to
exit early, dropping subsequent tool calls and assistant messages.
Gate the entire EventResult case body on event.Done: when an agent
emits a non-terminal result (Done=false) — e.g. mid-turn auto-compact —
slog.Debug it and continue the outer loop to read the next event. All
existing turn-completion side effects and the trailing return only run
when Done=true. The agent-side Done=!isCompaction logic from #1272 is
now effective end-to-end.
Test:
- New TestProcessInteractiveEvents_NonTerminalResultContinuesTurn
pins issue #481: emits EventResult{Done:false} → EventText →
EventResult{Done:true} and asserts noteUserTurnCompleted runs exactly
once (on the terminal result) and the final reply reaches the
platform. Verified manually that without the engine fix the test
fails because the compaction event produces an empty '(empty
response)' message and the loop returns before the final result.
- Existing tests in core/engine_test.go (line 12741, 13026) and
core/relay_test.go (line 102, 184, 223, 284) that previously sent
EventResult without setting Done now set Done:true — they were
relying on the implicit-on EventResult semantics that this change
inverts.
Verified:
- go test -count=1 -tags no_web -short ./core/... ok 42s
- go test -count=1 -tags no_web ./agent/claudecode/... ok
- golangci-lint run --new-from-rev origin/main ./core/... 0 issues
---------
Co-authored-by: Claude <noreply@anthropic.com>
Extends PR #1356 coverage: each runtime now has unit tests proving that
SetActiveProvider correctly restores providerEnv and model after a
simulated cc-connect process restart (activeIdx = -1 → SetActiveProvider
called by engine's restoreActiveProviderFromSession).
Tests added:
- TestCodex_SessionResume_PreservesActiveProvider
- TestCodex_SessionResume_ModelFollowsProvider
- TestOpencode_SessionResume_PreservesActiveProvider
- TestOpencode_SessionResume_ModelFollowsProvider
- TestKimi_SessionResume_PreservesActiveProvider
- TestKimi_SessionResume_ModelFollowsProvider
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
`codex exec resume` does NOT accept the `--sandbox <mode>` flag (only
`codex exec` does). Both subcommands accept `-c key=value` config
overrides though, so on resume we now express sandbox via
`-c sandbox_mode="..."` instead of `--sandbox <mode>`.
Without this fix, every codex `exec` backend resume (cc-connect process
restart, idle-reset, etc.) failed with:
error: unexpected argument '--sandbox' found
silently destroying the user's session on each restart. This regression
was introduced by PR #1351 when the sandbox+approval flags were
generalized across exec/resume paths.
Found while QA'ing the beta release of cc-connect; see release-gate
notes:
- agents/qa-cursor/release-gate/CODEX-PERMISSION-MATRIX.md (case
CODEX-RESUME-01 + "Known bug 1")
- agents/qa-cursor/release-gate/notes/2026-06-15-beta5-followup-test-plan.md
Tests:
- New: TestBuildExecArgs_ResumeUsesSandboxModeConfigOverride asserts
that for every mode in {suggest, auto-edit, full-auto, yolo}:
* the resume invocation never contains a bare `--sandbox` flag
* sandbox is correctly expressed via `-c sandbox_mode="..."`
(or `--dangerously-bypass-approvals-and-sandbox` for yolo)
* approval_policy="never" is still enforced (exec backend has no IPC)
- Existing: TestBuildExecArgs_ModeMapping unchanged (covers first-turn
`codex exec` path which still uses `--sandbox <mode>`).
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Fixes two long-standing inconsistencies on the codex exec backend:
1. `suggest` mode hung silently. Documentation said "ask permission for every
tool call" but `codex exec` has no approval IPC, so any approval prompt
blocks waiting for a TTY response that never arrives. Now `suggest` runs
under read-only sandbox with approval_policy=never — matching what users
actually need on the exec backend, and matching how lark-coding-agent-bridge
solves the same problem.
2. `--full-auto` was removed in codex-cli 0.137. Replaced with the canonical
`--sandbox <mode>` plus `-c approval_policy="never"`. Behavior unchanged.
Mode → flags after this change (exec backend):
- suggest: --sandbox read-only + approval_policy=never
- auto-edit: --sandbox workspace-write + approval_policy=never (alias)
- full-auto: --sandbox workspace-write + approval_policy=never
- yolo: --dangerously-bypass-approvals-and-sandbox
Documentation updates clarify that real interactive approvals require the
app_server backend (which already implements execCommandApproval /
applyPatchApproval / permissionsApproval / requestUserInput).
No breaking change: mode keys unchanged, existing user configs run with the
same effective sandbox tier. The `suggest` mode goes from "hangs" to
"completes safely under read-only sandbox" — strictly a bug fix.
Adds TestBuildExecArgs_ModeMapping covering all four modes and asserting
--full-auto is no longer emitted.
Refs t-20260614-st7ft2
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Previously a `/provider switch` was held only in the agent's in-memory
`activeIdx`. The user's first message after the switch ran on the
correct provider, and `agent_session_id` was persisted to disk; but
when cc-connect restarted (or any other path that recreated the
*Agent), the in-memory active provider reset to default while the
saved agent_session_id kept the conversation going. The next user
message resumed the saved CLI session under the *default* provider's
base_url and API key, with the model name the agent still "remembered"
from the previous turn — producing 100% reproducible
"There's an issue with the selected model (X). It may not exist..."
errors against the wrong endpoint.
This change persists the user's provider choice on the Session itself
(new `Session.ActiveProvider` field, `json:"active_provider"`) so it
survives a process restart, and re-binds the agent before each resume:
* `Session` gains `ActiveProvider` plus `Get/SetActiveProvider` helpers.
* `switchProvider` writes the choice to the session and saves.
* `/provider clear` wipes the persisted choice.
* `getOrCreateInteractiveStateWith` calls a new
`restoreActiveProviderFromSession` helper before `agent.StartSession`
so the resumed CLI is spawned with the right provider env.
The restore helper is a no-op when the agent already has the right
provider active (steady-state in-process path), when the agent does
not implement ProviderSwitcher, or when the recorded provider name is
no longer registered (gracefully logs a warning instead of clobbering
the default).
Tests:
- core: TestSwitchProvider_PersistsToSession,
TestProviderClear_ClearsSessionActiveProvider,
TestRestoreActiveProviderFromSession_AllPaths (5 sub-tests
covering every branch of the helper).
- claudecode: TestClaudecode_SessionResume_PreservesActiveProvider —
end-to-end providerEnv check that the simulated post-restart
re-bind produces ANTHROPIC_BASE_URL/ANTHROPIC_MODEL identical
to the pre-restart state.
Refs internal task t-20260614-qp7xnl. Same family as #1348 (resume
loses session state); a follow-up should audit codex / opencode /
kimi for the analogous gap.
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(claudecode): respect Claude Code PermissionRequest hooks
When cc-connect bridges Claude Code via --permission-prompt-tool stdio,
Claude Code's PermissionRequest hooks are executed but their results
are ignored — the control_request is sent on stdout regardless.
This fix adds a hook runner in the claudecode agent that independently
reads and executes the user's PermissionRequest hooks from
~/.claude/settings.json before forwarding permission requests to the
messaging platform. If a hook returns "allow" or "deny", the decision
is respected directly. If the hook returns "ask" or no hook matches,
the existing behavior (forward to platform) is preserved.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(claudecode): add CC_CONNECT_PERMISSION_HOOK_SKIP env var
When cc-connect spawns Claude Code, inject
CC_CONNECT_PERMISSION_HOOK_SKIP=1 into the subprocess environment.
PermissionRequest hooks can check this to skip expensive work (e.g.
LLM calls) on the Claude Code side, since the hook result is ignored
anyway when --permission-prompt-tool stdio is active.
cc-connect's own hook runner explicitly strips this env var so the
hook does real work only once — when cc-connect calls it directly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(claudecode): improve cc_hooks protocol compat and test coverage
- Add hook_event_name field to hook stdin for Claude Code protocol compat
- Change hook timeout from 10s to 60s to match Claude Code's own timeout
- Log non-ENOENT errors when loading settings files for debuggability
- Return original data on unclosed block comments in stripJSONC
- Add tests for multi-entry fallthrough, settings merging, deny message
field, CC_CONNECT_PERMISSION_HOOK_SKIP env stripping, timeout, and
unclosed block comments
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs: add Claude Code PermissionRequest hooks section to usage guide
Explain that cc-connect re-runs hooks independently (hooks execute twice
per request) and document the CC_CONNECT_PERMISSION_HOOK_SKIP env var
for LLM-based hooks to avoid double token cost.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(claudecode): load hook settings once per session instead of caching
Replace 30s TTL cache with sync.Once — settings are read on first
tryHook call and reused for the session lifetime. Avoids repeated
JSON parsing on every permission request.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(claudecode): introduce hookContext for richer hook stdin payload
Bundle hook parameters into a hookContext struct and add fields that
match Claude Code's PermissionRequest hook input spec: permission_mode,
transcript_path, permission_suggestions, agent_id, agent_type. Optional
fields are omitted from stdin JSON when empty.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix: lint - errcheck WriteFile/MkdirAll, staticcheck QF1003 switch
* fix: all remaining errcheck WriteFile/MkdirAll in cc_hooks_test.go
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Add an `append_system_prompt` option for the Claude Code agent so users
can inject extra system-prompt text while keeping Claude's default system
prompt — unlike `system_prompt`, which replaces it entirely.
Claude CLI's --append-system-prompt only honors its last occurrence (a
second flag overwrites the first), so the cc-connect functionality prompt,
platform formatting hints, and the user's custom text are concatenated
into a single flag value via the new buildAppendSystemPrompt helper.
Co-authored-by: wangsong.ws <wangsong.ws@bytedance.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cursor Agent CLI does not always write sessions to ~/.cursor/chats.
When XDG_CONFIG_HOME is set (common on macOS dev setups), chats live
under $XDG_CONFIG_HOME/Cursor/chats instead, so /list returned empty
even though sessions existed on disk.
Most CLI sessions also keep the default name "New Agent" and wrap the
real user prompt in <user_query> tags. The old summary extractor skipped
any content starting with "<", so Feishu /list fell back to opaque
session IDs.
Scan all known CLI storage roots and derive titles from <user_query>
content so /list and /switch are usable from messaging platforms.
Co-authored-by: Cursor <cursoragent@cursor.com>
When multiple cc-connect projects share a host, a stored AgentSessionID
can point at a session file that lives in a DIFFERENT project's
~/.claude/projects/<key>/ directory. Resuming it would silently load
the wrong project's conversation history into the current project.
Adds an opt-in SessionIDValidator interface (default-agent helper
included) so the engine asks the agent whether a stored ID is valid
BEFORE calling StartSession. If the agent reports the ID does not
belong to this project, the engine clears the stored ID and starts
fresh — preventing the cross-project context leak.
The ClaudeCode agent implements ValidateSessionID by checking for a
<sessionID>.jsonl file under the per-project directory derived from
the agent's workDir.
Fresh implementation supersedes the stale #604 branch (which mixed
#599 validation with the already-merged #603 duplicate-session prune
CLI). No prune-CLI changes are included here.
Co-authored-by: Claude <noreply@anthropic.com>
Root cause: AvailableModels() with a warm persistent cache calls
startPersistentModelRefresh(), which spawns a background goroutine that
writes the refreshed model list into the test's TempDir. When the test
function returns, t.TempDir() runs RemoveAll before that goroutine
finishes its last write — manifesting as:
TempDir RemoveAll cleanup: unlinkat /tmp/.../001: directory not empty
The race window is too small to hit reliably without -cover; coverage
instrumentation slows the goroutine enough to make it reproducible.
Fix (two-part):
1. Add refreshWg sync.WaitGroup to Agent and track every background
refresh goroutine with Add(1)/Done() in startPersistentModelRefresh.
This is a pure bookkeeping addition with zero production behaviour
change.
2. In TestAvailableModels_PrefersPersistentCacheOverDiscoveredModels,
register t.Cleanup(func() { a.refreshWg.Wait() }) immediately after
creating the agent (before AvailableModels is called). Cleanup
functions run LIFO: our Wait fires before t.TempDir's RemoveAll,
guaranteeing the goroutine has finished writing before the directory
is cleaned up.
Verified: go test -count=1 -cover ./agent/opencode/... run 10 times
consecutively — 10/10 PASS, 0 failures.
Co-authored-by: root <root@UYQQVGRAEKQKNQP>
Co-authored-by: Cursor <cursoragent@cursor.com>
When opencode rejects a tool call in default mode (e.g. bash permission
denied), it emits a tool_use event with status="error" then exits without
generating any follow-up text. This caused the engine to fall through to
the "(空响应)" / "(empty response)" placeholder since textParts was empty.
Fix: when handleToolUse receives status="error" with a non-empty error
message, emit an EventText containing the rejection reason. This ensures
textParts is populated and the user sees why the command was blocked (e.g.
"The user rejected permission to use this specific tool call.") rather
than a silent empty response.
Three scenarios handled:
- Permission denied (default mode): error text surfaced ✓
- Tool completed successfully: no change ✓
- Error with no message: no spurious empty EventText ✓
To avoid permission rejections entirely, set mode="yolo" in config or
configure opencode's permission settings to allow the required tools.
Fixes#178
Co-authored-by: root <root@UYQQVGRAEKQKNQP>
Co-authored-by: Cursor <cursoragent@cursor.com>
## P1 fixes
- core/engine_test: fix stubCompactProgressPlatform.BuildRichCard signature
The last parameter was still `elapsed time.Duration` (old interface) after
#1204 changed RichCardSupporter to `statusFooter string`. The mismatch
caused the stub to silently NOT satisfy the interface at runtime, so all
tests using it were bypassing the rich-card path entirely.
- platform/slack: warn when session_scope=thread without window_per_session
Thread-scoped sessions require per-session isolation at the agent level.
When using tmux, window_per_session=true must also be set or concurrent
threads will share a single pane and their output will interleave.
## P2 fixes
- core/relay: warn on unknown visibility mode instead of silent fallback
normalizeRelayVisibility previously fell back to "full" silently for
unrecognised values. Now logs a structured Warn with valid options listed.
- platform/feishu: map common video formats to FileTypeMp4 (Media message)
CLI accepted .webm/.mov/.avi/.mkv as video, but detectFeishuFileType only
recognised .mp4, sending others as generic file downloads. Expanded the
detection to cover all common video MIME types and extensions so they
render as native Feishu video player bubbles. Playback compatibility
(e.g. webm/mkv) depends on the Feishu client platform.
- agent/{claudecode,qoder}: surface root bypass-downgrade warning to IM user
When running as root, bypassPermissions (Claude) and yolo (Qoder) modes
are silently downgraded. Users were confused why the agent kept asking for
permissions. Added StartupWarner interface (core/interfaces.go) so the
engine can emit a one-time IM notification after session start.
Co-authored-by: root <root@UYQQVGRAEKQKNQP>
Co-authored-by: Cursor <cursoragent@cursor.com>
Root cause: in --print mode, Cursor Agent sends interaction_query/request on
stdout and waits for a JSON response on its stdin before executing or rejecting
a tool (WebFetch, Bash, etc.). cc-connect had no stdin pipe connected, so
Cursor received EOF and auto-rejected every tool call silently.
Changes in agent/cursor/session.go:
- Add stdin io.WriteCloser field (protected by stdinMu) to cursorSession.
- Send() creates a StdinPipe() and stores it; readLoop closes it on exit.
- handleInteractionQuery() now:
- auto-approves when skipApproval=true (Cursor marks as safe)
- emits EventPermissionRequest (default mode) so the engine can ask the user
- auto-denies for plan/ask modes (read-only, no tool execution expected)
- auto-denies unknown query types to unblock Cursor
- writeInteractionResponse() marshals the correct Cursor response JSON
("approved"/{} or "rejected"/{reason}) and writes it to stdin.
- RespondPermission() is now a real implementation that looks up the pending
interaction query and delegates to writeInteractionResponse().
New tests in agent/cursor/permission_test.go cover:
- EventPermissionRequest emitted for webFetchRequestQuery (default mode)
- EventPermissionRequest emitted for shellRequestQuery (Windows bug scenario)
- skipApproval=true → auto-approve, no EventPermissionRequest
- plan/ask modes → auto-deny, no EventPermissionRequest
- RespondPermission approve → correct JSON on stdin
- RespondPermission deny → correct JSON with reason on stdin
- RespondPermission with no pending → no-op (no panic)
- response JSON format for all four query×outcome combinations
Co-authored-by: root <root@UYQQVGRAEKQKNQP>
Co-authored-by: Cursor <cursoragent@cursor.com>
Add support for alternative JSON formats in ACP session/update messages
to handle different vendor implementations (OpenClaw, OpenCode, etc.).
Changes:
- Add debug logging to capture raw session/update JSON for troubleshooting
- Support multiple text field formats in mapAgentMessageChunk:
- Standard ACP: content.text with type field
- Alternative: content.text without type
- Alternative: top-level text field
- Alternative: content as string
- Enhance fallback text extraction for unknown sessionUpdate types
- Add tests for all alternative formats
Fixes#432
Co-authored-by: Claude <noreply@anthropic.com>
Cursor agent's SkillDirs() only returned .claude/skills paths, so skills
placed under .cursor/skills/<name>/SKILL.md (Cursor's documented layout)
were silently invisible to /skills until users renamed the directory.
Add .cursor/skills (project work dir + user home) ahead of the existing
.claude/skills entries so the cursor-native location is searched first
while keeping backward compatibility with setups that already rely on
.claude/skills.
Fixes#786
Claude Code replaces '.' and '@' with '-' when deriving project directory
names (e.g., '/home/user/.nvm/.../@anthropic-ai/...' becomes
'-home-user--nvm-...-anthropic-ai-...'). The existing encodeClaudeProjectKey
function only replaced '/', ':', '_', ' ', '~', and non-ASCII characters,
causing findProjectDir to fail when work directories contain dots (hidden
dirs, version numbers) or @ (scoped npm packages).
This made /list always return empty for affected paths because the generated
key didn't match Claude Code's on-disk project directory name.
Closes#1046
Claude Code and Qoder reject permission-bypass flags when the
process runs as root (EUID 0). This causes the agent to crash
with exit status 1, and the nil agentSession triggers a panic in
the engine's interactive message handler.
For Claude Code: downgrade bypassPermissions mode to auto (which
auto-approves permissions internally in cc-connect, avoiding the
CLI flag entirely).
For Qoder: skip --dangerously-skip-permissions under root.
Both log a warning so the user knows why YOLO mode is degraded.
Closes#1054
Adds optional 'agent' config option under [projects.agent.options] for
opencode. When set, the value is passed as --agent to every 'opencode run'
invocation, enabling plugin-defined agents (e.g. oh-my-openagent's
'Sisyphus - Ultraworker') to work correctly without falling back to the
default agent.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add `session_scope` to the Slack platform ("user" | "channel" | "thread").
"thread" keys each Slack thread to its own cc-connect session, so a new
top-level message starts a fresh conversation while replies in the same
thread continue it. Backward compatible: when unset, behaviour is
unchanged (share_session_in_channel maps to "channel").
Add `window_per_session` to the tmux agent: each cc-connect session gets
its own tmux window (and its own init_command/agent instance) instead of
sharing one pane. Required for real isolation when the platform splits
sessions per thread; otherwise concurrent threads interleave into a single
shared agent. The allocated window name doubles as the agent session ID so
resumes reuse the same window.
Both options default off. Includes unit tests and config.example.toml docs.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pi): read enabledModels from settings.json for /model
AvailableModels() now reads ~/.pi/agent/settings.json enabledModels
instead of returning nil. New() also falls back to defaultModel
from settings when opts don't specify model.
Add helpers: piSettingsDir, settingsPath, readSettings,
readSettingsModels, readDefaultModel. Respects PI_CODING_AGENT_DIR.
* fix(pi): check err return values in tests to pass errcheck linter
* fix(pi): silence errcheck for os.Setenv/Unsetenv in defer cleanup blocks
Reply chain headers start with "---", which pi's CLI parser
interprets as an option flag, producing "Unknown option" errors.
Pass the prompt via stdin instead of as a positional argument,
consistent with how gemini and codex agents handle it.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
pi agent implemented GetWorkDir() but not SetWorkDir(), so the
WorkDirSwitcher interface was incomplete and /dir always returned
'current agent does not support dynamic work directory switching'.
* fix(pi): correct SkillDirs paths for pi agent skill discovery
cc-connect's pi agent reported wrong skill directories (~/.pi/skills/
instead of ~/.pi/agent/skills/), causing SkillRegistry to never find
pi skills. This meant slash commands like /caveman were not intercepted
by cc-connect's command routing — they fell through to pi's print mode
as raw text, and the LLM never received the skill instructions.
Fix:
- ~/.pi/skills/ -> ~/.pi/agent/skills/ (pi's default agent dir)
- Add ~/.agents/skills/ (common shared skill dir)
- Add /skills/ if env var is set
* fix(pi): correct GlobalMemoryFile path
pi loads global AGENTS.md from getAgentDir() which defaults to
~/.pi/agent/, not ~/.pi/. Respect if set.
* chore(pi): consistent homeDir variable naming in SkillDirs
Resolves Makefile conflict with #865 (copilot): both antigravity and copilot now included in ALL_AGENTS.
Co-authored-by: Cursor <cursoragent@cursor.com>
Constraint: Resolve lint blockers without behavioral changes
Rejected: Broad lifecycle refactor | unnecessary for this CI-only failure class
Confidence: high
Scope-risk: narrow
Directive: Keep file/stream close sites explicitly checked or intentionally ignored for errcheck
Tested: go test ./agent/antigravity/... && go test ./core/... && go test ./cmd/cc-connect/...
Not-tested: GitHub Actions rerun not yet executed
Constraint: Keep runtime behavior unchanged while satisfying CI lint gates
Rejected: Broader refactor around file/stream lifecycle | Unnecessary for this blocking lint failure
Confidence: high
Scope-risk: narrow
Directive: Keep close/remove calls explicitly checked or intentionally ignored with clear intent in this package
Tested: go test ./agent/antigravity/... && go test ./core/... && go test ./cmd/cc-connect/...
Not-tested: Full GitHub Actions rerun output
Constraint: Keep antigravity adapter behavior compatible across Telegram/Discord while avoiding CLI-specific crashes
Rejected: Keep passing -m and rely on user wrapper scripts | Breaks agy v1.0.2 directly for normal users
Confidence: high
Scope-risk: narrow
Directive: Re-enable explicit model flag only after agy documents stable model flag support
Tested: go test ./agent/antigravity/... && go test ./core/... && go test ./cmd/cc-connect/... && go build ./cmd/cc-connect
Not-tested: Live agy v1.0.2 interactive permission prompts on Telegram with real long-running tool tasks
- Fix handleStepStart: prefer sessionID from top-level JSON field, fallback to part
- Fix handleStepFinish: send EventResult when reason=stop, not when tool-calls
- Add resultSent atomic.Bool to prevent duplicate EventResult
- Add unit tests with JSON string construction style matching existing tests
This fixes empty response issue when opencode uses tools (issue #1094, PR #697)
Constraint: Preserve current antigravity stream/event contracts while improving permission reliability
Rejected: Full structured agy protocol parser | Upstream schema is not yet stable/public
Confidence: medium
Scope-risk: narrow
Directive: Replace regex prompt detection with typed protocol once agy exposes machine-readable permission events
Tested: go test ./agent/antigravity/... && go test ./core/... && go test ./cmd/cc-connect/... && go build ./cmd/cc-connect
Not-tested: Live Discord permission prompts across all locales/terminal themes
Constraint: Preserve existing antigravity integration surface and core event handling contracts
Rejected: Introduce a custom permission event parser for agy stdout | No stable upstream protocol contract yet
Confidence: medium
Scope-risk: narrow
Directive: If agy publishes structured permission I/O, replace y/n terminal fallback with typed request/response framing
Tested: go test ./agent/antigravity/... && go test ./core/... && go test ./cmd/cc-connect/...
Not-tested: End-to-end interactive agy permission prompt against a live CLI binary