@hua-labs/tap 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +194 -194
- package/dist/bridges/codex-app-server-bridge.d.mts +10 -1
- package/dist/bridges/codex-app-server-bridge.mjs +93 -37
- package/dist/bridges/codex-app-server-bridge.mjs.map +1 -1
- package/dist/bridges/codex-bridge-runner.mjs +2 -0
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
- package/dist/cli.mjs +598 -109
- package/dist/cli.mjs.map +1 -1
- package/dist/index.mjs +275 -66
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-server.mjs +185 -185
- package/dist/mcp-server.mjs.map +1 -1
- package/package.json +65 -65
package/README.md
CHANGED
|
@@ -1,194 +1,194 @@
|
|
|
1
|
-
# @hua-labs/tap
|
|
2
|
-
|
|
3
|
-
Zero-dependency CLI for cross-model AI agent communication setup.
|
|
4
|
-
|
|
5
|
-
One command to connect Claude, Codex, and Gemini agents through a shared file-based communication layer.
|
|
6
|
-
|
|
7
|
-
## Quick Start
|
|
8
|
-
|
|
9
|
-
> `bun` is required to run the managed tap MCP server. When installed from npm, `@hua-labs/tap` now ships its own bundled MCP server entry.
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
# 1. Initialize comms directory and state
|
|
13
|
-
npx @hua-labs/tap init
|
|
14
|
-
|
|
15
|
-
# 2. Add runtimes
|
|
16
|
-
npx @hua-labs/tap add claude
|
|
17
|
-
npx @hua-labs/tap add codex
|
|
18
|
-
npx @hua-labs/tap add gemini
|
|
19
|
-
|
|
20
|
-
# 3. Check status
|
|
21
|
-
npx @hua-labs/tap status
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Your agents can now communicate through the shared comms directory.
|
|
25
|
-
|
|
26
|
-
## Commands
|
|
27
|
-
|
|
28
|
-
### `init`
|
|
29
|
-
|
|
30
|
-
Initialize the comms directory and `.tap-comms/` state.
|
|
31
|
-
|
|
32
|
-
By default, the comms directory is created inside the current repo at `./tap-comms`.
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
npx @hua-labs/tap init
|
|
36
|
-
npx @hua-labs/tap init --comms-dir /path/to/comms
|
|
37
|
-
npx @hua-labs/tap init --permissions safe # default: deny destructive ops
|
|
38
|
-
npx @hua-labs/tap init --permissions full # no restrictions (use with caution)
|
|
39
|
-
npx @hua-labs/tap init --force # re-initialize
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
### `add <runtime>`
|
|
43
|
-
|
|
44
|
-
Add a runtime. Probes config, plans patches, applies, and verifies.
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
npx @hua-labs/tap add claude
|
|
48
|
-
npx @hua-labs/tap add codex
|
|
49
|
-
npx @hua-labs/tap add gemini
|
|
50
|
-
npx @hua-labs/tap add claude --force # re-install
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### `remove <runtime>`
|
|
54
|
-
|
|
55
|
-
Remove a runtime and rollback config changes.
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
npx @hua-labs/tap remove claude
|
|
59
|
-
npx @hua-labs/tap remove codex
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### `status`
|
|
63
|
-
|
|
64
|
-
Show installed runtimes and their status.
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
npx @hua-labs/tap status
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Output shows three status levels:
|
|
71
|
-
|
|
72
|
-
- **installed** — config written but not verified
|
|
73
|
-
- **configured** — config written and verified
|
|
74
|
-
- **active** — runtime is running and connected
|
|
75
|
-
|
|
76
|
-
### `serve`
|
|
77
|
-
|
|
78
|
-
Start the tap-comms MCP server (stdio). Convenience command for running the MCP server locally.
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
npx @hua-labs/tap serve
|
|
82
|
-
npx @hua-labs/tap serve --comms-dir /path/to/comms
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
Requires `bun`. Uses the bundled MCP server entry from `@hua-labs/tap`, with a repo-local fallback for monorepo checkouts.
|
|
86
|
-
|
|
87
|
-
## Supported Runtimes
|
|
88
|
-
|
|
89
|
-
| Runtime | Config | Bridge | Mode |
|
|
90
|
-
| ------- | ----------------------- | ---------------------- | ------------------ |
|
|
91
|
-
| Claude | `.mcp.json` | native-push (fs.watch) | No daemon needed |
|
|
92
|
-
| Codex | `~/.codex/config.toml` | WebSocket bridge | Daemon per session |
|
|
93
|
-
| Gemini | `.gemini/settings.json` | polling | No daemon needed |
|
|
94
|
-
|
|
95
|
-
## `--json` Flag
|
|
96
|
-
|
|
97
|
-
All commands support `--json` for machine-readable output. Returns a single JSON object to stdout with no human log noise.
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
npx @hua-labs/tap status --json
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
```json
|
|
104
|
-
{
|
|
105
|
-
"ok": true,
|
|
106
|
-
"command": "status",
|
|
107
|
-
"code": "TAP_STATUS_OK",
|
|
108
|
-
"message": "2 runtime(s) installed",
|
|
109
|
-
"warnings": [],
|
|
110
|
-
"data": {
|
|
111
|
-
"version": "0.2.2",
|
|
112
|
-
"commsDir": "/path/to/comms",
|
|
113
|
-
"runtimes": {
|
|
114
|
-
"claude": { "status": "active", "bridgeMode": "native-push" },
|
|
115
|
-
"codex": { "status": "configured", "bridgeMode": "app-server" }
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
Error codes use `TAP_*` prefix: `TAP_ADD_OK`, `TAP_NO_OP`, `TAP_PATCH_FAILED`, etc.
|
|
122
|
-
|
|
123
|
-
Exit codes: `0` = ok, `1` = error.
|
|
124
|
-
|
|
125
|
-
## Permissions
|
|
126
|
-
|
|
127
|
-
`tap init` auto-configures runtime permissions.
|
|
128
|
-
|
|
129
|
-
### Safe mode (default)
|
|
130
|
-
|
|
131
|
-
**Claude**: Adds deny rules to `.claude/settings.local.json` blocking destructive operations (force push, hard reset, rm -rf, etc.).
|
|
132
|
-
|
|
133
|
-
**Codex**: Sets `workspace-write` sandbox, `full` network access, trusted project paths, and writable roots in `~/.codex/config.toml`.
|
|
134
|
-
|
|
135
|
-
### Full mode
|
|
136
|
-
|
|
137
|
-
```bash
|
|
138
|
-
npx @hua-labs/tap init --permissions full
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
**Claude**: Removes tap-managed deny rules. User-added rules preserved.
|
|
142
|
-
|
|
143
|
-
**Codex**: Sets `danger-full-access` sandbox. Use on trusted local machines only.
|
|
144
|
-
|
|
145
|
-
## How It Works
|
|
146
|
-
|
|
147
|
-
Agents communicate through a shared directory (`comms/`) using markdown files:
|
|
148
|
-
|
|
149
|
-
```
|
|
150
|
-
comms/
|
|
151
|
-
├── inbox/ # Agent-to-agent messages
|
|
152
|
-
├── reviews/ # Code review results
|
|
153
|
-
├── findings/ # Out-of-scope discoveries
|
|
154
|
-
├── handoff/ # Session handoff documents
|
|
155
|
-
├── retros/ # Retrospectives
|
|
156
|
-
└── archive/ # Archived messages
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
Each runtime has an adapter that:
|
|
160
|
-
|
|
161
|
-
1. **Probes** — finds config files, checks runtime installation
|
|
162
|
-
2. **Plans** — determines what patches to apply
|
|
163
|
-
3. **Applies** — backs up and patches config files
|
|
164
|
-
4. **Verifies** — confirms the runtime can read the config
|
|
165
|
-
|
|
166
|
-
The adapter contract (`RuntimeAdapter`) is the extension point for adding new runtimes.
|
|
167
|
-
|
|
168
|
-
## Changelog (0.2.2)
|
|
169
|
-
|
|
170
|
-
### Bridge
|
|
171
|
-
|
|
172
|
-
- **Auth gateway** — Managed bridge now includes an auth proxy with timing-safe token validation (M99)
|
|
173
|
-
- **`--no-auth` flag** — Skip auth gateway for localhost-only setups; app-server listens directly on public port (M102)
|
|
174
|
-
- **TUI connect URL** — `bridge start` and `bridge status` output shows where to connect Codex TUI (M102)
|
|
175
|
-
- **Identity routing** — Bridge matches inbox messages by both `agentId` and `agentName`; self echo-back filtered by both (M101)
|
|
176
|
-
- **Display labels** — Bridge prompts, `tap_who`, and notifications use `name [id]` format (M101)
|
|
177
|
-
|
|
178
|
-
### CLI
|
|
179
|
-
|
|
180
|
-
- **`tap doctor`** — Diagnose comms, bridge, message, and MCP issues (M95)
|
|
181
|
-
- **`tap doctor --fix`** — Auto-fix common issues with post-fix revalidation (M100)
|
|
182
|
-
- **Error codes** — 24 CLI error codes with consistent `TAP_*` prefix (M91)
|
|
183
|
-
- **Boot streamline** — Faster CLI startup with agent-name persistence (M92)
|
|
184
|
-
|
|
185
|
-
### Infrastructure
|
|
186
|
-
|
|
187
|
-
- **Auto-poll fallback** — Bridge falls back to polling when fs.watch is unavailable (M93)
|
|
188
|
-
- **Watcher dedup** — Root-cause fix for duplicate message dispatch (M90)
|
|
189
|
-
- **tap-plugin test infra** — In-memory test harness for MCP channel tests (M94)
|
|
190
|
-
- **Blind test CI** — Cross-model communication verification framework (M98)
|
|
191
|
-
|
|
192
|
-
## License
|
|
193
|
-
|
|
194
|
-
MIT
|
|
1
|
+
# @hua-labs/tap
|
|
2
|
+
|
|
3
|
+
Zero-dependency CLI for cross-model AI agent communication setup.
|
|
4
|
+
|
|
5
|
+
One command to connect Claude, Codex, and Gemini agents through a shared file-based communication layer.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
> `bun` is required to run the managed tap MCP server. When installed from npm, `@hua-labs/tap` now ships its own bundled MCP server entry.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# 1. Initialize comms directory and state
|
|
13
|
+
npx @hua-labs/tap init
|
|
14
|
+
|
|
15
|
+
# 2. Add runtimes
|
|
16
|
+
npx @hua-labs/tap add claude
|
|
17
|
+
npx @hua-labs/tap add codex
|
|
18
|
+
npx @hua-labs/tap add gemini
|
|
19
|
+
|
|
20
|
+
# 3. Check status
|
|
21
|
+
npx @hua-labs/tap status
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Your agents can now communicate through the shared comms directory.
|
|
25
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
|
|
28
|
+
### `init`
|
|
29
|
+
|
|
30
|
+
Initialize the comms directory and `.tap-comms/` state.
|
|
31
|
+
|
|
32
|
+
By default, the comms directory is created inside the current repo at `./tap-comms`.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx @hua-labs/tap init
|
|
36
|
+
npx @hua-labs/tap init --comms-dir /path/to/comms
|
|
37
|
+
npx @hua-labs/tap init --permissions safe # default: deny destructive ops
|
|
38
|
+
npx @hua-labs/tap init --permissions full # no restrictions (use with caution)
|
|
39
|
+
npx @hua-labs/tap init --force # re-initialize
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### `add <runtime>`
|
|
43
|
+
|
|
44
|
+
Add a runtime. Probes config, plans patches, applies, and verifies.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx @hua-labs/tap add claude
|
|
48
|
+
npx @hua-labs/tap add codex
|
|
49
|
+
npx @hua-labs/tap add gemini
|
|
50
|
+
npx @hua-labs/tap add claude --force # re-install
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `remove <runtime>`
|
|
54
|
+
|
|
55
|
+
Remove a runtime and rollback config changes.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx @hua-labs/tap remove claude
|
|
59
|
+
npx @hua-labs/tap remove codex
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### `status`
|
|
63
|
+
|
|
64
|
+
Show installed runtimes and their status.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npx @hua-labs/tap status
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Output shows three status levels:
|
|
71
|
+
|
|
72
|
+
- **installed** — config written but not verified
|
|
73
|
+
- **configured** — config written and verified
|
|
74
|
+
- **active** — runtime is running and connected
|
|
75
|
+
|
|
76
|
+
### `serve`
|
|
77
|
+
|
|
78
|
+
Start the tap-comms MCP server (stdio). Convenience command for running the MCP server locally.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npx @hua-labs/tap serve
|
|
82
|
+
npx @hua-labs/tap serve --comms-dir /path/to/comms
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Requires `bun`. Uses the bundled MCP server entry from `@hua-labs/tap`, with a repo-local fallback for monorepo checkouts.
|
|
86
|
+
|
|
87
|
+
## Supported Runtimes
|
|
88
|
+
|
|
89
|
+
| Runtime | Config | Bridge | Mode |
|
|
90
|
+
| ------- | ----------------------- | ---------------------- | ------------------ |
|
|
91
|
+
| Claude | `.mcp.json` | native-push (fs.watch) | No daemon needed |
|
|
92
|
+
| Codex | `~/.codex/config.toml` | WebSocket bridge | Daemon per session |
|
|
93
|
+
| Gemini | `.gemini/settings.json` | polling | No daemon needed |
|
|
94
|
+
|
|
95
|
+
## `--json` Flag
|
|
96
|
+
|
|
97
|
+
All commands support `--json` for machine-readable output. Returns a single JSON object to stdout with no human log noise.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npx @hua-labs/tap status --json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"ok": true,
|
|
106
|
+
"command": "status",
|
|
107
|
+
"code": "TAP_STATUS_OK",
|
|
108
|
+
"message": "2 runtime(s) installed",
|
|
109
|
+
"warnings": [],
|
|
110
|
+
"data": {
|
|
111
|
+
"version": "0.2.2",
|
|
112
|
+
"commsDir": "/path/to/comms",
|
|
113
|
+
"runtimes": {
|
|
114
|
+
"claude": { "status": "active", "bridgeMode": "native-push" },
|
|
115
|
+
"codex": { "status": "configured", "bridgeMode": "app-server" }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Error codes use `TAP_*` prefix: `TAP_ADD_OK`, `TAP_NO_OP`, `TAP_PATCH_FAILED`, etc.
|
|
122
|
+
|
|
123
|
+
Exit codes: `0` = ok, `1` = error.
|
|
124
|
+
|
|
125
|
+
## Permissions
|
|
126
|
+
|
|
127
|
+
`tap init` auto-configures runtime permissions.
|
|
128
|
+
|
|
129
|
+
### Safe mode (default)
|
|
130
|
+
|
|
131
|
+
**Claude**: Adds deny rules to `.claude/settings.local.json` blocking destructive operations (force push, hard reset, rm -rf, etc.).
|
|
132
|
+
|
|
133
|
+
**Codex**: Sets `workspace-write` sandbox, `full` network access, trusted project paths, and writable roots in `~/.codex/config.toml`.
|
|
134
|
+
|
|
135
|
+
### Full mode
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npx @hua-labs/tap init --permissions full
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Claude**: Removes tap-managed deny rules. User-added rules preserved.
|
|
142
|
+
|
|
143
|
+
**Codex**: Sets `danger-full-access` sandbox. Use on trusted local machines only.
|
|
144
|
+
|
|
145
|
+
## How It Works
|
|
146
|
+
|
|
147
|
+
Agents communicate through a shared directory (`comms/`) using markdown files:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
comms/
|
|
151
|
+
├── inbox/ # Agent-to-agent messages
|
|
152
|
+
├── reviews/ # Code review results
|
|
153
|
+
├── findings/ # Out-of-scope discoveries
|
|
154
|
+
├── handoff/ # Session handoff documents
|
|
155
|
+
├── retros/ # Retrospectives
|
|
156
|
+
└── archive/ # Archived messages
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Each runtime has an adapter that:
|
|
160
|
+
|
|
161
|
+
1. **Probes** — finds config files, checks runtime installation
|
|
162
|
+
2. **Plans** — determines what patches to apply
|
|
163
|
+
3. **Applies** — backs up and patches config files
|
|
164
|
+
4. **Verifies** — confirms the runtime can read the config
|
|
165
|
+
|
|
166
|
+
The adapter contract (`RuntimeAdapter`) is the extension point for adding new runtimes.
|
|
167
|
+
|
|
168
|
+
## Changelog (0.2.2)
|
|
169
|
+
|
|
170
|
+
### Bridge
|
|
171
|
+
|
|
172
|
+
- **Auth gateway** — Managed bridge now includes an auth proxy with timing-safe token validation (M99)
|
|
173
|
+
- **`--no-auth` flag** — Skip auth gateway for localhost-only setups; app-server listens directly on public port (M102)
|
|
174
|
+
- **TUI connect URL** — `bridge start` and `bridge status` output shows where to connect Codex TUI (M102)
|
|
175
|
+
- **Identity routing** — Bridge matches inbox messages by both `agentId` and `agentName`; self echo-back filtered by both (M101)
|
|
176
|
+
- **Display labels** — Bridge prompts, `tap_who`, and notifications use `name [id]` format (M101)
|
|
177
|
+
|
|
178
|
+
### CLI
|
|
179
|
+
|
|
180
|
+
- **`tap doctor`** — Diagnose comms, bridge, message, and MCP issues (M95)
|
|
181
|
+
- **`tap doctor --fix`** — Auto-fix common issues with post-fix revalidation (M100)
|
|
182
|
+
- **Error codes** — 24 CLI error codes with consistent `TAP_*` prefix (M91)
|
|
183
|
+
- **Boot streamline** — Faster CLI startup with agent-name persistence (M92)
|
|
184
|
+
|
|
185
|
+
### Infrastructure
|
|
186
|
+
|
|
187
|
+
- **Auto-poll fallback** — Bridge falls back to polling when fs.watch is unavailable (M93)
|
|
188
|
+
- **Watcher dedup** — Root-cause fix for duplicate message dispatch (M90)
|
|
189
|
+
- **tap-plugin test infra** — In-memory test harness for MCP channel tests (M94)
|
|
190
|
+
- **Blind test CI** — Cross-model communication verification framework (M98)
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT
|
|
@@ -36,12 +36,21 @@ interface HeadlessWarmupClient {
|
|
|
36
36
|
startTurn(inputText: string): Promise<string | null>;
|
|
37
37
|
refreshCurrentThreadState(): Promise<void>;
|
|
38
38
|
}
|
|
39
|
+
interface LoadedThreadCandidate {
|
|
40
|
+
id: string;
|
|
41
|
+
cwd: string;
|
|
42
|
+
updatedAt: number;
|
|
43
|
+
statusType: string | null;
|
|
44
|
+
thread: any;
|
|
45
|
+
}
|
|
39
46
|
interface HeartbeatStoreRecord {
|
|
40
47
|
id?: string;
|
|
41
48
|
agent?: string;
|
|
42
49
|
}
|
|
43
50
|
type HeartbeatStore = Record<string, HeartbeatStoreRecord>;
|
|
44
51
|
declare const HEADLESS_WARMUP_PROMPT: string;
|
|
52
|
+
declare function threadCwdMatches(expectedCwd: string, actualCwd: string | null | undefined): boolean;
|
|
53
|
+
declare function chooseLoadedThreadForCwd(cwd: string, threads: LoadedThreadCandidate[]): LoadedThreadCandidate | null;
|
|
45
54
|
declare function resolveAgentId(preferredAgentName?: string | null): string;
|
|
46
55
|
declare function recipientMatchesAgent(recipient: string, agentId: string, agentName: string): boolean;
|
|
47
56
|
declare function isOwnMessageSender(sender: string, agentId: string, agentName: string): boolean;
|
|
@@ -53,4 +62,4 @@ declare function maybeBootstrapHeadlessTurn(options: Options, cutoff: Date, clie
|
|
|
53
62
|
declare function buildOptions(argv: string[]): Options;
|
|
54
63
|
declare function main(): Promise<void>;
|
|
55
64
|
|
|
56
|
-
export { HEADLESS_WARMUP_PROMPT, type HeadlessWarmupClient, buildOptions, buildUserInput, isOwnMessageSender, main, maybeBootstrapHeadlessTurn, recipientMatchesAgent, resolveAddressLabel, resolveAgentId, resolveCurrentAgentName, waitForTurnCompletion };
|
|
65
|
+
export { HEADLESS_WARMUP_PROMPT, type HeadlessWarmupClient, type LoadedThreadCandidate, buildOptions, buildUserInput, chooseLoadedThreadForCwd, isOwnMessageSender, main, maybeBootstrapHeadlessTurn, recipientMatchesAgent, resolveAddressLabel, resolveAgentId, resolveCurrentAgentName, threadCwdMatches, waitForTurnCompletion };
|
|
@@ -31,6 +31,30 @@ var HEADLESS_WARMUP_PROMPT = [
|
|
|
31
31
|
var HEADLESS_WARMUP_TIMEOUT_MS = 3e4;
|
|
32
32
|
var TURN_COMPLETION_POLL_MS = 250;
|
|
33
33
|
var TURN_COMPLETION_REFRESH_MS = 1e3;
|
|
34
|
+
function normalizeThreadCwd(cwd) {
|
|
35
|
+
return resolve(cwd).replace(/\\/g, "/").toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
function threadCwdMatches(expectedCwd, actualCwd) {
|
|
38
|
+
if (!actualCwd) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return normalizeThreadCwd(expectedCwd) === normalizeThreadCwd(actualCwd);
|
|
42
|
+
}
|
|
43
|
+
function chooseLoadedThreadForCwd(cwd, threads) {
|
|
44
|
+
const matching = threads.filter((thread) => threadCwdMatches(cwd, thread.cwd));
|
|
45
|
+
if (matching.length === 0) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
matching.sort((left, right) => {
|
|
49
|
+
const leftActive = left.statusType === "active" ? 1 : 0;
|
|
50
|
+
const rightActive = right.statusType === "active" ? 1 : 0;
|
|
51
|
+
if (leftActive !== rightActive) {
|
|
52
|
+
return rightActive - leftActive;
|
|
53
|
+
}
|
|
54
|
+
return right.updatedAt - left.updatedAt;
|
|
55
|
+
});
|
|
56
|
+
return matching[0] ?? null;
|
|
57
|
+
}
|
|
34
58
|
function printHelp() {
|
|
35
59
|
console.log(`Codex App Server bridge
|
|
36
60
|
|
|
@@ -344,12 +368,13 @@ function readThreadState(stateDir) {
|
|
|
344
368
|
}
|
|
345
369
|
return null;
|
|
346
370
|
}
|
|
347
|
-
function persistThreadState(stateDir, threadId, appServerUrl, ephemeral) {
|
|
371
|
+
function persistThreadState(stateDir, threadId, appServerUrl, ephemeral, cwd) {
|
|
348
372
|
const payload = {
|
|
349
373
|
threadId,
|
|
350
374
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
351
375
|
appServerUrl,
|
|
352
|
-
ephemeral
|
|
376
|
+
ephemeral,
|
|
377
|
+
cwd
|
|
353
378
|
};
|
|
354
379
|
writeFileSync(
|
|
355
380
|
join(stateDir, "thread.json"),
|
|
@@ -646,7 +671,7 @@ async function waitForTurnCompletion(client, turnId, timeoutMs) {
|
|
|
646
671
|
throw new Error(`Timed out waiting for turn ${turnId} to complete`);
|
|
647
672
|
}
|
|
648
673
|
async function maybeBootstrapHeadlessTurn(options, cutoff, client) {
|
|
649
|
-
if (process.env.TAP_HEADLESS !== "true") {
|
|
674
|
+
if (process.env.TAP_HEADLESS !== "true" && process.env.TAP_COLD_START_WARMUP !== "true") {
|
|
650
675
|
return false;
|
|
651
676
|
}
|
|
652
677
|
const { candidates } = getPendingCandidates(options, cutoff);
|
|
@@ -714,6 +739,7 @@ var AppServerClient = class {
|
|
|
714
739
|
connected = false;
|
|
715
740
|
initialized = false;
|
|
716
741
|
threadId = null;
|
|
742
|
+
currentThreadCwd = null;
|
|
717
743
|
activeTurnId = null;
|
|
718
744
|
lastTurnStatus = null;
|
|
719
745
|
lastNotificationMethod = null;
|
|
@@ -797,7 +823,7 @@ var AppServerClient = class {
|
|
|
797
823
|
this.initialized = false;
|
|
798
824
|
this.socket = null;
|
|
799
825
|
}
|
|
800
|
-
async ensureThread(explicitThreadId,
|
|
826
|
+
async ensureThread(explicitThreadId, savedThread, cwd, ephemeral) {
|
|
801
827
|
if (explicitThreadId) {
|
|
802
828
|
try {
|
|
803
829
|
const resumeResponse = await this.request("thread/resume", {
|
|
@@ -820,22 +846,38 @@ var AppServerClient = class {
|
|
|
820
846
|
if (loadedThreadId) {
|
|
821
847
|
return loadedThreadId;
|
|
822
848
|
}
|
|
823
|
-
if (
|
|
824
|
-
|
|
825
|
-
const resumeResponse = await this.request("thread/resume", {
|
|
826
|
-
threadId: resumeThreadId,
|
|
827
|
-
persistExtendedHistory: false
|
|
828
|
-
});
|
|
829
|
-
const resumedThreadId = resumeResponse?.thread?.id ?? resumeThreadId;
|
|
830
|
-
await this.refreshThreadState(resumedThreadId);
|
|
831
|
-
this.logger(
|
|
832
|
-
`resumed saved thread ${resumedThreadId}${this.activeTurnId ? ` (active turn ${this.activeTurnId})` : ""}`
|
|
833
|
-
);
|
|
834
|
-
return resumedThreadId;
|
|
835
|
-
} catch (error) {
|
|
849
|
+
if (savedThread?.threadId) {
|
|
850
|
+
if (savedThread.cwd && !threadCwdMatches(cwd, savedThread.cwd)) {
|
|
836
851
|
this.logger(
|
|
837
|
-
`saved thread
|
|
852
|
+
`saved thread ${savedThread.threadId} cwd ${savedThread.cwd} does not match ${cwd}; skipping saved thread`
|
|
838
853
|
);
|
|
854
|
+
} else {
|
|
855
|
+
try {
|
|
856
|
+
const resumeResponse = await this.request("thread/resume", {
|
|
857
|
+
threadId: savedThread.threadId,
|
|
858
|
+
persistExtendedHistory: false
|
|
859
|
+
});
|
|
860
|
+
const resumedThreadId = resumeResponse?.thread?.id ?? savedThread.threadId;
|
|
861
|
+
await this.refreshThreadState(resumedThreadId);
|
|
862
|
+
if (!threadCwdMatches(cwd, this.currentThreadCwd)) {
|
|
863
|
+
this.logger(
|
|
864
|
+
`saved thread ${resumedThreadId} cwd ${this.currentThreadCwd ?? "unknown"} does not match ${cwd}; starting a fresh thread`
|
|
865
|
+
);
|
|
866
|
+
this.threadId = null;
|
|
867
|
+
this.currentThreadCwd = null;
|
|
868
|
+
this.activeTurnId = null;
|
|
869
|
+
this.lastTurnStatus = null;
|
|
870
|
+
} else {
|
|
871
|
+
this.logger(
|
|
872
|
+
`resumed saved thread ${resumedThreadId}${this.activeTurnId ? ` (active turn ${this.activeTurnId})` : ""}`
|
|
873
|
+
);
|
|
874
|
+
return resumedThreadId;
|
|
875
|
+
}
|
|
876
|
+
} catch (error) {
|
|
877
|
+
this.logger(
|
|
878
|
+
`saved thread resume failed for ${savedThread.threadId}; starting a fresh thread (${String(error)})`
|
|
879
|
+
);
|
|
880
|
+
}
|
|
839
881
|
}
|
|
840
882
|
}
|
|
841
883
|
const startResponse = await this.request("thread/start", {
|
|
@@ -848,7 +890,9 @@ var AppServerClient = class {
|
|
|
848
890
|
if (!startedThreadId) {
|
|
849
891
|
throw new Error("thread/start did not return a thread id");
|
|
850
892
|
}
|
|
893
|
+
this.syncThreadStateFromThread(startResponse?.thread);
|
|
851
894
|
this.threadId = startedThreadId;
|
|
895
|
+
this.currentThreadCwd = this.currentThreadCwd ?? cwd;
|
|
852
896
|
this.activeTurnId = null;
|
|
853
897
|
this.lastTurnStatus = null;
|
|
854
898
|
this.logger(`started thread ${startedThreadId}`);
|
|
@@ -886,20 +930,13 @@ var AppServerClient = class {
|
|
|
886
930
|
continue;
|
|
887
931
|
}
|
|
888
932
|
}
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
|
|
933
|
+
const chosen = chooseLoadedThreadForCwd(cwd, threads);
|
|
934
|
+
if (!chosen) {
|
|
935
|
+
if (threads.length > 0) {
|
|
936
|
+
this.logger(`loaded threads exist but none match cwd ${cwd}`);
|
|
937
|
+
}
|
|
892
938
|
return null;
|
|
893
939
|
}
|
|
894
|
-
candidates.sort((left, right) => {
|
|
895
|
-
const leftActive = left.statusType === "active" ? 1 : 0;
|
|
896
|
-
const rightActive = right.statusType === "active" ? 1 : 0;
|
|
897
|
-
if (leftActive !== rightActive) {
|
|
898
|
-
return rightActive - leftActive;
|
|
899
|
-
}
|
|
900
|
-
return right.updatedAt - left.updatedAt;
|
|
901
|
-
});
|
|
902
|
-
const chosen = candidates[0];
|
|
903
940
|
this.syncThreadStateFromThread(chosen.thread);
|
|
904
941
|
this.logger(
|
|
905
942
|
`attached to loaded thread ${chosen.id}${this.activeTurnId ? ` (active turn ${this.activeTurnId})` : ""}`
|
|
@@ -972,6 +1009,7 @@ var AppServerClient = class {
|
|
|
972
1009
|
if (typeof thread?.id === "string") {
|
|
973
1010
|
this.threadId = thread.id;
|
|
974
1011
|
}
|
|
1012
|
+
this.currentThreadCwd = typeof thread?.cwd === "string" ? thread.cwd : null;
|
|
975
1013
|
let activeTurnId = null;
|
|
976
1014
|
let lastTurnStatus = null;
|
|
977
1015
|
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
@@ -1020,6 +1058,9 @@ var AppServerClient = class {
|
|
|
1020
1058
|
if (params?.thread?.id) {
|
|
1021
1059
|
this.threadId = params.thread.id;
|
|
1022
1060
|
}
|
|
1061
|
+
if (typeof params?.thread?.cwd === "string") {
|
|
1062
|
+
this.currentThreadCwd = params.thread.cwd;
|
|
1063
|
+
}
|
|
1023
1064
|
this.logger(`thread started ${params?.thread?.id ?? ""}`.trim());
|
|
1024
1065
|
break;
|
|
1025
1066
|
case "thread/status/changed":
|
|
@@ -1075,6 +1116,16 @@ var AppServerClient = class {
|
|
|
1075
1116
|
}
|
|
1076
1117
|
};
|
|
1077
1118
|
function writeHeartbeat(options, client, health) {
|
|
1119
|
+
if (client?.threadId) {
|
|
1120
|
+
const savedThread = readThreadState(options.stateDir);
|
|
1121
|
+
persistThreadState(
|
|
1122
|
+
options.stateDir,
|
|
1123
|
+
client.threadId,
|
|
1124
|
+
options.appServerUrl,
|
|
1125
|
+
options.ephemeral,
|
|
1126
|
+
client.currentThreadCwd ?? savedThread?.cwd ?? null
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1078
1129
|
const payload = {
|
|
1079
1130
|
pid: process.pid,
|
|
1080
1131
|
agent: options.agentName,
|
|
@@ -1084,6 +1135,7 @@ function writeHeartbeat(options, client, health) {
|
|
|
1084
1135
|
connected: client?.connected ?? false,
|
|
1085
1136
|
initialized: client?.initialized ?? false,
|
|
1086
1137
|
threadId: client?.threadId ?? null,
|
|
1138
|
+
threadCwd: client?.currentThreadCwd ?? null,
|
|
1087
1139
|
activeTurnId: client?.activeTurnId ?? null,
|
|
1088
1140
|
lastTurnStatus: client?.lastTurnStatus ?? null,
|
|
1089
1141
|
lastNotificationMethod: client?.lastNotificationMethod ?? null,
|
|
@@ -1235,7 +1287,7 @@ async function main() {
|
|
|
1235
1287
|
options.messageLookbackMinutes,
|
|
1236
1288
|
options.processExistingMessages
|
|
1237
1289
|
);
|
|
1238
|
-
const
|
|
1290
|
+
const initialSavedThread = readThreadState(options.stateDir);
|
|
1239
1291
|
logStatus("codex app-server bridge ready");
|
|
1240
1292
|
console.log(` repo: ${options.repoRoot}`);
|
|
1241
1293
|
console.log(` comms: ${options.commsDir}`);
|
|
@@ -1251,14 +1303,15 @@ async function main() {
|
|
|
1251
1303
|
console.log(
|
|
1252
1304
|
` lookback: ${options.processExistingMessages ? "existing messages" : `${options.messageLookbackMinutes} minute(s)`}`
|
|
1253
1305
|
);
|
|
1254
|
-
if (options.threadId ||
|
|
1255
|
-
console.log(
|
|
1306
|
+
if (options.threadId || initialSavedThread?.threadId) {
|
|
1307
|
+
console.log(
|
|
1308
|
+
` thread: ${options.threadId ?? initialSavedThread?.threadId}`
|
|
1309
|
+
);
|
|
1256
1310
|
}
|
|
1257
1311
|
if (options.dryRun) {
|
|
1258
1312
|
logStatus("dry-run mode enabled");
|
|
1259
1313
|
}
|
|
1260
1314
|
let client = null;
|
|
1261
|
-
let savedThreadId = savedThread?.threadId ?? null;
|
|
1262
1315
|
const health = {
|
|
1263
1316
|
consecutiveFailureCount: 0
|
|
1264
1317
|
};
|
|
@@ -1272,9 +1325,10 @@ async function main() {
|
|
|
1272
1325
|
options.gatewayToken
|
|
1273
1326
|
);
|
|
1274
1327
|
await client.connect();
|
|
1328
|
+
const savedThread = readThreadState(options.stateDir);
|
|
1275
1329
|
const threadId = await client.ensureThread(
|
|
1276
1330
|
options.threadId,
|
|
1277
|
-
|
|
1331
|
+
savedThread,
|
|
1278
1332
|
options.repoRoot,
|
|
1279
1333
|
options.ephemeral
|
|
1280
1334
|
);
|
|
@@ -1282,9 +1336,9 @@ async function main() {
|
|
|
1282
1336
|
options.stateDir,
|
|
1283
1337
|
threadId,
|
|
1284
1338
|
options.appServerUrl,
|
|
1285
|
-
options.ephemeral
|
|
1339
|
+
options.ephemeral,
|
|
1340
|
+
client.currentThreadCwd ?? options.repoRoot
|
|
1286
1341
|
);
|
|
1287
|
-
savedThreadId = threadId;
|
|
1288
1342
|
writeHeartbeat(options, client, health);
|
|
1289
1343
|
const bootstrapped = await maybeBootstrapHeadlessTurn(
|
|
1290
1344
|
options,
|
|
@@ -1356,6 +1410,7 @@ export {
|
|
|
1356
1410
|
HEADLESS_WARMUP_PROMPT,
|
|
1357
1411
|
buildOptions,
|
|
1358
1412
|
buildUserInput,
|
|
1413
|
+
chooseLoadedThreadForCwd,
|
|
1359
1414
|
isOwnMessageSender,
|
|
1360
1415
|
main,
|
|
1361
1416
|
maybeBootstrapHeadlessTurn,
|
|
@@ -1363,6 +1418,7 @@ export {
|
|
|
1363
1418
|
resolveAddressLabel,
|
|
1364
1419
|
resolveAgentId,
|
|
1365
1420
|
resolveCurrentAgentName,
|
|
1421
|
+
threadCwdMatches,
|
|
1366
1422
|
waitForTurnCompletion
|
|
1367
1423
|
};
|
|
1368
1424
|
//# sourceMappingURL=codex-app-server-bridge.mjs.map
|