Skip to content

Commit 513b667

Browse files
committed
feat: supports install adapter with session
Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
1 parent 79332b0 commit 513b667

File tree

17 files changed

+251
-22
lines changed

17 files changed

+251
-22
lines changed

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,13 +188,15 @@ devtap --tag cargo-test --debounce 5s -- cargo watch -x test
188188
**Cross-machine builds** — with a shared [GreptimeDB](#greptimedb-optional) instance, the build and the AI tool can run on different machines. Use `--session` to give both sides the same logical session name:
189189

190190
```bash
191+
# Machine B (your laptop) — install once, bakes --session and --store into MCP config
192+
devtap install --adapter claude-code --session myproject --store greptimedb
193+
191194
# Machine A (CI / remote build server)
192195
devtap --store greptimedb --session myproject -- make
193-
194-
# Machine B (your laptop, running the AI tool)
195-
devtap mcp-serve --store greptimedb --session myproject
196196
```
197197

198+
`devtap install` writes the `--session` and `--store` flags into the MCP config file (e.g. `.mcp.json`), so the AI tool's MCP server automatically connects to the right GreptimeDB instance and session.
199+
198200
Multiple build machines can write to the same session simultaneously — each entry is tagged with its source, and the AI tool drains them all.
199201

200202
**Session auto-detection** — when `--session auto` (default), devtap resolves the project directory like this:
@@ -231,7 +233,7 @@ Flags:
231233
--debounce <dur> Flush interval for captured output (default "2s", 0 to disable)
232234
233235
Subcommands:
234-
install Configure AI tool integration
236+
install Configure AI tool integration (--session and --store are forwarded to MCP config)
235237
mcp-serve Start MCP stdio server
236238
drain Read pending messages as plain text
237239
status Show pending message counts

cmd/devtap/install.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,27 @@ func runInstall(cmd *cobra.Command, args []string) error {
3131
adapterName, _ := cmd.Flags().GetString("adapter")
3232
autoLoop, _ := cmd.Flags().GetBool("auto-loop")
3333
maxRetries, _ := cmd.Flags().GetInt("max-retries")
34+
sessionFlag, _ := cmd.Flags().GetString("session")
35+
storeFlag, _ := cmd.Flags().GetString("store")
3436

3537
projectDir, err := os.Getwd()
3638
if err != nil {
3739
return fmt.Errorf("get working directory: %w", err)
3840
}
3941

42+
var extraArgs []string
43+
if sessionFlag != "" && sessionFlag != "auto" {
44+
extraArgs = append(extraArgs, "--session", sessionFlag)
45+
}
46+
if storeFlag != "" {
47+
extraArgs = append(extraArgs, "--store", storeFlag)
48+
}
49+
4050
config := adapter.InstallConfig{
4151
ProjectDir: projectDir,
4252
AutoLoop: autoLoop,
4353
MaxRetries: maxRetries,
54+
ExtraArgs: extraArgs,
4455
}
4556

4657
a := getAdapter(adapterName)

internal/adapter/adapter.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ type InstallConfig struct {
1212
ProjectDir string
1313
AutoLoop bool
1414
MaxRetries int
15+
// ExtraArgs are additional CLI flags appended to mcp-serve / drain commands
16+
// (e.g. ["--session", "myproject", "--store", "greptimedb"]).
17+
ExtraArgs []string
1518
}
1619

1720
// Adapter abstracts the integration with different AI coding tools.

internal/adapter/aider/adapter.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,24 @@ func (a *Adapter) Install(config adapter.InstallConfig) error {
4848
return nil
4949
}
5050

51-
if err := createLintScript(config.ProjectDir); err != nil {
51+
if err := createLintScript(config.ProjectDir, config.ExtraArgs); err != nil {
5252
return err
5353
}
5454
installInstruction(config.ProjectDir)
5555
return nil
5656
}
5757

58-
func createLintScript(projectDir string) error {
58+
func createLintScript(projectDir string, extraArgs []string) error {
5959
binPath, err := os.Executable()
6060
if err != nil {
6161
binPath = "devtap"
6262
}
6363

64+
drainCmd := fmt.Sprintf("%s drain", binPath)
65+
for _, arg := range extraArgs {
66+
drainCmd += fmt.Sprintf(" %s", arg)
67+
}
68+
6469
// The lint script drains pending devtap messages and outputs plain text.
6570
// aider --lint-cmd expects: outputs errors to stdout, non-zero exit if errors.
6671
script := fmt.Sprintf(`#!/bin/sh
@@ -70,15 +75,15 @@ func createLintScript(projectDir string) error {
7075
# This script is called by aider after each edit.
7176
# It drains pending build errors from devtap and presents them to aider.
7277
73-
OUTPUT=$(%s drain 2>/dev/null)
78+
OUTPUT=$(%s 2>/dev/null)
7479
7580
if [ -n "$OUTPUT" ]; then
7681
echo "$OUTPUT"
7782
exit 1
7883
fi
7984
8085
exit 0
81-
`, lintScriptName, binPath)
86+
`, lintScriptName, drainCmd)
8287

8388
scriptPath := filepath.Join(projectDir, lintScriptName)
8489
return os.WriteFile(scriptPath, []byte(script), 0o755)

internal/adapter/aider/adapter_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,35 @@ func TestInstall(t *testing.T) {
3636
}
3737
}
3838

39+
func TestInstallExtraArgs(t *testing.T) {
40+
a := New()
41+
dir := t.TempDir()
42+
43+
config := adapter.InstallConfig{
44+
ProjectDir: dir,
45+
ExtraArgs: []string{"--session", "myproject", "--store", "greptimedb"},
46+
}
47+
if err := a.Install(config); err != nil {
48+
t.Fatalf("Install: %v", err)
49+
}
50+
51+
content, err := os.ReadFile(filepath.Join(dir, lintScriptName))
52+
if err != nil {
53+
t.Fatalf("read lint script: %v", err)
54+
}
55+
script := string(content)
56+
57+
if !strings.Contains(script, "--session myproject") {
58+
t.Error("lint script should contain --session myproject")
59+
}
60+
if !strings.Contains(script, "--store greptimedb") {
61+
t.Error("lint script should contain --store greptimedb")
62+
}
63+
if !strings.Contains(script, "drain") {
64+
t.Error("lint script should contain drain command")
65+
}
66+
}
67+
3968
func TestDiscoverSessions(t *testing.T) {
4069
a := New()
4170
sessions, err := a.DiscoverSessions("/tmp/test-project")

internal/adapter/claudecode/adapter.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func (a *Adapter) Install(config adapter.InstallConfig) error {
3131
return nil
3232
}
3333

34-
if err := writeMCPConfig(config.ProjectDir); err != nil {
34+
if err := writeMCPConfig(config.ProjectDir, config.ExtraArgs); err != nil {
3535
return err
3636
}
3737

internal/adapter/claudecode/hooks.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
const mcpConfigName = ".mcp.json"
1414

1515
// writeMCPConfig writes or merges devtap MCP server config into .mcp.json.
16-
func writeMCPConfig(projectDir string) error {
16+
func writeMCPConfig(projectDir string, extraArgs []string) error {
1717
binPath, err := devtapBinaryPath()
1818
if err != nil {
1919
return err
@@ -38,9 +38,10 @@ func writeMCPConfig(projectDir string) error {
3838
servers = make(map[string]any)
3939
}
4040

41+
args := append([]string{"mcp-serve"}, extraArgs...)
4142
servers["devtap"] = map[string]any{
4243
"command": binPath,
43-
"args": []string{"mcp-serve"},
44+
"args": args,
4445
}
4546

4647
existing["mcpServers"] = servers
@@ -88,6 +89,9 @@ func installStopHook(config adapter.InstallConfig) error {
8889
maxRetries = 5
8990
}
9091
stopCmd := fmt.Sprintf("\"%s\" drain --event Stop --auto-loop --max-retries %d", binPath, maxRetries)
92+
for _, arg := range config.ExtraArgs {
93+
stopCmd += fmt.Sprintf(" %s", arg)
94+
}
9195
hooks["Stop"] = upsertMatcherGroup(hooks["Stop"], "", stopCmd)
9296

9397
settings["hooks"] = hooks

internal/adapter/claudecode/hooks_test.go

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212

1313
func TestWriteMCPConfig(t *testing.T) {
1414
dir := t.TempDir()
15-
if err := writeMCPConfig(dir); err != nil {
15+
if err := writeMCPConfig(dir, nil); err != nil {
1616
t.Fatalf("writeMCPConfig: %v", err)
1717
}
1818

@@ -66,7 +66,7 @@ func TestWriteMCPConfigMergesExisting(t *testing.T) {
6666
t.Fatalf("write existing: %v", err)
6767
}
6868

69-
if err := writeMCPConfig(dir); err != nil {
69+
if err := writeMCPConfig(dir, nil); err != nil {
7070
t.Fatalf("writeMCPConfig: %v", err)
7171
}
7272

@@ -89,6 +89,69 @@ func TestWriteMCPConfigMergesExisting(t *testing.T) {
8989
}
9090
}
9191

92+
func TestWriteMCPConfigExtraArgs(t *testing.T) {
93+
dir := t.TempDir()
94+
extra := []string{"--session", "myproject", "--store", "greptimedb"}
95+
if err := writeMCPConfig(dir, extra); err != nil {
96+
t.Fatalf("writeMCPConfig: %v", err)
97+
}
98+
99+
data, err := os.ReadFile(filepath.Join(dir, ".mcp.json"))
100+
if err != nil {
101+
t.Fatalf("read .mcp.json: %v", err)
102+
}
103+
104+
var config map[string]any
105+
if err := json.Unmarshal(data, &config); err != nil {
106+
t.Fatalf("unmarshal: %v", err)
107+
}
108+
109+
servers := config["mcpServers"].(map[string]any)
110+
devtap := servers["devtap"].(map[string]any)
111+
args := devtap["args"].([]any)
112+
113+
want := []string{"mcp-serve", "--session", "myproject", "--store", "greptimedb"}
114+
if len(args) != len(want) {
115+
t.Fatalf("args length: got %d, want %d", len(args), len(want))
116+
}
117+
for i, w := range want {
118+
if args[i] != w {
119+
t.Errorf("args[%d]: got %v, want %s", i, args[i], w)
120+
}
121+
}
122+
}
123+
124+
func TestInstallExtraArgsInStopHook(t *testing.T) {
125+
a := New()
126+
dir := t.TempDir()
127+
128+
config := adapter.InstallConfig{
129+
ProjectDir: dir,
130+
AutoLoop: true,
131+
MaxRetries: 3,
132+
ExtraArgs: []string{"--session", "myproject"},
133+
}
134+
if err := a.Install(config); err != nil {
135+
t.Fatalf("Install: %v", err)
136+
}
137+
138+
// Check .mcp.json has extra args
139+
mcpData, _ := os.ReadFile(filepath.Join(dir, ".mcp.json"))
140+
if !strings.Contains(string(mcpData), "--session") {
141+
t.Error(".mcp.json should contain --session")
142+
}
143+
144+
// Check Stop hook has extra args
145+
settingsFile, _ := settingsPath()
146+
settingsData, err := os.ReadFile(settingsFile)
147+
if err != nil {
148+
t.Fatalf("read settings.json: %v", err)
149+
}
150+
if !strings.Contains(string(settingsData), "--session myproject") {
151+
t.Errorf("Stop hook should contain --session myproject, got: %s", settingsData)
152+
}
153+
}
154+
92155
func TestUpsertMatcherGroup(t *testing.T) {
93156
// Create initial group
94157
list := upsertMatcherGroup(nil, "", "devtap drain --event Stop")

internal/adapter/codex/adapter.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func (a *Adapter) Install(config adapter.InstallConfig) error {
4040
return nil
4141
}
4242

43-
if err := writeMCPConfig(config.ProjectDir); err != nil {
43+
if err := writeMCPConfig(config.ProjectDir, config.ExtraArgs); err != nil {
4444
return err
4545
}
4646
installInstruction(config.ProjectDir)

internal/adapter/codex/adapter_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,41 @@ args = ["--flag"]
7272
}
7373
}
7474

75+
func TestInstallExtraArgs(t *testing.T) {
76+
a := New()
77+
dir := t.TempDir()
78+
79+
config := adapter.InstallConfig{
80+
ProjectDir: dir,
81+
ExtraArgs: []string{"--session", "myproject", "--store", "greptimedb"},
82+
}
83+
if err := a.Install(config); err != nil {
84+
t.Fatalf("Install: %v", err)
85+
}
86+
87+
configPath := filepath.Join(dir, ".codex", "config.toml")
88+
data, err := os.ReadFile(configPath)
89+
if err != nil {
90+
t.Fatalf("read config.toml: %v", err)
91+
}
92+
93+
var cfg codexConfig
94+
if _, err := toml.Decode(string(data), &cfg); err != nil {
95+
t.Fatalf("invalid TOML: %v", err)
96+
}
97+
98+
entry := cfg.MCPServers["devtap"]
99+
want := []string{"mcp-serve", "--session", "myproject", "--store", "greptimedb"}
100+
if len(entry.Args) != len(want) {
101+
t.Fatalf("args length: got %d, want %d", len(entry.Args), len(want))
102+
}
103+
for i, w := range want {
104+
if entry.Args[i] != w {
105+
t.Errorf("args[%d]: got %q, want %q", i, entry.Args[i], w)
106+
}
107+
}
108+
}
109+
75110
func TestDiscoverSessions(t *testing.T) {
76111
a := New()
77112
sessions, err := a.DiscoverSessions("/tmp/test-project")

0 commit comments

Comments
 (0)