@aerostack/gateway 0.11.4 → 0.13.0
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 +76 -297
- package/dist/hook-server.js +269 -0
- package/dist/index.js +6 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# @aerostack/gateway
|
|
2
2
|
|
|
3
|
-
Connect any MCP client to an [Aerostack Workspace](https://aerostack.dev) — one URL, all your tools, with built-in human approval gates.
|
|
3
|
+
Connect any MCP client (Claude Desktop, Cline, Cursor, Goose, Windsurf, and more) to an [Aerostack Workspace](https://aerostack.dev) — one URL, all your tools, with built-in human approval gates.
|
|
4
4
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://www.npmjs.com/package/@aerostack/gateway)
|
|
7
7
|
|
|
8
8
|
## Why?
|
|
9
9
|
|
|
10
|
+
Many MCP clients only support **stdio** transport. Aerostack Workspaces use **HTTP**. This bridge connects them — no code required.
|
|
11
|
+
|
|
10
12
|
Instead of configuring dozens of separate MCP servers in your AI tool, point at one Aerostack Workspace and get:
|
|
11
13
|
|
|
12
14
|
- **All tools in one place** — composed from multiple MCP servers
|
|
@@ -15,33 +17,17 @@ Instead of configuring dozens of separate MCP servers in your AI tool, point at
|
|
|
15
17
|
- **Local Guardian** — approval gates for local operations (file delete, shell, git push, deploy)
|
|
16
18
|
- **Per-tool permissions** — workspace tokens scope exactly which tools are accessible
|
|
17
19
|
- **Usage tracking & audit** — every tool call logged and metered
|
|
20
|
+
- **Rate limiting** — per-token, plan-tiered
|
|
18
21
|
|
|
19
22
|
## Quick Start
|
|
20
23
|
|
|
21
|
-
### 1. Get
|
|
22
|
-
|
|
23
|
-
In your [Aerostack Dashboard](https://app.aerostack.dev), open your workspace and copy:
|
|
24
|
-
- **Workspace URL:** `https://mcp.aerostack.dev/ws/your-slug`
|
|
25
|
-
- **Token:** generate one from the Tokens tab (`mwt_...`)
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
## Setup by Client
|
|
30
|
-
|
|
31
|
-
Two connection modes:
|
|
32
|
-
|
|
33
|
-
| Mode | How | Best for |
|
|
34
|
-
|------|-----|----------|
|
|
35
|
-
| **URL** | Client connects directly over HTTP | Cursor, Windsurf, VS Code, Goose — HTTP-native clients |
|
|
36
|
-
| **NPX** | Runs a local bridge process | Claude Desktop, Cline, OpenClaw — stdio-only clients |
|
|
24
|
+
### 1. Get a workspace token
|
|
37
25
|
|
|
38
|
-
|
|
26
|
+
In your [Aerostack Admin Dashboard](https://app.aerostack.dev), open your workspace and generate a token (`mwt_...`).
|
|
39
27
|
|
|
40
|
-
###
|
|
28
|
+
### 2. Configure your AI tool
|
|
41
29
|
|
|
42
|
-
**
|
|
43
|
-
|
|
44
|
-
Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
30
|
+
**Claude Desktop** — edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
45
31
|
|
|
46
32
|
```json
|
|
47
33
|
{
|
|
@@ -50,7 +36,7 @@ Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
50
36
|
"command": "npx",
|
|
51
37
|
"args": ["-y", "@aerostack/gateway"],
|
|
52
38
|
"env": {
|
|
53
|
-
"AEROSTACK_WORKSPACE_URL": "https://
|
|
39
|
+
"AEROSTACK_WORKSPACE_URL": "https://api.aerostack.dev/api/gateway/ws/my-workspace",
|
|
54
40
|
"AEROSTACK_TOKEN": "mwt_your_token_here"
|
|
55
41
|
}
|
|
56
42
|
}
|
|
@@ -58,334 +44,127 @@ Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
58
44
|
}
|
|
59
45
|
```
|
|
60
46
|
|
|
61
|
-
**
|
|
47
|
+
**Cline (VS Code)** — add to VS Code settings under `cline.mcpServers`:
|
|
62
48
|
|
|
63
49
|
```json
|
|
64
50
|
{
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
|
|
51
|
+
"aerostack": {
|
|
52
|
+
"command": "npx",
|
|
53
|
+
"args": ["-y", "@aerostack/gateway"],
|
|
54
|
+
"env": {
|
|
55
|
+
"AEROSTACK_WORKSPACE_URL": "https://api.aerostack.dev/api/gateway/ws/my-workspace",
|
|
56
|
+
"AEROSTACK_TOKEN": "mwt_your_token_here"
|
|
69
57
|
}
|
|
70
58
|
}
|
|
71
59
|
}
|
|
72
60
|
```
|
|
73
61
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
### Claude Code
|
|
77
|
-
|
|
78
|
-
**NPX (recommended):**
|
|
62
|
+
**Cursor / Goose / HTTP-native clients** — use the workspace URL directly (no bridge needed):
|
|
79
63
|
|
|
80
|
-
```bash
|
|
81
|
-
claude mcp add aerostack \
|
|
82
|
-
--command "npx" \
|
|
83
|
-
--args "-y,@aerostack/gateway" \
|
|
84
|
-
--env "AEROSTACK_WORKSPACE_URL=https://mcp.aerostack.dev/ws/your-slug" \
|
|
85
|
-
--env "AEROSTACK_TOKEN=mwt_your_token_here"
|
|
86
64
|
```
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
```json
|
|
91
|
-
{
|
|
92
|
-
"mcpServers": {
|
|
93
|
-
"aerostack": {
|
|
94
|
-
"command": "npx",
|
|
95
|
-
"args": ["-y", "@aerostack/gateway"],
|
|
96
|
-
"env": {
|
|
97
|
-
"AEROSTACK_WORKSPACE_URL": "https://mcp.aerostack.dev/ws/your-slug",
|
|
98
|
-
"AEROSTACK_TOKEN": "mwt_your_token_here"
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
65
|
+
URL: https://api.aerostack.dev/api/gateway/ws/my-workspace
|
|
66
|
+
Token: mwt_your_token_here (Authorization: Bearer header)
|
|
103
67
|
```
|
|
104
68
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
### Cursor
|
|
69
|
+
### 3. Use it
|
|
108
70
|
|
|
109
|
-
**URL (recommended — no install needed):**
|
|
110
|
-
|
|
111
|
-
Open Cursor Settings → MCP → Add Server:
|
|
112
|
-
|
|
113
|
-
```json
|
|
114
|
-
{
|
|
115
|
-
"mcpServers": {
|
|
116
|
-
"aerostack": {
|
|
117
|
-
"url": "https://mcp.aerostack.dev/ws/your-slug",
|
|
118
|
-
"headers": { "Authorization": "Bearer mwt_your_token_here" }
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
71
|
```
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
```json
|
|
127
|
-
{
|
|
128
|
-
"mcpServers": {
|
|
129
|
-
"aerostack": {
|
|
130
|
-
"command": "npx",
|
|
131
|
-
"args": ["-y", "@aerostack/gateway"],
|
|
132
|
-
"env": {
|
|
133
|
-
"AEROSTACK_WORKSPACE_URL": "https://mcp.aerostack.dev/ws/your-slug",
|
|
134
|
-
"AEROSTACK_TOKEN": "mwt_your_token_here"
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
72
|
+
You: Create a GitHub issue for the login bug
|
|
73
|
+
AI: [calls github__create_issue via Aerostack workspace]
|
|
139
74
|
```
|
|
140
75
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
### Windsurf
|
|
144
|
-
|
|
145
|
-
**URL (recommended):**
|
|
76
|
+
Tools are namespaced as `{server}__{tool}` (e.g., `github__create_issue`, `slack__send_message`).
|
|
146
77
|
|
|
147
|
-
|
|
78
|
+
## How It Works
|
|
148
79
|
|
|
149
|
-
```json
|
|
150
|
-
{
|
|
151
|
-
"mcpServers": {
|
|
152
|
-
"aerostack": {
|
|
153
|
-
"serverUrl": "https://mcp.aerostack.dev/ws/your-slug",
|
|
154
|
-
"headers": { "Authorization": "Bearer mwt_your_token_here" }
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
80
|
```
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
"command": "npx",
|
|
167
|
-
"args": ["-y", "@aerostack/gateway"],
|
|
168
|
-
"env": {
|
|
169
|
-
"AEROSTACK_WORKSPACE_URL": "https://mcp.aerostack.dev/ws/your-slug",
|
|
170
|
-
"AEROSTACK_TOKEN": "mwt_your_token_here"
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
81
|
+
Your AI tool (stdio) → @aerostack/gateway → HTTPS → Aerostack Workspace
|
|
82
|
+
↓
|
|
83
|
+
┌────────────────────────┐
|
|
84
|
+
│ Fan-out to MCP servers │
|
|
85
|
+
│ GitHub, Slack, Gmail, │
|
|
86
|
+
│ custom Workers, etc. │
|
|
87
|
+
└────────────────────────┘
|
|
175
88
|
```
|
|
176
89
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
### VS Code
|
|
180
|
-
|
|
181
|
-
**URL:**
|
|
90
|
+
The bridge:
|
|
182
91
|
|
|
183
|
-
|
|
92
|
+
1. Starts as a stdio MCP server (what your AI tool expects)
|
|
93
|
+
2. Forwards all JSON-RPC requests to your workspace over HTTPS
|
|
94
|
+
3. Handles SSE streaming responses transparently
|
|
95
|
+
4. Manages the approval gate flow automatically
|
|
184
96
|
|
|
185
|
-
|
|
186
|
-
{
|
|
187
|
-
"servers": {
|
|
188
|
-
"aerostack": {
|
|
189
|
-
"type": "http",
|
|
190
|
-
"url": "https://mcp.aerostack.dev/ws/your-slug",
|
|
191
|
-
"headers": { "Authorization": "Bearer mwt_your_token_here" }
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
**NPX:**
|
|
198
|
-
|
|
199
|
-
```json
|
|
200
|
-
{
|
|
201
|
-
"servers": {
|
|
202
|
-
"aerostack": {
|
|
203
|
-
"type": "stdio",
|
|
204
|
-
"command": "npx",
|
|
205
|
-
"args": ["-y", "@aerostack/gateway"],
|
|
206
|
-
"env": {
|
|
207
|
-
"AEROSTACK_WORKSPACE_URL": "https://mcp.aerostack.dev/ws/your-slug",
|
|
208
|
-
"AEROSTACK_TOKEN": "mwt_your_token_here"
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
---
|
|
216
|
-
|
|
217
|
-
### Cline
|
|
218
|
-
|
|
219
|
-
**NPX:**
|
|
220
|
-
|
|
221
|
-
Open VS Code Settings → search `Cline MCP` → Edit in `settings.json`:
|
|
222
|
-
|
|
223
|
-
```json
|
|
224
|
-
{
|
|
225
|
-
"cline.mcpServers": {
|
|
226
|
-
"aerostack": {
|
|
227
|
-
"command": "npx",
|
|
228
|
-
"args": ["-y", "@aerostack/gateway"],
|
|
229
|
-
"env": {
|
|
230
|
-
"AEROSTACK_WORKSPACE_URL": "https://mcp.aerostack.dev/ws/your-slug",
|
|
231
|
-
"AEROSTACK_TOKEN": "mwt_your_token_here"
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
**URL:**
|
|
239
|
-
|
|
240
|
-
```json
|
|
241
|
-
{
|
|
242
|
-
"cline.mcpServers": {
|
|
243
|
-
"aerostack": {
|
|
244
|
-
"url": "https://mcp.aerostack.dev/ws/your-slug",
|
|
245
|
-
"headers": { "Authorization": "Bearer mwt_your_token_here" }
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
---
|
|
252
|
-
|
|
253
|
-
### Goose
|
|
254
|
-
|
|
255
|
-
**URL (recommended — Goose is HTTP-native):**
|
|
256
|
-
|
|
257
|
-
Edit `~/.config/goose/config.yaml`:
|
|
258
|
-
|
|
259
|
-
```yaml
|
|
260
|
-
mcp:
|
|
261
|
-
servers:
|
|
262
|
-
aerostack:
|
|
263
|
-
url: https://mcp.aerostack.dev/ws/your-slug
|
|
264
|
-
headers:
|
|
265
|
-
Authorization: Bearer mwt_your_token_here
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
**NPX:**
|
|
269
|
-
|
|
270
|
-
```yaml
|
|
271
|
-
mcp:
|
|
272
|
-
servers:
|
|
273
|
-
aerostack:
|
|
274
|
-
command: npx
|
|
275
|
-
args: ["-y", "@aerostack/gateway"]
|
|
276
|
-
env:
|
|
277
|
-
AEROSTACK_WORKSPACE_URL: https://mcp.aerostack.dev/ws/your-slug
|
|
278
|
-
AEROSTACK_TOKEN: mwt_your_token_here
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
---
|
|
282
|
-
|
|
283
|
-
### OpenClaw
|
|
284
|
-
|
|
285
|
-
**NPX:**
|
|
286
|
-
|
|
287
|
-
```json
|
|
288
|
-
{
|
|
289
|
-
"mcp": {
|
|
290
|
-
"servers": {
|
|
291
|
-
"aerostack": {
|
|
292
|
-
"command": "npx",
|
|
293
|
-
"args": ["-y", "@aerostack/gateway"],
|
|
294
|
-
"env": {
|
|
295
|
-
"AEROSTACK_WORKSPACE_URL": "https://mcp.aerostack.dev/ws/your-slug",
|
|
296
|
-
"AEROSTACK_TOKEN": "mwt_your_token_here"
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
---
|
|
305
|
-
|
|
306
|
-
### Any HTTP-native client
|
|
307
|
-
|
|
308
|
-
Use the workspace URL directly — no package needed:
|
|
309
|
-
|
|
310
|
-
```
|
|
311
|
-
URL: https://mcp.aerostack.dev/ws/your-slug
|
|
312
|
-
Header: Authorization: Bearer mwt_your_token_here
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
---
|
|
316
|
-
|
|
317
|
-
## Debugging
|
|
97
|
+
## Approval Gates
|
|
318
98
|
|
|
319
|
-
|
|
99
|
+
When your workspace has approval rules configured, the bridge handles them transparently:
|
|
320
100
|
|
|
321
|
-
```bash
|
|
322
|
-
AEROSTACK_LOG_LEVEL=debug \
|
|
323
|
-
AEROSTACK_WORKSPACE_URL=https://mcp.aerostack.dev/ws/your-slug \
|
|
324
|
-
AEROSTACK_TOKEN=mwt_your_token_here \
|
|
325
|
-
npx -y @aerostack/gateway
|
|
326
101
|
```
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
-H "Content-Type: application/json" \
|
|
334
|
-
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
|
|
102
|
+
AI calls dangerous_tool
|
|
103
|
+
→ Bridge forwards to workspace
|
|
104
|
+
→ Workspace returns "needs approval" (-32050)
|
|
105
|
+
→ Bridge waits via WebSocket (instant) or polls as fallback
|
|
106
|
+
→ You approve/reject in dashboard or mobile app
|
|
107
|
+
→ Bridge retries on approval, returns error on rejection
|
|
335
108
|
```
|
|
336
109
|
|
|
337
|
-
|
|
110
|
+
Your AI agent sees either a successful result or a clear error — no special handling needed.
|
|
338
111
|
|
|
339
|
-
|
|
340
|
-
|---------|-------|-----|
|
|
341
|
-
| `401 Unauthorized` | Invalid or expired token | Generate a new token from the Tokens tab |
|
|
342
|
-
| `404 Not Found` | Wrong workspace slug | Check the slug in your dashboard URL |
|
|
343
|
-
| Tools list is empty | No servers added to workspace | Add MCP servers from the marketplace |
|
|
344
|
-
| `npx: command not found` | Node.js not installed | Install Node.js 18+ from nodejs.org |
|
|
345
|
-
| Old version cached by npx | Stale npx cache | Run `npx --yes @aerostack/gateway@latest` |
|
|
112
|
+
## Local Guardian
|
|
346
113
|
|
|
347
|
-
|
|
114
|
+
Approval gates for **local operations** — file deletion, destructive shell commands, git push, deploys. The agent asks for approval before acting on your machine.
|
|
348
115
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
When workspace approval rules are configured, the bridge handles them transparently:
|
|
116
|
+
The bridge injects an `aerostack__local_guardian` tool. When the LLM is about to perform a risky local operation, it calls this tool first. You get a push notification and can approve or reject from the dashboard or your phone.
|
|
352
117
|
|
|
353
118
|
```
|
|
354
|
-
|
|
355
|
-
→
|
|
356
|
-
→
|
|
357
|
-
→
|
|
358
|
-
→
|
|
119
|
+
Agent wants to: rm -rf ./old-config/
|
|
120
|
+
→ Agent calls aerostack__local_guardian
|
|
121
|
+
→ You get push notification on your phone
|
|
122
|
+
→ Tap Approve or Reject
|
|
123
|
+
→ Agent proceeds or stops
|
|
359
124
|
```
|
|
360
125
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
126
|
+
| Category | What it covers |
|
|
127
|
+
|----------|---------------|
|
|
128
|
+
| `file_delete` | Deleting or overwriting files |
|
|
129
|
+
| `shell_destructive` | `rm -rf`, `DROP TABLE`, etc. |
|
|
130
|
+
| `git_push` | `git push`, force push, `git reset --hard` |
|
|
131
|
+
| `config_modify` | `.env`, credentials, production configs |
|
|
132
|
+
| `deploy` | Deploy, publish, release to any environment |
|
|
133
|
+
| `database` | Direct database mutations outside migrations |
|
|
366
134
|
|
|
367
135
|
## Configuration
|
|
368
136
|
|
|
369
137
|
| Variable | Required | Default | Description |
|
|
370
138
|
|----------|----------|---------|-------------|
|
|
371
|
-
| `AEROSTACK_WORKSPACE_URL` | Yes | — |
|
|
139
|
+
| `AEROSTACK_WORKSPACE_URL` | Yes | — | Full workspace endpoint URL |
|
|
372
140
|
| `AEROSTACK_TOKEN` | Yes | — | Workspace token (`mwt_...`) |
|
|
373
141
|
| `AEROSTACK_APPROVAL_POLL_MS` | No | `3000` | Approval polling interval (ms) |
|
|
374
142
|
| `AEROSTACK_APPROVAL_TIMEOUT_MS` | No | `300000` | Max approval wait time (5 min) |
|
|
375
143
|
| `AEROSTACK_REQUEST_TIMEOUT_MS` | No | `30000` | HTTP request timeout (30s) |
|
|
376
144
|
| `AEROSTACK_LOCAL_GUARDIAN` | No | `true` | Set `false` to disable Local Guardian |
|
|
377
|
-
| `AEROSTACK_LOG_LEVEL` | No | `info` | Log level:
|
|
145
|
+
| `AEROSTACK_LOG_LEVEL` | No | `info` | Log level: debug, info, warn, error |
|
|
146
|
+
|
|
147
|
+
## Supported MCP Methods
|
|
148
|
+
|
|
149
|
+
| Method | Description |
|
|
150
|
+
|--------|-------------|
|
|
151
|
+
| `tools/list` | List all tools from all workspace servers |
|
|
152
|
+
| `tools/call` | Call a namespaced tool (with approval handling) |
|
|
153
|
+
| `resources/list` | List resources from all workspace servers |
|
|
154
|
+
| `resources/read` | Read a specific resource |
|
|
155
|
+
| `prompts/list` | List prompts from all workspace servers |
|
|
156
|
+
| `prompts/get` | Get a prompt with arguments |
|
|
378
157
|
|
|
379
158
|
## Requirements
|
|
380
159
|
|
|
381
|
-
- Node.js 18+
|
|
160
|
+
- Node.js 18+
|
|
382
161
|
- An Aerostack account with a configured workspace
|
|
383
162
|
- A workspace token (`mwt_...`)
|
|
384
163
|
|
|
385
164
|
## Links
|
|
386
165
|
|
|
387
|
-
- [Aerostack Dashboard](https://app.aerostack.dev)
|
|
388
166
|
- [Aerostack Docs](https://docs.aerostack.dev)
|
|
167
|
+
- [Aerostack Admin](https://app.aerostack.dev)
|
|
389
168
|
- [MCP Specification](https://modelcontextprotocol.io)
|
|
390
169
|
|
|
391
170
|
## License
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Hook Server — receives Claude Code PreToolUse hooks on localhost,
|
|
3
|
+
* batches events, and flushes to Aerostack gateway periodically.
|
|
4
|
+
*
|
|
5
|
+
* Architecture:
|
|
6
|
+
* Claude Code → PreToolUse hook → POST http://localhost:{port}/hook
|
|
7
|
+
* → batched in memory (30s window)
|
|
8
|
+
* → flush: single POST to gateway with guardian_report for each event
|
|
9
|
+
*
|
|
10
|
+
* Also manages ~/.claude/settings.json hook entries when enabled/disabled.
|
|
11
|
+
*/
|
|
12
|
+
import { createServer } from 'node:http';
|
|
13
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { info, warn, debug } from './logger.js';
|
|
17
|
+
// ─── Config ───────────────────────────────────────────────────────────────
|
|
18
|
+
const DEFAULT_PORT = 18321;
|
|
19
|
+
const BATCH_INTERVAL_MS = 30_000; // 30 seconds
|
|
20
|
+
const MAX_BATCH_SIZE = 200;
|
|
21
|
+
// Tools that are mutations (worth tracking)
|
|
22
|
+
const MUTATION_TOOLS = new Set([
|
|
23
|
+
'Bash', 'Write', 'Edit', 'NotebookEdit',
|
|
24
|
+
// MCP tools caught by bridge auto-report, but hook might see them too
|
|
25
|
+
]);
|
|
26
|
+
// Tools that are read-only (skip by default)
|
|
27
|
+
const READONLY_TOOLS = new Set([
|
|
28
|
+
'Read', 'Glob', 'Grep', 'LS', 'WebSearch', 'WebFetch',
|
|
29
|
+
'Agent', 'AskUserQuestion', 'TodoRead', 'TaskList', 'TaskGet',
|
|
30
|
+
]);
|
|
31
|
+
// ─── Category detection ───────────────────────────────────────────────────
|
|
32
|
+
function detectCategory(toolName, toolInput) {
|
|
33
|
+
if (toolName === 'Bash') {
|
|
34
|
+
const cmd = toolInput.command || '';
|
|
35
|
+
const lower = cmd.toLowerCase();
|
|
36
|
+
if (lower.includes('rm ') || lower.includes('rm\t') || lower.includes('rmdir') || lower.includes('unlink'))
|
|
37
|
+
return { category: 'file_delete', risk: 'high' };
|
|
38
|
+
if (lower.includes('git push') || lower.includes('git reset'))
|
|
39
|
+
return { category: 'deploy', risk: 'high' };
|
|
40
|
+
if (lower.includes('npm install') || lower.includes('pip install') || lower.includes('yarn add'))
|
|
41
|
+
return { category: 'package_install', risk: 'medium' };
|
|
42
|
+
if (lower.includes('curl ') || lower.includes('wget ') || lower.includes('fetch'))
|
|
43
|
+
return { category: 'api_call', risk: 'low' };
|
|
44
|
+
if (lower.includes('deploy') || lower.includes('wrangler'))
|
|
45
|
+
return { category: 'deploy', risk: 'high' };
|
|
46
|
+
return { category: 'exec_command', risk: 'low' };
|
|
47
|
+
}
|
|
48
|
+
if (toolName === 'Write')
|
|
49
|
+
return { category: 'file_write', risk: 'medium' };
|
|
50
|
+
if (toolName === 'Edit' || toolName === 'NotebookEdit')
|
|
51
|
+
return { category: 'file_write', risk: 'low' };
|
|
52
|
+
return { category: 'other', risk: 'low' };
|
|
53
|
+
}
|
|
54
|
+
function summarizeToolInput(toolName, toolInput) {
|
|
55
|
+
if (toolName === 'Bash')
|
|
56
|
+
return toolInput.command?.slice(0, 200) || '';
|
|
57
|
+
if (toolName === 'Write')
|
|
58
|
+
return toolInput.file_path || '';
|
|
59
|
+
if (toolName === 'Edit')
|
|
60
|
+
return toolInput.file_path || '';
|
|
61
|
+
// Generic
|
|
62
|
+
const keys = Object.keys(toolInput);
|
|
63
|
+
if (keys.length === 0)
|
|
64
|
+
return '';
|
|
65
|
+
const first = toolInput[keys[0]];
|
|
66
|
+
return typeof first === 'string' ? first.slice(0, 200) : JSON.stringify(toolInput).slice(0, 200);
|
|
67
|
+
}
|
|
68
|
+
// ─── Batch buffer ─────────────────────────────────────────────────────────
|
|
69
|
+
let eventBatch = [];
|
|
70
|
+
let flushTimer = null;
|
|
71
|
+
let batchFlushFn = null;
|
|
72
|
+
// Bridge config — updated from gateway response on each batch flush
|
|
73
|
+
let bridgeConfig = {
|
|
74
|
+
enabled: true,
|
|
75
|
+
tools: ['Bash', 'Write', 'Edit'],
|
|
76
|
+
batch_interval_seconds: 30,
|
|
77
|
+
categories: ['exec_command', 'file_write', 'file_delete', 'deploy', 'config_change'],
|
|
78
|
+
};
|
|
79
|
+
let currentBatchInterval = BATCH_INTERVAL_MS;
|
|
80
|
+
export function getBridgeConfig() { return bridgeConfig; }
|
|
81
|
+
function addToBatch(event) {
|
|
82
|
+
if (!bridgeConfig.enabled)
|
|
83
|
+
return; // tracking disabled from dashboard
|
|
84
|
+
if (eventBatch.length >= MAX_BATCH_SIZE) {
|
|
85
|
+
eventBatch.shift();
|
|
86
|
+
}
|
|
87
|
+
eventBatch.push(event);
|
|
88
|
+
}
|
|
89
|
+
async function flushBatch() {
|
|
90
|
+
if (eventBatch.length === 0 || !batchFlushFn)
|
|
91
|
+
return;
|
|
92
|
+
const batch = eventBatch.splice(0);
|
|
93
|
+
debug(`Flushing ${batch.length} hook events`);
|
|
94
|
+
const newConfig = await batchFlushFn(batch);
|
|
95
|
+
// Update bridge config from gateway response (live config sync)
|
|
96
|
+
if (newConfig) {
|
|
97
|
+
const changed = JSON.stringify(bridgeConfig) !== JSON.stringify(newConfig);
|
|
98
|
+
bridgeConfig = newConfig;
|
|
99
|
+
if (changed) {
|
|
100
|
+
info('Bridge config updated from gateway', { enabled: newConfig.enabled, tools: newConfig.tools });
|
|
101
|
+
// Update batch interval if changed
|
|
102
|
+
if (newConfig.batch_interval_seconds * 1000 !== currentBatchInterval) {
|
|
103
|
+
currentBatchInterval = newConfig.batch_interval_seconds * 1000;
|
|
104
|
+
if (flushTimer) {
|
|
105
|
+
clearInterval(flushTimer);
|
|
106
|
+
flushTimer = setInterval(() => { flushBatch().catch(() => { }); }, currentBatchInterval);
|
|
107
|
+
info('Batch interval updated', { seconds: newConfig.batch_interval_seconds });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
info(`Flushed ${batch.length} hook events to gateway`);
|
|
113
|
+
}
|
|
114
|
+
// ─── HTTP Server ──────────────────────────────────────────────────────────
|
|
115
|
+
let httpServer = null;
|
|
116
|
+
let serverPort = null;
|
|
117
|
+
function handleHookRequest(req, res) {
|
|
118
|
+
if (req.method !== 'POST' || !req.url?.startsWith('/hook')) {
|
|
119
|
+
res.writeHead(404);
|
|
120
|
+
res.end();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const chunks = [];
|
|
124
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
125
|
+
req.on('end', () => {
|
|
126
|
+
try {
|
|
127
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
128
|
+
const toolName = body.tool_name ?? '';
|
|
129
|
+
// Skip tools not in the configured tracking list
|
|
130
|
+
if (!bridgeConfig.tools.includes(toolName) && !MUTATION_TOOLS.has(toolName)) {
|
|
131
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
132
|
+
res.end('{"status":"skipped"}');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Skip read-only tools unless explicitly configured
|
|
136
|
+
if (READONLY_TOOLS.has(toolName) && !bridgeConfig.tools.includes(toolName)) {
|
|
137
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
138
|
+
res.end('{"status":"skipped"}');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const toolInput = body.tool_input ?? {};
|
|
142
|
+
const { category, risk } = detectCategory(toolName, toolInput);
|
|
143
|
+
const summary = summarizeToolInput(toolName, toolInput);
|
|
144
|
+
addToBatch({
|
|
145
|
+
action: `${toolName}: ${summary}`.slice(0, 500),
|
|
146
|
+
category,
|
|
147
|
+
risk_level: risk,
|
|
148
|
+
details: JSON.stringify({ tool: toolName, ...toolInput }).slice(0, 500),
|
|
149
|
+
});
|
|
150
|
+
debug(`Hook received: ${toolName}`, { category, risk });
|
|
151
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
152
|
+
res.end('{"status":"queued"}');
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
res.writeHead(400);
|
|
156
|
+
res.end('{"error":"invalid JSON"}');
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
// ─── Public API ───────────────────────────────────────────────────────────
|
|
161
|
+
export async function startHookServer(flushFn, port = DEFAULT_PORT) {
|
|
162
|
+
batchFlushFn = flushFn;
|
|
163
|
+
// Find available port (try configured, then increment)
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
httpServer = createServer(handleHookRequest);
|
|
166
|
+
httpServer.on('error', (err) => {
|
|
167
|
+
if (err.code === 'EADDRINUSE') {
|
|
168
|
+
// Try next port
|
|
169
|
+
info(`Port ${port} in use, trying ${port + 1}`);
|
|
170
|
+
httpServer.close();
|
|
171
|
+
startHookServer(flushFn, port + 1).then(resolve).catch(reject);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
reject(err);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
httpServer.listen(port, '127.0.0.1', () => {
|
|
178
|
+
serverPort = port;
|
|
179
|
+
info(`Hook server listening on http://127.0.0.1:${port}/hook`);
|
|
180
|
+
// Start batch flush timer
|
|
181
|
+
flushTimer = setInterval(() => {
|
|
182
|
+
flushBatch().catch(err => {
|
|
183
|
+
warn('Batch flush failed', { error: err instanceof Error ? err.message : String(err) });
|
|
184
|
+
});
|
|
185
|
+
}, BATCH_INTERVAL_MS);
|
|
186
|
+
resolve(port);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
export function stopHookServer() {
|
|
191
|
+
if (flushTimer) {
|
|
192
|
+
clearInterval(flushTimer);
|
|
193
|
+
flushTimer = null;
|
|
194
|
+
}
|
|
195
|
+
// Final flush
|
|
196
|
+
flushBatch().catch(() => { });
|
|
197
|
+
if (httpServer) {
|
|
198
|
+
httpServer.close();
|
|
199
|
+
httpServer = null;
|
|
200
|
+
}
|
|
201
|
+
serverPort = null;
|
|
202
|
+
}
|
|
203
|
+
// ─── Claude Code hook management ──────────────────────────────────────────
|
|
204
|
+
const HOOK_MARKER = '/* aerostack-guardian-hook */';
|
|
205
|
+
export async function installClaudeHook(port) {
|
|
206
|
+
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
|
207
|
+
try {
|
|
208
|
+
let settings = {};
|
|
209
|
+
try {
|
|
210
|
+
const raw = await readFile(settingsPath, 'utf-8');
|
|
211
|
+
settings = JSON.parse(raw);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// File doesn't exist or invalid — create fresh
|
|
215
|
+
}
|
|
216
|
+
const hooks = (settings.hooks ?? {});
|
|
217
|
+
const preToolUse = (hooks.PreToolUse ?? []);
|
|
218
|
+
// Check if our hook already exists
|
|
219
|
+
const existing = preToolUse.findIndex(h => {
|
|
220
|
+
const innerHooks = (h.hooks ?? []);
|
|
221
|
+
return innerHooks.some(ih => ih.url?.includes('127.0.0.1') && ih.url?.includes('/hook'));
|
|
222
|
+
});
|
|
223
|
+
const hookEntry = {
|
|
224
|
+
matcher: 'Bash|Write|Edit',
|
|
225
|
+
hooks: [{
|
|
226
|
+
type: 'http',
|
|
227
|
+
url: `http://127.0.0.1:${port}/hook`,
|
|
228
|
+
}],
|
|
229
|
+
};
|
|
230
|
+
if (existing >= 0) {
|
|
231
|
+
preToolUse[existing] = hookEntry;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
preToolUse.push(hookEntry);
|
|
235
|
+
}
|
|
236
|
+
hooks.PreToolUse = preToolUse;
|
|
237
|
+
settings.hooks = hooks;
|
|
238
|
+
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
239
|
+
info('Installed Claude Code hook', { port, path: settingsPath });
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
warn('Failed to install Claude Code hook', { error: err instanceof Error ? err.message : String(err) });
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
export async function uninstallClaudeHook() {
|
|
248
|
+
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
|
249
|
+
try {
|
|
250
|
+
const raw = await readFile(settingsPath, 'utf-8');
|
|
251
|
+
const settings = JSON.parse(raw);
|
|
252
|
+
const hooks = (settings.hooks ?? {});
|
|
253
|
+
const preToolUse = (hooks.PreToolUse ?? []);
|
|
254
|
+
const filtered = preToolUse.filter(h => {
|
|
255
|
+
const innerHooks = (h.hooks ?? []);
|
|
256
|
+
return !innerHooks.some(ih => ih.url?.includes('127.0.0.1') && ih.url?.includes('/hook'));
|
|
257
|
+
});
|
|
258
|
+
if (filtered.length === preToolUse.length)
|
|
259
|
+
return false; // nothing to remove
|
|
260
|
+
hooks.PreToolUse = filtered;
|
|
261
|
+
settings.hooks = hooks;
|
|
262
|
+
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
263
|
+
info('Uninstalled Claude Code hook');
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import{Server as
|
|
4
|
-
`),process.exit(1)),
|
|
5
|
-
`),process.exit(1));let
|
|
6
|
-
`),process.exit(1)}
|
|
7
|
-
`);const w
|
|
8
|
-
`);let
|
|
3
|
+
import{Server as T}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as A}from"@modelcontextprotocol/sdk/server/stdio.js";import{ListToolsRequestSchema as v,CallToolRequestSchema as y,ListResourcesRequestSchema as P,ReadResourceRequestSchema as k,ListPromptsRequestSchema as C,GetPromptRequestSchema as K}from"@modelcontextprotocol/sdk/types.js";import{resolveApproval as R}from"./resolution.js";import{startHookServer as L,installClaudeHook as b,stopHookServer as h}from"./hook-server.js";import{info as p,warn as I,error as U}from"./logger.js";const w=process.env.AEROSTACK_WORKSPACE_URL,g=process.env.AEROSTACK_TOKEN;function _(t,s,r){const e=parseInt(t??String(s),10);return Number.isFinite(e)&&e>=r?e:s}const E=_(process.env.AEROSTACK_APPROVAL_POLL_MS,3e3,500),O=_(process.env.AEROSTACK_APPROVAL_TIMEOUT_MS,864e5,5e3),q=_(process.env.AEROSTACK_REQUEST_TIMEOUT_MS,3e4,1e3),j=process.env.AEROSTACK_HOOK_SERVER!=="false",x=_(process.env.AEROSTACK_HOOK_PORT,18321,1024),H=process.env.AEROSTACK_HOOK_AUTO_INSTALL!=="false";w||(process.stderr.write(`ERROR: AEROSTACK_WORKSPACE_URL is required
|
|
4
|
+
`),process.exit(1)),g||(process.stderr.write(`ERROR: AEROSTACK_TOKEN is required
|
|
5
|
+
`),process.exit(1));let f;try{if(f=new URL(w),f.protocol!=="https:"&&f.protocol!=="http:")throw new Error("must be http or https")}catch{process.stderr.write(`ERROR: AEROSTACK_WORKSPACE_URL must be a valid HTTP(S) URL
|
|
6
|
+
`),process.exit(1)}f.protocol==="http:"&&!f.hostname.match(/^(localhost|127\.0\.0\.1)$/)&&process.stderr.write(`WARNING: Using HTTP (not HTTPS) \u2014 token will be sent in plaintext
|
|
7
|
+
`);const d=w.replace(/\/+$/,"");async function c(t,s){const r={jsonrpc:"2.0",id:Date.now(),method:t,params:s??{}},e=new AbortController,n=setTimeout(()=>e.abort(),q);try{const o=await fetch(d,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${g}`,"User-Agent":"aerostack-gateway/0.12.0","X-Agent-Id":"aerostack-gateway"},body:JSON.stringify(r),signal:e.signal});if(clearTimeout(n),(o.headers.get("content-type")??"").includes("text/event-stream")){const i=await o.text();return N(i,r.id)}return await o.json()}catch(o){clearTimeout(n);const a=o instanceof Error?o.message:"Unknown error";return o instanceof Error&&o.name==="AbortError"?{jsonrpc:"2.0",id:r.id,error:{code:-32603,message:"Request timed out"}}:{jsonrpc:"2.0",id:r.id,error:{code:-32603,message:`HTTP error: ${a}`}}}}function N(t,s){const r=t.split(`
|
|
8
|
+
`);let e=null;for(const n of r)if(n.startsWith("data: "))try{e=JSON.parse(n.slice(6))}catch{}return e??{jsonrpc:"2.0",id:s,error:{code:-32603,message:"Empty SSE response"}}}const $=new Set(["aerostack__guardian_report","aerostack__check_approval","aerostack__guardian_check"]);function M(t,s){if($.has(t))return;let r="other";const e=t.toLowerCase();e.includes("exec")||e.includes("bash")||e.includes("shell")||e.includes("command")||e.includes("run")?r="exec_command":e.includes("write")||e.includes("edit")||e.includes("create")||e.includes("patch")?r="file_write":e.includes("delete")||e.includes("remove")||e.includes("trash")||e.includes("unlink")?r="file_delete":e.includes("fetch")||e.includes("http")||e.includes("request")||e.includes("api")||e.includes("get")||e.includes("post")?r="api_call":e.includes("install")||e.includes("package")||e.includes("npm")||e.includes("pip")?r="package_install":e.includes("config")||e.includes("setting")||e.includes("env")?r="config_change":e.includes("deploy")||e.includes("publish")||e.includes("release")?r="deploy":e.includes("send")||e.includes("message")||e.includes("email")||e.includes("notify")||e.includes("slack")||e.includes("telegram")?r="message_send":(e.includes("read")||e.includes("query")||e.includes("search")||e.includes("list")||e.includes("get"))&&(r="data_access");let n;try{const o=JSON.stringify(s);n=o.length>500?o.slice(0,500)+"...":o}catch{n="(unable to serialize)"}c("tools/call",{name:"aerostack__guardian_report",arguments:{action:`${t}(${Object.keys(s).join(", ")})`,category:r,risk_level:"low",details:n}}).catch(()=>{})}async function V(t,s){M(t,s);const r=await c("tools/call",{name:t,arguments:s});if(r.error?.code===-32050){const o=r.error.data,a=o?.approval_id;if(!a||!/^[a-zA-Z0-9_-]{4,128}$/.test(a))return{jsonrpc:"2.0",id:r.id,error:{code:-32603,message:"Approval required but no approval_id returned"}};p("Tool gate: waiting for approval",{approvalId:a,transport:o?.ws_url?"ws":"poll"});const l=o?.polling_url??`${d}/approval-status/${a}`,i=await R({approvalId:a,wsUrl:o?.ws_url,pollUrl:l,pollIntervalMs:E,timeoutMs:O});return i.status==="rejected"?{jsonrpc:"2.0",id:r.id,error:{code:-32603,message:`Tool call rejected: ${i.reviewer_note??"no reason given"}`}}:i.status==="expired"?{jsonrpc:"2.0",id:r.id,error:{code:-32603,message:"Approval request expired"}}:(p("Retrying tool call after approval",{approvalId:a,status:i.status}),c("tools/call",{name:t,arguments:s}))}const n=r.result?._meta;if(n?.approval_id&&n?.status==="pending"){const o=n.approval_id;p("Permission gate: waiting for approval",{approvalId:o,transport:n.ws_url?"ws":"poll"});const a=n.polling_url??`${d}/approval-status/${o}`,l=await R({approvalId:o,wsUrl:n.ws_url,pollUrl:a,pollIntervalMs:E,timeoutMs:O});let i;return l.status==="approved"||l.status==="executed"?i="APPROVED \u2014 Your request has been approved. You may proceed with the action.":l.status==="rejected"?i=`REJECTED \u2014 Your request was denied. Reason: ${l.reviewer_note??"No reason given."}. Do NOT proceed.`:i="EXPIRED \u2014 Your approval request timed out. Submit a new request if needed.",{jsonrpc:"2.0",id:r.id,result:{content:[{type:"text",text:i}]}}}return r}let S=null;async function m(){if(S)return;const t=await c("initialize",{protocolVersion:"2024-11-05",capabilities:{},clientInfo:{name:"aerostack-gateway",version:"0.12.0"}});if(t.result){const s=t.result;S={protocolVersion:s.protocolVersion??"2024-11-05",instructions:s.instructions}}}const u=new T({name:"aerostack-gateway",version:"0.12.0"},{capabilities:{tools:{},resources:{},prompts:{}}});u.setRequestHandler(v,async()=>{await m();const t=await c("tools/list");if(t.error)throw new Error(t.error.message);return{tools:t.result.tools??[]}}),u.setRequestHandler(y,async t=>{await m();const{name:s,arguments:r}=t.params,e=await V(s,r??{});return e.error?{content:[{type:"text",text:`Error: ${e.error.message}`}],isError:!0}:{content:e.result.content??[{type:"text",text:JSON.stringify(e.result)}]}}),u.setRequestHandler(P,async()=>{await m();const t=await c("resources/list");if(t.error)throw new Error(t.error.message);return{resources:t.result.resources??[]}}),u.setRequestHandler(k,async t=>{await m();const s=await c("resources/read",{uri:t.params.uri});if(s.error)throw new Error(s.error.message);return{contents:s.result.contents??[]}}),u.setRequestHandler(C,async()=>{await m();const t=await c("prompts/list");if(t.error)throw new Error(t.error.message);return{prompts:t.result.prompts??[]}}),u.setRequestHandler(K,async t=>{await m();const s=await c("prompts/get",{name:t.params.name,arguments:t.params.arguments});if(s.error)throw new Error(s.error.message);return{messages:s.result.messages??[]}});async function D(){p("Connecting to workspace",{url:d});const t=new A;if(await u.connect(t),p("Ready",{url:d}),j)try{const r=await L(async e=>{try{const n=await fetch(`${d}/guardian-batch`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${g}`,"User-Agent":"aerostack-gateway/0.13.0","X-Agent-Id":"aerostack-gateway"},body:JSON.stringify({events:e})});return n.ok?(await n.json()).config?.hook_tracking??null:null}catch{return null}},x);H&&await b(r)&&p("Claude Code hook auto-installed",{port:r})}catch(s){I("Hook server failed to start (non-fatal)",{error:s instanceof Error?s.message:String(s)})}}process.on("SIGTERM",()=>{h(),process.exit(0)}),process.on("SIGINT",()=>{h(),process.exit(0)}),D().catch(t=>{U("Fatal error",{error:t instanceof Error?t.message:String(t)}),process.exit(1)});
|