@axiomatic-labs/claudeflow 2.13.20 → 2.13.22
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 +177 -0
- package/lib/install.js +5 -0
- package/lib/panel.js +165 -25
- package/package.json +1 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// User hook overrides — toggle individual claudeflow hook handlers without
|
|
2
|
+
// editing the template-managed `.claude/settings.json` directly.
|
|
3
|
+
//
|
|
4
|
+
// File: `.claudeflow/config/user-hook-overrides.json`
|
|
5
|
+
// Shape:
|
|
6
|
+
// { "version": 1,
|
|
7
|
+
// "disabledHandlers": [
|
|
8
|
+
// { "event": "SessionStart", "matcher": "", "handler": "SessionStart/foo.js",
|
|
9
|
+
// "spec": { "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/SessionStart/foo.js\"" } } ] }
|
|
10
|
+
//
|
|
11
|
+
// The `spec` field captures the original hook entry verbatim so the panel
|
|
12
|
+
// can re-enable a handler later without consulting an external snapshot.
|
|
13
|
+
//
|
|
14
|
+
// Two operations:
|
|
15
|
+
// - `applyHookOverrides(settings, overrides)`: pure function, returns a
|
|
16
|
+
// new settings object with disabled handlers removed. Called by
|
|
17
|
+
// install.js right after writing `.claude/settings.json` so the
|
|
18
|
+
// effective config respects the overrides on every install/update.
|
|
19
|
+
// - `toggleHookOverride(...)`: stateful, used by the panel server.
|
|
20
|
+
// Reads / writes the overrides file and the resolved settings file.
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
const OVERRIDES_REL = path.join('.claudeflow', 'config', 'user-hook-overrides.json');
|
|
26
|
+
const SETTINGS_REL = path.join('.claude', 'settings.json');
|
|
27
|
+
const HOOK_PATH_REGEX = /\.claude\/hooks\/(.+?\.js)/;
|
|
28
|
+
|
|
29
|
+
function readJsonSafe(absPath) {
|
|
30
|
+
try { return JSON.parse(fs.readFileSync(absPath, 'utf8')); } catch { return null; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeJson(absPath, data) {
|
|
34
|
+
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
35
|
+
fs.writeFileSync(absPath, JSON.stringify(data, null, 2) + '\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeHandlerPath(command) {
|
|
39
|
+
if (!command) return '';
|
|
40
|
+
const m = HOOK_PATH_REGEX.exec(command);
|
|
41
|
+
return m ? m[1] : command.replace(/^node\s+/, '').replace(/^["']|["']$/g, '').trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function defaultOverrides() {
|
|
45
|
+
return { version: 1, disabledHandlers: [] };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readOverrides(projectRoot) {
|
|
49
|
+
const data = readJsonSafe(path.join(projectRoot, OVERRIDES_REL));
|
|
50
|
+
if (!data || typeof data !== 'object') return defaultOverrides();
|
|
51
|
+
return {
|
|
52
|
+
version: data.version || 1,
|
|
53
|
+
disabledHandlers: Array.isArray(data.disabledHandlers) ? data.disabledHandlers : [],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writeOverrides(projectRoot, overrides) {
|
|
58
|
+
writeJson(path.join(projectRoot, OVERRIDES_REL), overrides);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function matchesOverride(override, event, matcher, handlerCommand) {
|
|
62
|
+
if (override.event !== event) return false;
|
|
63
|
+
if ((override.matcher || '') !== (matcher || '')) return false;
|
|
64
|
+
return normalizeHandlerPath(handlerCommand) === override.handler;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Returns a fresh settings object with handlers disabled by `overrides` removed.
|
|
68
|
+
// Empty rules are pruned. The shape of `settings.hooks` is preserved otherwise.
|
|
69
|
+
function applyHookOverrides(settings, overrides) {
|
|
70
|
+
const disabled = (overrides && overrides.disabledHandlers) || [];
|
|
71
|
+
if (!disabled.length || !settings || !settings.hooks) return settings;
|
|
72
|
+
const next = JSON.parse(JSON.stringify(settings));
|
|
73
|
+
const hooks = next.hooks;
|
|
74
|
+
for (const event of Object.keys(hooks)) {
|
|
75
|
+
const rules = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
76
|
+
for (let i = rules.length - 1; i >= 0; i--) {
|
|
77
|
+
const rule = rules[i] || {};
|
|
78
|
+
const matcher = rule.matcher || '';
|
|
79
|
+
const remaining = (rule.hooks || []).filter(
|
|
80
|
+
(h) => !disabled.some((d) => matchesOverride(d, event, matcher, h.command || '')),
|
|
81
|
+
);
|
|
82
|
+
if (remaining.length === 0) rules.splice(i, 1);
|
|
83
|
+
else rule.hooks = remaining;
|
|
84
|
+
}
|
|
85
|
+
hooks[event] = rules;
|
|
86
|
+
}
|
|
87
|
+
return next;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Apply on disk: read settings.json + overrides, write the pruned settings
|
|
91
|
+
// back. Idempotent — calling twice produces the same file.
|
|
92
|
+
function applyOverridesToFile(projectRoot) {
|
|
93
|
+
const settingsPath = path.join(projectRoot, SETTINGS_REL);
|
|
94
|
+
const settings = readJsonSafe(settingsPath);
|
|
95
|
+
if (!settings) return false;
|
|
96
|
+
const overrides = readOverrides(projectRoot);
|
|
97
|
+
if (!overrides.disabledHandlers.length) return false;
|
|
98
|
+
const next = applyHookOverrides(settings, overrides);
|
|
99
|
+
fs.writeFileSync(settingsPath, JSON.stringify(next, null, 2) + '\n');
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Find a hook entry in current settings.json by event/matcher/handler.
|
|
104
|
+
// Used when disabling: we capture the spec before removing it.
|
|
105
|
+
function findHandlerInSettings(settings, event, matcher, handler) {
|
|
106
|
+
const rules = (settings.hooks && settings.hooks[event]) || [];
|
|
107
|
+
for (const rule of rules) {
|
|
108
|
+
if ((rule.matcher || '') !== (matcher || '')) continue;
|
|
109
|
+
for (const h of rule.hooks || []) {
|
|
110
|
+
if (normalizeHandlerPath(h.command || '') === handler) return h;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Add an entry back to the live settings under the right event/matcher.
|
|
117
|
+
function restoreHandlerInSettings(settings, event, matcher, spec) {
|
|
118
|
+
if (!settings.hooks) settings.hooks = {};
|
|
119
|
+
if (!settings.hooks[event]) settings.hooks[event] = [];
|
|
120
|
+
const rules = settings.hooks[event];
|
|
121
|
+
let rule = rules.find((r) => (r.matcher || '') === (matcher || ''));
|
|
122
|
+
if (!rule) {
|
|
123
|
+
rule = { matcher, hooks: [] };
|
|
124
|
+
rules.push(rule);
|
|
125
|
+
}
|
|
126
|
+
// Avoid duplicate restoration if the spec is already present
|
|
127
|
+
const already = rule.hooks.some((h) => normalizeHandlerPath(h.command || '') === normalizeHandlerPath(spec.command || ''));
|
|
128
|
+
if (!already) rule.hooks.push(spec);
|
|
129
|
+
}
|
|
130
|
+
|
|
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
|
|
135
|
+
function toggleHookOverride(projectRoot, { event, matcher = '', handler, disable }) {
|
|
136
|
+
const settingsPath = path.join(projectRoot, SETTINGS_REL);
|
|
137
|
+
const settings = readJsonSafe(settingsPath) || { hooks: {} };
|
|
138
|
+
const overrides = readOverrides(projectRoot);
|
|
139
|
+
const matcherKey = matcher || '';
|
|
140
|
+
|
|
141
|
+
if (disable) {
|
|
142
|
+
const already = overrides.disabledHandlers.find(
|
|
143
|
+
(d) => d.event === event && (d.matcher || '') === matcherKey && d.handler === handler,
|
|
144
|
+
);
|
|
145
|
+
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 });
|
|
149
|
+
writeOverrides(projectRoot, overrides);
|
|
150
|
+
const next = applyHookOverrides(settings, overrides);
|
|
151
|
+
fs.writeFileSync(settingsPath, JSON.stringify(next, null, 2) + '\n');
|
|
152
|
+
return { state: 'disabled' };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const idx = overrides.disabledHandlers.findIndex(
|
|
156
|
+
(d) => d.event === event && (d.matcher || '') === matcherKey && d.handler === handler,
|
|
157
|
+
);
|
|
158
|
+
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');
|
|
163
|
+
writeOverrides(projectRoot, overrides);
|
|
164
|
+
return { state: 'enabled' };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = {
|
|
168
|
+
OVERRIDES_REL,
|
|
169
|
+
SETTINGS_REL,
|
|
170
|
+
applyHookOverrides,
|
|
171
|
+
applyOverridesToFile,
|
|
172
|
+
readOverrides,
|
|
173
|
+
writeOverrides,
|
|
174
|
+
toggleHookOverride,
|
|
175
|
+
normalizeHandlerPath,
|
|
176
|
+
matchesOverride,
|
|
177
|
+
};
|
package/lib/install.js
CHANGED
|
@@ -4,6 +4,7 @@ const { execSync, execFileSync } = require('child_process');
|
|
|
4
4
|
const { requireAuth } = require('./auth.js');
|
|
5
5
|
const { getLatestRelease, downloadReleaseAsset } = require('./download.js');
|
|
6
6
|
const { writeLocalVersion, readLocalVersion } = require('./version.js');
|
|
7
|
+
const { applyOverridesToFile: applyHookOverridesToFile } = require('./hook-overrides.js');
|
|
7
8
|
const ui = require('./ui.js');
|
|
8
9
|
|
|
9
10
|
// Template skills are shipped inside the ZIP. Copy every managed
|
|
@@ -85,6 +86,10 @@ async function run() {
|
|
|
85
86
|
fs.mkdirSync(path.join(cwd, '.claude'), { recursive: true });
|
|
86
87
|
if (fs.existsSync(srcSettings)) {
|
|
87
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);
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
// Upsert playwright entry in .mcp.json with project-specific CDP port.
|
package/lib/panel.js
CHANGED
|
@@ -21,6 +21,11 @@ const {
|
|
|
21
21
|
checkStaleLockfiles,
|
|
22
22
|
readPlaywrightCdpEndpoint,
|
|
23
23
|
} = require('./doctor.js');
|
|
24
|
+
const {
|
|
25
|
+
readOverrides,
|
|
26
|
+
toggleHookOverride,
|
|
27
|
+
normalizeHandlerPath,
|
|
28
|
+
} = require('./hook-overrides.js');
|
|
24
29
|
|
|
25
30
|
const IDLE_SHUTDOWN_MS = 30 * 60 * 1000; // 30 min
|
|
26
31
|
|
|
@@ -85,38 +90,68 @@ function getHooksInfo(cwd) {
|
|
|
85
90
|
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); }
|
|
86
91
|
catch { return { settingsFound: false, events: [] }; }
|
|
87
92
|
|
|
93
|
+
const overrides = readOverrides(cwd);
|
|
94
|
+
const disabledIndex = new Map();
|
|
95
|
+
for (const d of overrides.disabledHandlers || []) {
|
|
96
|
+
disabledIndex.set(`${d.event}|${d.matcher || ''}|${d.handler}`, d);
|
|
97
|
+
}
|
|
98
|
+
|
|
88
99
|
const hooks = settings.hooks || {};
|
|
89
|
-
const
|
|
100
|
+
const eventsMap = new Map();
|
|
90
101
|
let totalHandlers = 0;
|
|
91
102
|
let warnings = 0;
|
|
103
|
+
let disabledCount = 0;
|
|
92
104
|
|
|
93
105
|
for (const [event, rules] of Object.entries(hooks)) {
|
|
94
|
-
|
|
106
|
+
if (!eventsMap.has(event)) eventsMap.set(event, []);
|
|
95
107
|
for (const rule of rules) {
|
|
96
108
|
const matcher = rule.matcher || '';
|
|
97
109
|
for (const h of rule.hooks || []) {
|
|
98
110
|
const cmd = h.command || '';
|
|
99
|
-
const handlerPath = cmd
|
|
100
|
-
.replace(/^node\s+/, '')
|
|
101
|
-
.replace(/^"?\$CLAUDE_PROJECT_DIR"?\/?\.claude\/hooks\//, '')
|
|
102
|
-
.replace(/^.*\/\.claude\/hooks\//, '')
|
|
103
|
-
.replace(/^"?\$CLAUDE_PROJECT_DIR"?\/?/, '')
|
|
104
|
-
.replace(/"/g, '')
|
|
105
|
-
.trim();
|
|
111
|
+
const handlerPath = normalizeHandlerPath(cmd) || (cmd ? cmd : '');
|
|
106
112
|
const isEmpty = !handlerPath;
|
|
107
113
|
if (isEmpty) warnings++;
|
|
108
|
-
|
|
114
|
+
eventsMap.get(event).push({
|
|
109
115
|
handler: handlerPath || '(empty command)',
|
|
110
|
-
matcher: matcher || '
|
|
116
|
+
matcher: matcher || '',
|
|
117
|
+
matcherDisplay: matcher || '(any)',
|
|
111
118
|
empty: isEmpty,
|
|
119
|
+
disabled: false,
|
|
112
120
|
rawCommand: cmd,
|
|
113
121
|
});
|
|
114
122
|
totalHandlers++;
|
|
115
123
|
}
|
|
116
124
|
}
|
|
117
|
-
events.push({ event, count: handlers.length, handlers });
|
|
118
125
|
}
|
|
119
|
-
|
|
126
|
+
|
|
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
|
+
return {
|
|
148
|
+
settingsFound: true,
|
|
149
|
+
events,
|
|
150
|
+
totalHandlers: totalHandlers + disabledCount,
|
|
151
|
+
activeHandlers: totalHandlers,
|
|
152
|
+
disabledHandlers: disabledCount,
|
|
153
|
+
warnings,
|
|
154
|
+
};
|
|
120
155
|
}
|
|
121
156
|
|
|
122
157
|
function getMcpInfo(cwd) {
|
|
@@ -262,17 +297,33 @@ header .cwd { color: var(--muted); font-family: var(--mono); font-size: 12px; fl
|
|
|
262
297
|
header .actions { display: flex; gap: 12px; align-items: center; color: var(--muted); font-size: 12px; }
|
|
263
298
|
header button { background: var(--panel-2); border: 1px solid var(--border); color: var(--fg); padding: 6px 12px; border-radius: 6px; cursor: pointer; font: inherit; }
|
|
264
299
|
header button:hover { border-color: var(--accent); }
|
|
265
|
-
main { display:
|
|
266
|
-
nav { border-right: 1px solid var(--border); background: var(--panel); padding: 12px 0; }
|
|
300
|
+
main { display: flex; min-height: calc(100vh - 56px); }
|
|
301
|
+
nav { width: 220px; flex-shrink: 0; border-right: 1px solid var(--border); background: var(--panel); padding: 12px 0; }
|
|
267
302
|
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; }
|
|
268
303
|
nav a:hover { background: var(--panel-2); }
|
|
269
304
|
nav a.active { background: var(--panel-2); border-left-color: var(--accent); }
|
|
305
|
+
@media (max-width: 720px) {
|
|
306
|
+
main { flex-direction: column; }
|
|
307
|
+
nav {
|
|
308
|
+
width: 100%; border-right: 0; border-bottom: 1px solid var(--border);
|
|
309
|
+
padding: 0; display: flex; overflow-x: auto;
|
|
310
|
+
}
|
|
311
|
+
nav a {
|
|
312
|
+
flex-shrink: 0; padding: 12px 16px; border-left: 0;
|
|
313
|
+
border-bottom: 3px solid transparent; white-space: nowrap;
|
|
314
|
+
}
|
|
315
|
+
nav a.active { border-left: 0; border-bottom-color: var(--accent); }
|
|
316
|
+
section { padding: 16px 18px; max-width: none; }
|
|
317
|
+
header { padding: 12px 16px; gap: 8px; }
|
|
318
|
+
header .cwd { font-size: 11px; }
|
|
319
|
+
header .actions { font-size: 11px; }
|
|
320
|
+
}
|
|
270
321
|
nav .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; }
|
|
271
322
|
.dot.ok { background: var(--ok); }
|
|
272
323
|
.dot.warn { background: var(--warn); }
|
|
273
324
|
.dot.err { background: var(--err); }
|
|
274
325
|
.dot.info { background: var(--muted); }
|
|
275
|
-
section { padding: 22px 28px; max-width: 980px; }
|
|
326
|
+
section { flex: 1; min-width: 0; padding: 22px 28px; max-width: 980px; }
|
|
276
327
|
section h2 { font-size: 16px; margin: 0 0 6px; letter-spacing: 0.3px; }
|
|
277
328
|
section .sub { color: var(--muted); font-size: 12px; margin-bottom: 18px; }
|
|
278
329
|
.card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 14px 18px; margin-bottom: 14px; }
|
|
@@ -297,6 +348,11 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
|
|
|
297
348
|
.warning { color: var(--warn); }
|
|
298
349
|
.toggle { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
|
|
299
350
|
.toggle input { accent-color: var(--accent); }
|
|
351
|
+
.disabled-row { opacity: 0.55; }
|
|
352
|
+
.disabled-row .matcher { font-style: italic; }
|
|
353
|
+
.toast { position: fixed; bottom: 18px; right: 18px; background: var(--panel); border: 1px solid var(--border); padding: 10px 14px; border-radius: 6px; font-size: 13px; box-shadow: 0 4px 12px rgba(0,0,0,0.4); }
|
|
354
|
+
.toast.err { border-color: var(--err); }
|
|
355
|
+
.toast.ok { border-color: var(--ok); }
|
|
300
356
|
</style>
|
|
301
357
|
</head>
|
|
302
358
|
<body>
|
|
@@ -451,14 +507,23 @@ function renderHooks() {
|
|
|
451
507
|
const h = state.hooks;
|
|
452
508
|
if (!h.settingsFound) return '<h2>Hooks</h2><p class="error">.claude/settings.json not found</p>';
|
|
453
509
|
const items = h.events.map(ev => {
|
|
454
|
-
const handlers = ev.handlers.map(
|
|
455
|
-
const mark =
|
|
456
|
-
|
|
510
|
+
const handlers = ev.handlers.map(hd => {
|
|
511
|
+
const mark = hd.empty ? '<span class="warning">⚠ empty command</span>' : '';
|
|
512
|
+
const cls = hd.disabled ? 'handler disabled-row' : 'handler';
|
|
513
|
+
const checked = hd.disabled ? '' : 'checked';
|
|
514
|
+
const dataset = \`data-event="\${escapeHtml(ev.event)}" data-matcher="\${escapeHtml(hd.matcher)}" data-handler="\${escapeHtml(hd.handler)}"\`;
|
|
515
|
+
const status = hd.disabled ? '<span class="badge info">disabled</span>' : '';
|
|
516
|
+
return \`<div class="\${cls}">
|
|
517
|
+
<label class="toggle"><input type="checkbox" class="hook-toggle" \${dataset} \${checked} \${hd.empty ? 'disabled' : ''} />
|
|
518
|
+
<span>\${escapeHtml(hd.handler)} \${status} \${mark}</span></label>
|
|
519
|
+
<span class="matcher">\${escapeHtml(hd.matcherDisplay)}</span>
|
|
520
|
+
</div>\`;
|
|
457
521
|
}).join('');
|
|
458
522
|
return \`<details><summary>\${escapeHtml(ev.event)} <span class="muted">(\${ev.count})</span></summary>\${handlers}</details>\`;
|
|
459
523
|
}).join('');
|
|
460
524
|
return \`<h2>Hooks</h2>
|
|
461
|
-
<p class="sub">\${h.events.length} events · \${h.
|
|
525
|
+
<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>
|
|
526
|
+
<p class="sub muted">Uncheck a handler to disable it. Disabled handlers are recorded in <code>.claudeflow/config/user-hook-overrides.json</code> and survive <code>claudeflow update</code>. Toggling rewrites <code>.claude/settings.json</code> immediately; reload Claude Code to pick up the change.</p>
|
|
462
527
|
\${items}\`;
|
|
463
528
|
}
|
|
464
529
|
|
|
@@ -513,6 +578,46 @@ function renderDoctor() {
|
|
|
513
578
|
</div>\`;
|
|
514
579
|
}
|
|
515
580
|
|
|
581
|
+
function showToast(msg, kind) {
|
|
582
|
+
const el = document.createElement('div');
|
|
583
|
+
el.className = 'toast ' + (kind || 'ok');
|
|
584
|
+
el.textContent = msg;
|
|
585
|
+
document.body.appendChild(el);
|
|
586
|
+
setTimeout(() => el.remove(), 2400);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function toggleHook(event, matcher, handler, disable) {
|
|
590
|
+
try {
|
|
591
|
+
const r = await fetch('/api/hooks/toggle', {
|
|
592
|
+
method: 'POST',
|
|
593
|
+
headers: { 'Content-Type': 'application/json' },
|
|
594
|
+
body: JSON.stringify({ event, matcher, handler, disable }),
|
|
595
|
+
});
|
|
596
|
+
const result = await r.json();
|
|
597
|
+
if (!r.ok || result.state === 'error') {
|
|
598
|
+
showToast('Toggle failed: ' + (result.reason || result.error || 'unknown'), 'err');
|
|
599
|
+
} else if (result.state === 'noop') {
|
|
600
|
+
showToast('Already in target state (' + result.reason + ')', 'ok');
|
|
601
|
+
} else {
|
|
602
|
+
showToast(disable ? 'Disabled. Reload Claude Code to apply.' : 'Enabled. Reload Claude Code to apply.', 'ok');
|
|
603
|
+
}
|
|
604
|
+
} catch (e) {
|
|
605
|
+
showToast('Network error: ' + e.message, 'err');
|
|
606
|
+
}
|
|
607
|
+
await refresh();
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
document.addEventListener('change', (e) => {
|
|
611
|
+
const t = e.target;
|
|
612
|
+
if (t && t.classList && t.classList.contains('hook-toggle')) {
|
|
613
|
+
const event = t.dataset.event;
|
|
614
|
+
const matcher = t.dataset.matcher;
|
|
615
|
+
const handler = t.dataset.handler;
|
|
616
|
+
const disable = !t.checked;
|
|
617
|
+
toggleHook(event, matcher, handler, disable);
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
|
|
516
621
|
document.getElementById('refresh').onclick = refresh;
|
|
517
622
|
document.getElementById('auto').onchange = (e) => {
|
|
518
623
|
if (timer) clearInterval(timer);
|
|
@@ -532,8 +637,26 @@ function readClaudeMdSafe(cwd) {
|
|
|
532
637
|
catch { return ''; }
|
|
533
638
|
}
|
|
534
639
|
|
|
640
|
+
function readRequestBody(req, limit = 64 * 1024) {
|
|
641
|
+
return new Promise((resolve, reject) => {
|
|
642
|
+
const chunks = [];
|
|
643
|
+
let total = 0;
|
|
644
|
+
req.on('data', (c) => {
|
|
645
|
+
total += c.length;
|
|
646
|
+
if (total > limit) {
|
|
647
|
+
reject(new Error('Request body too large'));
|
|
648
|
+
req.destroy();
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
chunks.push(c);
|
|
652
|
+
});
|
|
653
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
654
|
+
req.on('error', reject);
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
535
658
|
function handler(cwd) {
|
|
536
|
-
return (req, res) => {
|
|
659
|
+
return async (req, res) => {
|
|
537
660
|
try {
|
|
538
661
|
const url = new URL(req.url, 'http://localhost');
|
|
539
662
|
const route = url.pathname;
|
|
@@ -543,10 +666,27 @@ function handler(cwd) {
|
|
|
543
666
|
res.end(body);
|
|
544
667
|
};
|
|
545
668
|
|
|
546
|
-
if (
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
669
|
+
if (req.method === 'GET') {
|
|
670
|
+
if (route === '/' || route === '/index.html') return send(200, renderHtml(), 'text/html; charset=utf-8');
|
|
671
|
+
if (route === '/api/status') return send(200, JSON.stringify(collectStatus(cwd)), 'application/json');
|
|
672
|
+
if (route === '/api/claude-md') return send(200, readClaudeMdSafe(cwd), 'text/plain; charset=utf-8');
|
|
673
|
+
if (route === '/healthz') return send(200, 'ok', 'text/plain');
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (req.method === 'POST' && route === '/api/hooks/toggle') {
|
|
677
|
+
const raw = await readRequestBody(req);
|
|
678
|
+
let payload;
|
|
679
|
+
try { payload = JSON.parse(raw); }
|
|
680
|
+
catch { return send(400, JSON.stringify({ error: 'invalid json' }), 'application/json'); }
|
|
681
|
+
const { event, matcher, handler: hdlr, disable } = payload;
|
|
682
|
+
if (typeof event !== 'string' || typeof hdlr !== 'string' || typeof disable !== 'boolean') {
|
|
683
|
+
return send(400, JSON.stringify({ error: 'event, handler, disable required' }), 'application/json');
|
|
684
|
+
}
|
|
685
|
+
const result = toggleHookOverride(cwd, { event, matcher: matcher || '', handler: hdlr, disable });
|
|
686
|
+
const status = result.state === 'error' ? 500 : 200;
|
|
687
|
+
return send(status, JSON.stringify(result), 'application/json');
|
|
688
|
+
}
|
|
689
|
+
|
|
550
690
|
send(404, JSON.stringify({ error: 'not found' }), 'application/json');
|
|
551
691
|
} catch (err) {
|
|
552
692
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiomatic-labs/claudeflow",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.22",
|
|
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"
|