@agent-chat/mention-watcher 0.1.9 → 0.1.11
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/dist/watch.js +242 -23
- package/package.json +1 -1
package/dist/watch.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import * as fs from "fs";
|
|
5
5
|
import * as path from "path";
|
|
6
6
|
import * as os from "os";
|
|
7
|
+
import { spawn } from "child_process";
|
|
7
8
|
import * as pty from "node-pty";
|
|
8
9
|
import { WebSocket } from "ws";
|
|
9
10
|
function loadEnvFile(filePath) {
|
|
@@ -20,14 +21,16 @@ function loadEnvFile(filePath) {
|
|
|
20
21
|
}
|
|
21
22
|
return result;
|
|
22
23
|
}
|
|
23
|
-
function syncMcpToken(workspaceDir, mcpServerName) {
|
|
24
|
-
const
|
|
24
|
+
function syncMcpToken(workspaceDir, mcpServerName, preferredToken) {
|
|
25
|
+
const envVars = loadEnvFile(path.join(workspaceDir, ".env"));
|
|
26
|
+
const envToken = preferredToken ?? process.env.AGENT_TOKEN ?? envVars.AGENT_TOKEN ?? envVars.BOOTSTRAP_AGENT_TOKEN;
|
|
25
27
|
if (!envToken) return;
|
|
26
|
-
const
|
|
28
|
+
const mcpPaths = [
|
|
27
29
|
path.join(workspaceDir, ".mcp.json"),
|
|
28
|
-
path.join(workspaceDir, ".claude", "mcp.json")
|
|
30
|
+
path.join(workspaceDir, ".claude", "mcp.json"),
|
|
31
|
+
path.join(workspaceDir, ".cursor", "mcp.json")
|
|
29
32
|
];
|
|
30
|
-
for (const configPath of
|
|
33
|
+
for (const configPath of mcpPaths) {
|
|
31
34
|
if (!fs.existsSync(configPath)) continue;
|
|
32
35
|
let config;
|
|
33
36
|
try {
|
|
@@ -37,19 +40,53 @@ function syncMcpToken(workspaceDir, mcpServerName) {
|
|
|
37
40
|
`);
|
|
38
41
|
continue;
|
|
39
42
|
}
|
|
40
|
-
const
|
|
41
|
-
if (!
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
const servers = config?.mcpServers;
|
|
44
|
+
if (!servers || typeof servers !== "object") {
|
|
45
|
+
process.stderr.write(`[mention-watcher] No mcpServers in ${configPath}, skipping
|
|
46
|
+
`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const explicit = servers[mcpServerName];
|
|
50
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
51
|
+
if (explicit) candidates.add(mcpServerName);
|
|
52
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
53
|
+
const command = String(server?.command ?? "");
|
|
54
|
+
const args = Array.isArray(server?.args) ? server.args.map((a) => String(a)).join(" ") : "";
|
|
55
|
+
const signature = `${command} ${args}`;
|
|
56
|
+
if (signature.includes("agent-gateway") || signature.includes("@agent-chat")) {
|
|
57
|
+
candidates.add(name);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (candidates.size === 0) {
|
|
61
|
+
process.stderr.write(
|
|
62
|
+
`[mention-watcher] No matching MCP server in ${configPath} (expected "${mcpServerName}")
|
|
63
|
+
`
|
|
64
|
+
);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
let changed = false;
|
|
68
|
+
let updatedCount = 0;
|
|
69
|
+
for (const name of candidates) {
|
|
70
|
+
const server = servers[name] ?? {};
|
|
71
|
+
server.env = server.env ?? {};
|
|
72
|
+
const tokenBefore = server.env.AGENT_TOKEN;
|
|
73
|
+
const bootstrapBefore = server.env.BOOTSTRAP_AGENT_TOKEN;
|
|
74
|
+
if (tokenBefore !== envToken || bootstrapBefore !== envToken) {
|
|
75
|
+
server.env.AGENT_TOKEN = envToken;
|
|
76
|
+
server.env.BOOTSTRAP_AGENT_TOKEN = envToken;
|
|
77
|
+
servers[name] = server;
|
|
78
|
+
changed = true;
|
|
79
|
+
}
|
|
80
|
+
updatedCount += 1;
|
|
81
|
+
}
|
|
82
|
+
if (!changed) {
|
|
44
83
|
process.stderr.write(`[mention-watcher] MCP token up-to-date: ${configPath}
|
|
45
84
|
`);
|
|
46
85
|
continue;
|
|
47
86
|
}
|
|
48
|
-
server.env = server.env ?? {};
|
|
49
|
-
server.env.AGENT_TOKEN = envToken;
|
|
50
87
|
try {
|
|
51
88
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
52
|
-
process.stderr.write(`[mention-watcher] MCP token synced: ${configPath}
|
|
89
|
+
process.stderr.write(`[mention-watcher] MCP token synced (${updatedCount} server): ${configPath}
|
|
53
90
|
`);
|
|
54
91
|
} catch (err) {
|
|
55
92
|
process.stderr.write(`[mention-watcher] Failed to write ${configPath}: ${err.message}
|
|
@@ -57,6 +94,121 @@ function syncMcpToken(workspaceDir, mcpServerName) {
|
|
|
57
94
|
}
|
|
58
95
|
}
|
|
59
96
|
}
|
|
97
|
+
function buildPtyEnv(env) {
|
|
98
|
+
const out = {};
|
|
99
|
+
for (const [k, v] of Object.entries(env)) {
|
|
100
|
+
if (typeof v !== "string") continue;
|
|
101
|
+
if (k.includes("\0") || v.includes("\0")) continue;
|
|
102
|
+
if (/^(npm_|npm_config_|npm_package_|npm_lifecycle_|PNPM_|pnpm_)/.test(k)) continue;
|
|
103
|
+
out[k] = v;
|
|
104
|
+
}
|
|
105
|
+
if (!out.PATH && typeof env.PATH === "string") out.PATH = env.PATH;
|
|
106
|
+
if (!out.PATH && typeof env.Path === "string") out.PATH = env.Path;
|
|
107
|
+
if (!out.HOME && typeof env.HOME === "string") out.HOME = env.HOME;
|
|
108
|
+
if (!out.SHELL && typeof env.SHELL === "string") out.SHELL = env.SHELL;
|
|
109
|
+
if (!out.TERM) out.TERM = "xterm-256color";
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
function escapePosixArg(arg) {
|
|
113
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
114
|
+
}
|
|
115
|
+
function escapeCmdArg(arg) {
|
|
116
|
+
if (arg.length === 0) return '""';
|
|
117
|
+
if (/[\s"]/g.test(arg)) return `"${arg.replace(/"/g, '\\"')}"`;
|
|
118
|
+
return arg;
|
|
119
|
+
}
|
|
120
|
+
function buildCommandLine(command, args, platform) {
|
|
121
|
+
if (platform === "win32") {
|
|
122
|
+
return [command, ...args].map(escapeCmdArg).join(" ");
|
|
123
|
+
}
|
|
124
|
+
return [command, ...args].map(escapePosixArg).join(" ");
|
|
125
|
+
}
|
|
126
|
+
function getShellFallbackPlan(command, args, env) {
|
|
127
|
+
if (process.platform === "win32") {
|
|
128
|
+
const cmd = env.ComSpec || process.env.ComSpec || "cmd.exe";
|
|
129
|
+
const shellCmd2 = buildCommandLine(command, args, "win32");
|
|
130
|
+
return {
|
|
131
|
+
shell: cmd,
|
|
132
|
+
argv: ["/d", "/s", "/c", shellCmd2],
|
|
133
|
+
display: `${cmd} /d /s /c "${shellCmd2}"`
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const userShell = env.SHELL || process.env.SHELL || "/bin/zsh";
|
|
137
|
+
const shellCmd = `exec ${buildCommandLine(command, args, process.platform)}`;
|
|
138
|
+
return {
|
|
139
|
+
shell: userShell,
|
|
140
|
+
argv: ["-lc", shellCmd],
|
|
141
|
+
display: `${userShell} -lc "${shellCmd}"`
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function hasScriptPtySupport() {
|
|
145
|
+
if (process.platform === "win32") return false;
|
|
146
|
+
return fs.existsSync("/usr/bin/script") || fs.existsSync("/bin/script");
|
|
147
|
+
}
|
|
148
|
+
function spawnViaScriptPty(command, args, cwd, env) {
|
|
149
|
+
if (!hasScriptPtySupport()) {
|
|
150
|
+
throw new Error("script command is not available on this platform");
|
|
151
|
+
}
|
|
152
|
+
const scriptArgs = process.platform === "darwin" ? ["-q", "/dev/null", command, ...args] : ["-q", "-c", buildCommandLine(command, args, process.platform), "/dev/null"];
|
|
153
|
+
const child = spawn("script", scriptArgs, {
|
|
154
|
+
cwd,
|
|
155
|
+
env,
|
|
156
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
157
|
+
});
|
|
158
|
+
return {
|
|
159
|
+
mode: "script-pty",
|
|
160
|
+
write(data) {
|
|
161
|
+
if (!child.stdin.destroyed) child.stdin.write(data);
|
|
162
|
+
},
|
|
163
|
+
onData(cb) {
|
|
164
|
+
child.stdout.on("data", (buf) => cb(buf.toString()));
|
|
165
|
+
child.stderr.on("data", (buf) => cb(buf.toString()));
|
|
166
|
+
},
|
|
167
|
+
onExit(cb) {
|
|
168
|
+
child.on("exit", (code) => cb({ exitCode: code ?? 1 }));
|
|
169
|
+
child.on("error", () => cb({ exitCode: 1 }));
|
|
170
|
+
},
|
|
171
|
+
// Resize is not supported through the `script` wrapper.
|
|
172
|
+
resize() {
|
|
173
|
+
},
|
|
174
|
+
kill(signal) {
|
|
175
|
+
try {
|
|
176
|
+
child.kill(signal ?? "SIGINT");
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function spawnWithoutPty(command, args, cwd, env) {
|
|
183
|
+
const child = spawn(command, args, {
|
|
184
|
+
cwd,
|
|
185
|
+
env,
|
|
186
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
187
|
+
shell: process.platform === "win32"
|
|
188
|
+
});
|
|
189
|
+
return {
|
|
190
|
+
mode: "plain",
|
|
191
|
+
write(data) {
|
|
192
|
+
if (!child.stdin.destroyed) child.stdin.write(data);
|
|
193
|
+
},
|
|
194
|
+
onData(cb) {
|
|
195
|
+
child.stdout.on("data", (buf) => cb(buf.toString()));
|
|
196
|
+
child.stderr.on("data", (buf) => cb(buf.toString()));
|
|
197
|
+
},
|
|
198
|
+
onExit(cb) {
|
|
199
|
+
child.on("exit", (code) => cb({ exitCode: code ?? 1 }));
|
|
200
|
+
child.on("error", () => cb({ exitCode: 1 }));
|
|
201
|
+
},
|
|
202
|
+
resize() {
|
|
203
|
+
},
|
|
204
|
+
kill(signal) {
|
|
205
|
+
try {
|
|
206
|
+
child.kill(signal ?? "SIGINT");
|
|
207
|
+
} catch {
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
60
212
|
var _WORKSPACE = process.env.WORKSPACE_DIR ?? process.cwd();
|
|
61
213
|
{
|
|
62
214
|
const dirsToCheck = [.../* @__PURE__ */ new Set([process.cwd(), _WORKSPACE])];
|
|
@@ -165,6 +317,19 @@ function scheduleInject(proc) {
|
|
|
165
317
|
function drainQueue(proc) {
|
|
166
318
|
if (queue.length === 0) return;
|
|
167
319
|
const prompt = queue.shift();
|
|
320
|
+
if (proc.mode !== "pty") {
|
|
321
|
+
setTimeout(() => {
|
|
322
|
+
proc.write(prompt);
|
|
323
|
+
}, 30);
|
|
324
|
+
setTimeout(() => {
|
|
325
|
+
proc.write("\r");
|
|
326
|
+
setTimeout(() => proc.write("\n"), 20);
|
|
327
|
+
process.stderr.write(`[mention-watcher] Injected mention prompt (${proc.mode})
|
|
328
|
+
`);
|
|
329
|
+
if (queue.length > 0) scheduleInject(proc);
|
|
330
|
+
}, 120);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
168
333
|
proc.write("");
|
|
169
334
|
setTimeout(() => {
|
|
170
335
|
proc.write(prompt);
|
|
@@ -172,6 +337,7 @@ function drainQueue(proc) {
|
|
|
172
337
|
setTimeout(() => {
|
|
173
338
|
proc.write("\r");
|
|
174
339
|
setTimeout(() => proc.write("\n"), 20);
|
|
340
|
+
process.stderr.write("[mention-watcher] Injected mention prompt (pty)\n");
|
|
175
341
|
if (queue.length > 0) scheduleInject(proc);
|
|
176
342
|
}, 200);
|
|
177
343
|
}
|
|
@@ -189,8 +355,8 @@ async function main() {
|
|
|
189
355
|
process.stderr.write(" npx @agent-chat/mention-watcher setup\n");
|
|
190
356
|
process.exit(1);
|
|
191
357
|
}
|
|
192
|
-
syncMcpToken(WORKSPACE_DIR, MCP_NAME);
|
|
193
358
|
const jwt = await resolveJwt();
|
|
359
|
+
syncMcpToken(WORKSPACE_DIR, MCP_NAME, jwt);
|
|
194
360
|
const cols = process.stdout.columns || 80;
|
|
195
361
|
const rows = process.stdout.rows || 24;
|
|
196
362
|
const wdSource = process.env.WORKSPACE_DIR ? "env" : "auto-detected";
|
|
@@ -198,39 +364,92 @@ async function main() {
|
|
|
198
364
|
`);
|
|
199
365
|
process.stderr.write(`[mention-watcher] Spawning: ${COMMAND} ${CMD_ARGS.join(" ")}
|
|
200
366
|
`);
|
|
367
|
+
const ptyEnv = buildPtyEnv(process.env);
|
|
368
|
+
const spawnOptions = {
|
|
369
|
+
name: "xterm-256color",
|
|
370
|
+
cols,
|
|
371
|
+
rows,
|
|
372
|
+
cwd: WORKSPACE_DIR,
|
|
373
|
+
env: ptyEnv
|
|
374
|
+
};
|
|
201
375
|
let proc;
|
|
202
376
|
try {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
cols,
|
|
206
|
-
rows,
|
|
207
|
-
cwd: WORKSPACE_DIR,
|
|
208
|
-
env: process.env
|
|
209
|
-
});
|
|
377
|
+
const p = pty.spawn(COMMAND, CMD_ARGS, spawnOptions);
|
|
378
|
+
proc = { ...p, mode: "pty" };
|
|
210
379
|
} catch (err) {
|
|
211
|
-
|
|
380
|
+
const msg = String(err?.message ?? err);
|
|
381
|
+
if (!msg.includes("posix_spawnp failed")) {
|
|
382
|
+
process.stderr.write(`[mention-watcher] Failed to spawn "${COMMAND}": ${msg}
|
|
212
383
|
`);
|
|
213
|
-
|
|
384
|
+
process.stderr.write(` Run "which ${COMMAND}" in your terminal to confirm it is installed.
|
|
214
385
|
`);
|
|
215
|
-
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
const shellPlan = getShellFallbackPlan(COMMAND, CMD_ARGS, ptyEnv);
|
|
389
|
+
process.stderr.write(
|
|
390
|
+
`[mention-watcher] Direct spawn failed, retry via shell: ${shellPlan.display}
|
|
391
|
+
`
|
|
392
|
+
);
|
|
393
|
+
try {
|
|
394
|
+
const p = pty.spawn(shellPlan.shell, shellPlan.argv, spawnOptions);
|
|
395
|
+
proc = { ...p, mode: "pty" };
|
|
396
|
+
} catch (shellErr) {
|
|
397
|
+
const shellMsg = String(shellErr?.message ?? shellErr);
|
|
398
|
+
process.stderr.write(`[mention-watcher] Shell fallback failed: ${shellMsg}
|
|
399
|
+
`);
|
|
400
|
+
process.stderr.write("[mention-watcher] Falling back to script PTY mode (if available)\n");
|
|
401
|
+
try {
|
|
402
|
+
proc = spawnViaScriptPty(COMMAND, CMD_ARGS, WORKSPACE_DIR, ptyEnv);
|
|
403
|
+
} catch (scriptErr) {
|
|
404
|
+
const scriptMsg = String(scriptErr?.message ?? scriptErr);
|
|
405
|
+
process.stderr.write(`[mention-watcher] Script PTY fallback failed: ${scriptMsg}
|
|
406
|
+
`);
|
|
407
|
+
process.stderr.write("[mention-watcher] Falling back to non-PTY mode\n");
|
|
408
|
+
proc = spawnWithoutPty(COMMAND, CMD_ARGS, WORKSPACE_DIR, ptyEnv);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
216
411
|
}
|
|
217
412
|
proc.onData((data) => {
|
|
218
413
|
process.stdout.write(data);
|
|
219
414
|
lastOutputAt = Date.now();
|
|
220
415
|
});
|
|
416
|
+
let cleanedUp = false;
|
|
417
|
+
const cleanupTerminal = () => {
|
|
418
|
+
if (cleanedUp) return;
|
|
419
|
+
cleanedUp = true;
|
|
420
|
+
if (process.stdin.isTTY) {
|
|
421
|
+
try {
|
|
422
|
+
process.stdin.setRawMode(false);
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
process.stdin.pause();
|
|
427
|
+
};
|
|
221
428
|
if (process.stdin.isTTY) {
|
|
222
429
|
process.stdin.setRawMode(true);
|
|
223
430
|
}
|
|
224
431
|
process.stdin.resume();
|
|
225
432
|
process.stdin.on("data", (data) => {
|
|
433
|
+
if (data.length === 1 && data[0] === 3) {
|
|
434
|
+
proc.kill?.("SIGINT");
|
|
435
|
+
cleanupTerminal();
|
|
436
|
+
process.exit(130);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
226
439
|
proc.write(data.toString("binary"));
|
|
227
440
|
});
|
|
228
441
|
process.stdout.on("resize", () => {
|
|
229
442
|
proc.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
230
443
|
});
|
|
231
444
|
proc.onExit(({ exitCode }) => {
|
|
445
|
+
cleanupTerminal();
|
|
232
446
|
process.exit(exitCode);
|
|
233
447
|
});
|
|
448
|
+
process.on("SIGINT", () => {
|
|
449
|
+
proc.kill?.("SIGINT");
|
|
450
|
+
cleanupTerminal();
|
|
451
|
+
process.exit(130);
|
|
452
|
+
});
|
|
234
453
|
await startWsListener(
|
|
235
454
|
jwt,
|
|
236
455
|
// ── mention handler ──────────────────────────────────────────────────────
|