* feat(platform/max): add MAX messenger bridge with bidirectional file and voice support
- Implement core.Platform plus InlineButtonSender, ImageSender, FileSender,
AudioSender, MessageUpdater, FormattingInstructionProvider,
ReplyContextReconstructor
- Two-step attachment upload via POST /uploads + multipart to CDN
- Route incoming audio (type=audio and audio/* file mime) to
core.Message.Audio so the speech pipeline transcribes it for the agent
- Outgoing voice replies via SendAudio for the TTS pipeline
- Retry on attachment.not.ready with exponential backoff
- Long poll /updates with reconnect backoff and allow_from filtering
- Register max in Makefile ALL_PLATFORMS and document in config.example.toml
- Tighten .gitignore: ignore config.*.toml (except example) and *.old
- 19 unit/integration tests with httptest mock, passes -race
Refs #741
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(platform/max): handle real MAX CDN response shapes and audio download
Issues discovered when testing end-to-end with the live MAX API:
1. Image CDN returns nested {"photos": {<id>: {"token": ...}}}, not a flat
{"token": ...}. Parse the right shape per kind.
2. Audio CDN returns XML "<retval>1</retval>" rather than JSON. The real
attachment token is delivered in the /uploads response itself for
video/audio, so we use urlInfo.Token as the source of truth and only
scan the CDN body opportunistically.
3. /audios/{token} endpoint does not exist in MAX (returns 404). Incoming
voice payloads already carry the download URL directly, mirroring
images/files. Use payload.url; fall back to resolveMediaURL only if
empty (for video where token resolution may still apply).
Also raise the speech HTTP client timeout from 60s to 5m — enough for the
first-request cold-start case when a local Whisper backend has to download
or JIT-load the model before responding.
Tests updated to emit the real CDN response shapes per attachment type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(platform/max): address review — dedup + idiomatic session key parse
- Add core.MessageDedup to Platform and check msg.Body.Mid / callback ID
in handleMessage and handleCallback. Guards against reprocessing on
poll-loop reconnect edge cases, matching the pattern used by feishu/
dingtalk/wecom platforms.
- Replace fmt.Sscanf in ReconstructReplyCtx with strings.CutPrefix +
strings.Cut — more idiomatic Go, simpler to read, and handles the
"max:" prefix and optional ":userID" suffix without a manual loop.
- Add TestHandleMessageDedupsByID regression test.
Addresses review feedback on #742.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(feishu): add allow_chat option to filter by group chat_id
Allow binding specific group chats to a project via the allow_chat config
option, so multiple projects can share one Feishu app while each only
responds to its designated groups. Private (P2P) chats are unaffected.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(feishu): share WebSocket connection across projects with same app_id
When multiple projects use the same Feishu app_id, Feishu's server
load-balances messages across WebSocket connections, causing messages
to randomly reach the wrong project. This change deduplicates WebSocket
connections per app_id: the first platform creates the connection
(primary), subsequent platforms reuse it (secondary). All events
(messages, card actions, bot menu) are fanned out to every platform
in the group, letting each platform's own allow_chat/allow_from
filters decide what to process.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(feishu): add group_only option to ignore P2P messages
When group_only=true, the platform skips private (P2P) messages,
useful for projects that should only operate in group chats.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ci: retrigger after flaky opencode test failure
The unit-test failure was in agent/opencode (TempDir cleanup race),
unrelated to this PR's changes. Retriggering CI.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(feishu): address review feedback on shared WS and allow_chat
1. Card action fan-out now checks allow_chat before processing, so
the wrong project no longer claims card interactions from other chats.
2. Lark webhook mode respects isPrimary — secondary platforms no longer
start their own webhook, preventing double-processing.
3. Primary Stop() warns when secondaries will lose their event source.
4. Document group_only option in config.example.toml.
5. Add unit tests for ws_shared.go (register/unregister/allPlatforms).
6. Move event handler creation after isPrimary check so secondaries
skip building unused handlers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: xiongyuanwen <xiongyuanwen@bytedance.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds native support for Devin CLI (Cognition, https://cli.devin.ai/)
as a named cc-connect agent, alongside Claude Code / Codex / Cursor /
Gemini / iFlow / Qoder / OpenCode. Users can now configure Devin with
a minimal `type = "devin"` block rather than hand-rolling a generic
ACP config.
[[projects]]
name = "my-project"
[projects.agent]
type = "devin" # NEW
[projects.agent.options]
work_dir = "/path/to/code"
# command / args / display_name default to "devin" / ["acp"] / "Devin"
# mode, auth_method, env (for WINDSURF_API_KEY) passed through to ACP
Credentials come from the local `devin auth login` store — cc-connect
never sees or forwards any token. Windsurf Enterprise users can inject
WINDSURF_API_KEY via the agent env option.
The new `agent/devin/` package is a thin wrapper around `agent/acp/`:
Devin speaks the Agent Client Protocol over stdio via its `devin acp`
subcommand, so the session/prompt, session/update, permission, and
tool-call plumbing is shared. Embedding *acp.Agent and shadowing only
Name() gives us identity and defaulting without duplicating transport
code:
type Agent struct { *acp.Agent }
func (a *Agent) Name() string { return "devin" }
Wiring Devin revealed two protocol features that agent/acp had left
as MVP-skipped TODOs. Both are needed for `/list` and `/mode` to work
in IM, so this PR fills them in as part of the Devin integration —
Cursor Agent, OpenClaw, and Copilot CLI all pick up the same
improvements automatically.
ListSessions now spawns a short-lived `<command>` process that runs
just initialize + session/list, then tears down. Capability is
discovered via `agentCapabilities.sessionCapabilities.list`, and a
negative result is cached on the Agent so non-supporting servers pay
the spawn cost only once. Soft-degrades on JSON-RPC -32601 (method
not found) and -32600 (invalid request). Measured cost against
`devin acp` 2026.4.9-0: ~40 ms round trip.
Agent implements core.ModeSwitcher (SetMode / GetMode /
PermissionModes); the modes list is sourced from the modes block
returned by session/new or session/load. GetMode precedence: the most
recent explicit SetMode wins over the last server-advertised
currentModeId, so `/mode plan` reports the new mode back to the user
immediately rather than the cached previous one.
acpSession implements core.LiveModeSwitcher via session/set_mode with
client-side modeId validation against the cached availableModes.
Client-side validation is mandatory because at least one ACP server
(devin acp 2026.4.9-0) silently accepts unknown modeIds with an empty
result — server-only validation would let typos go undetected.
session/update notifications with sessionUpdate = current_mode_update
are absorbed so cc-connect stays in sync when the user switches modes
from another client (e.g. the Windsurf IDE).
newACPSession moved from seven positional args to an acpSessionConfig
struct. Private API, not a breaking change externally; the one
exported consumer (cursor_integration_test.go) is updated.
A sessionCallbacks interface lets running acpSession instances report
handshake-derived capabilities and modes back to the parent Agent,
since the engine (not the agent) owns the session lifetime.
- README.md / README.zh-CN.md: Devin added to the Support Matrix and
the agent count bumped from 7 to 8.
- config.example.toml: dedicated Devin section placed before the
generic ACP examples, with prerequisites and override notes.
- web/src/pages/Projects/ProjectList.tsx: Devin added to the agent
dropdown so the web admin UI surfaces it alongside other
first-class agents.
- Makefile: ALL_AGENTS += devin (enables selective compilation tags
`no_devin` / AGENTS=devin,...).
22 new tests total (7 for agent/devin, 15 for agent/acp). All pass
with -race. agent/acp wrapper tests avoid requiring `devin` in PATH
by pinning `command = "true"` to satisfy the upstream exec.LookPath
check while still exercising the Name() shadow and default injection.
End-to-end verified against live `devin acp` 2026.4.9-0 through the
cc-connect bridge:
- /mode lists Devin's five modes with ▶ Code marked current
- /mode plan switches and the engine reports "switched to **Plan**"
- /list shows "devin Sessions (1)" (confirms Name() propagates to UI)
- agent field in engine logs = "devin" (not "acp")
Closes none — net-new agent support.
Generated with [Devin](https://cli.devin.ai/docs)
Co-authored-by: youngshook <youngshook@users.noreply.github.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Session filtering that hides external CLI sessions is now opt-in via
`filter_external_sessions = true` in project config. By default, /list,
/switch, /delete show all agent sessions regardless of origin.
Includes integration tests covering /list, /switch, /delete, card
rendering, and dynamic toggle under both modes, plus config parse tests.
Made-with: Cursor
- Add global [[providers]] config with project-level provider_refs
- Add provider management page in web admin with preset support
- Implement card-based /provider add flow: preset selection card
with sponsor presets, API key prompt, and manual "Other" option
- Add inline button support for Telegram-style platforms
- Add remote provider-presets.json with sponsor data
- Add REST API endpoints for global providers and provider refs
- Update project detail providers tab with unified list UI
Made-with: Cursor
Implement Weibo private message platform via the Open IM WebSocket
protocol (open-im.api.weibo.com). Supports app_id/app_secret auth,
automatic token refresh, heartbeat keep-alive, reconnect with
exponential backoff, message dedup, and chunked sending for long
messages (2000 char limit).
Includes plugin registration, build tag (no_weibo), unit tests,
setup documentation, and cross-references in all platform docs.
Made-with: Cursor
Previously done_emoji only fired when stream preview had been updated
in-place at least once, which meant quick responses never triggered
the reaction. Remove the needsDoneReaction() gate so the platform's
AddDoneReaction is always called on EventResult.
Made-with: Cursor
When the agent's response is delivered via in-place card update (Feishu
streaming preview), the user gets no push notification. This adds a
configurable done_emoji option (e.g. "Done") that adds a reaction after
the "OnIt" reaction is removed, so the user knows the agent finished.
Only activates in quiet mode when the preview card was updated in-place
at least once (via UpdateMessage). Non-quiet mode is unaffected as
intermediate messages already trigger push notifications. Single-round
responses that only use SendPreviewStart are also unaffected, as the
initial send already triggers a push notification.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(core): add optional reply footer metadata
* feat(config): enable reply footer by default
* feat(codex): use runtime footer metadata from app-server
* feat(codex): resolve exec footer metadata at runtime
* feat(core): style reply footer and prefer context left
* feat(codex): use real runtime context for reply footer
* feat(core): use english left footer text
* fix(codex): use current context tokens for footer left
* fix(codex): match cli context left calculation
* test(codex): adapt footer tests to session base url arg
When resolve_mentions is enabled in feishu platform options, outgoing
messages are scanned for @displayName and replaced with native Feishu
at tags by reverse-matching against the group member list.
- New config option: resolve_mentions (default false)
- Lazy-load group members via chat members API with 1h cache TTL
- Match names longest-first to avoid partial matches
- Auto-select at syntax based on message type (text vs card)
- No special markup needed — just @name in natural text
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Claude Code agent now supports the /reasoning and /effort commands
that were previously only available for Codex. This passes --effort to
the Claude CLI when starting sessions.
Available levels: low, medium, high, max
Configurable via reasoning_effort in [projects.agent.options].
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add run_as_user for per-project OS-user isolation
Implements #496. Adds an optional `run_as_user` field at the project
level that, when set, spawns the project's agent command under a
different Unix user via `sudo -n -iu <user> --`. This provides OS-level
file-system isolation from the supervisor user that runs cc-connect
itself: a misbehaving agent can no longer reach files outside the
target user's permission scope, even via python -c / shell redirection
/ other non-hooked code paths.
## What lands in this PR
### Config (config/)
- new fields `run_as_user *string` and `run_as_env []string` on each
`[[projects]]` entry
- validation: rejects root, rejects invalid usernames, rejects use on
Windows, enforces POSIX username charset to close a sudo argv
injection vector
### Core primitives (core/runas*.go)
POSIX-only (Windows stubs provided for API compatibility).
- `BuildSpawnCommand(ctx, opts, name, args...)` — wraps exec.Cmd with
`sudo -n -iu <user> --preserve-env=<allowlist> -- name args...` when
opts.RunAsUser is set; passthrough otherwise
- `FilterEnvForSpawn(env, opts)` — strips supervisor env to a minimal
allowlist (PATH, LANG, LC_*, TERM) plus any project-level extensions
- `VerifyRunAsUserCheap` — the two cheap sudo probes, suitable for
per-spawn re-verification (catches sudoers edits between startup and
spawn)
- `PreflightRunAsUser` — the three go/no-go startup gates from #496:
(1) passwordless sudo to target works,
(2) target cannot passwordless sudo (if escalation succeeds, captures
`sudo -n -l` output in the error message), and
(3) target can read AND write work_dir root. Plus a non-fatal
descendant scan warning on unreadable/unwritable descendants with
prune-paths, 10s timeout, and a 50-entry cap.
- `RunIsolationProbe` + embedded runas_probe.sh (//go:embed) — the
leak-audit probe codex asked for: spawns a fixed shell script as the
target user and parses its output into a structured IsolationReport.
Reports effective id/groups/umask/pwd, target-home config presence,
cross-user denial attempts for every other configured run_as_user,
and supervisor-denial attempts. Never reads file contents, only
existence and access.
All of these are testable without a real second user via an injectable
`SudoRunner` interface; production uses ExecSudoRunner.
### Failure policy
Per-spec: all checks are hard go/no-go, not warnings.
- Any preflight fatal in any project: cc-connect refuses to start
entirely; no half-spawned processes.
- Any audit-level `CROSS_LEAKED` (target reads another project user's
secrets) or `SUPERVISOR_LEAKED` (target reads the supervisor's
secrets) or `WORKDIR_WRITABLE=no`: cc-connect refuses to start.
- Descendant-scan findings are warnings (inaccessibility inside a
project tree is sometimes intentional).
### Startup wiring (cmd/cc-connect/)
- `runRunAsUserStartupChecks` runs preflight + isolation probe for
every project with run_as_user set, in parallel, BEFORE any agent
or engine is constructed. Logs every outcome via slog. On any fatal,
prints the full list and `os.Exit(1)`.
- injects project-level run_as_user / run_as_env into the agent's
options map before calling `core.CreateAgent`, so agents can opt in
without new top-level plumbing.
### Doctor subcommand (cmd/cc-connect/doctor_runas.go)
New `cc-connect doctor user-isolation [--project NAME] [--out PATH]`
subcommand. Runs preflight + isolation probe for one or all
`run_as_user` projects, writes a JSON report per project, and prints a
human-readable summary. Default output is
`~/.cc-connect/audits/<timestamp>-<project>.json`.
Also supports `--print-script` to dump the embedded probe source for
inspection (and for running shellcheck against it).
### Claude Code wiring (agent/claudecode/)
- `Agent` struct gains `runAsUser` and `runAsEnv` fields
- `New()` parses them from the opts map and skips the
`exec.LookPath("claude")` check when run_as_user is set (the target
user's PATH is what matters, not the supervisor's)
- `newClaudeSession` takes `core.SpawnOptions`, runs the per-spawn
cheap re-check immediately before building the command, uses
`core.BuildSpawnCommand` instead of `exec.CommandContext`, and
filters env via `core.FilterEnvForSpawn`
### Docs
- new "Running agents as a different Unix user" section in
`docs/usage.md` with setup steps, sudoers example, ACL example,
environment-propagation migration checklist (PGSSL*, ~/.claude/
settings.json, ~/.pgpass, ~/keys/, ~/.ssh/, MCP binaries, shell
profile, language toolchains), opt-out instructions, and
failure-mode debug guide
- README section with a short configuration example and a pointer to
the full docs
- `config.example.toml` commented example with setup recipe
### Tests
- config: parse run_as_user/run_as_env, reject root, reject invalid
chars, Windows rejection, username validator valid/invalid cases
- core: BuildSpawnCommand legacy vs. sudo form, FilterEnvForSpawn
filtering, VerifyRunAsUserCheap success/no-sudo/escalation-possible/
empty-user, PreflightRunAsUser all-pass/no-sudo/escalation-fatal/
workdir-fatal/descendant-warnings/descendant-cap, probe parser
clean/cross-leak-fatal/supervisor-leak-fatal/workdir-not-writable-
fatal, shell quoting, other-users filtering, embedded probe script
sanity
- agent/claudecode: New() parses run_as_user/run_as_env, run_as_user
skips LookPath
- cmd/cc-connect: default audit dir suffix, printHumanReport doesn't
panic on populated report
Uses stubbed SudoRunners throughout — no tests require a real second
Unix user or real sudo binary.
## Scope
Only Claude Code is wired up in this PR. Each non-claudecode agent has
its own session/spawn lifecycle (codex re-spawns per turn, cursor has
its own state model, etc.) so the sweep needs per-agent reasoning and
is deferred to a follow-up. The core primitives land here so each
future agent migration is a small, self-contained PR.
Follow-up issue tracks: codex, cursor, gemini, iflow, opencode, qoder,
acp, pi.
Refs: #496
Credit: issue framing via user + codex adversarial review
(go/no-go gates, leak-audit probe, env propagation docs,
sudo -iu rationale pinning)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(run_as_user): simplify after code review
Addresses findings from a parallel code-reuse / code-quality / efficiency
review of #498. Behavior unchanged; tests still pass.
Highlights:
- cache VerifyRunAsUserCheap results for 30s keyed by runAsUser, so
rapid-fire messages from the same Slack user don't pay the ~100ms
per-spawn sudo tax. Cache evicts on failure so the next spawn
re-verifies fresh. Adds ResetVerifyCache for tests + operators.
- doctor user-isolation now fans out across projects in parallel
(mirroring runas_startup.go) instead of iterating serially. Per-project
output is buffered and printed in order so the report stays grouped.
- collapse Agent.runAsUser + Agent.runAsEnv into a single
core.SpawnOptions field — eliminates the reassemble-at-call-site
pattern.
- fold core/runas_username.go into core/runas.go (one 17-line file →
one helper near its only caller).
- IsolationReport.RawOutput is now only populated when the audit had a
fatal — clean reports are smaller on disk.
- introduce typed string constants for the probe wire format
(tagBegin / tagWorkDirReadable / tagCrossLeaked / ...) so the parser
and runas_probe.sh share a discoverable contract.
- itoa() in tests → strconv.Itoa
- contains() helper in tests → slices.Contains
- mass comment trim across runas.go / runas_check.go / runas_audit.go:
delete WHAT-restates-name comments (~70 lines) while keeping the
load-bearing WHY blocks (sudo -n -iu rationale, env propagation
reasoning, find expression format note, scan timeout justification).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(runas): drop PATH from default env allowlist
Preserving PATH across the sudo boundary defeats the whole point of
-i: it overrides the target user's login-profile PATH with the
supervisor's, and leaks supervisor work directories (e.g. the
cc-connect source tree on the supervisor's PATH) into the isolated
session.
sudo -i now rebuilds PATH from /etc/profile + the target user's
~/.profile. Binaries the isolated agent needs (like the claude CLI)
should be installed system-wide (e.g. /usr/local/bin symlinks) or put
on the target user's own shell profile.
Caught by Q6/Q8 daemon-spawn verification in data-worklog Phase 0a:
partseeker-coder sessions were running with leigh's PATH intact.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(engine): propagate run_as_user to per-workspace agents
In multi-workspace mode, getOrCreateWorkspaceAgent constructs a fresh
opts map for each per-workspace agent instead of inheriting the
project-level opts that main.go decorated at startup. As a result,
run_as_user (and run_as_env) silently dropped out of the per-workspace
spawn path — the project-level config said partseeker-coder but every
spawn actually ran as the supervisor.
Fix: expose GetRunAsUser/GetRunAsEnv accessors on *claudecode.Agent
and copy them across in getOrCreateWorkspaceAgent via interface
assertion, mirroring the existing GetModel/GetMode pattern. Two
regression tests cover the propagation and the no-propagation case
so multi-workspace deployments that don't use run_as_user are
unaffected.
Caught by Q6/Q8 daemon-spawn verification in data-worklog Phase 0a.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add OAuth credential sharing guidance for run_as_user setups
OAuth access tokens expire after a few hours and only get refreshed by
active Claude CLI sessions. When the target user has no active session
between cc-connect spawns, its token expires and auth fails with 401.
Document the symlink + ACL pattern: target user's credentials.json
symlinks to the supervisor's copy so both share one auto-refreshed token.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: graceful session shutdown — close stdin before signals
The old Close() called context.Cancel() which sends SIGKILL immediately
via Go's exec.CommandContext, giving Claude Code zero time to run Stop
hooks. This broke claude-mem session summaries (120s timeout) and any
other cleanup that depends on a clean exit.
New three-phase shutdown:
1. Close stdin (EOF) — Claude Code exits cleanly, running Stop hooks
2. SIGTERM after 30s grace period if still alive
3. SIGKILL after 5s more as last resort
Also increases engine-level close timeout from 10s to 45s to accommodate
the longer graceful window.
Tested: all agent/claudecode and core tests pass. Verified Claude Code
exits cleanly on stdin EOF (exit code 0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: 120s graceful close + hold message during session transition
Increases the default graceful stop timeout from 30s to 120s to match
claude-mem's Stop hook timeout. The wait ends as soon as the process
exits — typical shutdowns take seconds, not the full 120s.
When a user messages during an idle-reset that triggers a graceful
close, they now see "Wrapping up your previous session (usually a few
seconds, up to 2 minutes). Your new session will start automatically."
instead of a silent delay. The hold message is sent before the blocking
close, and the new session starts immediately when the old one finishes.
Engine-level close timeout raised to 130s (120s grace + 5s SIGTERM +
5s buffer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Leigh Stillard <leigh@stillard.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(observe): add --observe CLI flag and engine config
Add observe fields to the Engine struct and a SetObserveConfig setter
to enable terminal session observation in a later task. Define --observe
and --observe-channel CLI flags (wiring deferred).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(observe): add ObserverTarget interface, implement for Slack
Define ObserverTarget optional interface in core/ for platforms that can
receive terminal observation messages. Implement SendObservation on Slack
platform using PostMessageContext with link unfurl disabled.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(observe): implement JSONL session file watcher and parser
Adds sessionObserver to core/observer.go which tails ~/.claude/projects/{projectKey}/*.jsonl
files, parses user/assistant events, filters out sdk-cli sessions (cc-connect's own),
and forwards new messages to ObserverTarget. Includes full test coverage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(observe): wire observer into engine startup and shutdown
Add findObserverTarget to locate the first ObserverTarget platform,
startObserver to launch the JSONL watcher goroutine when configured,
wired into Engine.Start() and cancelled in Engine.Stop().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(observe): wire --observe and --observe-channel flags in CLI
Removes the TODO suppressor and connects the --observe/--observe-channel
CLI flags to engine.SetObserveConfig(), resolving the Claude Code project
directory from the configured work_dir via the new resolveClaudeProjectDir helper.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(observe): add config.toml support for observe settings
Add ObserveConfig struct to ProjectConfig so users can configure terminal
session observation via [projects.observe] in config.toml, with CLI flags
(--observe, --observe-channel) taking precedence over config file values.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(observe): rune-safe truncation, robust offset tracking, interface assertion
- Use file size for offset tracking instead of line-length calculation
to avoid drift from \r\n line endings or partial final lines
- Truncate messages at valid rune boundaries to prevent garbled UTF-8
- Add compile-time ObserverTarget interface assertion for Slack
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Leigh Stillard <leigh@stillard.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(references): normalize and render local agent references
* feat(show): add reference-aware file and directory viewing
* docs: document references config and show usage
* chore: remove local working docs from PR branch
* fix: address lint issues in reference pipeline
* fix: drop stale quiet leftovers after main rebase
* fix: clean remaining rebase markers in show tests
The multi-workspace example showed `permission_mode = "yolo"` under
[projects.agent], but this key does not exist in AgentConfig and is
silently ignored. Replace with the correct [projects.agent.options]
mode = "yolo" that all other agent examples use.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(cron): add global session_mode config for cron jobs
Allow setting a default session_mode ("reuse" or "new_per_run") for all
cron jobs via [cron] config, so users don't need to set it per-job.
Per-job session_mode still overrides the global default.
* test(cron): add unit tests for global session_mode default
---------
Co-authored-by: ma_k <ma_k@ctrip.com>
The --sandbox flag requires kernel v6.2+ and is unavailable on most
headless servers. The actual issue users hit ("only ls allowed") is
the Shell tool command allowlist in --trust (default) mode, which is
resolved by switching to mode = "force" (--force).
Reverts the sandbox config option; updates config.example.toml to
clearly document that default mode restricts shell to safe commands.
Made-with: Cursor
Cursor Agent CLI defaults to sandbox mode in headless environments,
limiting available tools to read-only operations (ls, etc.). Add a
configurable `sandbox` option ("enabled"/"disabled"/"") that maps to
the `--sandbox` CLI flag, allowing users to unlock full tool access
(shell, edit, write) when running via cc-connect.
Config example:
[projects.agent.options]
sandbox = "disabled"
Made-with: Cursor
Discord Gateway and API connections can now be routed through an HTTP
proxy, configured via `proxy`, `proxy_username`, and `proxy_password`
options — matching the existing Telegram proxy support.
Made-with: Cursor
* feat(wecom): handle inbound file messages over HTTP callback
Parse MsgType=file (FileName + MediaId), download via media/get API,
and forward as core.FileAttachment for agents. Add MIME inference helper
and unit tests. Document inbound capabilities in config.example.toml.
WebSocket AI bot mode unchanged; file receive there depends on WeChat policy.
* chore(wecom): add Info/Warn logs for callback diagnostics
Log every POST (size, query flags), parsed msg_type/msg_id/from_user,
duplicate/old/allow_from drops, unsupported types at Warn, and Debug
preview of decrypted XML (truncated). Helps trace missing file pushes.
* feat(wecom-ws): receive image, file, mixed and quote media over AI bot WebSocket
Parse msgtype image/file/mixed and quote (text, voice, image, file, mixed)
per WeCom smart-bot protocol. Download encrypted media from signed URLs
and decrypt with AES-256-CBC (IV = first 16 bytes of base64 key), matching
@wecom/aibot-node-sdk. Voice accepts both content and legacy text fields.
Adds wsCollectInboundParts, deliverWSMediaInbound, unit tests for collect,
Content-Disposition parsing, and round-trip decryption.
* fix(wecom-ws): handle file in mixed payloads and normalize aeskey decoding
- Collect file items inside mixed.msg_item and merge top-level file/image
when msgtype is file/image but a non-empty mixed block is also present.
- decodeWeComAESKey: trim whitespace, URL-safe base64, padding, optional hex.
- Add regression tests for mixed+file and aeskey variants.
* fix: satisfy golangci-lint (gosimple, staticcheck)
- wecom: use := for walkMixed/walkQuote closures (S1021).
- engine: drop unused assignment from sessions.NewSession in cmdNew (SA4006).
---------
Co-authored-by: Claude <noreply@anthropic.com>
* 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>
- 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
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>
* 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 MiniMax-M2.7-highspeed to the model list comment and model entries
- Include m27fast alias for quick model switching
- Keep all previous models (M2.5, M2.5-highspeed) as alternatives
Co-authored-by: PR Bot <pr-bot@minimaxi.com>
Add support for --disallowedTools CLI flag to disable specific built-in
tools like WebSearch and WebFetch.
Why this is needed:
- Built-in WebSearch/WebFetch tools often return no results in many
regions/countries due to network restrictions or service unavailability
- Users in affected regions waste time waiting for failed searches
- Disabling these tools allows users to rely on alternative methods
(e.g., using MCP tools with local search providers)
Usage in config.toml:
[projects.agent.options]
disallowed_tools = ["WebSearch", "WebFetch"]
Co-authored-by: hushicai <hushicai@gf.com.cn>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Added new /whoami command (with /myid alias) to show user identity info
- Updated /status command to include User ID in output
- Modified allow_from guidance to recommend /whoami instead of manual lookup
- Added i18n strings and tests for new functionality
- Updated help menus and config example comments to reference /whoami
generated by llmgit
Co-Authored-By: Claude <noreply@anthropic.com>