claude-session-insights 0.4.0 → 0.4.1

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/README.md CHANGED
@@ -10,8 +10,8 @@ Think "Spotify Wrapped" for your Claude Code usage — scores, summaries, badges
10
10
  - **Overall Summary** — natural-language assessment of your prompting habits with specific recommendations
11
11
  - **Per-Session Summary** — each session gets a plain-English breakdown of what happened, what went well, and what could improve
12
12
  - **Session Drill-down** — click any session to see the full conversation timeline with per-turn token counts, costs, tool calls, and prompt previews
13
- - **Workflow Optimizer** — analyzes your session patterns and recommends Claude Code setup improvements: skills to create, CLAUDE.md files to write, and subagents to configure. Optional AI generation produces ready-to-copy artifact content (full CLAUDE.md text, skill prompt bodies, agent configs)
14
- - **AI Insights** — on-demand deeper analysis powered by the Claude CLI, with model picker (Sonnet, Opus, Haiku) and streaming output
13
+ - **Session Intelligence** — unified card combining a rule-based overview (patterns, model choices, token habits) with on-demand **AI Analyze** powered by Claude, with model picker (Sonnet, Opus, Haiku) and streaming output
14
+ - **Workflow Optimizer** — analyzes session patterns and recommends Claude Code setup improvements: skills, CLAUDE.md files, agents, and plugins. Pattern-based suggestions load instantly; **AI Optimize** generates AI-enhanced suggestions (defaults to Opus). Optional AI artifact content generation produces ready-to-copy CLAUDE.md files, skill prompts, and agent configs
15
15
  - **Heaviest Sessions** — top sessions ranked by cost for quick identification of expensive outliers
16
16
  - **Daily Score Chart** — trend visualization of your efficiency score, session count, tokens, and cost over time
17
17
  - **Badges** — positive achievements (Surgical Prompter, Cache Whisperer, etc.) and negative anti-patterns (Opus Addict, Token Furnace, etc.)
@@ -79,27 +79,33 @@ The dashboard generates rule-based summaries at two levels:
79
79
 
80
80
  ## Workflow Optimizer
81
81
 
82
- Closes the loop from "how am I doing?" to "here's what to build to do better." The optimizer runs two phases:
82
+ Closes the loop from "how am I doing?" to "here's what to build to do better."
83
83
 
84
- **Phase 1 — Rule-based suggestions (instant):** Detects patterns from your badges, tool usage, session titles, and project history to recommend:
84
+ **Pattern-based suggestions (instant):** Detects patterns from your badges, tool usage, session titles, and project history to recommend:
85
85
 
86
86
  - **Skills** — slash commands to create in `~/.claude/skills/`. Example: if you have the Vague Commander badge, it suggests a `/spec` skill to help you write thorough task specs before Claude starts working
87
87
  - **CLAUDE.md** — project-scoped or global config files to reduce repeated explanations. Triggered by projects with 5+ sessions, the Context Hoarder badge, Opus overuse, etc.
88
88
  - **Agents** — subagent configurations for offloading exploration or repetitive task types
89
89
  - **Plugins** — MCP servers only where CLI tools have a genuine capability gap (e.g. Playwright for browser tasks, not tools with good CLI equivalents that would just add context overhead)
90
90
 
91
- **Phase 2 — AI content generation (optional):** Click "Generate artifact content with AI" to stream ready-to-copy content for each suggestion the actual CLAUDE.md file, the real skill prompt body, agent configuration not just a description of what to build.
91
+ **AI Optimize (optional):** Click "AI Optimize" to stream AI-generated suggestions that either replace or supplement the pattern-based ones — defaults to Opus. AI suggestions appear at the top of the card when generated and persist across refreshes until cleared.
92
92
 
93
- ## AI Insights
93
+ **Artifact content generation (optional):** Click "Generate artifact content with AI" on any suggestion card to stream ready-to-copy content — the actual CLAUDE.md file, the real skill prompt body, agent configuration — not just a description of what to build.
94
94
 
95
- Click "Generate AI Insights" to run a deeper analysis using the Claude CLI. This streams a response via SSE that covers:
95
+ ## Session Intelligence
96
+
97
+ The Session Intelligence card combines a rule-based overview of your prompting habits with on-demand AI analysis.
98
+
99
+ The static overview surfaces patterns across your sessions: cache efficiency, model selection, prompt specificity, and cost distribution.
100
+
101
+ Click **AI Analyze** to run a deeper analysis using the Claude CLI. This streams a response via SSE that covers:
96
102
 
97
103
  - **Key Patterns** — non-obvious trends the static rules miss (time-of-day patterns, project-specific habits, cost trajectories)
98
104
  - **Biggest Opportunities** — specific workflow changes with quantified potential savings
99
105
  - **What's Working Well** — habits worth keeping
100
106
  - **Standout Session** — the most interesting session and what can be learned from it
101
107
 
102
- You can pick which model to use (Sonnet, Opus, or Haiku) from the model picker. Results are cached for the session. Requires the `claude` CLI to be installed and in your PATH.
108
+ AI analysis appears at the top of the card when generated and persists across refreshes until cleared. You can pick which model to use (Sonnet, Opus, or Haiku) from the model picker. Requires the `claude` CLI to be installed and in your PATH.
103
109
 
104
110
  ## Badges
105
111
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-session-insights",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Analyze Claude Code sessions for efficiency insights, scores, and team metrics",
5
5
  "type": "module",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -163,13 +163,13 @@
163
163
  background: var(--surface3); color: var(--text); border-color: var(--border-hover);
164
164
  }
165
165
 
166
- /* Hero: two-column layout */
167
- .hero { display: flex; gap: 20px; margin-bottom: 28px; align-items: stretch; }
168
- .hero-left { display: flex; flex-direction: column; gap: 14px; width: 340px; flex-shrink: 0; }
169
- .hero-right { flex: 1; min-width: 0; }
166
+ /* Hero: single row — score card fixed-width, badges fill the rest */
167
+ .hero { margin-bottom: 28px; }
168
+ .hero-left { display: flex; flex-direction: row; gap: 14px; align-items: stretch; }
169
+ .hero-left .score-card { width: 230px; flex-shrink: 0; }
170
170
  @media (max-width: 720px) {
171
- .hero { flex-direction: column; }
172
- .hero-left { width: auto; min-width: 0; }
171
+ .hero-left { flex-direction: column; }
172
+ .hero-left .score-card { width: auto; }
173
173
  }
174
174
  .score-card {
175
175
  background: var(--surface); border: 1px solid var(--border);
@@ -320,7 +320,7 @@
320
320
 
321
321
  /* Score breakdown */
322
322
  .score-info-toggle {
323
- background: none; border: none; color: var(--text3); cursor: pointer;
323
+ display: block; background: none; border: none; color: var(--text3); cursor: pointer;
324
324
  font-size: 11px; margin-top: 10px; padding: 0; transition: color 0.15s;
325
325
  }
326
326
  .score-info-toggle:hover { color: var(--accent); }
@@ -526,8 +526,27 @@
526
526
  .ai-insights-card h3 {
527
527
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.8px;
528
528
  color: var(--purple); margin-bottom: 14px; font-weight: 600;
529
- display: flex; align-items: center; gap: 8px;
529
+ display: flex; align-items: center; gap: 8px; justify-content: space-between;
530
+ }
531
+ .card-header-actions {
532
+ display: flex; align-items: center; gap: 8px; flex-shrink: 0;
533
+ }
534
+ .ai-generate-btn-sm {
535
+ background: var(--purple-dim); border: 1px solid rgba(210,168,255,0.2);
536
+ color: var(--purple); padding: 5px 12px; border-radius: 7px;
537
+ cursor: pointer; font-size: 11px; font-weight: 600; transition: all 0.2s;
538
+ text-transform: none; letter-spacing: 0;
539
+ }
540
+ .ai-generate-btn-sm:hover { background: rgba(210,168,255,0.2); border-color: rgba(210,168,255,0.3); }
541
+ .ai-generate-btn-sm:disabled { opacity: 0.5; cursor: not-allowed; }
542
+ .optimizer-generate-btn-sm {
543
+ background: var(--accent-dim); border: 1px solid rgba(37,99,235,0.2);
544
+ color: var(--accent); padding: 5px 12px; border-radius: 7px;
545
+ cursor: pointer; font-size: 11px; font-weight: 600; transition: all 0.2s;
546
+ text-transform: none; letter-spacing: 0;
530
547
  }
548
+ .optimizer-generate-btn-sm:hover { background: rgba(37,99,235,0.15); border-color: rgba(37,99,235,0.3); }
549
+ .optimizer-generate-btn-sm:disabled { opacity: 0.5; cursor: not-allowed; }
531
550
  .ai-insights-content { font-size: 13px; line-height: 1.75; color: var(--text); }
532
551
  .ai-insights-content h2 {
533
552
  font-size: 13px; font-weight: 700; color: var(--text); margin: 16px 0 8px 0;
@@ -541,6 +560,21 @@
541
560
  font-family: var(--mono); font-size: 11px; background: var(--surface2);
542
561
  padding: 1px 5px; border-radius: 3px;
543
562
  }
563
+ /* Session Intelligence — merged static + AI card */
564
+ .session-section-label {
565
+ font-size: 10px; text-transform: uppercase; letter-spacing: 0.7px;
566
+ color: var(--text3); font-weight: 600; margin-bottom: 12px;
567
+ }
568
+ .ai-divider {
569
+ display: flex; align-items: center; gap: 10px;
570
+ margin: 22px 0 18px; color: var(--purple);
571
+ font-size: 10px; text-transform: uppercase; letter-spacing: 0.8px; font-weight: 600;
572
+ }
573
+ .ai-divider::before, .ai-divider::after {
574
+ content: ''; flex: 1; height: 1px;
575
+ background: linear-gradient(90deg, transparent, rgba(210,168,255,0.35), transparent);
576
+ }
577
+ .ai-divider span { white-space: nowrap; }
544
578
  .ai-generate-btn {
545
579
  background: var(--purple-dim); border: 1px solid rgba(210,168,255,0.2);
546
580
  color: var(--purple); padding: 10px 20px; border-radius: 7px;
@@ -569,7 +603,7 @@
569
603
  animation: ai-spin 0.8s linear infinite;
570
604
  }
571
605
  @keyframes ai-spin { to { transform: rotate(360deg); } }
572
- .ai-controls { display: flex; align-items: center; gap: 10px; }
606
+ .ai-controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
573
607
  .ai-model-picker { position: relative; display: inline-block; }
574
608
  .ai-model-pill {
575
609
  display: inline-flex; align-items: center; gap: 5px;
@@ -607,12 +641,22 @@
607
641
  .ai-model-option:hover { background: var(--surface2); }
608
642
  .ai-model-option.active { color: var(--text); font-weight: 600; }
609
643
  .ai-model-option .check { width: 14px; color: var(--purple); font-size: 13px; }
610
- .ai-token-warning {
611
- margin-top: 10px; padding: 8px 12px; border-radius: 6px;
612
- background: rgba(255,200,80,0.07); border: 1px solid rgba(255,200,80,0.2);
613
- font-size: 11px; color: var(--text3); display: flex; align-items: flex-start; gap: 7px;
644
+ [data-warn] { position: relative; }
645
+ [data-warn]:hover::before {
646
+ content: attr(data-warn);
647
+ position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%);
648
+ background: var(--surface); border: 1px solid rgba(255,200,80,0.3);
649
+ color: var(--text2); font-size: 11px; font-weight: 400;
650
+ padding: 7px 10px; border-radius: 6px; pointer-events: none; z-index: 300;
651
+ white-space: normal; max-width: 260px; width: max-content; line-height: 1.5;
652
+ box-shadow: 0 4px 14px rgba(0,0,0,0.15);
653
+ }
654
+ [data-warn]:hover::after {
655
+ content: '';
656
+ position: absolute; bottom: calc(100% + 3px); left: 50%; transform: translateX(-50%);
657
+ border: 5px solid transparent; border-top-color: rgba(255,200,80,0.3);
658
+ pointer-events: none; z-index: 300;
614
659
  }
615
- .ai-token-warning .warn-icon { color: #f5c842; flex-shrink: 0; margin-top: 1px; }
616
660
  .ai-model-label {
617
661
  font-size: 10px; color: var(--text3); font-family: var(--mono);
618
662
  display: flex; align-items: center; gap: 6px;
@@ -645,6 +689,71 @@
645
689
  }
646
690
  .ai-prompt-close:hover { color: var(--text); }
647
691
 
692
+ /* Workflow Optimizer — mode toggle (Static / AI Suggestions) */
693
+ .optimizer-mode-tabs {
694
+ display: flex; gap: 4px; margin-bottom: 16px;
695
+ }
696
+ .optimizer-mode-tab {
697
+ padding: 6px 14px; border-radius: 7px; border: 1px solid var(--border);
698
+ background: var(--surface2); color: var(--text2); font-size: 12px;
699
+ font-weight: 500; cursor: pointer; transition: all 0.15s;
700
+ }
701
+ .optimizer-mode-tab:hover { background: var(--surface3); color: var(--text); }
702
+ .optimizer-mode-tab.active {
703
+ background: var(--accent-dim); border-color: rgba(37,99,235,0.3);
704
+ color: var(--accent); font-weight: 600;
705
+ }
706
+ .optimizer-mode-panel { display: none; }
707
+ .optimizer-mode-panel.active { display: block; }
708
+ .optimizer-ai-panel-header {
709
+ display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 16px;
710
+ }
711
+ .optimizer-ai-suggestions-output { margin-top: 4px; }
712
+ .ai-optimizer-model-picker { position: relative; display: inline-block; }
713
+ .ai-optimizer-model-pill {
714
+ display: inline-flex; align-items: center; gap: 5px;
715
+ background: var(--surface2); border: 1px solid var(--border); color: var(--text3);
716
+ padding: 5px 10px; border-radius: 20px; font-size: 11px; font-family: var(--mono);
717
+ cursor: pointer; transition: all 0.2s; user-select: none; position: relative;
718
+ }
719
+ .ai-optimizer-model-pill:hover { border-color: var(--border-hover); color: var(--text2); }
720
+ .ai-optimizer-model-pill[data-tip]:hover::after {
721
+ content: attr(data-tip);
722
+ position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%);
723
+ background: var(--text); color: var(--bg); font-size: 10px; white-space: nowrap;
724
+ padding: 4px 8px; border-radius: 4px; pointer-events: none; z-index: 200;
725
+ }
726
+ .ai-optimizer-model-picker.open .pill-chevron { transform: rotate(180deg); }
727
+ .ai-optimizer-model-menu {
728
+ position: absolute; top: calc(100% + 4px); left: 0; z-index: 200;
729
+ background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
730
+ padding: 4px; min-width: 140px; box-shadow: var(--shadow-tooltip);
731
+ opacity: 0; transform: translateY(-4px); pointer-events: none;
732
+ transition: opacity 0.15s, transform 0.15s;
733
+ }
734
+ .ai-optimizer-model-picker.open .ai-optimizer-model-menu {
735
+ opacity: 1; transform: translateY(0); pointer-events: auto;
736
+ }
737
+ .ai-optimizer-model-option {
738
+ display: flex; align-items: center; gap: 8px;
739
+ padding: 6px 10px; border-radius: 5px; font-size: 11px; font-family: var(--mono);
740
+ color: var(--text2); cursor: pointer; transition: background 0.1s;
741
+ }
742
+ .ai-optimizer-model-option:hover { background: var(--surface2); }
743
+ .ai-optimizer-model-option.active { color: var(--text); font-weight: 600; }
744
+ .ai-optimizer-model-option .check { width: 14px; color: var(--purple); font-size: 13px; }
745
+ .ai-suggestions-type-group { margin-bottom: 18px; }
746
+ .ai-suggestions-type-group:last-child { margin-bottom: 0; }
747
+ .ai-suggestions-type-label {
748
+ font-size: 10px; text-transform: uppercase; letter-spacing: 0.7px;
749
+ color: var(--text3); font-weight: 600; margin-bottom: 8px;
750
+ }
751
+ .ai-suggestion-badge {
752
+ display: inline-flex; align-items: center; gap: 4px; font-size: 9px;
753
+ font-weight: 600; padding: 1px 6px; border-radius: 4px; margin-left: 6px;
754
+ background: rgba(37,99,235,0.1); color: var(--accent); vertical-align: middle;
755
+ }
756
+
648
757
  /* Workflow Optimizer */
649
758
  .optimizer-card {
650
759
  background: var(--surface); border: 1px solid var(--border);
@@ -659,7 +768,7 @@
659
768
  .optimizer-card h3 {
660
769
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.8px;
661
770
  color: var(--accent); margin-bottom: 14px; font-weight: 600;
662
- display: flex; align-items: center; gap: 8px;
771
+ display: flex; align-items: center; gap: 8px; justify-content: space-between;
663
772
  }
664
773
  .optimizer-tabs {
665
774
  display: flex; gap: 4px; margin-bottom: 16px; flex-wrap: wrap;
@@ -1228,12 +1337,19 @@ function renderDashboard(data) {
1228
1337
  : '<span style="color: var(--text3); font-size: 13px">Keep going -- badges unlock after 5+ sessions</span>'}
1229
1338
  </div>
1230
1339
  </div>
1231
- ${data.overallSummary ? `
1232
- <div class="hero-right">
1233
- <div class="summary-card">
1234
- <h3>How You're Using Claude Code
1235
- <span class="help-icon">?<span class="help-tooltip">Generated from static rules based on your session data — patterns, model choices, and token habits.<br><br><a onclick="document.getElementById('ai-insights-section').scrollIntoView({behavior:'smooth'})">Try AI Insights ↓</a> for a deeper, non-obvious analysis powered by Claude.</span></span>
1236
- </h3>
1340
+ </div>
1341
+ <section id="session-insights-section">
1342
+ <div class="ai-insights-card">
1343
+ <h3>Session Intelligence
1344
+ <span class="card-header-actions">
1345
+ <span class="ai-model-picker" id="ai-model-picker"><span class="ai-model-pill" onclick="toggleModelMenu(event)" data-tip="Click to switch model"></span><div class="ai-model-menu"></div></span>
1346
+ <button class="ai-generate-btn-sm" id="ai-gen-header-btn" onclick="generateAIInsights()" data-warn="⚠ This runs a real Claude session and will show up in your own usage stats. Each generation costs roughly the same as a short coding task.">AI Analyze</button>
1347
+ </span>
1348
+ </h3>
1349
+ <div id="ai-insights-body"></div>
1350
+ ${data.overallSummary ? `
1351
+ <div class="ai-divider"><span>Overview</span></div>
1352
+ <div class="session-static-section">
1237
1353
  <div class="summary-paragraphs">
1238
1354
  ${data.overallSummary.paragraphs.map(p => `<p>${escHtml(p)}</p>`).join('')}
1239
1355
  </div>
@@ -1242,28 +1358,12 @@ function renderDashboard(data) {
1242
1358
  <h4>Recommendations</h4>
1243
1359
  <ul>${data.overallSummary.recommendations.map(r => `<li>${escHtml(r)}</li>`).join('')}</ul>
1244
1360
  </div>` : ''}
1245
- </div>
1246
- </div>` : ''}
1247
- </div>
1248
- <section id="ai-insights-section">
1249
- <div class="ai-insights-card">
1250
- <h3>AI Insights</h3>
1251
- <div id="ai-insights-body">
1252
- <div class="ai-controls">
1253
- <button class="ai-generate-btn" onclick="generateAIInsights()">Generate AI Insights</button>
1254
- <span class="ai-model-picker" id="ai-model-picker"><span class="ai-model-pill" onclick="toggleModelMenu(event)" data-tip="Click to switch model"></span><div class="ai-model-menu"></div></span>
1255
- </div>
1256
- <button class="score-info-toggle" onclick="togglePromptView()">What prompt is it sending to Claude?</button>
1257
- <div class="ai-token-warning">
1258
- <span class="warn-icon">⚠</span>
1259
- <span>This runs a real Claude session and will show up in your own usage stats. Each generation costs roughly the same as a short coding task.</span>
1260
- </div>
1261
- </div>
1361
+ </div>` : ''}
1262
1362
  </div>
1263
1363
  </section>
1264
1364
  <section id="optimizer-section">
1265
1365
  <div class="optimizer-card">
1266
- <h3>Workflow Optimizer</h3>
1366
+ <h3>Workflow Optimizer<span id="optimizer-header-controls"></span></h3>
1267
1367
  <div id="optimizer-body">
1268
1368
  <div style="color:var(--text3); font-size:13px; padding: 4px 0 12px;">Loading suggestions<span id="optimizer-dots"></span></div>
1269
1369
  </div>
@@ -1638,6 +1738,7 @@ function selectModel(modelId) {
1638
1738
 
1639
1739
  document.addEventListener('click', () => {
1640
1740
  document.querySelectorAll('.ai-model-picker.open').forEach(p => p.classList.remove('open'));
1741
+ document.querySelectorAll('.ai-optimizer-model-picker.open').forEach(p => p.classList.remove('open'));
1641
1742
  });
1642
1743
 
1643
1744
  function applyAIState() {
@@ -1646,23 +1747,25 @@ function applyAIState() {
1646
1747
  } else {
1647
1748
  renderModelPicker();
1648
1749
  }
1750
+ if (optimizerSuggestions) renderOptimizerTabs();
1649
1751
  }
1650
1752
 
1651
1753
  function renderAIComplete(content, generatedAt, model) {
1652
1754
  cachedAIRender = { content, generatedAt, model };
1653
1755
  const body = document.getElementById('ai-insights-body');
1654
1756
  if (!body) return;
1757
+ const headerBtn = document.getElementById('ai-gen-header-btn');
1758
+ if (headerBtn) { headerBtn.textContent = 'AI Analyze'; headerBtn.disabled = false; }
1655
1759
  body.innerHTML = `
1760
+ <div class="ai-divider" style="margin-top:0"><span>AI Analysis</span></div>
1656
1761
  <div class="ai-insights-content">${renderMarkdown(escHtml(content))}</div>
1657
1762
  <div class="ai-timestamp">
1658
1763
  Generated ${new Date(generatedAt).toLocaleString()}
1659
1764
  ${model ? ` · ${modelLabel(model)}` : ''}
1660
- <div style="margin-top: 10px;" class="ai-controls">
1661
- <button class="ai-generate-btn" style="padding: 5px 14px; font-size: 11px;" onclick="generateAIInsights()">Regenerate</button>
1662
- <button class="ai-generate-btn" style="padding: 5px 14px; font-size: 11px; background: var(--surface2); color: var(--text2);" onclick="clearAIInsights()">Clear</button>
1663
- <span class="ai-model-picker" id="ai-model-picker"><span class="ai-model-pill" onclick="toggleModelMenu(event)" data-tip="Click to switch model"></span><div class="ai-model-menu"></div></span>
1765
+ <div style="margin-top:8px" class="ai-controls">
1766
+ <button class="ai-generate-btn" style="padding:5px 14px;font-size:11px;background:var(--surface2);border-color:var(--border);color:var(--text2)" onclick="clearAIInsights()">Clear</button>
1664
1767
  </div>
1665
- <button class="score-info-toggle" onclick="togglePromptView()">What prompt is it sending to Claude?</button>
1768
+ <div><button class="score-info-toggle" onclick="togglePromptView()">What prompt is it sending to Claude?</button></div>
1666
1769
  </div>`;
1667
1770
  renderModelPicker();
1668
1771
  }
@@ -1672,16 +1775,9 @@ function clearAIInsights() {
1672
1775
  fetch('/api/ai-analyze', { method: 'DELETE' }).catch(() => {});
1673
1776
  const body = document.getElementById('ai-insights-body');
1674
1777
  if (!body) return;
1675
- body.innerHTML = `
1676
- <div class="ai-controls">
1677
- <button class="ai-generate-btn" onclick="generateAIInsights()">Generate AI Insights</button>
1678
- <span class="ai-model-picker" id="ai-model-picker"><span class="ai-model-pill" onclick="toggleModelMenu(event)" data-tip="Click to switch model"></span><div class="ai-model-menu"></div></span>
1679
- </div>
1680
- <button class="score-info-toggle" onclick="togglePromptView()">What prompt is it sending to Claude?</button>
1681
- <div class="ai-token-warning">
1682
- <span class="warn-icon">⚠</span>
1683
- <span>This runs a real Claude session and will show up in your own usage stats. Each generation costs roughly the same as a short coding task.</span>
1684
- </div>`;
1778
+ body.innerHTML = '';
1779
+ const headerBtn = document.getElementById('ai-gen-header-btn');
1780
+ if (headerBtn) { headerBtn.textContent = 'AI Analyze'; headerBtn.disabled = false; }
1685
1781
  renderModelPicker();
1686
1782
  }
1687
1783
 
@@ -1707,6 +1803,9 @@ function generateAIInsights() {
1707
1803
  const body = document.getElementById('ai-insights-body');
1708
1804
  if (!body) return;
1709
1805
 
1806
+ const headerBtn = document.getElementById('ai-gen-header-btn');
1807
+ if (headerBtn) { headerBtn.textContent = 'Generating…'; headerBtn.disabled = true; }
1808
+
1710
1809
  const wasActive = autoRefreshActive;
1711
1810
  if (autoRefreshActive) pauseAutoRefresh();
1712
1811
 
@@ -1714,11 +1813,11 @@ function generateAIInsights() {
1714
1813
  const modelParam = modelId ? `?model=${encodeURIComponent(modelId)}` : '';
1715
1814
 
1716
1815
  body.innerHTML = `
1816
+ <div class="ai-divider" style="margin-top:0"><span>AI Analysis</span></div>
1717
1817
  <div class="ai-loading">
1718
1818
  <div class="ai-spinner"></div>
1719
1819
  <div>
1720
1820
  <div>Generating with ${modelId ? modelLabel(modelId) : 'default model'}, please wait...</div>
1721
- ${wasActive ? '<div style="font-size:11px; color:var(--yellow); margin-top:4px">Auto-refresh paused while generating</div>' : ''}
1722
1821
  </div>
1723
1822
  </div>
1724
1823
  <div class="ai-insights-content" id="ai-stream-content"></div>`;
@@ -1766,12 +1865,9 @@ async function streamAIFetch(modelParam, body) {
1766
1865
  renderAIComplete(fullContent, generatedAt, usedModel);
1767
1866
  } else if (currentEvent === 'error') {
1768
1867
  const { message } = JSON.parse(data);
1769
- body.innerHTML = `<div class="ai-error">${escHtml(message)}</div>
1770
- <div class="ai-controls" style="margin-top: 8px;">
1771
- <button class="ai-generate-btn" onclick="generateAIInsights()">Retry</button>
1772
- <span class="ai-model-picker" id="ai-model-picker"><span class="ai-model-pill" onclick="toggleModelMenu(event)" data-tip="Click to switch model"></span><div class="ai-model-menu"></div></span>
1773
- </div>`;
1774
- renderModelPicker();
1868
+ const headerBtn = document.getElementById('ai-gen-header-btn');
1869
+ if (headerBtn) { headerBtn.textContent = 'AI Analyze'; headerBtn.disabled = false; }
1870
+ body.innerHTML = `<div class="ai-error">${escHtml(message)}</div>`;
1775
1871
  resumeAutoRefreshIfPaused();
1776
1872
  }
1777
1873
  currentEvent = '';
@@ -1780,12 +1876,9 @@ async function streamAIFetch(modelParam, body) {
1780
1876
  }
1781
1877
  resumeAutoRefreshIfPaused();
1782
1878
  } catch (err) {
1783
- body.innerHTML = `<div class="ai-error">Failed to connect to server</div>
1784
- <div class="ai-controls" style="margin-top: 8px;">
1785
- <button class="ai-generate-btn" onclick="generateAIInsights()">Retry</button>
1786
- <span class="ai-model-picker" id="ai-model-picker"><span class="ai-model-pill" onclick="toggleModelMenu(event)" data-tip="Click to switch model"></span><div class="ai-model-menu"></div></span>
1787
- </div>`;
1788
- renderModelPicker();
1879
+ const headerBtnCatch = document.getElementById('ai-gen-header-btn');
1880
+ if (headerBtnCatch) { headerBtnCatch.textContent = 'AI Analyze'; headerBtnCatch.disabled = false; }
1881
+ body.innerHTML = `<div class="ai-error">Failed to connect to server</div>`;
1789
1882
  resumeAutoRefreshIfPaused();
1790
1883
  }
1791
1884
  }
@@ -1812,6 +1905,9 @@ async function loadCachedAIInsights() {
1812
1905
  let optimizerSuggestions = null;
1813
1906
  let activeOptimizerTab = 'all';
1814
1907
  let cachedOptimizerAI = null;
1908
+ let optimizerMode = 'static'; // 'static' | 'ai'
1909
+ let aiOptimizerModelId = 'claude-opus-4-6'; // default Opus for AI suggestions
1910
+ let cachedAIOptimizerSuggestions = null; // { suggestions, generatedAt, model }
1815
1911
 
1816
1912
  const OPTIMIZER_TABS = [
1817
1913
  { id: 'all', label: 'All' },
@@ -1830,8 +1926,15 @@ async function loadSuggestions() {
1830
1926
  }, 400);
1831
1927
 
1832
1928
  try {
1833
- const res = await fetch('/api/suggestions');
1834
- optimizerSuggestions = await res.json();
1929
+ const [suggestionsRes, aiCacheRes] = await Promise.all([
1930
+ fetch('/api/suggestions'),
1931
+ fetch('/api/ai-optimizer'),
1932
+ ]);
1933
+ optimizerSuggestions = await suggestionsRes.json();
1934
+ const aiCache = await aiCacheRes.json();
1935
+ if (aiCache.suggestions) {
1936
+ cachedAIOptimizerSuggestions = aiCache;
1937
+ }
1835
1938
  clearInterval(dotTimer);
1836
1939
  renderOptimizerTabs();
1837
1940
  } catch {
@@ -1875,7 +1978,7 @@ function renderOptimizerTabs() {
1875
1978
  const aiSection = `
1876
1979
  <div class="optimizer-ai-section">
1877
1980
  <div class="optimizer-ai-controls">
1878
- <button class="optimizer-generate-btn" onclick="generateOptimizerContent()" id="optimizer-gen-btn">
1981
+ <button class="optimizer-generate-btn" onclick="generateOptimizerContent()" id="optimizer-gen-btn" data-warn="⚠ This runs a real Claude session and will show up in your own usage stats. Each generation costs roughly the same as a short coding task.">
1879
1982
  Generate artifact content with AI
1880
1983
  </button>
1881
1984
  <span class="ai-model-picker" id="optimizer-model-picker">
@@ -1886,15 +1989,42 @@ function renderOptimizerTabs() {
1886
1989
  <div id="optimizer-ai-output"></div>
1887
1990
  </div>`;
1888
1991
 
1992
+ // Build AI Advisor model picker for card header
1993
+ const opusModel = aiModels.find(m => m.id === 'claude-opus-4-6') || aiModels[0];
1994
+ const currentAIModel = aiModels.find(m => m.id === aiOptimizerModelId) || opusModel || aiModels[0];
1995
+ const aiModelMenuHtml = aiModels.map(m =>
1996
+ `<div class="ai-optimizer-model-option${m.id === aiOptimizerModelId ? ' active' : ''}" onclick="selectAIOptimizerModel('${m.id}')">` +
1997
+ `<span class="check">${m.id === aiOptimizerModelId ? '●' : ''}</span>${m.label}</div>`
1998
+ ).join('');
1999
+
2000
+ // Populate card header controls
2001
+ const headerControls = document.getElementById('optimizer-header-controls');
2002
+ if (headerControls) {
2003
+ headerControls.innerHTML = `
2004
+ <span class="card-header-actions">
2005
+ <div class="ai-optimizer-model-picker" id="ai-optimizer-model-picker" onclick="event.stopPropagation()">
2006
+ <div class="ai-optimizer-model-pill" onclick="toggleAIOptimizerModelMenu()" data-tip="Click to switch model">${currentAIModel ? currentAIModel.label : 'Opus 4.6'} <span class="pill-chevron">&#x25BE;</span></div>
2007
+ <div class="ai-optimizer-model-menu">${aiModelMenuHtml}</div>
2008
+ </div>
2009
+ <button class="optimizer-generate-btn-sm" id="ai-optimizer-gen-btn" onclick="generateAIOptimizerSuggestions()" data-warn="⚠ This runs a real Claude session and will show up in your own usage stats. Each generation costs roughly the same as a short coding task.">AI Optimize</button>
2010
+ </span>`;
2011
+ }
2012
+
2013
+ // Body: AI section at top (hidden until generated), then patterns
1889
2014
  body.innerHTML = `
2015
+ <div id="ai-optimizer-top-section" style="display:none">
2016
+ <div class="ai-divider" style="margin-top:0"><span>AI Optimize</span></div>
2017
+ <div id="ai-optimizer-suggestions-output"></div>
2018
+ <div class="ai-divider"><span>Patterns</span></div>
2019
+ </div>
1890
2020
  <p style="font-size:12px; color:var(--text3); margin-bottom:12px;">
1891
- Based on ${(optimizerSuggestions.skills.length + optimizerSuggestions.claudeMd.length + optimizerSuggestions.agents.length + optimizerSuggestions.plugins.length)} patterns found in your sessions.
2021
+ Based on ${total} patterns found in your sessions.
1892
2022
  </p>
1893
2023
  <div class="optimizer-tabs">${tabsHtml}</div>
1894
2024
  ${panelsHtml}
1895
2025
  ${aiSection}`;
1896
2026
 
1897
- // Sync model picker — reuse same aiModels
2027
+ // Sync artifact content model picker
1898
2028
  const optimizerPicker = document.getElementById('optimizer-model-picker');
1899
2029
  if (optimizerPicker && aiModels.length > 0) {
1900
2030
  const current = aiModels.find(m => m.id === selectedModelId) ||
@@ -1911,9 +2041,13 @@ function renderOptimizerTabs() {
1911
2041
  if (cachedOptimizerAI) {
1912
2042
  renderOptimizerAIComplete(cachedOptimizerAI.content, cachedOptimizerAI.generatedAt, cachedOptimizerAI.model);
1913
2043
  }
2044
+
2045
+ if (cachedAIOptimizerSuggestions) {
2046
+ renderAIOptimizerSuggestionsComplete(cachedAIOptimizerSuggestions.suggestions, cachedAIOptimizerSuggestions.generatedAt, cachedAIOptimizerSuggestions.model);
2047
+ }
1914
2048
  }
1915
2049
 
1916
- function renderSuggestionList(type, items) {
2050
+ function renderSuggestionList(type, items, isAI = false) {
1917
2051
  if (!items.length) return '<div class="suggestion-empty">No suggestions for this category.</div>';
1918
2052
  return items.map(item => {
1919
2053
  const priorityClass = `priority-${item.priority || 'medium'}`;
@@ -1923,10 +2057,12 @@ function renderSuggestionList(type, items) {
1923
2057
  if (type === 'claudeMd') meta = `<span>scope: <strong>${escHtml(item.scope)}${item.projectName ? ' · ' + escHtml(item.projectName) : ''}</strong></span>`;
1924
2058
  if (type === 'plugins' && item.installCmd) meta = `<span style="font-family:var(--mono)">${escHtml(item.installCmd)}</span>`;
1925
2059
  if (type === 'agents' && item.use_case) meta = `<span>${escHtml(item.use_case)}</span>`;
2060
+ const aiNewBadge = isAI && item.aiNew ? '<span class="ai-suggestion-badge">AI new</span>' : '';
2061
+ const aiEnhancedBadge = isAI && item.aiEnhanced ? '<span class="ai-suggestion-badge">AI enhanced</span>' : '';
1926
2062
  return `
1927
2063
  <div class="suggestion-card">
1928
2064
  <div class="suggestion-header">
1929
- <div class="suggestion-title">${escHtml(item.title || item.name || item.use_case || '')}</div>
2065
+ <div class="suggestion-title">${escHtml(item.title || item.name || item.use_case || '')}${aiNewBadge}${aiEnhancedBadge}</div>
1930
2066
  <span class="suggestion-priority ${priorityClass}">${priorityLabel}</span>
1931
2067
  </div>
1932
2068
  <div class="suggestion-rationale">${escHtml(item.rationale)}</div>
@@ -1947,6 +2083,202 @@ function switchOptimizerTab(tabId) {
1947
2083
  });
1948
2084
  }
1949
2085
 
2086
+ function switchOptimizerMode(mode) {
2087
+ optimizerMode = mode;
2088
+ document.querySelectorAll('.optimizer-mode-panel').forEach(p => {
2089
+ p.classList.toggle('active', p.id === `optimizer-${mode === 'static' ? 'static' : 'ai-suggestions'}-panel`);
2090
+ });
2091
+ document.querySelectorAll('.optimizer-mode-tab').forEach(t => {
2092
+ const onclick = t.getAttribute('onclick') || '';
2093
+ const match = onclick.match(/switchOptimizerMode\('([^']+)'\)/);
2094
+ if (match) t.classList.toggle('active', match[1] === mode);
2095
+ });
2096
+ }
2097
+
2098
+ function toggleAIOptimizerModelMenu() {
2099
+ const picker = document.getElementById('ai-optimizer-model-picker');
2100
+ if (!picker) return;
2101
+ const wasOpen = picker.classList.contains('open');
2102
+ document.querySelectorAll('.ai-optimizer-model-picker.open').forEach(p => p.classList.remove('open'));
2103
+ if (!wasOpen) picker.classList.add('open');
2104
+ }
2105
+
2106
+ function selectAIOptimizerModel(modelId) {
2107
+ aiOptimizerModelId = modelId;
2108
+ document.querySelectorAll('.ai-optimizer-model-picker.open').forEach(p => p.classList.remove('open'));
2109
+ // Update pill label
2110
+ const pill = document.querySelector('#ai-optimizer-model-picker .ai-optimizer-model-pill');
2111
+ const menu = document.querySelector('#ai-optimizer-model-picker .ai-optimizer-model-menu');
2112
+ const model = aiModels.find(m => m.id === modelId);
2113
+ if (pill && model) { pill.innerHTML = `${model.label} <span class="pill-chevron">&#x25BE;</span>`; pill.setAttribute('data-tip', 'Click to switch model'); }
2114
+ if (menu) menu.innerHTML = aiModels.map(m =>
2115
+ `<div class="ai-optimizer-model-option${m.id === modelId ? ' active' : ''}" onclick="selectAIOptimizerModel('${m.id}')">` +
2116
+ `<span class="check">${m.id === modelId ? '●' : ''}</span>${m.label}</div>`
2117
+ ).join('');
2118
+ }
2119
+
2120
+ function generateAIOptimizerSuggestions() {
2121
+ const btn = document.getElementById('ai-optimizer-gen-btn');
2122
+ const output = document.getElementById('ai-optimizer-suggestions-output');
2123
+ const topSection = document.getElementById('ai-optimizer-top-section');
2124
+ if (!btn || !output) return;
2125
+
2126
+ const wasActive = autoRefreshActive || autoRefreshPaused;
2127
+ if (autoRefreshActive) pauseAutoRefresh();
2128
+
2129
+ const modelParam = aiOptimizerModelId ? `?model=${encodeURIComponent(aiOptimizerModelId)}` : '';
2130
+ const modelName = modelLabel(aiOptimizerModelId) || aiOptimizerModelId;
2131
+
2132
+ btn.disabled = true;
2133
+ if (topSection) topSection.style.display = '';
2134
+ output.innerHTML = `
2135
+ <div class="ai-loading">
2136
+ <div class="ai-spinner"></div>
2137
+ <div>
2138
+ <div>Optimizing with ${modelName}, please wait...</div>
2139
+ </div>
2140
+ </div>
2141
+ <div id="ai-optimizer-cards-streaming" style="margin-top:12px"></div>`;
2142
+
2143
+ streamAIOptimizerFetch(modelParam, output, wasActive);
2144
+ }
2145
+
2146
+ async function streamAIOptimizerFetch(modelParam, output, wasActive) {
2147
+ const accumulatedSuggestions = [];
2148
+
2149
+ try {
2150
+ const res = await fetch('/api/ai-optimizer' + modelParam, { method: 'POST' });
2151
+ const reader = res.body.getReader();
2152
+ const decoder = new TextDecoder();
2153
+ let buffer = '';
2154
+
2155
+ while (true) {
2156
+ const { done, value } = await reader.read();
2157
+ if (done) break;
2158
+
2159
+ buffer += decoder.decode(value, { stream: true });
2160
+ const lines = buffer.split('\n');
2161
+ buffer = lines.pop();
2162
+
2163
+ let currentEvent = '';
2164
+ for (const line of lines) {
2165
+ if (line.startsWith('event: ')) {
2166
+ currentEvent = line.slice(7);
2167
+ } else if (line.startsWith('data: ')) {
2168
+ const data = line.slice(6);
2169
+ if (currentEvent === 'model') {
2170
+ const usedModel = JSON.parse(data);
2171
+ const loading = output.querySelector('.ai-loading div:last-child');
2172
+ if (loading) loading.textContent = `Optimizing with ${modelLabel(usedModel)}, please wait...`;
2173
+ } else if (currentEvent === 'suggestion') {
2174
+ const suggestion = JSON.parse(data);
2175
+ accumulatedSuggestions.push(suggestion);
2176
+ // Render cards progressively
2177
+ const streamEl = document.getElementById('ai-optimizer-cards-streaming');
2178
+ if (streamEl) streamEl.innerHTML = renderAIOptimizerCards(accumulatedSuggestions);
2179
+ } else if (currentEvent === 'done') {
2180
+ const { generatedAt, model } = JSON.parse(data);
2181
+ const loading = output.querySelector('.ai-loading');
2182
+ if (loading) loading.remove();
2183
+ // Merge AI suggestions with static and cache
2184
+ const merged = optimizerSuggestions
2185
+ ? mergeAISuggestionsWithStatic(optimizerSuggestions, accumulatedSuggestions)
2186
+ : null;
2187
+ cachedAIOptimizerSuggestions = { suggestions: accumulatedSuggestions, mergedSuggestions: merged, generatedAt, model };
2188
+ renderAIOptimizerSuggestionsComplete(accumulatedSuggestions, generatedAt, model);
2189
+ if (wasActive) resumeAutoRefreshIfPaused();
2190
+ } else if (currentEvent === 'error') {
2191
+ const { message } = JSON.parse(data);
2192
+ const btn = document.getElementById('ai-optimizer-gen-btn');
2193
+ if (btn) { btn.disabled = false; btn.textContent = 'AI Optimize'; }
2194
+ output.innerHTML = `<div class="ai-error">${escHtml(message)}</div>`;
2195
+ if (wasActive) resumeAutoRefreshIfPaused();
2196
+ }
2197
+ }
2198
+ }
2199
+ }
2200
+ } catch (err) {
2201
+ const btn = document.getElementById('ai-optimizer-gen-btn');
2202
+ if (btn) { btn.disabled = false; btn.textContent = 'AI Optimize'; }
2203
+ output.innerHTML = `<div class="ai-error">Failed to connect to server</div>`;
2204
+ if (wasActive) resumeAutoRefreshIfPaused();
2205
+ }
2206
+ }
2207
+
2208
+ function mergeAISuggestionsWithStatic(staticSuggestions, aiSuggestions) {
2209
+ const normalize = (s) => (s || '').toLowerCase().replace(/[^a-z0-9 ]/g, '').trim();
2210
+ const priorityOrder = { high: 0, medium: 1, low: 2 };
2211
+
2212
+ const result = {
2213
+ skills: [...staticSuggestions.skills],
2214
+ claudeMd: [...staticSuggestions.claudeMd],
2215
+ agents: [...staticSuggestions.agents],
2216
+ plugins: [...staticSuggestions.plugins],
2217
+ };
2218
+
2219
+ for (const ai of aiSuggestions) {
2220
+ const type = ai.type;
2221
+ if (!result[type]) continue;
2222
+ const aiTitle = normalize(ai.title);
2223
+ const idx = result[type].findIndex(s => {
2224
+ const st = normalize(s.title || s.name || s.use_case || '');
2225
+ return st === aiTitle || st.includes(aiTitle) || aiTitle.includes(st);
2226
+ });
2227
+ if (idx >= 0) {
2228
+ result[type][idx] = { ...result[type][idx], ...ai, aiEnhanced: true };
2229
+ } else {
2230
+ result[type].push({ ...ai, aiNew: true });
2231
+ }
2232
+ }
2233
+
2234
+ for (const type of Object.keys(result)) {
2235
+ result[type].sort((a, b) => (priorityOrder[a.priority] ?? 1) - (priorityOrder[b.priority] ?? 1));
2236
+ }
2237
+ return result;
2238
+ }
2239
+
2240
+ function renderAIOptimizerCards(suggestions) {
2241
+ if (!suggestions.length) return '';
2242
+ const groups = { skills: [], claudeMd: [], agents: [], plugins: [] };
2243
+ for (const s of suggestions) {
2244
+ if (groups[s.type]) groups[s.type].push(s);
2245
+ }
2246
+ const typeLabels = { skills: 'Skills', claudeMd: 'CLAUDE.md', agents: 'Agents', plugins: 'Plugins' };
2247
+ return Object.entries(groups).filter(([, items]) => items.length > 0).map(([type, items]) => `
2248
+ <div class="ai-suggestions-type-group">
2249
+ <div class="ai-suggestions-type-label">${typeLabels[type] || type}</div>
2250
+ ${renderSuggestionList(type, items, true)}
2251
+ </div>`).join('');
2252
+ }
2253
+
2254
+ function renderAIOptimizerSuggestionsComplete(suggestions, generatedAt, model) {
2255
+ const output = document.getElementById('ai-optimizer-suggestions-output');
2256
+ const btn = document.getElementById('ai-optimizer-gen-btn');
2257
+ const topSection = document.getElementById('ai-optimizer-top-section');
2258
+ if (!output) return;
2259
+ if (btn) { btn.disabled = false; btn.textContent = 'AI Optimize'; }
2260
+ if (topSection) topSection.style.display = '';
2261
+ output.innerHTML = `
2262
+ <div>${renderAIOptimizerCards(suggestions)}</div>
2263
+ <div class="optimizer-timestamp">
2264
+ Generated ${new Date(generatedAt).toLocaleString()}${model ? ' · ' + modelLabel(model) : ''}
2265
+ <div style="margin-top:8px">
2266
+ <button class="optimizer-generate-btn" style="padding:4px 12px;font-size:11px;background:var(--surface2);border-color:var(--border);color:var(--text2)" onclick="clearAIOptimizerSuggestions()">Clear</button>
2267
+ </div>
2268
+ </div>`;
2269
+ }
2270
+
2271
+ function clearAIOptimizerSuggestions() {
2272
+ cachedAIOptimizerSuggestions = null;
2273
+ fetch('/api/ai-optimizer', { method: 'DELETE' }).catch(() => {});
2274
+ const output = document.getElementById('ai-optimizer-suggestions-output');
2275
+ const btn = document.getElementById('ai-optimizer-gen-btn');
2276
+ const topSection = document.getElementById('ai-optimizer-top-section');
2277
+ if (output) output.innerHTML = '';
2278
+ if (topSection) topSection.style.display = 'none';
2279
+ if (btn) { btn.disabled = false; btn.textContent = 'AI Optimize'; }
2280
+ }
2281
+
1950
2282
  function generateOptimizerContent() {
1951
2283
  const btn = document.getElementById('optimizer-gen-btn');
1952
2284
  const output = document.getElementById('optimizer-ai-output');
@@ -2071,7 +2403,7 @@ function renderOptimizerAIComplete(content, generatedAt, model) {
2071
2403
  <div class="optimizer-ai-content">${renderOptimizerMarkdown(escHtml(content))}</div>
2072
2404
  <div class="optimizer-timestamp">
2073
2405
  Generated ${new Date(generatedAt).toLocaleString()}${model ? ' · ' + modelLabel(model) : ''}
2074
- <button class="optimizer-generate-btn" style="padding:4px 12px;font-size:11px;margin-left:8px" onclick="generateOptimizerContent()">Regenerate</button>
2406
+ <button class="optimizer-generate-btn" style="padding:4px 12px;font-size:11px;margin-left:8px" onclick="generateOptimizerContent()" data-warn="⚠ This runs a real Claude session and will show up in your own usage stats. Each generation costs roughly the same as a short coding task.">Regenerate</button>
2075
2407
  <button class="optimizer-generate-btn" style="padding:4px 12px;font-size:11px;background:var(--surface2);color:var(--text2)" onclick="clearOptimizerAI()">Clear</button>
2076
2408
  </div>`;
2077
2409
  }
@@ -2566,7 +2898,7 @@ sse.onmessage = async (e) => {
2566
2898
  location.reload();
2567
2899
  } else {
2568
2900
  currentData = await fetchData();
2569
- if (currentView === 'dashboard') renderDashboard(currentData);
2901
+ if (currentView === 'dashboard') { renderDashboard(currentData); applyAIState(); }
2570
2902
  }
2571
2903
  };
2572
2904
  </script>
package/src/ai-analyze.js CHANGED
@@ -458,3 +458,225 @@ export function getCachedSuggestions() {
458
458
  export function clearCachedSuggestions() {
459
459
  cachedSuggestionResult = null;
460
460
  }
461
+
462
+ // --- AI Optimizer Suggestions (structured suggestion objects) ---
463
+
464
+ function buildOptimizerSuggestionsPrompt(scoredData, staticSuggestions) {
465
+ const { sessions, badges, overallScore } = scoredData;
466
+ const totalCost = sessions.reduce((s, x) => s + x.totals.estimatedCost, 0);
467
+ const projectNames = [...new Set(sessions.map((s) => s.project).filter(Boolean))].slice(0, 5);
468
+ const badgeNames = badges.map((b) => `${b.negative ? "⚠ " : "✓ "}${b.name}`).join(", ");
469
+
470
+ const staticSummary = [
471
+ ...staticSuggestions.skills.map(s => `skills: "${s.title}" (${s.priority})`),
472
+ ...staticSuggestions.claudeMd.map(s => `claudeMd: "${s.title}" (${s.priority})`),
473
+ ...staticSuggestions.agents.map(s => `agents: "${s.title || s.name}" (${s.priority})`),
474
+ ...staticSuggestions.plugins.map(s => `plugins: "${s.title || s.name}" (${s.priority})`),
475
+ ].join("\n");
476
+
477
+ return `You are analyzing a developer's Claude Code session data to generate workflow improvement suggestions.
478
+
479
+ ## Their Usage Context
480
+ - ${sessions.length} total sessions across ${projectNames.length} project(s): ${projectNames.join(", ")}
481
+ - Overall efficiency score: ${overallScore}/100
482
+ - Total spend: $${totalCost.toFixed(2)}
483
+ - Badges: ${badgeNames || "none"}
484
+
485
+ ## Existing rule-based suggestions (already shown to user)
486
+ ${staticSummary || "none"}
487
+
488
+ ## Your task
489
+ Generate 4-8 workflow suggestions based on a deeper analysis of their patterns. You may enhance existing rule-based suggestions with better rationale, or add entirely new suggestions the rules missed.
490
+
491
+ Output ONLY a series of JSON objects, one per line, with NO other text, markdown, or explanation. Each line must be valid JSON in exactly one of these formats:
492
+
493
+ For skills: {"type":"skills","title":"Short action title","rationale":"Why this helps, referencing their specific data","priority":"high","trigger":"/slash-command"}
494
+ For CLAUDE.md: {"type":"claudeMd","title":"Short title","rationale":"Why this helps","priority":"medium","scope":"global","projectName":"optional project name"}
495
+ For agents: {"type":"agents","title":"Agent name","rationale":"Why this helps","priority":"low","use_case":"One-line description"}
496
+ For plugins: {"type":"plugins","title":"Plugin name","rationale":"Why this helps","priority":"high","installCmd":"mcp install command"}
497
+
498
+ Rules:
499
+ - Output 4-8 total suggestions
500
+ - Reference specific patterns from their data (costs, badges, session counts)
501
+ - Prioritize suggestions NOT already in the rule-based list, or meaningfully improve on them with richer rationale
502
+ - Output ONLY JSON lines, nothing else`;
503
+ }
504
+
505
+ let cachedOptimizerSuggestionsResult = null;
506
+
507
+ /**
508
+ * Stream AI-generated structured suggestion objects.
509
+ * Emits: { event: "model", data: modelId }
510
+ * { event: "suggestion", data: { type, title, rationale, priority, ... } }
511
+ * { event: "done", data: { generatedAt, model } }
512
+ * { event: "error", data: { message } }
513
+ */
514
+ export function streamAIOptimizerSuggestions(scoredData, staticSuggestions, res, modelId) {
515
+ const prompt = buildOptimizerSuggestionsPrompt(scoredData, staticSuggestions);
516
+
517
+ const args = ["-p", "-", "--output-format", "stream-json", "--verbose"];
518
+ if (modelId) args.push("--model", modelId);
519
+
520
+ const resolvedModel = modelId || "default";
521
+ console.log(`[ai-optimizer] Starting (model: ${resolvedModel})`);
522
+ res.write(`event: model\ndata: ${JSON.stringify(resolvedModel)}\n\n`);
523
+
524
+ const child = spawn("claude", args, {
525
+ stdio: ["pipe", "pipe", "pipe"],
526
+ env: cleanEnv,
527
+ });
528
+ activeChildren.add(child);
529
+
530
+ child.stdin.write(prompt);
531
+ child.stdin.end();
532
+
533
+ const aiSuggestions = [];
534
+ let errOutput = "";
535
+ let detectedModel = modelId || detectedDefaultModel || null;
536
+ let lineBuf = "";
537
+ let textBuf = "";
538
+
539
+ child.stdout.on("data", (buf) => {
540
+ lineBuf += buf.toString();
541
+ const lines = lineBuf.split("\n");
542
+ lineBuf = lines.pop();
543
+ for (const line of lines) {
544
+ if (!line.trim()) continue;
545
+ try {
546
+ const obj = JSON.parse(line);
547
+ if (obj.type === "system" || obj.type === "rate_limit_event") continue;
548
+ if (obj.type === "result") {
549
+ const models = Object.keys(obj.modelUsage || {});
550
+ if (models.length > 0) {
551
+ detectedModel = models[0];
552
+ if (!modelId) detectedDefaultModel = models[0];
553
+ }
554
+ continue;
555
+ }
556
+ if (obj.type === "assistant" && obj.message?.content) {
557
+ for (const block of obj.message.content) {
558
+ if (block.type === "text" && block.text) {
559
+ textBuf += block.text;
560
+ // Try to parse complete JSON lines from the text buffer
561
+ const textLines = textBuf.split("\n");
562
+ textBuf = textLines.pop(); // keep last (possibly incomplete) line
563
+ for (const tl of textLines) {
564
+ const trimmed = tl.trim();
565
+ if (!trimmed) continue;
566
+ try {
567
+ const suggestion = JSON.parse(trimmed);
568
+ if (suggestion.type && suggestion.title) {
569
+ aiSuggestions.push(suggestion);
570
+ res.write(`event: suggestion\ndata: ${JSON.stringify(suggestion)}\n\n`);
571
+ }
572
+ } catch {
573
+ // not a valid JSON suggestion line, skip
574
+ }
575
+ }
576
+ }
577
+ }
578
+ if (obj.message.model && !detectedModel) {
579
+ detectedModel = obj.message.model;
580
+ if (!modelId) detectedDefaultModel = obj.message.model;
581
+ }
582
+ }
583
+ } catch {
584
+ // not stream-json, treat as raw text
585
+ }
586
+ }
587
+ });
588
+
589
+ child.stderr.on("data", (buf) => {
590
+ errOutput += buf.toString();
591
+ });
592
+
593
+ child.on("error", (err) => {
594
+ const message =
595
+ err.code === "ENOENT"
596
+ ? "Claude CLI not found. Make sure `claude` is installed and in your PATH."
597
+ : `Claude CLI error: ${err.message}`;
598
+ res.write(`event: error\ndata: ${JSON.stringify({ message })}\n\n`);
599
+ res.end();
600
+ });
601
+
602
+ child.on("close", (code) => {
603
+ activeChildren.delete(child);
604
+ // Flush any remaining text buffer
605
+ if (textBuf.trim()) {
606
+ try {
607
+ const suggestion = JSON.parse(textBuf.trim());
608
+ if (suggestion.type && suggestion.title) {
609
+ aiSuggestions.push(suggestion);
610
+ res.write(`event: suggestion\ndata: ${JSON.stringify(suggestion)}\n\n`);
611
+ }
612
+ } catch { /* ignore */ }
613
+ }
614
+ if (code !== 0 && aiSuggestions.length === 0) {
615
+ const message = errOutput.trim() || `Claude CLI exited with code ${code}`;
616
+ console.error(`[ai-optimizer] Failed (exit ${code}): ${message}`);
617
+ res.write(`event: error\ndata: ${JSON.stringify({ message })}\n\n`);
618
+ } else {
619
+ const finalModel = detectedModel || resolvedModel;
620
+ console.log(`[ai-optimizer] Done (model: ${finalModel}, ${aiSuggestions.length} suggestions)`);
621
+ cachedOptimizerSuggestionsResult = {
622
+ suggestions: aiSuggestions,
623
+ generatedAt: new Date().toISOString(),
624
+ model: finalModel,
625
+ };
626
+ res.write(`event: done\ndata: ${JSON.stringify({ generatedAt: cachedOptimizerSuggestionsResult.generatedAt, model: finalModel })}\n\n`);
627
+ }
628
+ res.end();
629
+ });
630
+
631
+ res.on("close", () => {
632
+ if (child.exitCode === null) child.kill();
633
+ });
634
+ }
635
+
636
+ export function getCachedOptimizerSuggestions() {
637
+ return cachedOptimizerSuggestionsResult;
638
+ }
639
+
640
+ export function clearCachedOptimizerSuggestions() {
641
+ cachedOptimizerSuggestionsResult = null;
642
+ }
643
+
644
+ /**
645
+ * Merge AI-generated suggestions with static rule-based ones.
646
+ * AI suggestions matching an existing one (by type + title similarity) replace it;
647
+ * novel AI suggestions are appended.
648
+ */
649
+ export function mergeAISuggestionsWithStatic(staticSuggestions, aiSuggestions) {
650
+ const normalize = (s) => (s || "").toLowerCase().replace(/[^a-z0-9 ]/g, "").trim();
651
+ const priorityOrder = { high: 0, medium: 1, low: 2 };
652
+
653
+ const result = {
654
+ skills: [...staticSuggestions.skills],
655
+ claudeMd: [...staticSuggestions.claudeMd],
656
+ agents: [...staticSuggestions.agents],
657
+ plugins: [...staticSuggestions.plugins],
658
+ };
659
+
660
+ for (const ai of aiSuggestions) {
661
+ const type = ai.type;
662
+ if (!result[type]) continue;
663
+
664
+ const aiTitle = normalize(ai.title);
665
+ const idx = result[type].findIndex((s) => {
666
+ const st = normalize(s.title || s.name || s.use_case || "");
667
+ return st === aiTitle || st.includes(aiTitle) || aiTitle.includes(st);
668
+ });
669
+
670
+ if (idx >= 0) {
671
+ result[type][idx] = { ...result[type][idx], ...ai };
672
+ } else {
673
+ result[type].push(ai);
674
+ }
675
+ }
676
+
677
+ for (const type of Object.keys(result)) {
678
+ result[type].sort((a, b) => (priorityOrder[a.priority] ?? 1) - (priorityOrder[b.priority] ?? 1));
679
+ }
680
+
681
+ return result;
682
+ }
package/src/server.js CHANGED
@@ -7,7 +7,7 @@ import { execFile } from "node:child_process";
7
7
  import { promisify } from "node:util";
8
8
  import { parseAllSessions } from "./parser.js";
9
9
  import { scoreAllSessions } from "./scorer.js";
10
- import { streamAIAnalysis, getCachedAnalysis, clearCachedAnalysis, getAvailableModels, killActiveProcesses, detectDefaultModel, buildPrompt, buildDataSnapshot, streamAISuggestions, getCachedSuggestions, clearCachedSuggestions } from "./ai-analyze.js";
10
+ import { streamAIAnalysis, getCachedAnalysis, clearCachedAnalysis, getAvailableModels, killActiveProcesses, detectDefaultModel, buildPrompt, buildDataSnapshot, streamAISuggestions, getCachedSuggestions, clearCachedSuggestions, streamAIOptimizerSuggestions, getCachedOptimizerSuggestions, clearCachedOptimizerSuggestions } from "./ai-analyze.js";
11
11
  import { generateSuggestions } from "./suggestions.js";
12
12
 
13
13
  const execFileAsync = promisify(execFile);
@@ -207,6 +207,29 @@ export function startServer(port = 6543) {
207
207
  return json(res, cached || { content: null });
208
208
  }
209
209
 
210
+ if (url.pathname === "/api/ai-optimizer" && req.method === "POST") {
211
+ const data = await getData();
212
+ const suggestions = generateSuggestions(data);
213
+ const modelId = url.searchParams.get("model") || "";
214
+ res.writeHead(200, {
215
+ "Content-Type": "text/event-stream",
216
+ "Cache-Control": "no-cache",
217
+ Connection: "keep-alive",
218
+ });
219
+ streamAIOptimizerSuggestions(data, suggestions, res, modelId || undefined);
220
+ return;
221
+ }
222
+
223
+ if (url.pathname === "/api/ai-optimizer" && req.method === "DELETE") {
224
+ clearCachedOptimizerSuggestions();
225
+ return json(res, { ok: true });
226
+ }
227
+
228
+ if (url.pathname === "/api/ai-optimizer" && req.method === "GET") {
229
+ const cached = getCachedOptimizerSuggestions();
230
+ return json(res, cached || { suggestions: null });
231
+ }
232
+
210
233
  if (url.pathname === "/api/refresh") {
211
234
  const data = await getData(true);
212
235
  return json(res, { sessions: data.sessions.length, overallScore: data.overallScore });