@axiomatic-labs/claudeflow 2.13.59 → 2.13.61

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
@@ -149,13 +149,36 @@ function checkStaleLockfiles(cwd) {
149
149
  // the configured port, the daemon is up.
150
150
  function readObserverState(cwd) {
151
151
  const observerJsonPath = path.join(cwd, '.claudeflow', 'tmp', 'error-observer.json');
152
- let meta;
153
- try {
154
- meta = JSON.parse(fs.readFileSync(observerJsonPath, 'utf8'));
155
- } catch {
156
- return { running: false, pid: null, port: null, source: 'no-observer-json', observerJsonPath };
152
+ // Defense-in-depth retry: even though the daemon now writes atomically
153
+ // (rename-after-write), older daemon versions and any future non-atomic
154
+ // writer would expose a 0-byte window during overwrite. We retry up to
155
+ // 3 times with a tiny pause between attempts so a transient empty read
156
+ // doesn't flip the panel UI between running/stopped every refresh.
157
+ let meta = null;
158
+ let lastErr = null;
159
+ for (let attempt = 0; attempt < 3; attempt += 1) {
160
+ try {
161
+ const raw = fs.readFileSync(observerJsonPath, 'utf8');
162
+ if (raw.trim()) {
163
+ meta = JSON.parse(raw);
164
+ break;
165
+ }
166
+ lastErr = new Error('empty file');
167
+ } catch (e) {
168
+ lastErr = e;
169
+ if (e.code === 'ENOENT') break; // file genuinely missing — no retry helps
170
+ }
171
+ if (attempt < 2) {
172
+ // Sync sleep (~30ms) via spawnSync — sufficient to outlast a writeFile race.
173
+ const { spawnSync } = require('child_process');
174
+ spawnSync('sleep', ['0.03']);
175
+ }
176
+ }
177
+ if (!meta) {
178
+ const source = (lastErr && lastErr.code === 'ENOENT') ? 'no-observer-json' : 'observer-json-malformed';
179
+ return { running: false, pid: null, port: null, source, observerJsonPath };
157
180
  }
158
- if (!meta || typeof meta.port !== 'number') {
181
+ if (typeof meta.port !== 'number') {
159
182
  return { running: false, pid: null, port: null, source: 'observer-json-malformed', observerJsonPath };
160
183
  }
161
184
  const port = meta.port;
package/lib/panel.js CHANGED
@@ -1839,14 +1839,47 @@ function openInBrowser(url) {
1839
1839
  } catch {}
1840
1840
  }
1841
1841
 
1842
- function start({ cwd = process.cwd(), port = 0, openBrowser = true } = {}) {
1842
+ // Derive a stable port from the cwd. Without this, every panel restart picks
1843
+ // a different random port and any open browser tab pointing at the OLD port
1844
+ // becomes a dead bookmark — confusingly, the bookmark keeps rendering the
1845
+ // last-known UI state because /api/status fails silently. Range 9300-9399
1846
+ // (above Chrome CDP's 9200-9299).
1847
+ function derivePanelPort(projectPath) {
1848
+ const crypto = require('crypto');
1849
+ const hash = crypto.createHash('sha1').update(projectPath).digest();
1850
+ return 9300 + (hash.readUInt16BE(0) % 100);
1851
+ }
1852
+
1853
+ function start({ cwd = process.cwd(), port = null, openBrowser = true } = {}) {
1843
1854
  const server = http.createServer(handler(cwd));
1844
1855
  let lastActivity = Date.now();
1845
1856
  server.on('request', () => { lastActivity = Date.now(); });
1846
1857
 
1858
+ // Port resolution:
1859
+ // - explicit port arg (e.g. --port=9999) → use as-is
1860
+ // - port === 0 → OS-assigned random (legacy behavior, retained for tests)
1861
+ // - default (null) → derive from cwd; on EADDRINUSE fall back to OS-assigned
1862
+ const explicitPort = typeof port === 'number' && port > 0;
1863
+ const derivedPort = port === null ? derivePanelPort(cwd) : null;
1864
+ const initialPort = explicitPort ? port : (derivedPort !== null ? derivedPort : 0);
1865
+
1847
1866
  return new Promise((resolve, reject) => {
1848
- server.once('error', reject);
1849
- server.listen(port, '127.0.0.1', () => {
1867
+ let usingFallback = false;
1868
+ server.on('error', (err) => {
1869
+ if (err.code === 'EADDRINUSE' && !usingFallback && !explicitPort) {
1870
+ // Another process holds the derived port. Fall back to OS-assigned
1871
+ // so the panel can still start, but warn the user — they may have
1872
+ // another panel running for this project, or unrelated process took
1873
+ // the port.
1874
+ usingFallback = true;
1875
+ console.error(`\n [panel] derived port ${initialPort} is in use — falling back to OS-assigned port.`);
1876
+ console.error(` [panel] If another \`claudeflow panel\` is running for this project, kill it first to recover URL stability.\n`);
1877
+ server.listen(0, '127.0.0.1');
1878
+ return;
1879
+ }
1880
+ reject(err);
1881
+ });
1882
+ server.on('listening', () => {
1850
1883
  const addr = server.address();
1851
1884
  const url = `http://127.0.0.1:${addr.port}`;
1852
1885
  const idleTimer = setInterval(() => {
@@ -1858,8 +1891,9 @@ function start({ cwd = process.cwd(), port = 0, openBrowser = true } = {}) {
1858
1891
  }, 60 * 1000);
1859
1892
  idleTimer.unref();
1860
1893
  if (openBrowser) openInBrowser(url);
1861
- resolve({ url, server });
1894
+ resolve({ url, server, derivedPort, usedFallback: usingFallback });
1862
1895
  });
1896
+ server.listen(initialPort, '127.0.0.1');
1863
1897
  });
1864
1898
  }
1865
1899
 
@@ -1867,13 +1901,18 @@ async function run(argv = []) {
1867
1901
  const cwd = process.cwd();
1868
1902
  const noOpen = argv.includes('--no-open');
1869
1903
  const portArg = argv.find((a) => a.startsWith('--port='));
1870
- const port = portArg ? parseInt(portArg.split('=')[1], 10) : 0;
1904
+ // Default port (null) derive from cwd so the URL is stable across restarts.
1905
+ // --port=N overrides; --port=0 keeps the legacy OS-assigned behavior.
1906
+ const port = portArg ? parseInt(portArg.split('=')[1], 10) : null;
1871
1907
 
1872
1908
  ui.banner();
1873
1909
  console.log(` Starting panel for ${ui.CYAN}${cwd}${ui.RESET}`);
1874
- const { url } = await start({ cwd, port, openBrowser: !noOpen });
1910
+ const { url, derivedPort, usedFallback } = await start({ cwd, port, openBrowser: !noOpen });
1875
1911
  console.log('');
1876
1912
  console.log(` ${ui.GREEN}▸${ui.RESET} ${ui.CYAN}${url}${ui.RESET}`);
1913
+ if (derivedPort !== null && !usedFallback) {
1914
+ console.log(` ${ui.DIM}Port ${derivedPort} is derived from the cwd — same URL across restarts.${ui.RESET}`);
1915
+ }
1877
1916
  console.log(` ${ui.DIM}Ctrl+C to stop. Auto-shutdown after 30 min idle.${ui.RESET}`);
1878
1917
  console.log('');
1879
1918
  return new Promise(() => {}); // keep alive
@@ -1882,6 +1921,7 @@ async function run(argv = []) {
1882
1921
  module.exports = run;
1883
1922
  module.exports.start = start;
1884
1923
  module.exports.createPanelServer = handler;
1924
+ module.exports.derivePanelPort = derivePanelPort;
1885
1925
  module.exports.collectStatus = collectStatus;
1886
1926
  module.exports.getClaudeMdInfo = getClaudeMdInfo;
1887
1927
  module.exports.getHooksInfo = getHooksInfo;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.13.59",
3
+ "version": "2.13.61",
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"