@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.
- package/lib/hook-overrides.js +24 -16
- package/lib/install.js +3 -4
- package/lib/panel.js +101 -39
- package/package.json +1 -1
package/lib/hook-overrides.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
41
|
-
|
|
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
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
123
|
+
matcher,
|
|
117
124
|
matcherDisplay: matcher || '(any)',
|
|
118
125
|
empty: isEmpty,
|
|
119
|
-
disabled:
|
|
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
|
|
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:
|
|
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
|
|
374
|
-
|
|
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">
|
|
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 => ({'&':'&','<':'<','>':'>'}[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.
|
|
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"
|