@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.
Files changed (2) hide show
  1. package/dist/watch.js +242 -23
  2. 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 envToken = loadEnvFile(path.join(workspaceDir, ".env")).AGENT_TOKEN;
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 claudePaths = [
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 claudePaths) {
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 server = config?.mcpServers?.[mcpServerName];
41
- if (!server) continue;
42
- const current = server.env?.AGENT_TOKEN;
43
- if (current === envToken) {
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
- proc = pty.spawn(COMMAND, CMD_ARGS, {
204
- name: "xterm-256color",
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
- process.stderr.write(`[mention-watcher] Failed to spawn "${COMMAND}": ${err.message}
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
- process.stderr.write(` Run "which ${COMMAND}" in your terminal to confirm it is installed.
384
+ process.stderr.write(` Run "which ${COMMAND}" in your terminal to confirm it is installed.
214
385
  `);
215
- process.exit(1);
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 ──────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-chat/mention-watcher",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "PTY wrapper that pushes @mentions from agent-chat into Claude Code (or any LLM CLI)",
5
5
  "type": "module",
6
6
  "bin": {