@agent-chat/mention-watcher 0.1.8 → 0.1.10

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 +198 -28
  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,75 @@ 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.HOME && typeof env.HOME === "string") out.HOME = env.HOME;
107
+ if (!out.SHELL && typeof env.SHELL === "string") out.SHELL = env.SHELL;
108
+ if (!out.TERM) out.TERM = "xterm-256color";
109
+ return out;
110
+ }
111
+ function spawnViaScriptPty(command, args, cwd, env) {
112
+ const child = spawn("script", ["-q", "/dev/null", command, ...args], {
113
+ cwd,
114
+ env,
115
+ stdio: ["pipe", "pipe", "pipe"]
116
+ });
117
+ return {
118
+ mode: "script-pty",
119
+ write(data) {
120
+ if (!child.stdin.destroyed) child.stdin.write(data);
121
+ },
122
+ onData(cb) {
123
+ child.stdout.on("data", (buf) => cb(buf.toString()));
124
+ child.stderr.on("data", (buf) => cb(buf.toString()));
125
+ },
126
+ onExit(cb) {
127
+ child.on("exit", (code) => cb({ exitCode: code ?? 1 }));
128
+ child.on("error", () => cb({ exitCode: 1 }));
129
+ },
130
+ // Resize is not supported through the `script` wrapper.
131
+ resize() {
132
+ },
133
+ kill(signal) {
134
+ try {
135
+ child.kill(signal ?? "SIGINT");
136
+ } catch {
137
+ }
138
+ }
139
+ };
140
+ }
141
+ function spawnWithoutPty(command, args, cwd, env) {
142
+ const child = spawn(command, args, { cwd, env, stdio: ["pipe", "pipe", "pipe"] });
143
+ return {
144
+ mode: "plain",
145
+ write(data) {
146
+ if (!child.stdin.destroyed) child.stdin.write(data);
147
+ },
148
+ onData(cb) {
149
+ child.stdout.on("data", (buf) => cb(buf.toString()));
150
+ child.stderr.on("data", (buf) => cb(buf.toString()));
151
+ },
152
+ onExit(cb) {
153
+ child.on("exit", (code) => cb({ exitCode: code ?? 1 }));
154
+ child.on("error", () => cb({ exitCode: 1 }));
155
+ },
156
+ resize() {
157
+ },
158
+ kill(signal) {
159
+ try {
160
+ child.kill(signal ?? "SIGINT");
161
+ } catch {
162
+ }
163
+ }
164
+ };
165
+ }
60
166
  var _WORKSPACE = process.env.WORKSPACE_DIR ?? process.cwd();
61
167
  {
62
168
  const dirsToCheck = [.../* @__PURE__ */ new Set([process.cwd(), _WORKSPACE])];
@@ -165,6 +271,19 @@ function scheduleInject(proc) {
165
271
  function drainQueue(proc) {
166
272
  if (queue.length === 0) return;
167
273
  const prompt = queue.shift();
274
+ if (proc.mode !== "pty") {
275
+ setTimeout(() => {
276
+ proc.write(prompt);
277
+ }, 30);
278
+ setTimeout(() => {
279
+ proc.write("\r");
280
+ setTimeout(() => proc.write("\n"), 20);
281
+ process.stderr.write(`[mention-watcher] Injected mention prompt (${proc.mode})
282
+ `);
283
+ if (queue.length > 0) scheduleInject(proc);
284
+ }, 120);
285
+ return;
286
+ }
168
287
  proc.write("");
169
288
  setTimeout(() => {
170
289
  proc.write(prompt);
@@ -172,6 +291,7 @@ function drainQueue(proc) {
172
291
  setTimeout(() => {
173
292
  proc.write("\r");
174
293
  setTimeout(() => proc.write("\n"), 20);
294
+ process.stderr.write("[mention-watcher] Injected mention prompt (pty)\n");
175
295
  if (queue.length > 0) scheduleInject(proc);
176
296
  }, 200);
177
297
  }
@@ -189,8 +309,8 @@ async function main() {
189
309
  process.stderr.write(" npx @agent-chat/mention-watcher setup\n");
190
310
  process.exit(1);
191
311
  }
192
- syncMcpToken(WORKSPACE_DIR, MCP_NAME);
193
312
  const jwt = await resolveJwt();
313
+ syncMcpToken(WORKSPACE_DIR, MCP_NAME, jwt);
194
314
  const cols = process.stdout.columns || 80;
195
315
  const rows = process.stdout.rows || 24;
196
316
  const wdSource = process.env.WORKSPACE_DIR ? "env" : "auto-detected";
@@ -198,44 +318,94 @@ async function main() {
198
318
  `);
199
319
  process.stderr.write(`[mention-watcher] Spawning: ${COMMAND} ${CMD_ARGS.join(" ")}
200
320
  `);
201
- const userShell = process.env.SHELL || "/bin/zsh";
202
- const quotedArgs = [COMMAND, ...CMD_ARGS].map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ");
203
- const shellCmd = `exec ${quotedArgs}`;
204
- process.stderr.write(`[mention-watcher] Spawning via: ${userShell} -l -c "${shellCmd}"
205
- `);
321
+ const ptyEnv = buildPtyEnv(process.env);
322
+ const spawnOptions = {
323
+ name: "xterm-256color",
324
+ cols,
325
+ rows,
326
+ cwd: WORKSPACE_DIR,
327
+ env: ptyEnv
328
+ };
206
329
  let proc;
207
330
  try {
208
- proc = pty.spawn(userShell, ["-l", "-c", shellCmd], {
209
- name: "xterm-256color",
210
- cols,
211
- rows,
212
- cwd: WORKSPACE_DIR,
213
- env: process.env
214
- });
331
+ const p = pty.spawn(COMMAND, CMD_ARGS, spawnOptions);
332
+ proc = { ...p, mode: "pty" };
215
333
  } catch (err) {
216
- process.stderr.write(`[mention-watcher] Failed to spawn "${COMMAND}": ${err.message}
334
+ const msg = String(err?.message ?? err);
335
+ if (!msg.includes("posix_spawnp failed")) {
336
+ process.stderr.write(`[mention-watcher] Failed to spawn "${COMMAND}": ${msg}
217
337
  `);
218
- process.stderr.write(` Run "which ${COMMAND}" in your terminal to confirm it is installed.
338
+ process.stderr.write(` Run "which ${COMMAND}" in your terminal to confirm it is installed.
219
339
  `);
220
- process.exit(1);
340
+ process.exit(1);
341
+ }
342
+ const userShell = process.env.SHELL || "/bin/zsh";
343
+ const quotedArgs = [COMMAND, ...CMD_ARGS].map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ");
344
+ const shellCmd = `exec ${quotedArgs}`;
345
+ process.stderr.write(
346
+ `[mention-watcher] Direct spawn failed, retry via shell: ${userShell} -lc "${shellCmd}"
347
+ `
348
+ );
349
+ try {
350
+ const p = pty.spawn(userShell, ["-lc", shellCmd], spawnOptions);
351
+ proc = { ...p, mode: "pty" };
352
+ } catch (shellErr) {
353
+ const shellMsg = String(shellErr?.message ?? shellErr);
354
+ process.stderr.write(`[mention-watcher] Shell fallback failed: ${shellMsg}
355
+ `);
356
+ process.stderr.write("[mention-watcher] Falling back to script PTY mode\n");
357
+ try {
358
+ proc = spawnViaScriptPty(COMMAND, CMD_ARGS, WORKSPACE_DIR, ptyEnv);
359
+ } catch (scriptErr) {
360
+ const scriptMsg = String(scriptErr?.message ?? scriptErr);
361
+ process.stderr.write(`[mention-watcher] Script PTY fallback failed: ${scriptMsg}
362
+ `);
363
+ process.stderr.write("[mention-watcher] Falling back to non-PTY mode\n");
364
+ proc = spawnWithoutPty(COMMAND, CMD_ARGS, WORKSPACE_DIR, ptyEnv);
365
+ }
366
+ }
221
367
  }
222
368
  proc.onData((data) => {
223
369
  process.stdout.write(data);
224
370
  lastOutputAt = Date.now();
225
371
  });
372
+ let cleanedUp = false;
373
+ const cleanupTerminal = () => {
374
+ if (cleanedUp) return;
375
+ cleanedUp = true;
376
+ if (process.stdin.isTTY) {
377
+ try {
378
+ process.stdin.setRawMode(false);
379
+ } catch {
380
+ }
381
+ }
382
+ process.stdin.pause();
383
+ };
226
384
  if (process.stdin.isTTY) {
227
385
  process.stdin.setRawMode(true);
228
386
  }
229
387
  process.stdin.resume();
230
388
  process.stdin.on("data", (data) => {
389
+ if (data.length === 1 && data[0] === 3) {
390
+ proc.kill?.("SIGINT");
391
+ cleanupTerminal();
392
+ process.exit(130);
393
+ return;
394
+ }
231
395
  proc.write(data.toString("binary"));
232
396
  });
233
397
  process.stdout.on("resize", () => {
234
398
  proc.resize(process.stdout.columns || 80, process.stdout.rows || 24);
235
399
  });
236
400
  proc.onExit(({ exitCode }) => {
401
+ cleanupTerminal();
237
402
  process.exit(exitCode);
238
403
  });
404
+ process.on("SIGINT", () => {
405
+ proc.kill?.("SIGINT");
406
+ cleanupTerminal();
407
+ process.exit(130);
408
+ });
239
409
  await startWsListener(
240
410
  jwt,
241
411
  // ── mention handler ──────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-chat/mention-watcher",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
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": {