@hua-labs/tap 0.1.1 → 0.2.1

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 CHANGED
@@ -1,223 +1,194 @@
1
- # tap
1
+ # @hua-labs/tap
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/@hua-labs/tap.svg)](https://www.npmjs.com/package/@hua-labs/tap)
4
- [![license](https://img.shields.io/npm/l/@hua-labs/tap.svg)](LICENSE)
3
+ Zero-dependency CLI for cross-model AI agent communication setup.
5
4
 
6
- > Your AI sessions can already work in parallel. tap gives them a shared protocol.
7
- >
8
- > Sessions end. Systems grow.
5
+ One command to connect Claude, Codex, and Gemini agents through a shared file-based communication layer.
9
6
 
10
- **A local-first, cross-model orchestration protocol for AI coding agents.**
11
-
12
- Run multiple AI agents (Claude, Codex, Gemini) on the same codebase — in parallel.
13
-
14
- ---
15
-
16
- ## What you can do with tap
17
-
18
- - Run multiple AI agents in parallel on one repo
19
- - Split work into independent tasks with isolated worktrees
20
- - Communicate between Claude, Codex, and Gemini sessions
21
- - Review code across models (Codex reviews Claude's code, and vice versa)
22
- - Keep all communication and history in git
23
- - Continue work across sessions and machines — nothing is lost
24
-
25
- ---
26
-
27
- ## Why tap?
28
-
29
- Because using one AI at a time is slow.
30
-
31
- You already have Claude Code, Codex, Gemini CLI. They're powerful alone. But they can't see each other. tap connects them — through files, not proxies.
32
-
33
- - **No server.** Messages are markdown files in a git repo.
34
- - **No vendor lock-in.** Works with any model that can read/write files.
35
- - **No lost context.** Everything persists in git — messages, reviews, findings, retros.
36
- - **No TOS violations.** Only official interfaces — MCP, App Server WebSocket, fs.watch.
37
-
38
- ---
7
+ ## Quick Start
39
8
 
40
- ## Install
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.
41
10
 
42
11
  ```bash
12
+ # 1. Initialize comms directory and state
43
13
  npx @hua-labs/tap init
44
- ```
45
14
 
46
- One command. Sets up comms directory, config, and MCP server entry. Works on Windows, macOS, and Linux.
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
47
19
 
48
- ---
49
-
50
- ## How It Works
51
-
52
- ```
53
- tap (protocol) --> bridge (delivery) --> agent (execution)
54
- git (memory) <-- human (governance) <-- tower (decision)
20
+ # 3. Check status
21
+ npx @hua-labs/tap status
55
22
  ```
56
23
 
57
- No central server. The file system is the message bus — git is the transport and history.
24
+ Your agents can now communicate through the shared comms directory.
58
25
 
59
- Messages are plain markdown files: `inbox/YYYYMMDD-{from}-{to}-{subject}.md`
26
+ ## Commands
60
27
 
61
- **Delivery modes:**
62
- - **Claude**: MCP channel push — real-time
63
- - **Codex**: App Server WebSocket bridge — real-time
64
- - **Gemini**: File-watch notification — experimental
28
+ ### `init`
65
29
 
66
- **Cross-device?** Point both machines at the same git repo. Done.
30
+ Initialize the comms directory and `.tap-comms/` state.
67
31
 
68
- ---
69
-
70
- ## Quick Start
71
-
72
- ### 1. Two agents talking
32
+ By default, the comms directory is created inside the current repo at `./tap-comms`.
73
33
 
74
34
  ```bash
75
- # Terminal 1 — Agent A
76
35
  npx @hua-labs/tap init
77
- claude --dangerously-load-development-channels server:tap-comms
78
- # Inside Claude: tap_set_name("agent-a")
79
- # Inside Claude: tap_reply(to: "agent-b", subject: "hello", content: "can you see this?")
80
-
81
- # Terminal 2 — Agent B
82
- cd same-repo
83
- claude
84
- # Inside Claude: tap_set_name("agent-b")
85
- # Inside Claude: tap_list_unread()
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
86
40
  ```
87
41
 
88
- That's it. Two Claude sessions, talking through files. You should see messages appear in the other session instantly.
42
+ ### `add <runtime>`
89
43
 
90
- ### 2. Parallel work with worktrees
44
+ Add a runtime. Probes config, plans patches, applies, and verifies.
91
45
 
92
46
  ```bash
93
- # Set up isolated workspaces
94
- npx @hua-labs/tap init-worktree --path ../wt-1 --branch feat/auth
95
- npx @hua-labs/tap init-worktree --path ../wt-2 --branch feat/dashboard
96
-
97
- # Each agent works in its own worktree — no git conflicts
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
98
51
  ```
99
52
 
100
- ### 3. Add Codex to the team
53
+ ### `remove <runtime>`
101
54
 
102
- ```bash
103
- # Register and start bridge
104
- npx @hua-labs/tap add codex --name reviewer --port 4501
105
- npx @hua-labs/tap bridge start codex-reviewer --agent-name reviewer
55
+ Remove a runtime and rollback config changes.
106
56
 
107
- # You can now send a message from Claude and see it appear in Codex in real time.
57
+ ```bash
58
+ npx @hua-labs/tap remove claude
59
+ npx @hua-labs/tap remove codex
108
60
  ```
109
61
 
110
- ---
111
-
112
- ## CLI Commands
62
+ ### `status`
113
63
 
114
- | Command | Description |
115
- |---------|-------------|
116
- | `tap init` | Initialize tap in a project |
117
- | `tap init-worktree` | Bootstrap a git worktree with deps, permissions, MCP config |
118
- | `tap add <runtime>` | Register a runtime (codex, gemini) |
119
- | `tap remove <runtime>` | Remove a registered runtime |
120
- | `tap bridge start` | Start real-time bridge for a runtime |
121
- | `tap bridge stop` | Stop a running bridge |
122
- | `tap bridge status` | Show bridge health and heartbeat |
123
- | `tap status` | Show registered runtimes and instances |
124
- | `tap dashboard` | Unified ops view — agents, bridges, PRs |
125
- | `tap serve` | Start MCP server for development |
64
+ Show installed runtimes and their status.
126
65
 
127
- All commands support `--json` for automation.
66
+ ```bash
67
+ npx @hua-labs/tap status
68
+ ```
128
69
 
129
- ---
70
+ Output shows three status levels:
130
71
 
131
- ## Advanced Features
72
+ - **installed** — config written but not verified
73
+ - **configured** — config written and verified
74
+ - **active** — runtime is running and connected
132
75
 
133
- ### Multi-Instance Bridge
76
+ ### `serve`
134
77
 
135
- Run multiple agents on the same runtime:
78
+ Start the tap-comms MCP server (stdio). Convenience command for running the MCP server locally.
136
79
 
137
80
  ```bash
138
- npx @hua-labs/tap add codex --name reviewer --port 4501
139
- npx @hua-labs/tap add codex --name builder --port 4502
81
+ npx @hua-labs/tap serve
82
+ npx @hua-labs/tap serve --comms-dir /path/to/comms
140
83
  ```
141
84
 
142
- Each instance gets its own PID, state directory, and heartbeat.
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 |
143
94
 
144
- ### Headless Reviewer
95
+ ## `--json` Flag
145
96
 
146
- Run Codex as a background review daemon no TUI:
97
+ All commands support `--json` for machine-readable output. Returns a single JSON object to stdout with no human log noise.
147
98
 
148
99
  ```bash
149
- npx @hua-labs/tap bridge start codex --headless --role reviewer --agent-name my-reviewer
100
+ npx @hua-labs/tap status --json
150
101
  ```
151
102
 
152
- Auto-polls inbox for review requests. Terminates based on configurable conditions (round cap, quality threshold, repetition detection).
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.0",
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
+ ```
153
120
 
154
- ### Ops Dashboard
121
+ Error codes use `TAP_*` prefix: `TAP_ADD_OK`, `TAP_NO_OP`, `TAP_PATCH_FAILED`, etc.
155
122
 
156
- ```bash
157
- npx @hua-labs/tap dashboard
158
- ```
123
+ Exit codes: `0` = ok, `1` = error.
159
124
 
160
- Agents, bridges, and PR status in one screen. `--watch` for live updates.
125
+ ## Permissions
161
126
 
162
- ### Config System
127
+ `tap init` auto-configures runtime permissions.
163
128
 
164
- Two-layer config for portability:
129
+ ### Safe mode (default)
165
130
 
166
- - `tap-config.json` shared, git-tracked
167
- - `tap-config.local.json` — machine-specific, gitignored
131
+ **Claude**: Adds deny rules to `.claude/settings.local.json` blocking destructive operations (force push, hard reset, rm -rf, etc.).
168
132
 
169
- Automatic runtime resolution: `.node-version` + fnm probing + tsx fallback.
133
+ **Codex**: Sets `workspace-write` sandbox, `full` network access, trusted project paths, and writable roots in `~/.codex/config.toml`.
170
134
 
171
- ### Generational Knowledge Transfer
135
+ ### Full mode
172
136
 
173
- Agents are stateless — sessions end, context is lost. tap solves this with files:
137
+ ```bash
138
+ npx @hua-labs/tap init --permissions full
139
+ ```
174
140
 
175
- - **Retros** what worked, what didn't
176
- - **Handoffs** — context for the next session
177
- - **Findings** — discoveries that become backlog items
178
- - **Letters** — agent-to-agent or agent-to-human messages
141
+ **Claude**: Removes tap-managed deny rules. User-added rules preserved.
179
142
 
180
- Used across multiple sessions ("generations") where each session builds on previous findings and handoffs.
143
+ **Codex**: Sets `danger-full-access` sandbox. Use on trusted local machines only.
181
144
 
182
- ---
145
+ ## How It Works
183
146
 
184
- ## Results
147
+ Agents communicate through a shared directory (`comms/`) using markdown files:
185
148
 
186
- Used in production multi-session workflows:
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
+ ```
187
158
 
188
- | Metric | Value |
189
- |--------|-------|
190
- | Generations | 11 |
191
- | Agents | 50+ |
192
- | Models | Claude + Codex + Gemini |
193
- | PRs merged | 55+ |
194
- | Platforms | Windows + macOS + Linux |
159
+ Each runtime has an adapter that:
195
160
 
196
- ---
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
197
165
 
198
- ## Docs
166
+ The adapter contract (`RuntimeAdapter`) is the extension point for adding new runtimes.
199
167
 
200
- | Document | Description |
201
- |----------|-------------|
202
- | [ARCHITECTURE.md](ARCHITECTURE.md) | Design decisions, data model, delivery modes |
203
- | [CHANGELOG.md](CHANGELOG.md) | Version history |
204
- | [Protocol Rules](docs/) | Rules for agent coordination |
205
- | [Cross-Platform Strategy](docs/cross-platform-strategy.md) | Windows, macOS, Linux support |
206
- | [Codex Bridge Deep Dive](docs/codex-app-server-bridge.md) | WebSocket injection details |
207
- | [Multi-Device Hub](docs/MULTI-DEVICE-HUB.md) | Cross-device via git sync |
168
+ ## Changelog (0.2.0)
208
169
 
209
- ---
170
+ ### Bridge
210
171
 
211
- ## Contributing
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)
212
177
 
213
- Built by [HUA Labs](https://github.com/HUA-Labs). Issues and PRs welcome.
178
+ ### CLI
214
179
 
215
- The best way to contribute? Use tap, run a generation, and submit your retro as a PR.
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)
216
184
 
217
- ---
185
+ ### Infrastructure
218
186
 
219
- *Built by Claude (Anthropic) + Codex (OpenAI) + Gemini (Google) + Devin (human).*
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)
220
191
 
221
- *"Sessions end. Systems grow."*
192
+ ## License
222
193
 
223
- **MIT License** — HUA Labs
194
+ MIT
@@ -0,0 +1,8 @@
1
+ interface GatewayOptions {
2
+ listenUrl: string;
3
+ upstreamUrl: string;
4
+ token: string;
5
+ }
6
+ declare function buildGatewayOptions(argv: string[]): GatewayOptions;
7
+
8
+ export { buildGatewayOptions };
@@ -0,0 +1,183 @@
1
+ // src/bridges/codex-app-server-auth-gateway.ts
2
+ import { readFileSync } from "fs";
3
+ import { resolve } from "path";
4
+ import { pathToFileURL } from "url";
5
+ import { timingSafeEqual } from "crypto";
6
+ import { WebSocket, WebSocketServer } from "ws";
7
+ var AUTH_QUERY_PARAM = "tap_token";
8
+ var CLOSE_UNAUTHORIZED = 4401;
9
+ var CLOSE_UPSTREAM_ERROR = 1013;
10
+ function normalizeUrl(value) {
11
+ return value.replace(/\/$/, "");
12
+ }
13
+ function closeSocket(socket, code, reason) {
14
+ if (socket.readyState === WebSocket.CLOSING || socket.readyState === WebSocket.CLOSED) {
15
+ return;
16
+ }
17
+ try {
18
+ socket.close(code, reason);
19
+ } catch {
20
+ }
21
+ }
22
+ function readFlagValue(argv, index, flag) {
23
+ const current = argv[index] ?? "";
24
+ const eqIndex = current.indexOf("=");
25
+ if (eqIndex >= 0) {
26
+ return current.slice(eqIndex + 1);
27
+ }
28
+ const next = argv[index + 1];
29
+ if (!next || next.startsWith("--")) {
30
+ throw new Error(`Missing value for ${flag}`);
31
+ }
32
+ return next;
33
+ }
34
+ function buildGatewayOptions(argv) {
35
+ let listenUrl = process.env.TAP_GATEWAY_LISTEN_URL?.trim() || "";
36
+ let upstreamUrl = process.env.TAP_GATEWAY_UPSTREAM_URL?.trim() || "";
37
+ let tokenFile = process.env.TAP_GATEWAY_TOKEN_FILE?.trim() || "";
38
+ let token = process.env.TAP_GATEWAY_TOKEN?.trim() || "";
39
+ for (let index = 0; index < argv.length; index += 1) {
40
+ const flag = argv[index] ?? "";
41
+ const consumesNext = !flag.includes("=");
42
+ if (flag.startsWith("--listen-url")) {
43
+ listenUrl = readFlagValue(argv, index, "--listen-url").trim();
44
+ if (consumesNext) index += 1;
45
+ continue;
46
+ }
47
+ if (flag.startsWith("--upstream-url")) {
48
+ upstreamUrl = readFlagValue(argv, index, "--upstream-url").trim();
49
+ if (consumesNext) index += 1;
50
+ continue;
51
+ }
52
+ if (flag.startsWith("--token")) {
53
+ token = readFlagValue(argv, index, "--token").trim();
54
+ if (consumesNext) index += 1;
55
+ continue;
56
+ }
57
+ if (flag.startsWith("--token-file")) {
58
+ tokenFile = readFlagValue(argv, index, "--token-file").trim();
59
+ if (consumesNext) index += 1;
60
+ continue;
61
+ }
62
+ }
63
+ if (tokenFile) {
64
+ token = readFileSync(tokenFile, "utf8").trim();
65
+ }
66
+ if (!listenUrl) {
67
+ throw new Error("Missing gateway listen URL");
68
+ }
69
+ if (!upstreamUrl) {
70
+ throw new Error("Missing gateway upstream URL");
71
+ }
72
+ if (!token) {
73
+ throw new Error("Missing gateway auth token");
74
+ }
75
+ const listen = new URL(listenUrl);
76
+ const upstream = new URL(upstreamUrl);
77
+ if (!/^wss?:$/.test(listen.protocol)) {
78
+ throw new Error(`Unsupported gateway listen protocol: ${listen.protocol}`);
79
+ }
80
+ if (!/^wss?:$/.test(upstream.protocol)) {
81
+ throw new Error(
82
+ `Unsupported gateway upstream protocol: ${upstream.protocol}`
83
+ );
84
+ }
85
+ return {
86
+ listenUrl: normalizeUrl(listen.toString()),
87
+ upstreamUrl: normalizeUrl(upstream.toString()),
88
+ token
89
+ };
90
+ }
91
+ function tokensMatch(presentedToken, expectedToken) {
92
+ if (!presentedToken) {
93
+ return false;
94
+ }
95
+ const presented = Buffer.from(presentedToken, "utf8");
96
+ const expected = Buffer.from(expectedToken, "utf8");
97
+ if (presented.length !== expected.length) {
98
+ return false;
99
+ }
100
+ return timingSafeEqual(presented, expected);
101
+ }
102
+ async function main() {
103
+ const options = buildGatewayOptions(process.argv.slice(2));
104
+ const listen = new URL(options.listenUrl);
105
+ const host = listen.hostname === "localhost" ? "127.0.0.1" : listen.hostname;
106
+ const port = Number.parseInt(listen.port, 10);
107
+ if (!Number.isFinite(port) || port <= 0) {
108
+ throw new Error(`Gateway listen URL must include a valid port: ${options.listenUrl}`);
109
+ }
110
+ const server = new WebSocketServer({
111
+ host,
112
+ port,
113
+ path: listen.pathname === "/" ? void 0 : listen.pathname,
114
+ perMessageDeflate: false
115
+ });
116
+ server.on("connection", (client, request) => {
117
+ const requestUrl = new URL(request.url ?? "/", options.listenUrl);
118
+ const presentedToken = requestUrl.searchParams.get(AUTH_QUERY_PARAM);
119
+ if (!tokensMatch(presentedToken, options.token)) {
120
+ closeSocket(client, CLOSE_UNAUTHORIZED, "Unauthorized");
121
+ return;
122
+ }
123
+ const upstream = new WebSocket(options.upstreamUrl, {
124
+ perMessageDeflate: false
125
+ });
126
+ upstream.on("message", (data, isBinary) => {
127
+ if (client.readyState === WebSocket.OPEN) {
128
+ client.send(data, { binary: isBinary });
129
+ }
130
+ });
131
+ client.on("message", (data, isBinary) => {
132
+ if (upstream.readyState === WebSocket.OPEN) {
133
+ upstream.send(data, { binary: isBinary });
134
+ }
135
+ });
136
+ upstream.on("close", (code, reasonBuffer) => {
137
+ const reason = reasonBuffer.toString() || "Upstream closed";
138
+ closeSocket(client, code || 1e3, reason);
139
+ });
140
+ client.on("close", (code, reasonBuffer) => {
141
+ const reason = reasonBuffer.toString() || "Client closed";
142
+ closeSocket(upstream, code || 1e3, reason);
143
+ });
144
+ upstream.on("error", (error) => {
145
+ console.error(`[auth-gateway] upstream error: ${String(error)}`);
146
+ closeSocket(client, CLOSE_UPSTREAM_ERROR, "Upstream unavailable");
147
+ closeSocket(upstream, CLOSE_UPSTREAM_ERROR, "Upstream unavailable");
148
+ });
149
+ client.on("error", (error) => {
150
+ console.error(`[auth-gateway] client error: ${String(error)}`);
151
+ closeSocket(upstream, 1011, "Client error");
152
+ });
153
+ });
154
+ server.on("listening", () => {
155
+ console.log(
156
+ `[auth-gateway] listening ${options.listenUrl} -> ${options.upstreamUrl}`
157
+ );
158
+ });
159
+ const shutdown = () => {
160
+ server.close(() => {
161
+ process.exit(0);
162
+ });
163
+ };
164
+ process.on("SIGINT", shutdown);
165
+ process.on("SIGTERM", shutdown);
166
+ }
167
+ function isDirectExecution() {
168
+ const entry = process.argv[1];
169
+ if (!entry) return false;
170
+ return import.meta.url === pathToFileURL(resolve(entry)).href;
171
+ }
172
+ if (isDirectExecution()) {
173
+ main().catch((error) => {
174
+ console.error(
175
+ error instanceof Error ? error.stack ?? error.message : String(error)
176
+ );
177
+ process.exit(1);
178
+ });
179
+ }
180
+ export {
181
+ buildGatewayOptions
182
+ };
183
+ //# sourceMappingURL=codex-app-server-auth-gateway.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/bridges/codex-app-server-auth-gateway.ts"],"sourcesContent":["import type { IncomingMessage } from \"node:http\";\nimport { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { timingSafeEqual } from \"node:crypto\";\nimport { WebSocket, WebSocketServer, type RawData } from \"ws\";\n\nconst AUTH_QUERY_PARAM = \"tap_token\";\nconst CLOSE_UNAUTHORIZED = 4401;\nconst CLOSE_UPSTREAM_ERROR = 1013;\n\ninterface GatewayOptions {\n listenUrl: string;\n upstreamUrl: string;\n token: string;\n}\n\nfunction normalizeUrl(value: string): string {\n return value.replace(/\\/$/, \"\");\n}\n\nfunction closeSocket(\n socket: Pick<WebSocket, \"readyState\" | \"close\">,\n code: number,\n reason: string,\n): void {\n if (\n socket.readyState === WebSocket.CLOSING ||\n socket.readyState === WebSocket.CLOSED\n ) {\n return;\n }\n\n try {\n socket.close(code, reason);\n } catch {\n // Best-effort cleanup only.\n }\n}\n\nfunction readFlagValue(argv: string[], index: number, flag: string): string {\n const current = argv[index] ?? \"\";\n const eqIndex = current.indexOf(\"=\");\n if (eqIndex >= 0) {\n return current.slice(eqIndex + 1);\n }\n\n const next = argv[index + 1];\n if (!next || next.startsWith(\"--\")) {\n throw new Error(`Missing value for ${flag}`);\n }\n return next;\n}\n\nexport function buildGatewayOptions(argv: string[]): GatewayOptions {\n let listenUrl = process.env.TAP_GATEWAY_LISTEN_URL?.trim() || \"\";\n let upstreamUrl = process.env.TAP_GATEWAY_UPSTREAM_URL?.trim() || \"\";\n let tokenFile = process.env.TAP_GATEWAY_TOKEN_FILE?.trim() || \"\";\n let token = process.env.TAP_GATEWAY_TOKEN?.trim() || \"\";\n\n for (let index = 0; index < argv.length; index += 1) {\n const flag = argv[index] ?? \"\";\n const consumesNext = !flag.includes(\"=\");\n\n if (flag.startsWith(\"--listen-url\")) {\n listenUrl = readFlagValue(argv, index, \"--listen-url\").trim();\n if (consumesNext) index += 1;\n continue;\n }\n\n if (flag.startsWith(\"--upstream-url\")) {\n upstreamUrl = readFlagValue(argv, index, \"--upstream-url\").trim();\n if (consumesNext) index += 1;\n continue;\n }\n\n if (flag.startsWith(\"--token\")) {\n token = readFlagValue(argv, index, \"--token\").trim();\n if (consumesNext) index += 1;\n continue;\n }\n\n if (flag.startsWith(\"--token-file\")) {\n tokenFile = readFlagValue(argv, index, \"--token-file\").trim();\n if (consumesNext) index += 1;\n continue;\n }\n }\n\n if (tokenFile) {\n token = readFileSync(tokenFile, \"utf8\").trim();\n }\n\n if (!listenUrl) {\n throw new Error(\"Missing gateway listen URL\");\n }\n if (!upstreamUrl) {\n throw new Error(\"Missing gateway upstream URL\");\n }\n if (!token) {\n throw new Error(\"Missing gateway auth token\");\n }\n\n const listen = new URL(listenUrl);\n const upstream = new URL(upstreamUrl);\n if (!/^wss?:$/.test(listen.protocol)) {\n throw new Error(`Unsupported gateway listen protocol: ${listen.protocol}`);\n }\n if (!/^wss?:$/.test(upstream.protocol)) {\n throw new Error(\n `Unsupported gateway upstream protocol: ${upstream.protocol}`,\n );\n }\n\n return {\n listenUrl: normalizeUrl(listen.toString()),\n upstreamUrl: normalizeUrl(upstream.toString()),\n token,\n };\n}\n\nfunction tokensMatch(presentedToken: string | null, expectedToken: string): boolean {\n if (!presentedToken) {\n return false;\n }\n\n const presented = Buffer.from(presentedToken, \"utf8\");\n const expected = Buffer.from(expectedToken, \"utf8\");\n if (presented.length !== expected.length) {\n return false;\n }\n\n return timingSafeEqual(presented, expected);\n}\n\nasync function main(): Promise<void> {\n const options = buildGatewayOptions(process.argv.slice(2));\n const listen = new URL(options.listenUrl);\n const host = listen.hostname === \"localhost\" ? \"127.0.0.1\" : listen.hostname;\n const port = Number.parseInt(listen.port, 10);\n if (!Number.isFinite(port) || port <= 0) {\n throw new Error(`Gateway listen URL must include a valid port: ${options.listenUrl}`);\n }\n\n const server = new WebSocketServer({\n host,\n port,\n path: listen.pathname === \"/\" ? undefined : listen.pathname,\n perMessageDeflate: false,\n });\n\n server.on(\"connection\", (client: WebSocket, request: IncomingMessage) => {\n const requestUrl = new URL(request.url ?? \"/\", options.listenUrl);\n const presentedToken = requestUrl.searchParams.get(AUTH_QUERY_PARAM);\n if (!tokensMatch(presentedToken, options.token)) {\n closeSocket(client, CLOSE_UNAUTHORIZED, \"Unauthorized\");\n return;\n }\n\n const upstream = new WebSocket(options.upstreamUrl, {\n perMessageDeflate: false,\n });\n\n upstream.on(\"message\", (data: RawData, isBinary: boolean) => {\n if (client.readyState === WebSocket.OPEN) {\n client.send(data, { binary: isBinary });\n }\n });\n\n client.on(\"message\", (data: RawData, isBinary: boolean) => {\n if (upstream.readyState === WebSocket.OPEN) {\n upstream.send(data, { binary: isBinary });\n }\n });\n\n upstream.on(\"close\", (code: number, reasonBuffer: Buffer) => {\n const reason = reasonBuffer.toString() || \"Upstream closed\";\n closeSocket(client, code || 1000, reason);\n });\n\n client.on(\"close\", (code: number, reasonBuffer: Buffer) => {\n const reason = reasonBuffer.toString() || \"Client closed\";\n closeSocket(upstream, code || 1000, reason);\n });\n\n upstream.on(\"error\", (error: Error) => {\n console.error(`[auth-gateway] upstream error: ${String(error)}`);\n closeSocket(client, CLOSE_UPSTREAM_ERROR, \"Upstream unavailable\");\n closeSocket(upstream, CLOSE_UPSTREAM_ERROR, \"Upstream unavailable\");\n });\n\n client.on(\"error\", (error: Error) => {\n console.error(`[auth-gateway] client error: ${String(error)}`);\n closeSocket(upstream, 1011, \"Client error\");\n });\n });\n\n server.on(\"listening\", () => {\n console.log(\n `[auth-gateway] listening ${options.listenUrl} -> ${options.upstreamUrl}`,\n );\n });\n\n const shutdown = () => {\n server.close(() => {\n process.exit(0);\n });\n };\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nfunction isDirectExecution(): boolean {\n const entry = process.argv[1];\n if (!entry) return false;\n return import.meta.url === pathToFileURL(resolve(entry)).href;\n}\n\nif (isDirectExecution()) {\n main().catch((error) => {\n console.error(\n error instanceof Error ? (error.stack ?? error.message) : String(error),\n );\n process.exit(1);\n });\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,55 @@
1
+ type BusyMode = "wait" | "steer";
2
+ interface Options {
3
+ repoRoot: string;
4
+ commsDir: string;
5
+ agentId: string;
6
+ agentName: string;
7
+ stateDir: string;
8
+ pollSeconds: number;
9
+ reconnectSeconds: number;
10
+ messageLookbackMinutes: number;
11
+ processExistingMessages: boolean;
12
+ dryRun: boolean;
13
+ runOnce: boolean;
14
+ waitAfterDispatchSeconds: number;
15
+ appServerUrl: string;
16
+ connectAppServerUrl: string;
17
+ gatewayTokenFile: string | null;
18
+ busyMode: BusyMode;
19
+ threadId: string | null;
20
+ ephemeral: boolean;
21
+ }
22
+ interface Candidate {
23
+ markerId: string;
24
+ filePath: string;
25
+ fileName: string;
26
+ sender: string;
27
+ recipient: string;
28
+ subject: string;
29
+ body: string;
30
+ mtimeMs: number;
31
+ }
32
+ interface HeadlessWarmupClient {
33
+ activeTurnId: string | null;
34
+ lastTurnStatus: string | null;
35
+ startTurn(inputText: string): Promise<string | null>;
36
+ refreshCurrentThreadState(): Promise<void>;
37
+ }
38
+ interface HeartbeatStoreRecord {
39
+ id?: string;
40
+ agent?: string;
41
+ }
42
+ type HeartbeatStore = Record<string, HeartbeatStoreRecord>;
43
+ declare const HEADLESS_WARMUP_PROMPT: string;
44
+ declare function resolveAgentId(preferredAgentName?: string | null): string;
45
+ declare function recipientMatchesAgent(recipient: string, agentId: string, agentName: string): boolean;
46
+ declare function isOwnMessageSender(sender: string, agentId: string, agentName: string): boolean;
47
+ declare function resolveAddressLabel(address: string, heartbeats: HeartbeatStore): string;
48
+ declare function resolveCurrentAgentName(agentId: string, fallbackAgentName: string, heartbeats: HeartbeatStore): string;
49
+ declare function buildUserInput(candidate: Candidate, agentName: string, heartbeats: HeartbeatStore): string;
50
+ declare function waitForTurnCompletion(client: Pick<HeadlessWarmupClient, "activeTurnId" | "lastTurnStatus" | "refreshCurrentThreadState">, turnId: string, timeoutMs: number): Promise<string | null>;
51
+ declare function maybeBootstrapHeadlessTurn(options: Options, cutoff: Date, client: HeadlessWarmupClient): Promise<boolean>;
52
+ declare function buildOptions(argv: string[]): Options;
53
+ declare function main(): Promise<void>;
54
+
55
+ export { HEADLESS_WARMUP_PROMPT, type HeadlessWarmupClient, buildOptions, buildUserInput, isOwnMessageSender, main, maybeBootstrapHeadlessTurn, recipientMatchesAgent, resolveAddressLabel, resolveAgentId, resolveCurrentAgentName, waitForTurnCompletion };