@axiomatic-labs/claudeflow 2.13.22 → 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
  };
@@ -385,12 +373,66 @@ let state = null;
385
373
  let active = 'overview';
386
374
  let timer = null;
387
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
+
388
421
  async function refresh() {
389
- const r = await fetch('/api/status');
390
- 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
+ }
391
430
  renderHeader();
392
431
  renderNav();
393
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();
394
436
  }
395
437
 
396
438
  function renderHeader() {
@@ -494,12 +536,7 @@ function renderClaudeMd() {
494
536
  \${row('Default rules block', blk, { kind: c.block.present ? 'ok' : 'info', text: c.block.present ? '✓' : '·' })}
495
537
  \${row('Opt-out marker', c.optOut ? 'present (block disabled)' : 'absent', { kind: c.optOut ? 'info' : 'ok', text: c.optOut ? '·' : '✓' })}
496
538
  </div>
497
- <details><summary>Preview content</summary><div id="claude-md-preview">Loading…</div></details>
498
- <script>
499
- fetch('/api/claude-md').then(r => r.text()).then(t => {
500
- document.getElementById('claude-md-preview').innerHTML = '<pre>' + t.replace(/[&<>]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[c])) + '</pre>';
501
- });
502
- </\${'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>
503
540
  \`;
504
541
  }
505
542
 
@@ -519,7 +556,7 @@ function renderHooks() {
519
556
  <span class="matcher">\${escapeHtml(hd.matcherDisplay)}</span>
520
557
  </div>\`;
521
558
  }).join('');
522
- 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>\`;
523
560
  }).join('');
524
561
  return \`<h2>Hooks</h2>
525
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>
@@ -618,6 +655,15 @@ document.addEventListener('change', (e) => {
618
655
  }
619
656
  });
620
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
+
621
667
  document.getElementById('refresh').onclick = refresh;
622
668
  document.getElementById('auto').onchange = (e) => {
623
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.22",
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"