@axiomatic-labs/claudeflow 2.13.21 → 2.13.23

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.
@@ -24,7 +24,10 @@ const path = require('path');
24
24
 
25
25
  const OVERRIDES_REL = path.join('.claudeflow', 'config', 'user-hook-overrides.json');
26
26
  const SETTINGS_REL = path.join('.claude', 'settings.json');
27
- const HOOK_PATH_REGEX = /\.claude\/hooks\/(.+?\.js)/;
27
+ // Capture every `.claude/hooks/<path>.js` token in a command string. Wrapped
28
+ // commands include both `run-with-override.js` (the wrapper) and the actual
29
+ // hook path; we want the LAST match — that's the real handler.
30
+ const HOOK_PATH_REGEX_GLOBAL = /\.claude\/hooks\/([^"'\s]+\.js)/g;
28
31
 
29
32
  function readJsonSafe(absPath) {
30
33
  try { return JSON.parse(fs.readFileSync(absPath, 'utf8')); } catch { return null; }
@@ -37,8 +40,14 @@ function writeJson(absPath, data) {
37
40
 
38
41
  function normalizeHandlerPath(command) {
39
42
  if (!command) return '';
40
- const m = HOOK_PATH_REGEX.exec(command);
41
- return m ? m[1] : command.replace(/^node\s+/, '').replace(/^["']|["']$/g, '').trim();
43
+ const matches = [...command.matchAll(HOOK_PATH_REGEX_GLOBAL)];
44
+ if (matches.length === 0) {
45
+ return command.replace(/^node\s+/, '').replace(/^["']|["']$/g, '').trim();
46
+ }
47
+ // Skip the wrapper itself when present; pick the last non-wrapper match.
48
+ const real = matches.filter((m) => m[1] !== 'run-with-override.js');
49
+ const chosen = real.length ? real[real.length - 1] : matches[matches.length - 1];
50
+ return chosen[1];
42
51
  }
43
52
 
44
53
  function defaultOverrides() {
@@ -128,10 +137,13 @@ function restoreHandlerInSettings(settings, event, matcher, spec) {
128
137
  if (!already) rule.hooks.push(spec);
129
138
  }
130
139
 
131
- // Stateful toggle used by the panel.
132
- // - disable=true: capture spec from current settings.json, append to
133
- // overrides, remove from settings.json
134
- // - disable=false: pop spec from overrides, restore to settings.json
140
+ // Stateful toggle used by the panel. Only edits overrides.json — settings.json
141
+ // is never mutated. The wrapper (.claude/hooks/run-with-override.js) reads
142
+ // overrides on every invocation, so disable/enable takes effect on the next
143
+ // hook fire (no Claude Code restart required).
144
+ //
145
+ // Settings.json is also consulted to verify the handler exists at all
146
+ // (refuses to disable a phantom handler), but it is never written.
135
147
  function toggleHookOverride(projectRoot, { event, matcher = '', handler, disable }) {
136
148
  const settingsPath = path.join(projectRoot, SETTINGS_REL);
137
149
  const settings = readJsonSafe(settingsPath) || { hooks: {} };
@@ -143,12 +155,11 @@ function toggleHookOverride(projectRoot, { event, matcher = '', handler, disable
143
155
  (d) => d.event === event && (d.matcher || '') === matcherKey && d.handler === handler,
144
156
  );
145
157
  if (already) return { state: 'noop', reason: 'already-disabled' };
146
- const spec = findHandlerInSettings(settings, event, matcherKey, handler);
147
- if (!spec) return { state: 'error', reason: 'handler-not-found-in-settings' };
148
- overrides.disabledHandlers.push({ event, matcher: matcherKey, handler, spec });
158
+ if (!findHandlerInSettings(settings, event, matcherKey, handler)) {
159
+ return { state: 'error', reason: 'handler-not-found-in-settings' };
160
+ }
161
+ overrides.disabledHandlers.push({ event, matcher: matcherKey, handler });
149
162
  writeOverrides(projectRoot, overrides);
150
- const next = applyHookOverrides(settings, overrides);
151
- fs.writeFileSync(settingsPath, JSON.stringify(next, null, 2) + '\n');
152
163
  return { state: 'disabled' };
153
164
  }
154
165
 
@@ -156,10 +167,7 @@ function toggleHookOverride(projectRoot, { event, matcher = '', handler, disable
156
167
  (d) => d.event === event && (d.matcher || '') === matcherKey && d.handler === handler,
157
168
  );
158
169
  if (idx === -1) return { state: 'noop', reason: 'not-disabled' };
159
- const [removed] = overrides.disabledHandlers.splice(idx, 1);
160
- if (!removed.spec) return { state: 'error', reason: 'no-spec-recorded' };
161
- restoreHandlerInSettings(settings, event, matcherKey, removed.spec);
162
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
170
+ overrides.disabledHandlers.splice(idx, 1);
163
171
  writeOverrides(projectRoot, overrides);
164
172
  return { state: 'enabled' };
165
173
  }
package/lib/install.js CHANGED
@@ -86,10 +86,9 @@ async function run() {
86
86
  fs.mkdirSync(path.join(cwd, '.claude'), { recursive: true });
87
87
  if (fs.existsSync(srcSettings)) {
88
88
  syncClaudeSettings(srcSettings, dstSettings);
89
- // Re-apply user hook overrides (if any) so disabled handlers stay
90
- // disabled across update without this, every update would resurrect
91
- // hooks the user explicitly turned off via `claudeflow panel`.
92
- applyHookOverridesToFile(cwd);
89
+ // Settings.json now ships with every hook command wrapped through
90
+ // run-with-override.js. The wrapper consults user-hook-overrides.json
91
+ // at invocation time, so toggles persist without any settings rewrite.
93
92
  }
94
93
 
95
94
  // Upsert playwright entry in .mcp.json with project-specific CDP port.
package/lib/panel.js CHANGED
@@ -96,14 +96,18 @@ function getHooksInfo(cwd) {
96
96
  disabledIndex.set(`${d.event}|${d.matcher || ''}|${d.handler}`, d);
97
97
  }
98
98
 
99
+ // settings.json lists every hook (with each command wrapped through
100
+ // run-with-override.js). The wrapper checks overrides at invocation time,
101
+ // so the panel just annotates each handler with its current disabled
102
+ // state from the overrides file.
99
103
  const hooks = settings.hooks || {};
100
- const eventsMap = new Map();
104
+ const events = [];
101
105
  let totalHandlers = 0;
102
106
  let warnings = 0;
103
107
  let disabledCount = 0;
104
108
 
105
109
  for (const [event, rules] of Object.entries(hooks)) {
106
- if (!eventsMap.has(event)) eventsMap.set(event, []);
110
+ const handlers = [];
107
111
  for (const rule of rules) {
108
112
  const matcher = rule.matcher || '';
109
113
  for (const h of rule.hooks || []) {
@@ -111,44 +115,28 @@ function getHooksInfo(cwd) {
111
115
  const handlerPath = normalizeHandlerPath(cmd) || (cmd ? cmd : '');
112
116
  const isEmpty = !handlerPath;
113
117
  if (isEmpty) warnings++;
114
- eventsMap.get(event).push({
118
+ const key = `${event}|${matcher}|${handlerPath}`;
119
+ const isDisabled = disabledIndex.has(key);
120
+ if (isDisabled) disabledCount++;
121
+ handlers.push({
115
122
  handler: handlerPath || '(empty command)',
116
- matcher: matcher || '',
123
+ matcher,
117
124
  matcherDisplay: matcher || '(any)',
118
125
  empty: isEmpty,
119
- disabled: false,
126
+ disabled: isDisabled,
120
127
  rawCommand: cmd,
121
128
  });
122
129
  totalHandlers++;
123
130
  }
124
131
  }
132
+ events.push({ event, count: handlers.length, handlers });
125
133
  }
126
134
 
127
- // Add disabled handlers (live in overrides, not in settings.json)
128
- for (const d of overrides.disabledHandlers || []) {
129
- if (!eventsMap.has(d.event)) eventsMap.set(d.event, []);
130
- eventsMap.get(d.event).push({
131
- handler: d.handler,
132
- matcher: d.matcher || '',
133
- matcherDisplay: d.matcher || '(any)',
134
- empty: false,
135
- disabled: true,
136
- rawCommand: (d.spec && d.spec.command) || '',
137
- });
138
- disabledCount++;
139
- }
140
-
141
- const events = Array.from(eventsMap.entries()).map(([event, handlers]) => ({
142
- event,
143
- count: handlers.length,
144
- handlers,
145
- }));
146
-
147
135
  return {
148
136
  settingsFound: true,
149
137
  events,
150
- totalHandlers: totalHandlers + disabledCount,
151
- activeHandlers: totalHandlers,
138
+ totalHandlers,
139
+ activeHandlers: totalHandlers - disabledCount,
152
140
  disabledHandlers: disabledCount,
153
141
  warnings,
154
142
  };
@@ -297,17 +285,33 @@ header .cwd { color: var(--muted); font-family: var(--mono); font-size: 12px; fl
297
285
  header .actions { display: flex; gap: 12px; align-items: center; color: var(--muted); font-size: 12px; }
298
286
  header button { background: var(--panel-2); border: 1px solid var(--border); color: var(--fg); padding: 6px 12px; border-radius: 6px; cursor: pointer; font: inherit; }
299
287
  header button:hover { border-color: var(--accent); }
300
- main { display: grid; grid-template-columns: 220px 1fr; min-height: calc(100vh - 56px); }
301
- nav { border-right: 1px solid var(--border); background: var(--panel); padding: 12px 0; }
288
+ main { display: flex; min-height: calc(100vh - 56px); }
289
+ nav { width: 220px; flex-shrink: 0; border-right: 1px solid var(--border); background: var(--panel); padding: 12px 0; }
302
290
  nav a { display: flex; align-items: center; gap: 10px; padding: 10px 22px; color: var(--fg); text-decoration: none; cursor: pointer; border-left: 3px solid transparent; font-size: 13px; }
303
291
  nav a:hover { background: var(--panel-2); }
304
292
  nav a.active { background: var(--panel-2); border-left-color: var(--accent); }
293
+ @media (max-width: 720px) {
294
+ main { flex-direction: column; }
295
+ nav {
296
+ width: 100%; border-right: 0; border-bottom: 1px solid var(--border);
297
+ padding: 0; display: flex; overflow-x: auto;
298
+ }
299
+ nav a {
300
+ flex-shrink: 0; padding: 12px 16px; border-left: 0;
301
+ border-bottom: 3px solid transparent; white-space: nowrap;
302
+ }
303
+ nav a.active { border-left: 0; border-bottom-color: var(--accent); }
304
+ section { padding: 16px 18px; max-width: none; }
305
+ header { padding: 12px 16px; gap: 8px; }
306
+ header .cwd { font-size: 11px; }
307
+ header .actions { font-size: 11px; }
308
+ }
305
309
  nav .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; }
306
310
  .dot.ok { background: var(--ok); }
307
311
  .dot.warn { background: var(--warn); }
308
312
  .dot.err { background: var(--err); }
309
313
  .dot.info { background: var(--muted); }
310
- section { padding: 22px 28px; max-width: 980px; }
314
+ section { flex: 1; min-width: 0; padding: 22px 28px; max-width: 980px; }
311
315
  section h2 { font-size: 16px; margin: 0 0 6px; letter-spacing: 0.3px; }
312
316
  section .sub { color: var(--muted); font-size: 12px; margin-bottom: 18px; }
313
317
  .card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 14px 18px; margin-bottom: 14px; }
@@ -369,12 +373,66 @@ let state = null;
369
373
  let active = 'overview';
370
374
  let timer = null;
371
375
 
376
+ // Preserve which <details> were open across re-renders (auto-refresh would
377
+ // otherwise collapse them every 5s).
378
+ function captureOpenDetails() {
379
+ const ids = new Set();
380
+ document.querySelectorAll('details[open][data-detail-id]').forEach(d => ids.add(d.dataset.detailId));
381
+ // Also capture anonymous details by event name (hooks accordion).
382
+ document.querySelectorAll('details[open][data-event]').forEach(d => ids.add('event:' + d.dataset.event));
383
+ return ids;
384
+ }
385
+
386
+ function restoreOpenDetails(ids) {
387
+ if (!ids.size) return;
388
+ document.querySelectorAll('details[data-detail-id]').forEach(d => {
389
+ if (ids.has(d.dataset.detailId)) d.open = true;
390
+ });
391
+ document.querySelectorAll('details[data-event]').forEach(d => {
392
+ if (ids.has('event:' + d.dataset.event)) d.open = true;
393
+ });
394
+ }
395
+
396
+ // Cache fetched CLAUDE.md content so re-renders during auto-refresh don't
397
+ // drop the preview the user already loaded.
398
+ let claudeMdCache = null;
399
+
400
+ async function loadClaudeMdPreview() {
401
+ const target = document.getElementById('claude-md-preview');
402
+ if (!target) return;
403
+ if (target.dataset.loaded === '1') return;
404
+ target.dataset.loaded = '1';
405
+ target.textContent = 'Loading…';
406
+ try {
407
+ if (!claudeMdCache) {
408
+ const r = await fetch('/api/claude-md');
409
+ claudeMdCache = await r.text();
410
+ }
411
+ const pre = document.createElement('pre');
412
+ pre.textContent = claudeMdCache;
413
+ target.innerHTML = '';
414
+ target.appendChild(pre);
415
+ } catch (e) {
416
+ target.textContent = 'Failed to load: ' + e.message;
417
+ target.dataset.loaded = '';
418
+ }
419
+ }
420
+
372
421
  async function refresh() {
373
- const r = await fetch('/api/status');
374
- state = await r.json();
422
+ const open = captureOpenDetails();
423
+ try {
424
+ const r = await fetch('/api/status');
425
+ state = await r.json();
426
+ } catch (e) {
427
+ showToast && showToast('Refresh failed: ' + e.message, 'err');
428
+ return;
429
+ }
375
430
  renderHeader();
376
431
  renderNav();
377
432
  renderContent();
433
+ restoreOpenDetails(open);
434
+ // If preview was visible/open, ensure it stays loaded after re-render.
435
+ if (open.has('claude-md-preview')) loadClaudeMdPreview();
378
436
  }
379
437
 
380
438
  function renderHeader() {
@@ -478,12 +536,7 @@ function renderClaudeMd() {
478
536
  \${row('Default rules block', blk, { kind: c.block.present ? 'ok' : 'info', text: c.block.present ? '✓' : '·' })}
479
537
  \${row('Opt-out marker', c.optOut ? 'present (block disabled)' : 'absent', { kind: c.optOut ? 'info' : 'ok', text: c.optOut ? '·' : '✓' })}
480
538
  </div>
481
- <details><summary>Preview content</summary><div id="claude-md-preview">Loading…</div></details>
482
- <script>
483
- fetch('/api/claude-md').then(r => r.text()).then(t => {
484
- document.getElementById('claude-md-preview').innerHTML = '<pre>' + t.replace(/[&<>]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[c])) + '</pre>';
485
- });
486
- </\${'script'}>
539
+ <details data-detail-id="claude-md-preview"><summary>Preview content</summary><div id="claude-md-preview" class="muted">Click to load…</div></details>
487
540
  \`;
488
541
  }
489
542
 
@@ -503,7 +556,7 @@ function renderHooks() {
503
556
  <span class="matcher">\${escapeHtml(hd.matcherDisplay)}</span>
504
557
  </div>\`;
505
558
  }).join('');
506
- return \`<details><summary>\${escapeHtml(ev.event)} <span class="muted">(\${ev.count})</span></summary>\${handlers}</details>\`;
559
+ return \`<details data-event="\${escapeHtml(ev.event)}"><summary>\${escapeHtml(ev.event)} <span class="muted">(\${ev.count})</span></summary>\${handlers}</details>\`;
507
560
  }).join('');
508
561
  return \`<h2>Hooks</h2>
509
562
  <p class="sub">\${h.events.length} events · \${h.activeHandlers} active\${h.disabledHandlers ? ' · <span class="muted">' + h.disabledHandlers + ' disabled</span>' : ''}\${h.warnings ? ' · <span class="warning">' + h.warnings + ' warnings</span>' : ''}</p>
@@ -602,6 +655,15 @@ document.addEventListener('change', (e) => {
602
655
  }
603
656
  });
604
657
 
658
+ // Lazy-load CLAUDE.md preview when the user opens the details element.
659
+ // The "toggle" event does not bubble by default — capture phase keeps delegation working.
660
+ document.addEventListener('toggle', (e) => {
661
+ const t = e.target;
662
+ if (t && t.tagName === 'DETAILS' && t.dataset.detailId === 'claude-md-preview' && t.open) {
663
+ loadClaudeMdPreview();
664
+ }
665
+ }, true);
666
+
605
667
  document.getElementById('refresh').onclick = refresh;
606
668
  document.getElementById('auto').onchange = (e) => {
607
669
  if (timer) clearInterval(timer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.13.21",
3
+ "version": "2.13.23",
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"