Files
cc-connect/Makefile
T
1b8a0843ef feat(agent): add Devin CLI as a first-class agent (#672)
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>
2026-04-21 21:19:30 +08:00

175 lines
5.3 KiB
Makefile

APP := cc-connect
MODULE := github.com/chenhg5/cc-connect
CMD := ./cmd/cc-connect
DIST := dist
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "none")
BUILD_TIME := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ')
LDFLAGS := -s -w \
-X main.version=$(VERSION) \
-X main.commit=$(COMMIT) \
-X main.buildTime=$(BUILD_TIME)
PLATFORMS := \
linux/amd64 \
linux/arm64 \
darwin/amd64 \
darwin/arm64 \
windows/amd64 \
windows/arm64
# ---------------------------------------------------------------------------
# Selective compilation via build tags.
#
# By default all agents and platforms are included. To build with only
# specific ones, set AGENTS and/or PLATFORMS_INCLUDE:
#
# make build AGENTS=claudecode PLATFORMS_INCLUDE=feishu,telegram
#
# You can also exclude specific ones:
#
# make build EXCLUDE=discord,dingtalk,qq,qqbot,line
# ---------------------------------------------------------------------------
ALL_AGENTS := acp claudecode codex cursor devin gemini iflow kimi opencode pi qoder
ALL_PLATFORMS := feishu telegram discord slack dingtalk wecom weixin qq qqbot line weibo
ALL_EXTRAS := web
COMMA := ,
# Compute exclusion tags from AGENTS / PLATFORMS_INCLUDE / EXCLUDE variables
_EXCLUDE_TAGS :=
ifdef AGENTS
_WANTED_AGENTS := $(subst $(COMMA), ,$(AGENTS))
_EXCLUDE_AGENTS := $(filter-out $(_WANTED_AGENTS),$(ALL_AGENTS))
_EXCLUDE_TAGS += $(addprefix no_,$(_EXCLUDE_AGENTS))
endif
ifdef PLATFORMS_INCLUDE
_WANTED_PLATFORMS := $(subst $(COMMA), ,$(PLATFORMS_INCLUDE))
_EXCLUDE_PLATFORMS := $(filter-out $(_WANTED_PLATFORMS),$(ALL_PLATFORMS))
_EXCLUDE_TAGS += $(addprefix no_,$(_EXCLUDE_PLATFORMS))
endif
ifdef EXCLUDE
_EXCLUDE_TAGS += $(addprefix no_,$(subst $(COMMA), ,$(EXCLUDE)))
endif
ifdef NO_WEB
_EXCLUDE_TAGS += no_web
endif
_BUILD_TAGS := $(strip $(_EXCLUDE_TAGS))
_TAGS_FLAG := $(if $(_BUILD_TAGS),-tags '$(_BUILD_TAGS)',)
.PHONY: build run clean test test-fast test-full test-smoke test-e2e test-release test-performance pre-test lint release release-all web
web:
@if [ ! -d web/node_modules ]; then cd web && npm install; fi
cd web && npm run build
build: web
go build $(_TAGS_FLAG) -ldflags "$(LDFLAGS)" -o $(APP) $(CMD)
build-noweb:
go build $(_TAGS_FLAG) -tags 'no_web' -ldflags "$(LDFLAGS)" -o $(APP) $(CMD)
run: build
./$(APP)
clean:
rm -f $(APP)
rm -rf $(DIST)
# ---------------------------------------------------------------------------
# Testing targets.
#
# test-fast: Unit tests + smoke tests (< 2 min). Runs on every push.
# test-full: Full test suite including regression (< 10 min). PR requirement.
# test-smoke: Smoke tests only (< 1 min). Quick sanity check.
# test-e2e: E2E and regression tests only.
# test-release: Full + performance benchmarks. Before release.
# pre-test: Prerequisites (build + vet) before running tests.
# ---------------------------------------------------------------------------
pre-test:
go build ./...
go vet ./...
# Fast test: unit tests + smoke tests
test-fast: pre-test
go test -parallel=4 -race ./...
go test -parallel=4 -tags=smoke ./tests/e2e/...
# Full test: unit + smoke + regression (PR requirement)
test-full: pre-test
go test -parallel=4 -race ./...
go test -parallel=4 -tags=smoke ./tests/e2e/...
go test -parallel=2 -tags=regression ./tests/e2e/...
# Smoke tests only
test-smoke: pre-test
go test -v -tags=smoke ./tests/e2e/...
# E2E/regression tests only
test-e2e: pre-test
go test -v -tags=regression ./tests/e2e/...
# Performance benchmarks only
test-performance: pre-test
go test -bench=. -benchmem -tags=performance ./tests/performance/...
# Release test: full + performance benchmarks
test-release: pre-test
go test -parallel=4 -race ./...
go test -parallel=4 -tags=smoke ./tests/e2e/...
go test -parallel=2 -tags=regression ./tests/e2e/...
go test -bench=. -benchmem -tags=performance ./tests/performance/...
# Legacy: runs unit tests only
test:
go test -v ./...
lint:
golangci-lint run ./...
release-all: clean
@mkdir -p $(DIST)
@$(foreach platform,$(PLATFORMS), \
$(eval GOOS := $(word 1,$(subst /, ,$(platform)))) \
$(eval GOARCH := $(word 2,$(subst /, ,$(platform)))) \
$(eval EXT := $(if $(filter windows,$(GOOS)),.exe,)) \
$(eval OUT := $(DIST)/$(APP)-$(VERSION)-$(GOOS)-$(GOARCH)$(EXT)) \
echo "Building $(OUT)" && \
GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=0 \
go build $(_TAGS_FLAG) -ldflags "$(LDFLAGS)" -o $(OUT) $(CMD) && \
) true
@echo "Packaging archives..."
@cd $(DIST) && for f in $(APP)-*; do \
case "$$f" in \
*.tar.gz|*.zip) continue ;; \
*.exe) zip "$${f%.exe}.zip" "$$f" ;; \
*) tar czf "$$f.tar.gz" "$$f" ;; \
esac; \
done
@cd $(DIST) && sha256sum * > checksums.txt
@echo "Done. Binaries and archives in $(DIST)/"
release:
@if [ -z "$(TARGET)" ]; then \
echo "Usage: make release TARGET=linux/amd64"; \
echo "Available: $(PLATFORMS)"; \
exit 1; \
fi
@mkdir -p $(DIST)
$(eval GOOS := $(word 1,$(subst /, ,$(TARGET))))
$(eval GOARCH := $(word 2,$(subst /, ,$(TARGET))))
$(eval EXT := $(if $(filter windows,$(GOOS)),.exe,))
$(eval OUT := $(DIST)/$(APP)-$(VERSION)-$(GOOS)-$(GOARCH)$(EXT))
GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=0 \
go build $(_TAGS_FLAG) -ldflags "$(LDFLAGS)" -o $(OUT) $(CMD)
@echo "Built: $(OUT)"