@agent-chat/mention-watcher 0.1.9 → 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 -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,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,39 +318,94 @@ async function main() {
198
318
  `);
199
319
  process.stderr.write(`[mention-watcher] Spawning: ${COMMAND} ${CMD_ARGS.join(" ")}
200
320
  `);
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
+ };
201
329
  let proc;
202
330
  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
- });
331
+ const p = pty.spawn(COMMAND, CMD_ARGS, spawnOptions);
332
+ proc = { ...p, mode: "pty" };
210
333
  } catch (err) {
211
- 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}
212
337
  `);
213
- 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.
214
339
  `);
215
- 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
+ }
216
367
  }
217
368
  proc.onData((data) => {
218
369
  process.stdout.write(data);
219
370
  lastOutputAt = Date.now();
220
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
+ };
221
384
  if (process.stdin.isTTY) {
222
385
  process.stdin.setRawMode(true);
223
386
  }
224
387
  process.stdin.resume();
225
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
+ }
226
395
  proc.write(data.toString("binary"));
227
396
  });
228
397
  process.stdout.on("resize", () => {
229
398
  proc.resize(process.stdout.columns || 80, process.stdout.rows || 24);
230
399
  });
231
400
  proc.onExit(({ exitCode }) => {
401
+ cleanupTerminal();
232
402
  process.exit(exitCode);
233
403
  });
404
+ process.on("SIGINT", () => {
405
+ proc.kill?.("SIGINT");
406
+ cleanupTerminal();
407
+ process.exit(130);
408
+ });
234
409
  await startWsListener(
235
410
  jwt,
236
411
  // ── 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.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": {