@aerostack/gateway 0.11.5 → 0.13.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,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
  [![License: MIT](https://img.shields.io/badge/LICENSE_//_MIT-3b5bdb?style=for-the-badge&labelColor=eff6ff)](https://opensource.org/licenses/MIT)
6
6
  [![npm](https://img.shields.io/npm/v/@aerostack/gateway?style=for-the-badge&color=3b5bdb&labelColor=eff6ff)](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 your workspace URL and token
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
- ### Claude Desktop
28
+ ### 2. Configure your AI tool
41
29
 
42
- **NPX (recommended):**
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://mcp.aerostack.dev/ws/your-slug",
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
- **URL:**
47
+ **Cline (VS Code)** — add to VS Code settings under `cline.mcpServers`:
62
48
 
63
49
  ```json
64
50
  {
65
- "mcpServers": {
66
- "aerostack": {
67
- "url": "https://mcp.aerostack.dev/ws/your-slug",
68
- "headers": { "Authorization": "Bearer mwt_your_token_here" }
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
- Or edit `.claude/mcp.json`:
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
- **NPX:**
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
- Edit `~/.codeium/windsurf/mcp_config.json`:
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
- **NPX:**
161
-
162
- ```json
163
- {
164
- "mcpServers": {
165
- "aerostack": {
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
- Edit `.vscode/mcp.json`:
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
- ```json
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
- If the connection isn't working, run the bridge directly in your terminal:
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
- Or test the workspace endpoint with curl:
329
-
330
- ```bash
331
- curl -X POST https://mcp.aerostack.dev/ws/your-slug \
332
- -H "Authorization: Bearer mwt_your_token_here" \
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
- **Common issues:**
110
+ Your AI agent sees either a successful result or a clear error — no special handling needed.
338
111
 
339
- | Symptom | Cause | Fix |
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
- ## Approval Gates
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
- AI calls a gated tool
355
- Workspace returns "needs approval"
356
- Bridge waits (WebSocket for instant wake, polling as fallback)
357
- You approve or reject from dashboard or mobile app
358
- Bridge retries on approval, returns error on rejection
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
- Your agent sees either a successful result or a clear error — no special handling needed.
362
-
363
- ## Local Guardian
364
-
365
- Approval gates for local operations file deletion, shell commands, git push, deploys. The `aerostack__local_guardian` tool is injected automatically when enabled in your workspace settings.
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 | — | `https://mcp.aerostack.dev/ws/your-slug` |
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: `debug`, `info`, `warn`, `error` |
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+ (for NPX mode only)
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
@@ -10,8 +10,9 @@
10
10
  * Also manages ~/.claude/settings.json hook entries when enabled/disabled.
11
11
  */
12
12
  import { createServer } from 'node:http';
13
- import { readFile, writeFile } from 'node:fs/promises';
14
- import { homedir } from 'node:os';
13
+ import { readFile, writeFile, appendFile, stat } from 'node:fs/promises';
14
+ import { watch } from 'node:fs';
15
+ import { homedir, tmpdir } from 'node:os';
15
16
  import { join } from 'node:path';
16
17
  import { info, warn, debug } from './logger.js';
17
18
  // ─── Config ───────────────────────────────────────────────────────────────
@@ -158,34 +159,93 @@ function handleHookRequest(req, res) {
158
159
  });
159
160
  }
160
161
  // ─── Public API ───────────────────────────────────────────────────────────
162
+ let fileWatcher = null;
163
+ let lastFileSize = 0;
164
+ /** Process new lines appended to the JSONL events file. */
165
+ async function processNewEvents() {
166
+ try {
167
+ const st = await stat(HOOK_EVENTS_FILE).catch(() => null);
168
+ if (!st || st.size <= lastFileSize)
169
+ return;
170
+ const content = await readFile(HOOK_EVENTS_FILE, 'utf-8');
171
+ const lines = content.split('\n').filter(Boolean);
172
+ // Only process lines we haven't seen (based on byte offset)
173
+ const newContent = content.slice(lastFileSize);
174
+ lastFileSize = st.size;
175
+ const newLines = newContent.split('\n').filter(Boolean);
176
+ for (const line of newLines) {
177
+ try {
178
+ const data = JSON.parse(line);
179
+ const toolName = data.tool_name ?? '';
180
+ // Skip read-only tools
181
+ if (READONLY_TOOLS.has(toolName))
182
+ continue;
183
+ if (!MUTATION_TOOLS.has(toolName) && !bridgeConfig.tools.includes(toolName))
184
+ continue;
185
+ const toolInput = data.tool_input ?? {};
186
+ const { category, risk } = detectCategory(toolName, toolInput);
187
+ const summary = summarizeToolInput(toolName, toolInput);
188
+ addToBatch({
189
+ action: `${toolName}: ${summary}`.slice(0, 500),
190
+ category,
191
+ risk_level: risk,
192
+ details: JSON.stringify({ tool: toolName, ...toolInput }).slice(0, 500),
193
+ });
194
+ debug(`File hook event: ${toolName}`, { category });
195
+ }
196
+ catch { /* skip malformed lines */ }
197
+ }
198
+ // Truncate file if it gets too large (>1MB)
199
+ if (st.size > 1_048_576) {
200
+ await writeFile(HOOK_EVENTS_FILE, '', 'utf-8');
201
+ lastFileSize = 0;
202
+ }
203
+ }
204
+ catch { /* file may not exist yet */ }
205
+ }
161
206
  export async function startHookServer(flushFn, port = DEFAULT_PORT) {
162
207
  batchFlushFn = flushFn;
163
- // Find available port (try configured, then increment)
164
- return new Promise((resolve, reject) => {
208
+ // Start HTTP server (still useful for non-Claude clients)
209
+ const actualPort = await new Promise((resolve, reject) => {
165
210
  httpServer = createServer(handleHookRequest);
166
211
  httpServer.on('error', (err) => {
167
212
  if (err.code === 'EADDRINUSE') {
168
- // Try next port
169
213
  info(`Port ${port} in use, trying ${port + 1}`);
170
214
  httpServer.close();
171
215
  startHookServer(flushFn, port + 1).then(resolve).catch(reject);
172
216
  }
173
217
  else {
174
- reject(err);
218
+ // Non-fatal — file-based hooks still work without HTTP server
219
+ warn('HTTP hook server failed, using file-based hooks only', { error: err.message });
220
+ resolve(0);
175
221
  }
176
222
  });
177
223
  httpServer.listen(port, '127.0.0.1', () => {
178
224
  serverPort = port;
179
225
  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
226
  resolve(port);
187
227
  });
188
228
  });
229
+ // Start file watcher for JSONL events (primary hook mechanism — no port conflicts)
230
+ try {
231
+ // Touch file so watcher has something to watch
232
+ await appendFile(HOOK_EVENTS_FILE, '');
233
+ fileWatcher = watch(HOOK_EVENTS_FILE, () => {
234
+ processNewEvents().catch(() => { });
235
+ });
236
+ info('File watcher started', { path: HOOK_EVENTS_FILE });
237
+ }
238
+ catch (err) {
239
+ warn('File watcher failed', { error: err instanceof Error ? err.message : String(err) });
240
+ }
241
+ // Start batch flush timer — also polls JSONL file as safety net for fs.watch misses
242
+ flushTimer = setInterval(() => {
243
+ processNewEvents().catch(() => { });
244
+ flushBatch().catch(err => {
245
+ warn('Batch flush failed', { error: err instanceof Error ? err.message : String(err) });
246
+ });
247
+ }, BATCH_INTERVAL_MS);
248
+ return actualPort;
189
249
  }
190
250
  export function stopHookServer() {
191
251
  if (flushTimer) {
@@ -194,6 +254,10 @@ export function stopHookServer() {
194
254
  }
195
255
  // Final flush
196
256
  flushBatch().catch(() => { });
257
+ if (fileWatcher) {
258
+ fileWatcher.close();
259
+ fileWatcher = null;
260
+ }
197
261
  if (httpServer) {
198
262
  httpServer.close();
199
263
  httpServer = null;
@@ -202,7 +266,9 @@ export function stopHookServer() {
202
266
  }
203
267
  // ─── Claude Code hook management ──────────────────────────────────────────
204
268
  const HOOK_MARKER = '/* aerostack-guardian-hook */';
205
- export async function installClaudeHook(port) {
269
+ /** Path where hook events are written as JSONL (one JSON object per line). */
270
+ export const HOOK_EVENTS_FILE = join(tmpdir(), 'aerostack-guardian-events.jsonl');
271
+ export async function installClaudeHook(_port) {
206
272
  const settingsPath = join(homedir(), '.claude', 'settings.json');
207
273
  try {
208
274
  let settings = {};
@@ -215,28 +281,26 @@ export async function installClaudeHook(port) {
215
281
  }
216
282
  const hooks = (settings.hooks ?? {});
217
283
  const preToolUse = (hooks.PreToolUse ?? []);
218
- // Check if our hook already exists
219
- const existing = preToolUse.findIndex(h => {
284
+ // Remove any old HTTP-based aerostack hooks
285
+ const cleaned = preToolUse.filter(h => {
220
286
  const innerHooks = (h.hooks ?? []);
221
- return innerHooks.some(ih => ih.url?.includes('127.0.0.1') && ih.url?.includes('/hook'));
287
+ return !innerHooks.some(ih => (ih.url?.includes('127.0.0.1') && ih.url?.includes('/hook')) ||
288
+ (ih.command?.includes('aerostack-guardian')));
222
289
  });
290
+ // Command hook: reads stdin JSON, appends to JSONL file
291
+ // No HTTP, no port, no conflicts between sessions
223
292
  const hookEntry = {
224
293
  matcher: 'Bash|Write|Edit',
225
294
  hooks: [{
226
- type: 'http',
227
- url: `http://127.0.0.1:${port}/hook`,
295
+ type: 'command',
296
+ command: `cat >> ${HOOK_EVENTS_FILE}`,
228
297
  }],
229
298
  };
230
- if (existing >= 0) {
231
- preToolUse[existing] = hookEntry;
232
- }
233
- else {
234
- preToolUse.push(hookEntry);
235
- }
236
- hooks.PreToolUse = preToolUse;
299
+ cleaned.push(hookEntry);
300
+ hooks.PreToolUse = cleaned;
237
301
  settings.hooks = hooks;
238
302
  await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
239
- info('Installed Claude Code hook', { port, path: settingsPath });
303
+ info('Installed Claude Code hook (file-based)', { eventsFile: HOOK_EVENTS_FILE, path: settingsPath });
240
304
  return true;
241
305
  }
242
306
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aerostack/gateway",
3
- "version": "0.11.5",
3
+ "version": "0.13.1",
4
4
  "description": "stdio-to-HTTP bridge connecting any MCP client to Aerostack Workspaces",
5
5
  "author": "Aerostack",
6
6
  "license": "MIT",