@axiomatic-labs/claudeflow 2.13.23 → 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.
@@ -51,7 +51,7 @@ function normalizeHandlerPath(command) {
51
51
  }
52
52
 
53
53
  function defaultOverrides() {
54
- return { version: 1, disabledHandlers: [] };
54
+ return { version: 1, disabledHandlers: [], disabledReminders: [] };
55
55
  }
56
56
 
57
57
  function readOverrides(projectRoot) {
@@ -60,6 +60,7 @@ function readOverrides(projectRoot) {
60
60
  return {
61
61
  version: data.version || 1,
62
62
  disabledHandlers: Array.isArray(data.disabledHandlers) ? data.disabledHandlers : [],
63
+ disabledReminders: Array.isArray(data.disabledReminders) ? data.disabledReminders : [],
63
64
  };
64
65
  }
65
66
 
@@ -172,6 +173,26 @@ function toggleHookOverride(projectRoot, { event, matcher = '', handler, disable
172
173
  return { state: 'enabled' };
173
174
  }
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);
192
+ writeOverrides(projectRoot, overrides);
193
+ return { state: 'enabled' };
194
+ }
195
+
175
196
  module.exports = {
176
197
  OVERRIDES_REL,
177
198
  SETTINGS_REL,
@@ -180,6 +201,7 @@ module.exports = {
180
201
  readOverrides,
181
202
  writeOverrides,
182
203
  toggleHookOverride,
204
+ toggleReminderOverride,
183
205
  normalizeHandlerPath,
184
206
  matchesOverride,
185
207
  };
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 ─────────────────────────────────────────────
@@ -227,6 +244,29 @@ function getActiveRunInfo(cwd) {
227
244
  };
228
245
  }
229
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
+
230
270
  function getDoctorInfo(cwd) {
231
271
  const checks = [checkCdpPortMismatch(cwd), checkStaleLockfiles(cwd)];
232
272
  const issues = checks.filter((c) => c.severity !== 'ok' && c.severity !== 'info');
@@ -247,6 +287,7 @@ function collectStatus(cwd) {
247
287
  mcp: getMcpInfo(cwd),
248
288
  setupContext: getSetupContextInfo(cwd),
249
289
  activeRun: getActiveRunInfo(cwd),
290
+ reminders: getRemindersInfo(cwd),
250
291
  doctor: getDoctorInfo(cwd),
251
292
  };
252
293
  }
@@ -338,6 +379,12 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
338
379
  .toggle input { accent-color: var(--accent); }
339
380
  .disabled-row { opacity: 0.55; }
340
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; }
341
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); }
342
389
  .toast.err { border-color: var(--err); }
343
390
  .toast.ok { border-color: var(--ok); }
@@ -363,6 +410,7 @@ const SECTIONS = [
363
410
  { id: 'overview', label: 'Overview' },
364
411
  { id: 'claudeMd', label: 'CLAUDE.md' },
365
412
  { id: 'hooks', label: 'Hooks' },
413
+ { id: 'reminders', label: 'Reminders' },
366
414
  { id: 'mcp', label: 'MCP & observer' },
367
415
  { id: 'setupContext', label: 'Setup context' },
368
416
  { id: 'activeRun', label: 'Active run' },
@@ -446,6 +494,7 @@ function severityFor(id) {
446
494
  case 'overview': return s.doctor.issueCount > 0 ? 'warn' : 'ok';
447
495
  case 'claudeMd': return !s.claudeMd.exists ? 'err' : (s.claudeMd.optOut ? 'info' : 'ok');
448
496
  case 'hooks': return s.hooks.warnings > 0 ? 'warn' : 'ok';
497
+ case 'reminders': return s.reminders && s.reminders.disabledCount > 0 ? 'info' : 'ok';
449
498
  case 'mcp': return !s.mcp.configFound ? 'info' : (s.mcp.playwright.match && s.mcp.staleLockfiles.length === 0 ? 'ok' : 'warn');
450
499
  case 'setupContext': return !s.setupContext.exists ? 'err' : (s.setupContext.toolingComplete ? 'ok' : 'warn');
451
500
  case 'activeRun': return s.activeRun.active ? 'info' : 'info';
@@ -489,6 +538,7 @@ function renderContent() {
489
538
  overview: renderOverview,
490
539
  claudeMd: renderClaudeMd,
491
540
  hooks: renderHooks,
541
+ reminders: renderReminders,
492
542
  mcp: renderMcp,
493
543
  setupContext: renderSetup,
494
544
  activeRun: renderRun,
@@ -510,6 +560,9 @@ function renderOverview() {
510
560
  const sc = !s.setupContext.exists ? 'missing' : (s.setupContext.toolingComplete ? 'complete' : 'incomplete (' + s.setupContext.missingTooling.join(', ') + ')');
511
561
  const ar = s.activeRun.active ? s.activeRun.runId : 'none';
512
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';
513
566
  return \`<h2>Overview</h2>
514
567
  <p class="sub">Snapshot at \${new Date(s.timestamp).toLocaleTimeString()}</p>
515
568
  <div class="card">
@@ -519,6 +572,7 @@ function renderOverview() {
519
572
  \${row('MCP & observer', mc, { kind: !s.mcp.configFound ? 'info' : (s.mcp.playwright.match ? 'ok' : 'warn'), text: !s.mcp.configFound ? '·' : (s.mcp.playwright.match ? '✓' : '!') })}
520
573
  \${row('Setup context', sc, { kind: s.setupContext.exists ? (s.setupContext.toolingComplete ? 'ok' : 'warn') : 'err', text: s.setupContext.exists ? (s.setupContext.toolingComplete ? '✓' : '!') : '✗' })}
521
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 ? '·' : '✓' })}
522
576
  \${row('Doctor', dc, { kind: s.doctor.issueCount === 0 ? 'ok' : 'warn', text: s.doctor.issueCount === 0 ? '✓' : '!' })}
523
577
  </div>\`;
524
578
  }
@@ -564,6 +618,35 @@ function renderHooks() {
564
618
  \${items}\`;
565
619
  }
566
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
+
567
650
  function renderMcp() {
568
651
  const m = state.mcp;
569
652
  if (!m.configFound) return '<h2>MCP & observer</h2><p class="muted">.mcp.json not found</p>';
@@ -644,14 +727,40 @@ async function toggleHook(event, matcher, handler, disable) {
644
727
  await refresh();
645
728
  }
646
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
+
647
751
  document.addEventListener('change', (e) => {
648
752
  const t = e.target;
649
- if (t && t.classList && t.classList.contains('hook-toggle')) {
753
+ if (!t || !t.classList) return;
754
+ if (t.classList.contains('hook-toggle')) {
650
755
  const event = t.dataset.event;
651
756
  const matcher = t.dataset.matcher;
652
757
  const handler = t.dataset.handler;
653
758
  const disable = !t.checked;
654
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);
655
764
  }
656
765
  });
657
766
 
@@ -733,6 +842,20 @@ function handler(cwd) {
733
842
  return send(status, JSON.stringify(result), 'application/json');
734
843
  }
735
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
+
736
859
  send(404, JSON.stringify({ error: 'not found' }), 'application/json');
737
860
  } catch (err) {
738
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.23",
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"