chrome-ai-bridge 2.0.19 → 2.3.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/build/src/config.js +26 -0
- package/build/src/fast-cdp/agent-context.js +108 -0
- package/build/src/fast-cdp/fast-chat.js +85 -126
- package/build/src/fast-cdp/session-manager.js +214 -0
- package/build/src/main.js +190 -0
- package/build/src/process-lock.js +262 -0
- package/build/src/stdio-http-proxy.js +157 -0
- package/package.json +3 -2
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process lock management using exclusive file lock.
|
|
3
|
+
*
|
|
4
|
+
* Lock file stores JSON: {pid, port, startedAt}
|
|
5
|
+
* Multi-client mode: alive processes are NOT killed — Secondary
|
|
6
|
+
* instances connect to the Primary via HTTP proxy instead.
|
|
7
|
+
*/
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { logger } from './logger.js';
|
|
13
|
+
const LOCK_DIR = path.join(os.homedir(), '.cache', 'chrome-ai-bridge');
|
|
14
|
+
const LOCK_FILE = path.join(LOCK_DIR, 'mcp.lock');
|
|
15
|
+
let lockFd = null;
|
|
16
|
+
function isProcessAlive(pid) {
|
|
17
|
+
try {
|
|
18
|
+
process.kill(pid, 0);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function sleep(ms) {
|
|
26
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Read lock file content. Supports both JSON (new) and plain PID (legacy).
|
|
30
|
+
*/
|
|
31
|
+
export function readLockInfo() {
|
|
32
|
+
try {
|
|
33
|
+
const content = fs.readFileSync(LOCK_FILE, 'utf-8').trim();
|
|
34
|
+
if (!content)
|
|
35
|
+
return null;
|
|
36
|
+
// Try JSON format first
|
|
37
|
+
if (content.startsWith('{')) {
|
|
38
|
+
const parsed = JSON.parse(content);
|
|
39
|
+
if (parsed.pid && parsed.pid > 0) {
|
|
40
|
+
return {
|
|
41
|
+
pid: parsed.pid,
|
|
42
|
+
port: parsed.port ?? 0,
|
|
43
|
+
startedAt: parsed.startedAt ?? '',
|
|
44
|
+
instanceId: parsed.instanceId ?? '',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
// Legacy: plain PID number
|
|
50
|
+
const pid = Number(content);
|
|
51
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
52
|
+
return { pid, port: 0, startedAt: '', instanceId: '' };
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check if an existing Primary is alive and reachable.
|
|
62
|
+
* Reads the lock file and checks process liveness.
|
|
63
|
+
*/
|
|
64
|
+
export function checkExistingPrimary() {
|
|
65
|
+
const info = readLockInfo();
|
|
66
|
+
if (!info)
|
|
67
|
+
return null;
|
|
68
|
+
if (!isProcessAlive(info.pid)) {
|
|
69
|
+
logger(`[process-lock] Stale lock (pid=${info.pid}, not running).`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return { alive: true, port: info.port, instanceId: info.instanceId };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Try to create lock file exclusively (wx flag).
|
|
76
|
+
* Writes JSON {pid, port, startedAt}.
|
|
77
|
+
* Returns the file descriptor on success, null on EEXIST.
|
|
78
|
+
*/
|
|
79
|
+
function tryCreateLock(port, instanceId) {
|
|
80
|
+
try {
|
|
81
|
+
fs.mkdirSync(LOCK_DIR, { recursive: true });
|
|
82
|
+
const fd = fs.openSync(LOCK_FILE, 'wx');
|
|
83
|
+
const lockInfo = {
|
|
84
|
+
pid: process.pid,
|
|
85
|
+
port,
|
|
86
|
+
startedAt: new Date().toISOString(),
|
|
87
|
+
instanceId,
|
|
88
|
+
};
|
|
89
|
+
fs.writeSync(fd, JSON.stringify(lockInfo));
|
|
90
|
+
return fd;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error instanceof Error && 'code' in error && error.code === 'EEXIST') {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Handle an existing lock file.
|
|
101
|
+
* In multi-client mode, alive processes are NOT killed.
|
|
102
|
+
* Only stale (dead process) locks are removed.
|
|
103
|
+
* Returns true if the stale lock was removed and retry is possible.
|
|
104
|
+
* Returns false if the lock holder is alive (should enter proxy mode).
|
|
105
|
+
*/
|
|
106
|
+
async function handleExistingLock() {
|
|
107
|
+
const info = readLockInfo();
|
|
108
|
+
if (info === null) {
|
|
109
|
+
logger('[process-lock] Corrupted lock file found. Removing.');
|
|
110
|
+
try {
|
|
111
|
+
fs.unlinkSync(LOCK_FILE);
|
|
112
|
+
}
|
|
113
|
+
catch { /* ignore */ }
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
// Don't conflict with ourselves
|
|
117
|
+
if (info.pid === process.pid) {
|
|
118
|
+
try {
|
|
119
|
+
fs.unlinkSync(LOCK_FILE);
|
|
120
|
+
}
|
|
121
|
+
catch { /* ignore */ }
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
if (!isProcessAlive(info.pid)) {
|
|
125
|
+
logger(`[process-lock] Stale lock (pid=${info.pid}, not running). Removing.`);
|
|
126
|
+
try {
|
|
127
|
+
fs.unlinkSync(LOCK_FILE);
|
|
128
|
+
}
|
|
129
|
+
catch { /* ignore */ }
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
// Process is alive — do NOT kill. Caller should enter proxy mode.
|
|
133
|
+
logger(`[process-lock] Primary is alive (pid=${info.pid}, port=${info.port}). Cannot acquire lock.`);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Acquire an exclusive process lock. Call once at startup for Primary mode.
|
|
138
|
+
*
|
|
139
|
+
* Flow:
|
|
140
|
+
* 1. Try fs.openSync(LOCK_FILE, 'wx') for atomic exclusive creation
|
|
141
|
+
* 2. Success -> write JSON {pid, port, startedAt}, hold FD
|
|
142
|
+
* 3. EEXIST -> check holder; remove only if stale, retry once
|
|
143
|
+
* 4. If holder is alive -> throw (caller should enter proxy mode)
|
|
144
|
+
*/
|
|
145
|
+
export async function acquireLock(port, instanceId) {
|
|
146
|
+
const fd = tryCreateLock(port, instanceId);
|
|
147
|
+
if (fd !== null) {
|
|
148
|
+
lockFd = fd;
|
|
149
|
+
logger(`[process-lock] Lock acquired (pid=${process.pid}, port=${port}, instanceId=${instanceId.slice(0, 8)})`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// Lock file exists - handle the existing holder
|
|
153
|
+
const canRetry = await handleExistingLock();
|
|
154
|
+
if (!canRetry) {
|
|
155
|
+
throw new Error('[process-lock] Primary is alive. Use proxy mode.');
|
|
156
|
+
}
|
|
157
|
+
// Retry once after stale removal
|
|
158
|
+
const fd2 = tryCreateLock(port, instanceId);
|
|
159
|
+
if (fd2 !== null) {
|
|
160
|
+
lockFd = fd2;
|
|
161
|
+
logger(`[process-lock] Lock acquired after cleanup (pid=${process.pid}, port=${port})`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
throw new Error('[process-lock] Failed to acquire lock after retry');
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Update the port in an existing lock file (e.g. after dynamic port fallback).
|
|
168
|
+
* Rewrites the lock file content while keeping the FD open.
|
|
169
|
+
*/
|
|
170
|
+
export function updateLockPort(newPort) {
|
|
171
|
+
if (lockFd === null) {
|
|
172
|
+
logger('[process-lock] Cannot update port: no lock held.');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const info = readLockInfo();
|
|
176
|
+
if (!info) {
|
|
177
|
+
logger('[process-lock] Cannot update port: lock file unreadable.');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
info.port = newPort;
|
|
181
|
+
// Truncate and rewrite
|
|
182
|
+
fs.ftruncateSync(lockFd);
|
|
183
|
+
const buf = Buffer.from(JSON.stringify(info));
|
|
184
|
+
fs.writeSync(lockFd, buf, 0, buf.length, 0);
|
|
185
|
+
logger(`[process-lock] Lock port updated to ${newPort}`);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Release the process lock. Call during shutdown.
|
|
189
|
+
*/
|
|
190
|
+
export function releaseLock() {
|
|
191
|
+
if (lockFd !== null) {
|
|
192
|
+
try {
|
|
193
|
+
fs.closeSync(lockFd);
|
|
194
|
+
}
|
|
195
|
+
catch { /* ignore */ }
|
|
196
|
+
lockFd = null;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
fs.unlinkSync(LOCK_FILE);
|
|
200
|
+
}
|
|
201
|
+
catch { /* ignore */ }
|
|
202
|
+
logger('[process-lock] Lock released.');
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Kill all sibling chrome-ai-bridge processes (bulk cleanup).
|
|
206
|
+
*
|
|
207
|
+
* Uses pgrep to find processes matching 'chrome-ai-bridge/build/src/main.js',
|
|
208
|
+
* excludes self and parent, then SIGTERM -> wait -> SIGKILL survivors.
|
|
209
|
+
*
|
|
210
|
+
* Returns the number of processes killed.
|
|
211
|
+
* On pgrep failure (e.g. not installed), returns 0 silently.
|
|
212
|
+
*/
|
|
213
|
+
export async function killSiblings() {
|
|
214
|
+
let pids;
|
|
215
|
+
try {
|
|
216
|
+
const output = execFileSync('pgrep', ['-f', 'chrome-ai-bridge/build/src/main.js'], {
|
|
217
|
+
encoding: 'utf-8',
|
|
218
|
+
timeout: 5000,
|
|
219
|
+
});
|
|
220
|
+
pids = output.trim().split('\n')
|
|
221
|
+
.map(s => Number(s.trim()))
|
|
222
|
+
.filter(n => Number.isFinite(n) && n > 0);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// pgrep returns exit code 1 when no matches, or not available
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
// Exclude self and parent (cli.mjs wrapper)
|
|
229
|
+
const selfPid = process.pid;
|
|
230
|
+
const parentPid = process.ppid;
|
|
231
|
+
const targets = pids.filter(pid => pid !== selfPid && pid !== parentPid);
|
|
232
|
+
if (targets.length === 0) {
|
|
233
|
+
return 0;
|
|
234
|
+
}
|
|
235
|
+
logger(`[process-lock] Found ${targets.length} stale sibling(s): ${targets.join(', ')}`);
|
|
236
|
+
// Send SIGTERM to all
|
|
237
|
+
for (const pid of targets) {
|
|
238
|
+
try {
|
|
239
|
+
process.kill(pid, 'SIGTERM');
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// Process already gone
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Wait for graceful shutdown
|
|
246
|
+
await sleep(2000);
|
|
247
|
+
// SIGKILL survivors
|
|
248
|
+
let killed = 0;
|
|
249
|
+
for (const pid of targets) {
|
|
250
|
+
if (isProcessAlive(pid)) {
|
|
251
|
+
logger(`[process-lock] Process ${pid} still alive after SIGTERM. Sending SIGKILL...`);
|
|
252
|
+
try {
|
|
253
|
+
process.kill(pid, 'SIGKILL');
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// ignore
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
killed++;
|
|
260
|
+
}
|
|
261
|
+
return killed;
|
|
262
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stdio-to-HTTP proxy for multi-client MCP support.
|
|
3
|
+
*
|
|
4
|
+
* When a Primary MCP server is already running, Secondary instances
|
|
5
|
+
* start in proxy mode: they bridge stdio (for Claude Code) to the
|
|
6
|
+
* Primary's Streamable HTTP endpoint.
|
|
7
|
+
*
|
|
8
|
+
* Uses MCP SDK transports to avoid custom JSON-RPC parsing.
|
|
9
|
+
*
|
|
10
|
+
* Resilience: On Primary disconnect, retries with exponential backoff
|
|
11
|
+
* (1s, 2s, 4s — max 3 attempts, ~7s total) before giving up.
|
|
12
|
+
*/
|
|
13
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
14
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
15
|
+
import { logger } from './logger.js';
|
|
16
|
+
import { IPC_CONFIG } from './config.js';
|
|
17
|
+
import { readLockInfo } from './process-lock.js';
|
|
18
|
+
/**
|
|
19
|
+
* Check if the Primary's /health endpoint is reachable.
|
|
20
|
+
* Optionally verifies instanceId matches the lock file.
|
|
21
|
+
*/
|
|
22
|
+
export async function checkPrimaryHealth(port, expectedInstanceId) {
|
|
23
|
+
try {
|
|
24
|
+
const resp = await fetch(`http://${IPC_CONFIG.host}:${port}${IPC_CONFIG.healthPath}`, { signal: AbortSignal.timeout(2000) });
|
|
25
|
+
if (!resp.ok)
|
|
26
|
+
return false;
|
|
27
|
+
if (expectedInstanceId) {
|
|
28
|
+
const body = (await resp.json());
|
|
29
|
+
if (body.instanceId && body.instanceId !== expectedInstanceId) {
|
|
30
|
+
logger(`[proxy] instanceId mismatch: expected=${expectedInstanceId.slice(0, 8)}, got=${body.instanceId.slice(0, 8)}`);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const RETRY_DELAYS = [1000, 2000, 4000]; // exponential backoff
|
|
41
|
+
/**
|
|
42
|
+
* Attempt to reconnect to the Primary with exponential backoff.
|
|
43
|
+
* Re-reads lock file on every attempt (follow-the-leader strategy):
|
|
44
|
+
* - Port may have changed (dynamic fallback)
|
|
45
|
+
* - instanceId may have changed (Primary restart)
|
|
46
|
+
* Returns the current port for reconnection.
|
|
47
|
+
*/
|
|
48
|
+
async function waitForPrimaryRecovery(initialPort) {
|
|
49
|
+
for (let i = 0; i < RETRY_DELAYS.length; i++) {
|
|
50
|
+
const delay = RETRY_DELAYS[i];
|
|
51
|
+
logger(`[proxy] Retry ${i + 1}/${RETRY_DELAYS.length} in ${delay}ms...`);
|
|
52
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
53
|
+
// Re-read lock file each attempt (port/instanceId may have changed)
|
|
54
|
+
const lockInfo = readLockInfo();
|
|
55
|
+
if (!lockInfo) {
|
|
56
|
+
logger('[proxy] Lock file missing or unreadable, retrying...');
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const currentPort = lockInfo.port || initialPort;
|
|
60
|
+
// Follow-the-leader: use lock file's instanceId as truth
|
|
61
|
+
const expectedId = lockInfo.instanceId || undefined;
|
|
62
|
+
if (await checkPrimaryHealth(currentPort, expectedId)) {
|
|
63
|
+
logger(`[proxy] Primary recovered (port=${currentPort})`);
|
|
64
|
+
return { recovered: true, port: currentPort };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { recovered: false, port: initialPort };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Start in proxy mode: bridge stdio <-> Primary HTTP.
|
|
71
|
+
*
|
|
72
|
+
* This function never returns normally — the process exits
|
|
73
|
+
* when stdin closes or the Primary becomes unreachable after retries.
|
|
74
|
+
*/
|
|
75
|
+
export async function startProxyMode(port) {
|
|
76
|
+
let currentPort = port;
|
|
77
|
+
const mcpUrl = new URL(`http://${IPC_CONFIG.host}:${currentPort}${IPC_CONFIG.mcpPath}`);
|
|
78
|
+
logger(`[proxy] Entering proxy mode -> ${mcpUrl}`);
|
|
79
|
+
const stdio = new StdioServerTransport();
|
|
80
|
+
let http = new StreamableHTTPClientTransport(mcpUrl);
|
|
81
|
+
let isReconnecting = false;
|
|
82
|
+
/**
|
|
83
|
+
* Try to reconnect to the Primary after disconnect.
|
|
84
|
+
* Follows the leader: adopts new port/instanceId from lock file.
|
|
85
|
+
* On success, creates a new HTTP transport and re-wires the bridge.
|
|
86
|
+
* On failure, exits with code 1.
|
|
87
|
+
*/
|
|
88
|
+
async function handlePrimaryDisconnect() {
|
|
89
|
+
if (isReconnecting)
|
|
90
|
+
return;
|
|
91
|
+
isReconnecting = true;
|
|
92
|
+
logger('[proxy] Primary disconnected. Attempting recovery...');
|
|
93
|
+
const result = await waitForPrimaryRecovery(currentPort);
|
|
94
|
+
if (!result.recovered) {
|
|
95
|
+
logger('[proxy] Primary not recovered after retries. Exiting.');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
// Port may have changed after Primary restart
|
|
99
|
+
currentPort = result.port;
|
|
100
|
+
const newMcpUrl = new URL(`http://${IPC_CONFIG.host}:${currentPort}${IPC_CONFIG.mcpPath}`);
|
|
101
|
+
// Create new transport and re-wire
|
|
102
|
+
try {
|
|
103
|
+
const newHttp = new StreamableHTTPClientTransport(newMcpUrl);
|
|
104
|
+
wireHttpTransport(newHttp);
|
|
105
|
+
await newHttp.start();
|
|
106
|
+
http = newHttp;
|
|
107
|
+
isReconnecting = false;
|
|
108
|
+
logger(`[proxy] Reconnected to Primary (port=${currentPort})`);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
logger(`[proxy] Reconnection failed: ${err}`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Wire event handlers for an HTTP transport instance.
|
|
117
|
+
*/
|
|
118
|
+
function wireHttpTransport(transport) {
|
|
119
|
+
transport.onmessage = (message) => {
|
|
120
|
+
stdio.send(message).catch((err) => {
|
|
121
|
+
logger(`[proxy] Failed to write to stdout: ${err}`);
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
transport.onclose = () => {
|
|
125
|
+
logger('[proxy] HTTP connection to Primary closed');
|
|
126
|
+
handlePrimaryDisconnect();
|
|
127
|
+
};
|
|
128
|
+
transport.onerror = (err) => {
|
|
129
|
+
logger(`[proxy] HTTP error: ${err.message}`);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// Bridge: stdin (Claude Code) -> HTTP POST (Primary)
|
|
133
|
+
stdio.onmessage = (message) => {
|
|
134
|
+
http.send(message).catch((err) => {
|
|
135
|
+
logger(`[proxy] Failed to forward to Primary: ${err}`);
|
|
136
|
+
handlePrimaryDisconnect();
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
// Handle stdio close (Claude Code disconnected)
|
|
140
|
+
stdio.onclose = () => {
|
|
141
|
+
logger('[proxy] stdio closed');
|
|
142
|
+
http
|
|
143
|
+
.terminateSession()
|
|
144
|
+
.catch(() => { })
|
|
145
|
+
.finally(() => http.close().catch(() => { }))
|
|
146
|
+
.finally(() => process.exit(0));
|
|
147
|
+
};
|
|
148
|
+
// Wire initial HTTP transport
|
|
149
|
+
wireHttpTransport(http);
|
|
150
|
+
// Start HTTP transport first (sets up AbortController),
|
|
151
|
+
// then stdio (starts reading from stdin).
|
|
152
|
+
await http.start();
|
|
153
|
+
await stdio.start();
|
|
154
|
+
logger('[proxy] Proxy mode active');
|
|
155
|
+
// Keep process alive; exit is handled by event handlers above.
|
|
156
|
+
return new Promise(() => { });
|
|
157
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-ai-bridge",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "MCP server bridging Chrome extension and AI assistants (ChatGPT, Gemini). Extension-only mode - no Puppeteer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "./scripts/cli.mjs",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"check-format": "eslint --cache . && prettier --check --cache .;",
|
|
20
20
|
"start": "npm run build && node build/src/index.js",
|
|
21
21
|
"start-debug": "DEBUG=mcp:* DEBUG_COLORS=false npm run build && node build/src/index.js",
|
|
22
|
-
"
|
|
22
|
+
"cleanup": "node scripts/cleanup.mjs",
|
|
23
|
+
"restart-mcp": "node scripts/cleanup.mjs",
|
|
23
24
|
"test:chatgpt": "npm run build && node scripts/test-fast-chat.mjs chatgpt",
|
|
24
25
|
"test:gemini": "npm run build && node scripts/test-fast-chat.mjs gemini",
|
|
25
26
|
"test:both": "npm run build && node scripts/test-fast-chat.mjs both",
|