@axiomatic-labs/claudeflow 2.13.22 → 2.13.24
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 +47 -17
- package/lib/install.js +3 -4
- package/lib/panel.js +206 -37
- 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,12 +40,18 @@ 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() {
|
|
45
|
-
return { version: 1, disabledHandlers: [] };
|
|
54
|
+
return { version: 1, disabledHandlers: [], disabledReminders: [] };
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
function readOverrides(projectRoot) {
|
|
@@ -51,6 +60,7 @@ function readOverrides(projectRoot) {
|
|
|
51
60
|
return {
|
|
52
61
|
version: data.version || 1,
|
|
53
62
|
disabledHandlers: Array.isArray(data.disabledHandlers) ? data.disabledHandlers : [],
|
|
63
|
+
disabledReminders: Array.isArray(data.disabledReminders) ? data.disabledReminders : [],
|
|
54
64
|
};
|
|
55
65
|
}
|
|
56
66
|
|
|
@@ -128,10 +138,13 @@ function restoreHandlerInSettings(settings, event, matcher, spec) {
|
|
|
128
138
|
if (!already) rule.hooks.push(spec);
|
|
129
139
|
}
|
|
130
140
|
|
|
131
|
-
// Stateful toggle used by the panel.
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
141
|
+
// Stateful toggle used by the panel. Only edits overrides.json — settings.json
|
|
142
|
+
// is never mutated. The wrapper (.claude/hooks/run-with-override.js) reads
|
|
143
|
+
// overrides on every invocation, so disable/enable takes effect on the next
|
|
144
|
+
// hook fire (no Claude Code restart required).
|
|
145
|
+
//
|
|
146
|
+
// Settings.json is also consulted to verify the handler exists at all
|
|
147
|
+
// (refuses to disable a phantom handler), but it is never written.
|
|
135
148
|
function toggleHookOverride(projectRoot, { event, matcher = '', handler, disable }) {
|
|
136
149
|
const settingsPath = path.join(projectRoot, SETTINGS_REL);
|
|
137
150
|
const settings = readJsonSafe(settingsPath) || { hooks: {} };
|
|
@@ -143,12 +156,11 @@ function toggleHookOverride(projectRoot, { event, matcher = '', handler, disable
|
|
|
143
156
|
(d) => d.event === event && (d.matcher || '') === matcherKey && d.handler === handler,
|
|
144
157
|
);
|
|
145
158
|
if (already) return { state: 'noop', reason: 'already-disabled' };
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
159
|
+
if (!findHandlerInSettings(settings, event, matcherKey, handler)) {
|
|
160
|
+
return { state: 'error', reason: 'handler-not-found-in-settings' };
|
|
161
|
+
}
|
|
162
|
+
overrides.disabledHandlers.push({ event, matcher: matcherKey, handler });
|
|
149
163
|
writeOverrides(projectRoot, overrides);
|
|
150
|
-
const next = applyHookOverrides(settings, overrides);
|
|
151
|
-
fs.writeFileSync(settingsPath, JSON.stringify(next, null, 2) + '\n');
|
|
152
164
|
return { state: 'disabled' };
|
|
153
165
|
}
|
|
154
166
|
|
|
@@ -156,10 +168,27 @@ function toggleHookOverride(projectRoot, { event, matcher = '', handler, disable
|
|
|
156
168
|
(d) => d.event === event && (d.matcher || '') === matcherKey && d.handler === handler,
|
|
157
169
|
);
|
|
158
170
|
if (idx === -1) return { state: 'noop', reason: 'not-disabled' };
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
171
|
+
overrides.disabledHandlers.splice(idx, 1);
|
|
172
|
+
writeOverrides(projectRoot, overrides);
|
|
173
|
+
return { state: 'enabled' };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Toggle a single reminder by id. Same persistence file, same hot-reload
|
|
177
|
+
// semantics — `reload-reminder.js` re-reads overrides on every invocation.
|
|
178
|
+
function toggleReminderOverride(projectRoot, { id, disable }) {
|
|
179
|
+
if (typeof id !== 'string' || !id.trim()) return { state: 'error', reason: 'id-required' };
|
|
180
|
+
const overrides = readOverrides(projectRoot);
|
|
181
|
+
const list = overrides.disabledReminders;
|
|
182
|
+
const idx = list.indexOf(id);
|
|
183
|
+
|
|
184
|
+
if (disable) {
|
|
185
|
+
if (idx !== -1) return { state: 'noop', reason: 'already-disabled' };
|
|
186
|
+
list.push(id);
|
|
187
|
+
writeOverrides(projectRoot, overrides);
|
|
188
|
+
return { state: 'disabled' };
|
|
189
|
+
}
|
|
190
|
+
if (idx === -1) return { state: 'noop', reason: 'not-disabled' };
|
|
191
|
+
list.splice(idx, 1);
|
|
163
192
|
writeOverrides(projectRoot, overrides);
|
|
164
193
|
return { state: 'enabled' };
|
|
165
194
|
}
|
|
@@ -172,6 +201,7 @@ module.exports = {
|
|
|
172
201
|
readOverrides,
|
|
173
202
|
writeOverrides,
|
|
174
203
|
toggleHookOverride,
|
|
204
|
+
toggleReminderOverride,
|
|
175
205
|
normalizeHandlerPath,
|
|
176
206
|
matchesOverride,
|
|
177
207
|
};
|
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
|
@@ -24,9 +24,26 @@ const {
|
|
|
24
24
|
const {
|
|
25
25
|
readOverrides,
|
|
26
26
|
toggleHookOverride,
|
|
27
|
+
toggleReminderOverride,
|
|
27
28
|
normalizeHandlerPath,
|
|
28
29
|
} = require('./hook-overrides.js');
|
|
29
30
|
|
|
31
|
+
// Lazy-load the reminder catalog from the template hook helper. The path
|
|
32
|
+
// resolves the same way at install time and from the template repo.
|
|
33
|
+
function loadReminderHelpers(cwd) {
|
|
34
|
+
const candidates = [
|
|
35
|
+
path.join(cwd, '.claude', 'hooks', 'shared', 'reload-reminder.js'),
|
|
36
|
+
];
|
|
37
|
+
for (const c of candidates) {
|
|
38
|
+
if (fs.existsSync(c)) {
|
|
39
|
+
// Bypass require cache so updates to the template are picked up.
|
|
40
|
+
delete require.cache[require.resolve(c)];
|
|
41
|
+
return require(c);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
const IDLE_SHUTDOWN_MS = 30 * 60 * 1000; // 30 min
|
|
31
48
|
|
|
32
49
|
// ─── data collectors ─────────────────────────────────────────────
|
|
@@ -96,14 +113,18 @@ function getHooksInfo(cwd) {
|
|
|
96
113
|
disabledIndex.set(`${d.event}|${d.matcher || ''}|${d.handler}`, d);
|
|
97
114
|
}
|
|
98
115
|
|
|
116
|
+
// settings.json lists every hook (with each command wrapped through
|
|
117
|
+
// run-with-override.js). The wrapper checks overrides at invocation time,
|
|
118
|
+
// so the panel just annotates each handler with its current disabled
|
|
119
|
+
// state from the overrides file.
|
|
99
120
|
const hooks = settings.hooks || {};
|
|
100
|
-
const
|
|
121
|
+
const events = [];
|
|
101
122
|
let totalHandlers = 0;
|
|
102
123
|
let warnings = 0;
|
|
103
124
|
let disabledCount = 0;
|
|
104
125
|
|
|
105
126
|
for (const [event, rules] of Object.entries(hooks)) {
|
|
106
|
-
|
|
127
|
+
const handlers = [];
|
|
107
128
|
for (const rule of rules) {
|
|
108
129
|
const matcher = rule.matcher || '';
|
|
109
130
|
for (const h of rule.hooks || []) {
|
|
@@ -111,44 +132,28 @@ function getHooksInfo(cwd) {
|
|
|
111
132
|
const handlerPath = normalizeHandlerPath(cmd) || (cmd ? cmd : '');
|
|
112
133
|
const isEmpty = !handlerPath;
|
|
113
134
|
if (isEmpty) warnings++;
|
|
114
|
-
|
|
135
|
+
const key = `${event}|${matcher}|${handlerPath}`;
|
|
136
|
+
const isDisabled = disabledIndex.has(key);
|
|
137
|
+
if (isDisabled) disabledCount++;
|
|
138
|
+
handlers.push({
|
|
115
139
|
handler: handlerPath || '(empty command)',
|
|
116
|
-
matcher
|
|
140
|
+
matcher,
|
|
117
141
|
matcherDisplay: matcher || '(any)',
|
|
118
142
|
empty: isEmpty,
|
|
119
|
-
disabled:
|
|
143
|
+
disabled: isDisabled,
|
|
120
144
|
rawCommand: cmd,
|
|
121
145
|
});
|
|
122
146
|
totalHandlers++;
|
|
123
147
|
}
|
|
124
148
|
}
|
|
149
|
+
events.push({ event, count: handlers.length, handlers });
|
|
125
150
|
}
|
|
126
151
|
|
|
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
152
|
return {
|
|
148
153
|
settingsFound: true,
|
|
149
154
|
events,
|
|
150
|
-
totalHandlers
|
|
151
|
-
activeHandlers: totalHandlers,
|
|
155
|
+
totalHandlers,
|
|
156
|
+
activeHandlers: totalHandlers - disabledCount,
|
|
152
157
|
disabledHandlers: disabledCount,
|
|
153
158
|
warnings,
|
|
154
159
|
};
|
|
@@ -239,6 +244,29 @@ function getActiveRunInfo(cwd) {
|
|
|
239
244
|
};
|
|
240
245
|
}
|
|
241
246
|
|
|
247
|
+
function getRemindersInfo(cwd) {
|
|
248
|
+
const helpers = loadReminderHelpers(cwd);
|
|
249
|
+
if (!helpers || typeof helpers.listReminderCatalog !== 'function') {
|
|
250
|
+
return { available: false, reminders: [], disabledCount: 0 };
|
|
251
|
+
}
|
|
252
|
+
const catalog = helpers.listReminderCatalog();
|
|
253
|
+
const overrides = readOverrides(cwd);
|
|
254
|
+
const disabledSet = new Set(overrides.disabledReminders || []);
|
|
255
|
+
const reminders = catalog.map((r) => ({
|
|
256
|
+
id: r.id,
|
|
257
|
+
label: r.label,
|
|
258
|
+
description: r.description,
|
|
259
|
+
slot: r.slot,
|
|
260
|
+
disabled: disabledSet.has(r.id),
|
|
261
|
+
}));
|
|
262
|
+
return {
|
|
263
|
+
available: true,
|
|
264
|
+
reminders,
|
|
265
|
+
disabledCount: reminders.filter((r) => r.disabled).length,
|
|
266
|
+
activeCount: reminders.filter((r) => !r.disabled).length,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
242
270
|
function getDoctorInfo(cwd) {
|
|
243
271
|
const checks = [checkCdpPortMismatch(cwd), checkStaleLockfiles(cwd)];
|
|
244
272
|
const issues = checks.filter((c) => c.severity !== 'ok' && c.severity !== 'info');
|
|
@@ -259,6 +287,7 @@ function collectStatus(cwd) {
|
|
|
259
287
|
mcp: getMcpInfo(cwd),
|
|
260
288
|
setupContext: getSetupContextInfo(cwd),
|
|
261
289
|
activeRun: getActiveRunInfo(cwd),
|
|
290
|
+
reminders: getRemindersInfo(cwd),
|
|
262
291
|
doctor: getDoctorInfo(cwd),
|
|
263
292
|
};
|
|
264
293
|
}
|
|
@@ -350,6 +379,12 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
|
|
|
350
379
|
.toggle input { accent-color: var(--accent); }
|
|
351
380
|
.disabled-row { opacity: 0.55; }
|
|
352
381
|
.disabled-row .matcher { font-style: italic; }
|
|
382
|
+
.reminder-row { padding: 10px 0; border-bottom: 1px solid var(--border); }
|
|
383
|
+
.reminder-row:last-child { border-bottom: 0; }
|
|
384
|
+
.reminder-row .reminder-label { font-size: 13px; }
|
|
385
|
+
.reminder-row .reminder-desc { font-size: 12px; margin-left: 22px; margin-top: 2px; }
|
|
386
|
+
.reminder-row .reminder-id { font-size: 11px; margin-left: 22px; margin-top: 2px; }
|
|
387
|
+
.reminder-row code { background: var(--panel-2); padding: 1px 5px; border-radius: 3px; font-size: 11px; }
|
|
353
388
|
.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
389
|
.toast.err { border-color: var(--err); }
|
|
355
390
|
.toast.ok { border-color: var(--ok); }
|
|
@@ -375,6 +410,7 @@ const SECTIONS = [
|
|
|
375
410
|
{ id: 'overview', label: 'Overview' },
|
|
376
411
|
{ id: 'claudeMd', label: 'CLAUDE.md' },
|
|
377
412
|
{ id: 'hooks', label: 'Hooks' },
|
|
413
|
+
{ id: 'reminders', label: 'Reminders' },
|
|
378
414
|
{ id: 'mcp', label: 'MCP & observer' },
|
|
379
415
|
{ id: 'setupContext', label: 'Setup context' },
|
|
380
416
|
{ id: 'activeRun', label: 'Active run' },
|
|
@@ -385,12 +421,66 @@ let state = null;
|
|
|
385
421
|
let active = 'overview';
|
|
386
422
|
let timer = null;
|
|
387
423
|
|
|
424
|
+
// Preserve which <details> were open across re-renders (auto-refresh would
|
|
425
|
+
// otherwise collapse them every 5s).
|
|
426
|
+
function captureOpenDetails() {
|
|
427
|
+
const ids = new Set();
|
|
428
|
+
document.querySelectorAll('details[open][data-detail-id]').forEach(d => ids.add(d.dataset.detailId));
|
|
429
|
+
// Also capture anonymous details by event name (hooks accordion).
|
|
430
|
+
document.querySelectorAll('details[open][data-event]').forEach(d => ids.add('event:' + d.dataset.event));
|
|
431
|
+
return ids;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function restoreOpenDetails(ids) {
|
|
435
|
+
if (!ids.size) return;
|
|
436
|
+
document.querySelectorAll('details[data-detail-id]').forEach(d => {
|
|
437
|
+
if (ids.has(d.dataset.detailId)) d.open = true;
|
|
438
|
+
});
|
|
439
|
+
document.querySelectorAll('details[data-event]').forEach(d => {
|
|
440
|
+
if (ids.has('event:' + d.dataset.event)) d.open = true;
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Cache fetched CLAUDE.md content so re-renders during auto-refresh don't
|
|
445
|
+
// drop the preview the user already loaded.
|
|
446
|
+
let claudeMdCache = null;
|
|
447
|
+
|
|
448
|
+
async function loadClaudeMdPreview() {
|
|
449
|
+
const target = document.getElementById('claude-md-preview');
|
|
450
|
+
if (!target) return;
|
|
451
|
+
if (target.dataset.loaded === '1') return;
|
|
452
|
+
target.dataset.loaded = '1';
|
|
453
|
+
target.textContent = 'Loading…';
|
|
454
|
+
try {
|
|
455
|
+
if (!claudeMdCache) {
|
|
456
|
+
const r = await fetch('/api/claude-md');
|
|
457
|
+
claudeMdCache = await r.text();
|
|
458
|
+
}
|
|
459
|
+
const pre = document.createElement('pre');
|
|
460
|
+
pre.textContent = claudeMdCache;
|
|
461
|
+
target.innerHTML = '';
|
|
462
|
+
target.appendChild(pre);
|
|
463
|
+
} catch (e) {
|
|
464
|
+
target.textContent = 'Failed to load: ' + e.message;
|
|
465
|
+
target.dataset.loaded = '';
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
388
469
|
async function refresh() {
|
|
389
|
-
const
|
|
390
|
-
|
|
470
|
+
const open = captureOpenDetails();
|
|
471
|
+
try {
|
|
472
|
+
const r = await fetch('/api/status');
|
|
473
|
+
state = await r.json();
|
|
474
|
+
} catch (e) {
|
|
475
|
+
showToast && showToast('Refresh failed: ' + e.message, 'err');
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
391
478
|
renderHeader();
|
|
392
479
|
renderNav();
|
|
393
480
|
renderContent();
|
|
481
|
+
restoreOpenDetails(open);
|
|
482
|
+
// If preview was visible/open, ensure it stays loaded after re-render.
|
|
483
|
+
if (open.has('claude-md-preview')) loadClaudeMdPreview();
|
|
394
484
|
}
|
|
395
485
|
|
|
396
486
|
function renderHeader() {
|
|
@@ -404,6 +494,7 @@ function severityFor(id) {
|
|
|
404
494
|
case 'overview': return s.doctor.issueCount > 0 ? 'warn' : 'ok';
|
|
405
495
|
case 'claudeMd': return !s.claudeMd.exists ? 'err' : (s.claudeMd.optOut ? 'info' : 'ok');
|
|
406
496
|
case 'hooks': return s.hooks.warnings > 0 ? 'warn' : 'ok';
|
|
497
|
+
case 'reminders': return s.reminders && s.reminders.disabledCount > 0 ? 'info' : 'ok';
|
|
407
498
|
case 'mcp': return !s.mcp.configFound ? 'info' : (s.mcp.playwright.match && s.mcp.staleLockfiles.length === 0 ? 'ok' : 'warn');
|
|
408
499
|
case 'setupContext': return !s.setupContext.exists ? 'err' : (s.setupContext.toolingComplete ? 'ok' : 'warn');
|
|
409
500
|
case 'activeRun': return s.activeRun.active ? 'info' : 'info';
|
|
@@ -447,6 +538,7 @@ function renderContent() {
|
|
|
447
538
|
overview: renderOverview,
|
|
448
539
|
claudeMd: renderClaudeMd,
|
|
449
540
|
hooks: renderHooks,
|
|
541
|
+
reminders: renderReminders,
|
|
450
542
|
mcp: renderMcp,
|
|
451
543
|
setupContext: renderSetup,
|
|
452
544
|
activeRun: renderRun,
|
|
@@ -468,6 +560,9 @@ function renderOverview() {
|
|
|
468
560
|
const sc = !s.setupContext.exists ? 'missing' : (s.setupContext.toolingComplete ? 'complete' : 'incomplete (' + s.setupContext.missingTooling.join(', ') + ')');
|
|
469
561
|
const ar = s.activeRun.active ? s.activeRun.runId : 'none';
|
|
470
562
|
const dc = s.doctor.issueCount === 0 ? '0 issues' : s.doctor.issueCount + ' issue(s) — run \`claudeflow doctor\`';
|
|
563
|
+
const rm = s.reminders && s.reminders.available
|
|
564
|
+
? \`\${s.reminders.activeCount} active · \${s.reminders.disabledCount} disabled\`
|
|
565
|
+
: 'unavailable';
|
|
471
566
|
return \`<h2>Overview</h2>
|
|
472
567
|
<p class="sub">Snapshot at \${new Date(s.timestamp).toLocaleTimeString()}</p>
|
|
473
568
|
<div class="card">
|
|
@@ -477,6 +572,7 @@ function renderOverview() {
|
|
|
477
572
|
\${row('MCP & observer', mc, { kind: !s.mcp.configFound ? 'info' : (s.mcp.playwright.match ? 'ok' : 'warn'), text: !s.mcp.configFound ? '·' : (s.mcp.playwright.match ? '✓' : '!') })}
|
|
478
573
|
\${row('Setup context', sc, { kind: s.setupContext.exists ? (s.setupContext.toolingComplete ? 'ok' : 'warn') : 'err', text: s.setupContext.exists ? (s.setupContext.toolingComplete ? '✓' : '!') : '✗' })}
|
|
479
574
|
\${row('Active run', ar, { kind: 'info', text: s.activeRun.active ? '·' : '·' })}
|
|
575
|
+
\${row('Reminders', rm, { kind: s.reminders && s.reminders.available ? (s.reminders.disabledCount ? 'info' : 'ok') : 'err', text: s.reminders && s.reminders.disabledCount ? '·' : '✓' })}
|
|
480
576
|
\${row('Doctor', dc, { kind: s.doctor.issueCount === 0 ? 'ok' : 'warn', text: s.doctor.issueCount === 0 ? '✓' : '!' })}
|
|
481
577
|
</div>\`;
|
|
482
578
|
}
|
|
@@ -494,12 +590,7 @@ function renderClaudeMd() {
|
|
|
494
590
|
\${row('Default rules block', blk, { kind: c.block.present ? 'ok' : 'info', text: c.block.present ? '✓' : '·' })}
|
|
495
591
|
\${row('Opt-out marker', c.optOut ? 'present (block disabled)' : 'absent', { kind: c.optOut ? 'info' : 'ok', text: c.optOut ? '·' : '✓' })}
|
|
496
592
|
</div>
|
|
497
|
-
<details><summary>Preview content</summary><div id="claude-md-preview">
|
|
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 => ({'&':'&','<':'<','>':'>'}[c])) + '</pre>';
|
|
501
|
-
});
|
|
502
|
-
</\${'script'}>
|
|
593
|
+
<details data-detail-id="claude-md-preview"><summary>Preview content</summary><div id="claude-md-preview" class="muted">Click to load…</div></details>
|
|
503
594
|
\`;
|
|
504
595
|
}
|
|
505
596
|
|
|
@@ -519,7 +610,7 @@ function renderHooks() {
|
|
|
519
610
|
<span class="matcher">\${escapeHtml(hd.matcherDisplay)}</span>
|
|
520
611
|
</div>\`;
|
|
521
612
|
}).join('');
|
|
522
|
-
return \`<details><summary>\${escapeHtml(ev.event)} <span class="muted">(\${ev.count})</span></summary>\${handlers}</details>\`;
|
|
613
|
+
return \`<details data-event="\${escapeHtml(ev.event)}"><summary>\${escapeHtml(ev.event)} <span class="muted">(\${ev.count})</span></summary>\${handlers}</details>\`;
|
|
523
614
|
}).join('');
|
|
524
615
|
return \`<h2>Hooks</h2>
|
|
525
616
|
<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>
|
|
@@ -527,6 +618,35 @@ function renderHooks() {
|
|
|
527
618
|
\${items}\`;
|
|
528
619
|
}
|
|
529
620
|
|
|
621
|
+
function renderReminders() {
|
|
622
|
+
const r = state.reminders;
|
|
623
|
+
if (!r || !r.available) return '<h2>Reminders</h2><p class="error">Reminder catalog unavailable (template helper not found).</p>';
|
|
624
|
+
const slotLabel = { reads: 'Reads / anchors', rules: 'Rules', 'claude-md': 'CLAUDE.md content' };
|
|
625
|
+
const grouped = {};
|
|
626
|
+
for (const item of r.reminders) {
|
|
627
|
+
if (!grouped[item.slot]) grouped[item.slot] = [];
|
|
628
|
+
grouped[item.slot].push(item);
|
|
629
|
+
}
|
|
630
|
+
const sections = Object.entries(grouped).map(([slot, items]) => {
|
|
631
|
+
const rows = items.map(item => {
|
|
632
|
+
const checked = item.disabled ? '' : 'checked';
|
|
633
|
+
const dataset = \`data-id="\${escapeHtml(item.id)}"\`;
|
|
634
|
+
const status = item.disabled ? '<span class="badge info">disabled</span>' : '';
|
|
635
|
+
const cls = item.disabled ? 'reminder-row disabled-row' : 'reminder-row';
|
|
636
|
+
return \`<div class="\${cls}">
|
|
637
|
+
<label class="toggle"><input type="checkbox" class="reminder-toggle" \${dataset} \${checked} />
|
|
638
|
+
<span class="reminder-label"><strong>\${escapeHtml(item.label)}</strong> \${status}</span></label>
|
|
639
|
+
<div class="reminder-desc muted">\${escapeHtml(item.description)}</div>
|
|
640
|
+
<div class="reminder-id muted">id: <code>\${escapeHtml(item.id)}</code></div>
|
|
641
|
+
</div>\`;
|
|
642
|
+
}).join('');
|
|
643
|
+
return \`<details open><summary>\${escapeHtml(slotLabel[slot] || slot)} <span class="muted">(\${items.length})</span></summary>\${rows}</details>\`;
|
|
644
|
+
}).join('');
|
|
645
|
+
return \`<h2>Reminders</h2>
|
|
646
|
+
<p class="sub">\${r.activeCount} active · \${r.disabledCount} disabled. Toggles are read by <code>reload-reminder.js</code> on every hook invocation — disabling takes effect on the next user prompt without reloading Claude Code.</p>
|
|
647
|
+
\${sections}\`;
|
|
648
|
+
}
|
|
649
|
+
|
|
530
650
|
function renderMcp() {
|
|
531
651
|
const m = state.mcp;
|
|
532
652
|
if (!m.configFound) return '<h2>MCP & observer</h2><p class="muted">.mcp.json not found</p>';
|
|
@@ -607,17 +727,52 @@ async function toggleHook(event, matcher, handler, disable) {
|
|
|
607
727
|
await refresh();
|
|
608
728
|
}
|
|
609
729
|
|
|
730
|
+
async function toggleReminder(id, disable) {
|
|
731
|
+
try {
|
|
732
|
+
const r = await fetch('/api/reminders/toggle', {
|
|
733
|
+
method: 'POST',
|
|
734
|
+
headers: { 'Content-Type': 'application/json' },
|
|
735
|
+
body: JSON.stringify({ id, disable }),
|
|
736
|
+
});
|
|
737
|
+
const result = await r.json();
|
|
738
|
+
if (!r.ok || result.state === 'error') {
|
|
739
|
+
showToast('Toggle failed: ' + (result.reason || result.error || 'unknown'), 'err');
|
|
740
|
+
} else if (result.state === 'noop') {
|
|
741
|
+
showToast('Already in target state', 'ok');
|
|
742
|
+
} else {
|
|
743
|
+
showToast(disable ? 'Reminder silenced. Next prompt will skip it.' : 'Reminder restored. Next prompt will include it.', 'ok');
|
|
744
|
+
}
|
|
745
|
+
} catch (e) {
|
|
746
|
+
showToast('Network error: ' + e.message, 'err');
|
|
747
|
+
}
|
|
748
|
+
await refresh();
|
|
749
|
+
}
|
|
750
|
+
|
|
610
751
|
document.addEventListener('change', (e) => {
|
|
611
752
|
const t = e.target;
|
|
612
|
-
if (t
|
|
753
|
+
if (!t || !t.classList) return;
|
|
754
|
+
if (t.classList.contains('hook-toggle')) {
|
|
613
755
|
const event = t.dataset.event;
|
|
614
756
|
const matcher = t.dataset.matcher;
|
|
615
757
|
const handler = t.dataset.handler;
|
|
616
758
|
const disable = !t.checked;
|
|
617
759
|
toggleHook(event, matcher, handler, disable);
|
|
760
|
+
} else if (t.classList.contains('reminder-toggle')) {
|
|
761
|
+
const id = t.dataset.id;
|
|
762
|
+
const disable = !t.checked;
|
|
763
|
+
toggleReminder(id, disable);
|
|
618
764
|
}
|
|
619
765
|
});
|
|
620
766
|
|
|
767
|
+
// Lazy-load CLAUDE.md preview when the user opens the details element.
|
|
768
|
+
// The "toggle" event does not bubble by default — capture phase keeps delegation working.
|
|
769
|
+
document.addEventListener('toggle', (e) => {
|
|
770
|
+
const t = e.target;
|
|
771
|
+
if (t && t.tagName === 'DETAILS' && t.dataset.detailId === 'claude-md-preview' && t.open) {
|
|
772
|
+
loadClaudeMdPreview();
|
|
773
|
+
}
|
|
774
|
+
}, true);
|
|
775
|
+
|
|
621
776
|
document.getElementById('refresh').onclick = refresh;
|
|
622
777
|
document.getElementById('auto').onchange = (e) => {
|
|
623
778
|
if (timer) clearInterval(timer);
|
|
@@ -687,6 +842,20 @@ function handler(cwd) {
|
|
|
687
842
|
return send(status, JSON.stringify(result), 'application/json');
|
|
688
843
|
}
|
|
689
844
|
|
|
845
|
+
if (req.method === 'POST' && route === '/api/reminders/toggle') {
|
|
846
|
+
const raw = await readRequestBody(req);
|
|
847
|
+
let payload;
|
|
848
|
+
try { payload = JSON.parse(raw); }
|
|
849
|
+
catch { return send(400, JSON.stringify({ error: 'invalid json' }), 'application/json'); }
|
|
850
|
+
const { id, disable } = payload;
|
|
851
|
+
if (typeof id !== 'string' || typeof disable !== 'boolean') {
|
|
852
|
+
return send(400, JSON.stringify({ error: 'id, disable required' }), 'application/json');
|
|
853
|
+
}
|
|
854
|
+
const result = toggleReminderOverride(cwd, { id, disable });
|
|
855
|
+
const status = result.state === 'error' ? 500 : 200;
|
|
856
|
+
return send(status, JSON.stringify(result), 'application/json');
|
|
857
|
+
}
|
|
858
|
+
|
|
690
859
|
send(404, JSON.stringify({ error: 'not found' }), 'application/json');
|
|
691
860
|
} catch (err) {
|
|
692
861
|
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.24",
|
|
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"
|