@axiomatic-labs/claudeflow 2.13.72 → 2.13.74

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/lib/doctor.js CHANGED
@@ -42,7 +42,9 @@ function readPlaywrightCdpEndpoint(mcpPath) {
42
42
  const flagIdx = entry.args.indexOf('--cdp-endpoint');
43
43
  if (flagIdx === -1) return { state: 'no-flag', parsed };
44
44
  const value = entry.args[flagIdx + 1];
45
- const match = /localhost:(\d+)/.exec(value || '');
45
+ // Accept localhost OR 127.0.0.1 (v2.13.72+ writes the latter explicitly to
46
+ // dodge the IPv4/IPv6 dual-daemon trap — see observer-loopback lint test).
47
+ const match = /(?:localhost|127\.0\.0\.1):(\d+)/.exec(value || '');
46
48
  if (!match) return { state: 'unparseable', value, parsed };
47
49
  return { state: 'ok', port: Number(match[1]), flagIdx, parsed };
48
50
  }
@@ -78,7 +80,7 @@ function checkCdpPortMismatch(cwd) {
78
80
 
79
81
  function applyCdpPortFix(check) {
80
82
  const { mcpPath, expected, parsed, flagIdx } = check.fix;
81
- parsed.mcpServers.playwright.args[flagIdx + 1] = `http://localhost:${expected}`;
83
+ parsed.mcpServers.playwright.args[flagIdx + 1] = `http://127.0.0.1:${expected}`;
82
84
  fs.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + '\n');
83
85
  }
84
86
 
@@ -251,6 +253,46 @@ function checkObserverState(cwd) {
251
253
  };
252
254
  }
253
255
 
256
+ // Detects more than one process listening on the observer port. macOS allows
257
+ // distinct sockets on the same port for IPv4 and IPv6, so a stale daemon
258
+ // bound to "localhost" (which old code resolved to ::1) could coexist with a
259
+ // fresh daemon bound to 127.0.0.1. Snapshots routed via `localhost` then
260
+ // landed on the stale daemon, which carried the pre-v2.13.71 auto-resolve
261
+ // bug — panel saw the new daemon's empty state, hook saw 1 incident. This
262
+ // check surfaces that condition explicitly.
263
+ function checkObserverDualBind(cwd) {
264
+ const observer = readObserverState(cwd);
265
+ if (!observer.port) {
266
+ return { id: 'observer-dual-bind', severity: 'info', message: 'No observer port known — skipping dual-bind check.' };
267
+ }
268
+ const port = observer.port;
269
+ let listeners = [];
270
+ try {
271
+ const { spawnSync } = require('child_process');
272
+ const r = spawnSync('lsof', [`-iTCP:${port}`, '-sTCP:LISTEN', '-P', '-n', '-F', 'pn'], { encoding: 'utf8', timeout: 1500 });
273
+ if (r.status === 0 && r.stdout) {
274
+ let currentPid = null;
275
+ for (const line of r.stdout.split('\n')) {
276
+ if (line.startsWith('p')) currentPid = Number(line.slice(1));
277
+ else if (line.startsWith('n') && currentPid) listeners.push({ pid: currentPid, name: line.slice(1) });
278
+ }
279
+ }
280
+ } catch {
281
+ return { id: 'observer-dual-bind', severity: 'info', message: 'lsof unavailable — cannot verify single-bind on observer port.' };
282
+ }
283
+ const uniquePids = new Set(listeners.map((l) => l.pid));
284
+ if (uniquePids.size <= 1) {
285
+ return { id: 'observer-dual-bind', severity: 'ok', message: `Observer port ${port} has a single listener.` };
286
+ }
287
+ return {
288
+ id: 'observer-dual-bind',
289
+ severity: 'error',
290
+ message: `Observer port ${port} has ${uniquePids.size} distinct processes listening (PIDs: ${[...uniquePids].join(', ')}). One is likely a stale daemon from a previous version. Browser snapshots may route to the wrong instance, causing the panel and the Stop hook to disagree. Fix: stop the older PID (\`kill <pid>\`) and let the SessionStart hook respawn a single daemon.`,
291
+ detail: listeners.map((l) => ({ file: `pid ${l.pid}`, reason: l.name })),
292
+ fix: { kind: 'dual-bind', listeners },
293
+ };
294
+ }
295
+
254
296
  function applyStaleLockfileFix(check) {
255
297
  for (const { file } of check.fix.stale) {
256
298
  try { fs.unlinkSync(file); } catch {}
@@ -286,6 +328,7 @@ async function run(argv = []) {
286
328
  checkCdpPortMismatch(cwd),
287
329
  checkStaleLockfiles(cwd),
288
330
  checkObserverState(cwd),
331
+ checkObserverDualBind(cwd),
289
332
  ];
290
333
 
291
334
  for (const check of checks) printCheck(check);
@@ -326,6 +369,7 @@ module.exports.deriveCdpPort = deriveCdpPort;
326
369
  module.exports.checkCdpPortMismatch = checkCdpPortMismatch;
327
370
  module.exports.checkStaleLockfiles = checkStaleLockfiles;
328
371
  module.exports.checkObserverState = checkObserverState;
372
+ module.exports.checkObserverDualBind = checkObserverDualBind;
329
373
  module.exports.readObserverState = readObserverState;
330
374
  module.exports.readPlaywrightCdpEndpoint = readPlaywrightCdpEndpoint;
331
375
  module.exports.applyCdpPortFix = applyCdpPortFix;
package/lib/install.js CHANGED
@@ -117,7 +117,7 @@ async function run() {
117
117
  command: "npx",
118
118
  args: [
119
119
  "@playwright/mcp@latest",
120
- "--cdp-endpoint", `http://localhost:${cdpPort}`,
120
+ "--cdp-endpoint", `http://127.0.0.1:${cdpPort}`,
121
121
  "--caps", "vision,devtools",
122
122
  ],
123
123
  };
@@ -220,6 +220,12 @@ async function run() {
220
220
  fs.rmSync(tmpExtract, { recursive: true, force: true });
221
221
  }
222
222
 
223
+ // Kill any running observer daemon so the next request spawns a fresh one
224
+ // with the updated runtime code. Pre-fix: a running daemon held the old
225
+ // version in memory until manually killed, causing the bridge to talk to
226
+ // stale logic (see v2.13.71→v2.13.72 dual-daemon incident).
227
+ killStaleObserverDaemon(cwd);
228
+
223
229
  // Install analysis tools (ast-grep + Serena MCP)
224
230
  installAnalysisTools();
225
231
 
@@ -282,6 +288,33 @@ async function run() {
282
288
  showGettingStarted(cwd, cliStatus);
283
289
  }
284
290
 
291
+ function killStaleObserverDaemon(cwd) {
292
+ const pidPath = path.join(cwd, '.claudeflow', 'tmp', 'error-observer.pid');
293
+ if (!fs.existsSync(pidPath)) return;
294
+ let pid;
295
+ try {
296
+ pid = Number(fs.readFileSync(pidPath, 'utf8').trim());
297
+ } catch {
298
+ return;
299
+ }
300
+ if (!Number.isFinite(pid) || pid <= 1) return;
301
+ try {
302
+ // signal 0 = "is this PID alive and ours to signal?"
303
+ process.kill(pid, 0);
304
+ } catch {
305
+ // Not running or not ours. Clean up the stale pidfile and move on.
306
+ try { fs.unlinkSync(pidPath); } catch {}
307
+ return;
308
+ }
309
+ try {
310
+ process.kill(pid, 'SIGTERM');
311
+ ui.success(`Stopped running observer daemon (pid ${pid}) so the update takes effect`);
312
+ } catch {
313
+ // Swallow — non-fatal. The user can restart manually if needed.
314
+ }
315
+ try { fs.unlinkSync(pidPath); } catch {}
316
+ }
317
+
285
318
  function ensureGlobalCli(version) {
286
319
  // Returns { available: boolean, autoInstalled: boolean, upgraded: boolean, fromVersion?: string, error?: string }
287
320
  // `commandExists('claudeflow')` yields a false positive when install.js runs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.13.72",
3
+ "version": "2.13.74",
4
4
  "description": "Claudeflow — AI-powered development toolkit for Claude Code. Skills, agents, hooks, and quality gates that ship production apps.",
5
5
  "bin": {
6
6
  "claudeflow": "./bin/cli.js"