claude-session-insights 0.3.2 → 0.4.0
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 +14 -0
- package/package.json +1 -1
- package/public/index.html +551 -0
- package/src/ai-analyze.js +183 -0
- package/src/server.js +31 -1
- package/src/suggestions.js +280 -0
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ 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)
|
|
13
14
|
- **AI Insights** — on-demand deeper analysis powered by the Claude CLI, with model picker (Sonnet, Opus, Haiku) and streaming output
|
|
14
15
|
- **Heaviest Sessions** — top sessions ranked by cost for quick identification of expensive outliers
|
|
15
16
|
- **Daily Score Chart** — trend visualization of your efficiency score, session count, tokens, and cost over time
|
|
@@ -76,6 +77,19 @@ The dashboard generates rule-based summaries at two levels:
|
|
|
76
77
|
|
|
77
78
|
**Per-session** — classifies each session (quick fix, focused task, long refactor), identifies the main cost driver, and highlights strengths. Displayed at the top of the session detail view.
|
|
78
79
|
|
|
80
|
+
## Workflow Optimizer
|
|
81
|
+
|
|
82
|
+
Closes the loop from "how am I doing?" to "here's what to build to do better." The optimizer runs two phases:
|
|
83
|
+
|
|
84
|
+
**Phase 1 — Rule-based suggestions (instant):** Detects patterns from your badges, tool usage, session titles, and project history to recommend:
|
|
85
|
+
|
|
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
|
+
- **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
|
+
- **Agents** — subagent configurations for offloading exploration or repetitive task types
|
|
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
|
+
|
|
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.
|
|
92
|
+
|
|
79
93
|
## AI Insights
|
|
80
94
|
|
|
81
95
|
Click "Generate AI Insights" to run a deeper analysis using the Claude CLI. This streams a response via SSE that covers:
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -645,6 +645,129 @@
|
|
|
645
645
|
}
|
|
646
646
|
.ai-prompt-close:hover { color: var(--text); }
|
|
647
647
|
|
|
648
|
+
/* Workflow Optimizer */
|
|
649
|
+
.optimizer-card {
|
|
650
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
651
|
+
border-radius: var(--radius); padding: 24px 28px; margin-bottom: 24px;
|
|
652
|
+
position: relative; overflow: visible; box-shadow: var(--shadow-card);
|
|
653
|
+
}
|
|
654
|
+
.optimizer-card::before {
|
|
655
|
+
content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px;
|
|
656
|
+
background: linear-gradient(90deg, var(--accent), var(--green));
|
|
657
|
+
opacity: 0.7; border-radius: var(--radius) var(--radius) 0 0;
|
|
658
|
+
}
|
|
659
|
+
.optimizer-card h3 {
|
|
660
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: 0.8px;
|
|
661
|
+
color: var(--accent); margin-bottom: 14px; font-weight: 600;
|
|
662
|
+
display: flex; align-items: center; gap: 8px;
|
|
663
|
+
}
|
|
664
|
+
.optimizer-tabs {
|
|
665
|
+
display: flex; gap: 4px; margin-bottom: 16px; flex-wrap: wrap;
|
|
666
|
+
}
|
|
667
|
+
.optimizer-tab {
|
|
668
|
+
padding: 5px 12px; border-radius: 6px; border: 1px solid var(--border);
|
|
669
|
+
background: var(--surface2); color: var(--text2); font-size: 11px;
|
|
670
|
+
font-weight: 500; cursor: pointer; transition: all 0.15s;
|
|
671
|
+
}
|
|
672
|
+
.optimizer-tab:hover { background: var(--surface3); color: var(--text); }
|
|
673
|
+
.optimizer-tab.active {
|
|
674
|
+
background: var(--accent-dim); border-color: rgba(37,99,235,0.3);
|
|
675
|
+
color: var(--accent); font-weight: 600;
|
|
676
|
+
}
|
|
677
|
+
.optimizer-tab .tab-count {
|
|
678
|
+
display: inline-block; min-width: 16px; height: 16px; border-radius: 8px;
|
|
679
|
+
background: var(--surface3); color: var(--text3); font-size: 10px;
|
|
680
|
+
text-align: center; line-height: 16px; margin-left: 4px;
|
|
681
|
+
}
|
|
682
|
+
.optimizer-tab.active .tab-count {
|
|
683
|
+
background: rgba(37,99,235,0.2); color: var(--accent);
|
|
684
|
+
}
|
|
685
|
+
.optimizer-panel { display: none; }
|
|
686
|
+
.optimizer-panel.active { display: block; }
|
|
687
|
+
.suggestion-empty {
|
|
688
|
+
color: var(--text3); font-size: 13px; padding: 16px 0;
|
|
689
|
+
display: flex; align-items: center; gap: 8px;
|
|
690
|
+
}
|
|
691
|
+
.suggestion-card {
|
|
692
|
+
border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px;
|
|
693
|
+
margin-bottom: 10px; background: var(--surface2);
|
|
694
|
+
transition: border-color 0.15s;
|
|
695
|
+
}
|
|
696
|
+
.suggestion-card:last-child { margin-bottom: 0; }
|
|
697
|
+
.suggestion-card:hover { border-color: var(--border-hover); }
|
|
698
|
+
.suggestion-header {
|
|
699
|
+
display: flex; align-items: flex-start; justify-content: space-between; gap: 10px;
|
|
700
|
+
margin-bottom: 6px;
|
|
701
|
+
}
|
|
702
|
+
.suggestion-title { font-size: 13px; font-weight: 600; color: var(--text); flex: 1; }
|
|
703
|
+
.suggestion-priority {
|
|
704
|
+
font-size: 10px; font-weight: 600; padding: 2px 8px; border-radius: 10px;
|
|
705
|
+
white-space: nowrap; flex-shrink: 0;
|
|
706
|
+
}
|
|
707
|
+
.priority-high { background: var(--red-dim); color: var(--red); }
|
|
708
|
+
.priority-medium { background: var(--yellow-dim); color: var(--yellow); }
|
|
709
|
+
.priority-low { background: var(--surface3); color: var(--text3); }
|
|
710
|
+
.suggestion-rationale {
|
|
711
|
+
font-size: 12px; color: var(--text2); line-height: 1.6; margin-bottom: 8px;
|
|
712
|
+
}
|
|
713
|
+
.suggestion-meta {
|
|
714
|
+
font-size: 11px; color: var(--text3); font-family: var(--mono);
|
|
715
|
+
display: flex; gap: 12px; flex-wrap: wrap;
|
|
716
|
+
}
|
|
717
|
+
.suggestion-meta span { display: flex; align-items: center; gap: 4px; }
|
|
718
|
+
.optimizer-ai-section {
|
|
719
|
+
margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--border);
|
|
720
|
+
}
|
|
721
|
+
.optimizer-ai-controls {
|
|
722
|
+
display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px;
|
|
723
|
+
}
|
|
724
|
+
.optimizer-generate-btn {
|
|
725
|
+
background: var(--accent-dim); border: 1px solid rgba(37,99,235,0.2);
|
|
726
|
+
color: var(--accent); padding: 8px 16px; border-radius: 7px;
|
|
727
|
+
cursor: pointer; font-size: 12px; font-weight: 600; transition: all 0.2s;
|
|
728
|
+
}
|
|
729
|
+
.optimizer-generate-btn:hover {
|
|
730
|
+
background: rgba(37,99,235,0.15); border-color: rgba(37,99,235,0.3);
|
|
731
|
+
}
|
|
732
|
+
.optimizer-generate-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
733
|
+
.optimizer-ai-content {
|
|
734
|
+
font-size: 13px; line-height: 1.75; color: var(--text);
|
|
735
|
+
}
|
|
736
|
+
.optimizer-ai-content h3 {
|
|
737
|
+
font-size: 13px; font-weight: 700; color: var(--text);
|
|
738
|
+
margin: 16px 0 8px; text-transform: none; letter-spacing: normal;
|
|
739
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
740
|
+
}
|
|
741
|
+
.optimizer-ai-content p { margin-bottom: 8px; }
|
|
742
|
+
.optimizer-ai-content ul { padding-left: 18px; margin-bottom: 8px; }
|
|
743
|
+
.optimizer-ai-content li { margin-bottom: 4px; list-style: disc; }
|
|
744
|
+
.optimizer-ai-content strong { color: var(--text); }
|
|
745
|
+
.optimizer-ai-content code {
|
|
746
|
+
font-family: var(--mono); font-size: 11px; background: var(--surface3);
|
|
747
|
+
padding: 1px 5px; border-radius: 3px;
|
|
748
|
+
}
|
|
749
|
+
.optimizer-ai-content pre {
|
|
750
|
+
background: var(--surface3); border-radius: 6px; padding: 12px 14px;
|
|
751
|
+
font-family: var(--mono); font-size: 11px; line-height: 1.6;
|
|
752
|
+
overflow-x: auto; margin-bottom: 10px; white-space: pre-wrap; word-break: break-word;
|
|
753
|
+
position: relative;
|
|
754
|
+
}
|
|
755
|
+
.optimizer-ai-content hr {
|
|
756
|
+
border: none; border-top: 1px solid var(--border); margin: 16px 0;
|
|
757
|
+
}
|
|
758
|
+
.copy-btn {
|
|
759
|
+
background: var(--surface); border: 1px solid var(--border); color: var(--text3);
|
|
760
|
+
padding: 2px 8px; border-radius: 5px; cursor: pointer; font-size: 10px;
|
|
761
|
+
font-family: var(--mono); transition: all 0.15s; white-space: nowrap;
|
|
762
|
+
}
|
|
763
|
+
.copy-btn:hover { color: var(--text); border-color: var(--border-hover); }
|
|
764
|
+
.copy-btn.copied { color: var(--green); border-color: rgba(22,163,74,0.3); }
|
|
765
|
+
.optimizer-timestamp {
|
|
766
|
+
font-size: 10px; color: var(--text3); font-family: var(--mono);
|
|
767
|
+
margin-top: 14px; padding-top: 10px; border-top: 1px solid var(--border);
|
|
768
|
+
display: flex; align-items: center; gap: 10px;
|
|
769
|
+
}
|
|
770
|
+
|
|
648
771
|
/* Share modal */
|
|
649
772
|
.share-btn {
|
|
650
773
|
background: var(--surface2); border: 1px solid var(--border); color: var(--text2);
|
|
@@ -653,6 +776,65 @@
|
|
|
653
776
|
transition: all 0.2s; font-size: 12px; font-weight: 500; white-space: nowrap;
|
|
654
777
|
}
|
|
655
778
|
.share-btn:hover { background: var(--surface3); color: var(--text); border-color: var(--border-hover); }
|
|
779
|
+
/* What's New */
|
|
780
|
+
.whatsnew-overlay {
|
|
781
|
+
display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.45);
|
|
782
|
+
z-index: 1100; align-items: center; justify-content: center;
|
|
783
|
+
backdrop-filter: blur(3px);
|
|
784
|
+
}
|
|
785
|
+
.whatsnew-overlay.open { display: flex; }
|
|
786
|
+
.whatsnew-modal {
|
|
787
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
788
|
+
border-radius: var(--radius); padding: 28px 32px; max-width: 480px; width: 90%;
|
|
789
|
+
box-shadow: 0 12px 40px rgba(0,0,0,0.22); position: relative;
|
|
790
|
+
}
|
|
791
|
+
.whatsnew-modal::before {
|
|
792
|
+
content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px;
|
|
793
|
+
background: linear-gradient(90deg, var(--accent), var(--green));
|
|
794
|
+
border-radius: var(--radius) var(--radius) 0 0;
|
|
795
|
+
}
|
|
796
|
+
.whatsnew-version {
|
|
797
|
+
font-size: 10px; font-family: var(--mono); color: var(--accent);
|
|
798
|
+
font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px;
|
|
799
|
+
margin-bottom: 6px;
|
|
800
|
+
}
|
|
801
|
+
.whatsnew-modal h2 {
|
|
802
|
+
font-size: 17px; font-weight: 700; color: var(--text);
|
|
803
|
+
}
|
|
804
|
+
.whatsnew-items { list-style: none; margin-bottom: 22px; }
|
|
805
|
+
.whatsnew-items li {
|
|
806
|
+
display: flex; gap: 10px; align-items: flex-start;
|
|
807
|
+
font-size: 13px; color: var(--text2); line-height: 1.55; margin-bottom: 12px;
|
|
808
|
+
}
|
|
809
|
+
.whatsnew-items li:last-child { margin-bottom: 0; }
|
|
810
|
+
.whatsnew-items li .wi-icon {
|
|
811
|
+
flex-shrink: 0; width: 20px; height: 20px; border-radius: 5px;
|
|
812
|
+
background: var(--accent-dim); color: var(--accent);
|
|
813
|
+
display: flex; align-items: center; justify-content: center;
|
|
814
|
+
font-size: 11px; margin-top: 1px;
|
|
815
|
+
}
|
|
816
|
+
.whatsnew-items li strong { color: var(--text); }
|
|
817
|
+
.whatsnew-x {
|
|
818
|
+
position: absolute; top: 12px; right: 14px; background: none; border: none;
|
|
819
|
+
color: var(--text3); cursor: pointer; font-size: 18px; line-height: 1; padding: 4px;
|
|
820
|
+
}
|
|
821
|
+
.whatsnew-x:hover { color: var(--text); }
|
|
822
|
+
.whatsnew-actions { display: flex; gap: 8px; }
|
|
823
|
+
.whatsnew-dismiss {
|
|
824
|
+
flex: 1; padding: 9px; border-radius: 7px;
|
|
825
|
+
background: var(--accent-dim); border: 1px solid rgba(37,99,235,0.2);
|
|
826
|
+
color: var(--accent); font-size: 13px; font-weight: 600;
|
|
827
|
+
cursor: pointer; transition: all 0.15s;
|
|
828
|
+
}
|
|
829
|
+
.whatsnew-dismiss:hover { background: rgba(37,99,235,0.15); }
|
|
830
|
+
.whatsnew-showme {
|
|
831
|
+
padding: 3px 10px; border-radius: 5px;
|
|
832
|
+
background: transparent; border: 1px solid var(--border);
|
|
833
|
+
color: var(--text3); font-size: 11px; font-weight: 500;
|
|
834
|
+
cursor: pointer; transition: all 0.15s; white-space: nowrap;
|
|
835
|
+
}
|
|
836
|
+
.whatsnew-showme:hover { color: var(--accent); border-color: var(--accent); }
|
|
837
|
+
|
|
656
838
|
.share-overlay {
|
|
657
839
|
display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.4);
|
|
658
840
|
z-index: 1000; align-items: center; justify-content: center;
|
|
@@ -1079,6 +1261,14 @@ function renderDashboard(data) {
|
|
|
1079
1261
|
</div>
|
|
1080
1262
|
</div>
|
|
1081
1263
|
</section>
|
|
1264
|
+
<section id="optimizer-section">
|
|
1265
|
+
<div class="optimizer-card">
|
|
1266
|
+
<h3>Workflow Optimizer</h3>
|
|
1267
|
+
<div id="optimizer-body">
|
|
1268
|
+
<div style="color:var(--text3); font-size:13px; padding: 4px 0 12px;">Loading suggestions<span id="optimizer-dots"></span></div>
|
|
1269
|
+
</div>
|
|
1270
|
+
</div>
|
|
1271
|
+
</section>
|
|
1082
1272
|
<div class="score-breakdown-overlay" id="score-breakdown-overlay" onclick="if(event.target===this)toggleScoreBreakdown()">
|
|
1083
1273
|
<div class="score-breakdown">
|
|
1084
1274
|
<button class="score-breakdown-close" onclick="toggleScoreBreakdown()">×</button>
|
|
@@ -1120,6 +1310,33 @@ function renderDashboard(data) {
|
|
|
1120
1310
|
<pre id="ai-prompt-content">Loading...</pre>
|
|
1121
1311
|
</div>
|
|
1122
1312
|
</div>
|
|
1313
|
+
<div class="whatsnew-overlay" id="whatsnew-overlay" onclick="if(event.target===this)closeWhatsNew(false)">
|
|
1314
|
+
<div class="whatsnew-modal">
|
|
1315
|
+
<button class="whatsnew-x" onclick="closeWhatsNew(false)">×</button>
|
|
1316
|
+
<div class="whatsnew-version">v0.4.0 — What's New</div>
|
|
1317
|
+
<div style="display:flex; align-items:baseline; gap:10px; margin-bottom:18px;">
|
|
1318
|
+
<h2 style="margin-bottom:0">Workflow Optimizer</h2>
|
|
1319
|
+
<button class="whatsnew-showme" onclick="showMeWhatsNew()">Take me there!</button>
|
|
1320
|
+
</div>
|
|
1321
|
+
<ul class="whatsnew-items">
|
|
1322
|
+
<li>
|
|
1323
|
+
<span class="wi-icon">⚙</span>
|
|
1324
|
+
<span><strong>Rule-based suggestions</strong> — analyzes your badges, tool patterns, and session history to recommend skills, CLAUDE.md files, and subagents to create</span>
|
|
1325
|
+
</li>
|
|
1326
|
+
<li>
|
|
1327
|
+
<span class="wi-icon">✨</span>
|
|
1328
|
+
<span><strong>AI artifact generation</strong> — streams ready-to-copy content: full CLAUDE.md text, skill prompt bodies, and agent configs tailored to your workflow</span>
|
|
1329
|
+
</li>
|
|
1330
|
+
<li>
|
|
1331
|
+
<span class="wi-icon">🔒</span>
|
|
1332
|
+
<span><strong>Context-aware plugin advice</strong> — only suggests MCP plugins where CLI tools have a real gap, avoiding unnecessary context overhead</span>
|
|
1333
|
+
</li>
|
|
1334
|
+
</ul>
|
|
1335
|
+
<div class="whatsnew-actions">
|
|
1336
|
+
<button class="whatsnew-dismiss" onclick="closeWhatsNew(true)">OK. Don't show again.</button>
|
|
1337
|
+
</div>
|
|
1338
|
+
</div>
|
|
1339
|
+
</div>
|
|
1123
1340
|
<div class="share-overlay" id="share-overlay" onclick="if(event.target===this)closeShareModal()">
|
|
1124
1341
|
<div class="share-modal">
|
|
1125
1342
|
<button class="share-close" onclick="closeShareModal()">×</button>
|
|
@@ -1590,6 +1807,309 @@ async function loadCachedAIInsights() {
|
|
|
1590
1807
|
} catch {}
|
|
1591
1808
|
}
|
|
1592
1809
|
|
|
1810
|
+
// --- Workflow Optimizer ---
|
|
1811
|
+
|
|
1812
|
+
let optimizerSuggestions = null;
|
|
1813
|
+
let activeOptimizerTab = 'all';
|
|
1814
|
+
let cachedOptimizerAI = null;
|
|
1815
|
+
|
|
1816
|
+
const OPTIMIZER_TABS = [
|
|
1817
|
+
{ id: 'all', label: 'All' },
|
|
1818
|
+
{ id: 'skills', label: 'Skills' },
|
|
1819
|
+
{ id: 'claudeMd', label: 'CLAUDE.md' },
|
|
1820
|
+
{ id: 'agents', label: 'Agents' },
|
|
1821
|
+
{ id: 'plugins', label: 'Plugins' },
|
|
1822
|
+
];
|
|
1823
|
+
|
|
1824
|
+
async function loadSuggestions() {
|
|
1825
|
+
let dotCount = 0;
|
|
1826
|
+
const dotsEl = document.getElementById('optimizer-dots');
|
|
1827
|
+
const dotTimer = setInterval(() => {
|
|
1828
|
+
dotCount = (dotCount + 1) % 4;
|
|
1829
|
+
if (dotsEl) dotsEl.textContent = '.'.repeat(dotCount);
|
|
1830
|
+
}, 400);
|
|
1831
|
+
|
|
1832
|
+
try {
|
|
1833
|
+
const res = await fetch('/api/suggestions');
|
|
1834
|
+
optimizerSuggestions = await res.json();
|
|
1835
|
+
clearInterval(dotTimer);
|
|
1836
|
+
renderOptimizerTabs();
|
|
1837
|
+
} catch {
|
|
1838
|
+
clearInterval(dotTimer);
|
|
1839
|
+
const body = document.getElementById('optimizer-body');
|
|
1840
|
+
if (body) body.innerHTML = '<div class="suggestion-empty">Could not load suggestions.</div>';
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
function renderOptimizerTabs() {
|
|
1845
|
+
const body = document.getElementById('optimizer-body');
|
|
1846
|
+
if (!body || !optimizerSuggestions) return;
|
|
1847
|
+
|
|
1848
|
+
const total = OPTIMIZER_TABS.reduce((s, t) => s + (optimizerSuggestions[t.id] || []).length, 0);
|
|
1849
|
+
if (total === 0) {
|
|
1850
|
+
body.innerHTML = '<div class="suggestion-empty">No suggestions — your workflow looks well-optimized!</div>';
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
const tabsHtml = OPTIMIZER_TABS.map(t => {
|
|
1855
|
+
const count = t.id === 'all' ? total : (optimizerSuggestions[t.id] || []).length;
|
|
1856
|
+
return `<button class="optimizer-tab${t.id === activeOptimizerTab ? ' active' : ''}" onclick="switchOptimizerTab('${t.id}')">
|
|
1857
|
+
${t.label}<span class="tab-count">${count}</span>
|
|
1858
|
+
</button>`;
|
|
1859
|
+
}).join('');
|
|
1860
|
+
|
|
1861
|
+
const allPanelHtml = OPTIMIZER_TABS.filter(t => t.id !== 'all').map(t => {
|
|
1862
|
+
const items = optimizerSuggestions[t.id] || [];
|
|
1863
|
+
if (!items.length) return '';
|
|
1864
|
+
return `<div style="margin-bottom:18px">
|
|
1865
|
+
<div style="font-size:10px;text-transform:uppercase;letter-spacing:0.7px;color:var(--text3);font-weight:600;margin-bottom:8px">${t.label}</div>
|
|
1866
|
+
${renderSuggestionList(t.id, items)}
|
|
1867
|
+
</div>`;
|
|
1868
|
+
}).join('');
|
|
1869
|
+
|
|
1870
|
+
const panelsHtml = OPTIMIZER_TABS.map(t => `
|
|
1871
|
+
<div class="optimizer-panel${t.id === activeOptimizerTab ? ' active' : ''}" id="optimizer-panel-${t.id}">
|
|
1872
|
+
${t.id === 'all' ? allPanelHtml : renderSuggestionList(t.id, optimizerSuggestions[t.id] || [])}
|
|
1873
|
+
</div>`).join('');
|
|
1874
|
+
|
|
1875
|
+
const aiSection = `
|
|
1876
|
+
<div class="optimizer-ai-section">
|
|
1877
|
+
<div class="optimizer-ai-controls">
|
|
1878
|
+
<button class="optimizer-generate-btn" onclick="generateOptimizerContent()" id="optimizer-gen-btn">
|
|
1879
|
+
Generate artifact content with AI
|
|
1880
|
+
</button>
|
|
1881
|
+
<span class="ai-model-picker" id="optimizer-model-picker">
|
|
1882
|
+
<span class="ai-model-pill" onclick="toggleModelMenu(event)" data-tip="Click to switch model"></span>
|
|
1883
|
+
<div class="ai-model-menu"></div>
|
|
1884
|
+
</span>
|
|
1885
|
+
</div>
|
|
1886
|
+
<div id="optimizer-ai-output"></div>
|
|
1887
|
+
</div>`;
|
|
1888
|
+
|
|
1889
|
+
body.innerHTML = `
|
|
1890
|
+
<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.
|
|
1892
|
+
</p>
|
|
1893
|
+
<div class="optimizer-tabs">${tabsHtml}</div>
|
|
1894
|
+
${panelsHtml}
|
|
1895
|
+
${aiSection}`;
|
|
1896
|
+
|
|
1897
|
+
// Sync model picker — reuse same aiModels
|
|
1898
|
+
const optimizerPicker = document.getElementById('optimizer-model-picker');
|
|
1899
|
+
if (optimizerPicker && aiModels.length > 0) {
|
|
1900
|
+
const current = aiModels.find(m => m.id === selectedModelId) ||
|
|
1901
|
+
aiModels.find(m => m.id === aiDefaultModel) || aiModels[0];
|
|
1902
|
+
const pill = optimizerPicker.querySelector('.ai-model-pill');
|
|
1903
|
+
const menu = optimizerPicker.querySelector('.ai-model-menu');
|
|
1904
|
+
if (pill) pill.innerHTML = `${current.label} <span class="pill-chevron">▾</span>`;
|
|
1905
|
+
if (menu) menu.innerHTML = aiModels.map(m =>
|
|
1906
|
+
`<div class="ai-model-option${m.id === selectedModelId ? ' active' : ''}" data-model="${m.id}" onclick="selectModel('${m.id}')">` +
|
|
1907
|
+
`<span class="check">${m.id === selectedModelId ? '●' : ''}</span>${m.label}</div>`
|
|
1908
|
+
).join('');
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
if (cachedOptimizerAI) {
|
|
1912
|
+
renderOptimizerAIComplete(cachedOptimizerAI.content, cachedOptimizerAI.generatedAt, cachedOptimizerAI.model);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
function renderSuggestionList(type, items) {
|
|
1917
|
+
if (!items.length) return '<div class="suggestion-empty">No suggestions for this category.</div>';
|
|
1918
|
+
return items.map(item => {
|
|
1919
|
+
const priorityClass = `priority-${item.priority || 'medium'}`;
|
|
1920
|
+
const priorityLabel = (item.priority || 'medium').charAt(0).toUpperCase() + (item.priority || 'medium').slice(1);
|
|
1921
|
+
let meta = '';
|
|
1922
|
+
if (type === 'skills' && item.trigger) meta = `<span>trigger: <strong>${escHtml(item.trigger)}</strong></span>`;
|
|
1923
|
+
if (type === 'claudeMd') meta = `<span>scope: <strong>${escHtml(item.scope)}${item.projectName ? ' · ' + escHtml(item.projectName) : ''}</strong></span>`;
|
|
1924
|
+
if (type === 'plugins' && item.installCmd) meta = `<span style="font-family:var(--mono)">${escHtml(item.installCmd)}</span>`;
|
|
1925
|
+
if (type === 'agents' && item.use_case) meta = `<span>${escHtml(item.use_case)}</span>`;
|
|
1926
|
+
return `
|
|
1927
|
+
<div class="suggestion-card">
|
|
1928
|
+
<div class="suggestion-header">
|
|
1929
|
+
<div class="suggestion-title">${escHtml(item.title || item.name || item.use_case || '')}</div>
|
|
1930
|
+
<span class="suggestion-priority ${priorityClass}">${priorityLabel}</span>
|
|
1931
|
+
</div>
|
|
1932
|
+
<div class="suggestion-rationale">${escHtml(item.rationale)}</div>
|
|
1933
|
+
${meta ? `<div class="suggestion-meta">${meta}</div>` : ''}
|
|
1934
|
+
</div>`;
|
|
1935
|
+
}).join('');
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
function switchOptimizerTab(tabId) {
|
|
1939
|
+
activeOptimizerTab = tabId;
|
|
1940
|
+
document.querySelectorAll('.optimizer-panel').forEach(p => {
|
|
1941
|
+
p.classList.toggle('active', p.id === `optimizer-panel-${tabId}`);
|
|
1942
|
+
});
|
|
1943
|
+
document.querySelectorAll('.optimizer-tab').forEach(t => {
|
|
1944
|
+
const onclick = t.getAttribute('onclick') || '';
|
|
1945
|
+
const match = onclick.match(/switchOptimizerTab\('([^']+)'\)/);
|
|
1946
|
+
if (match) t.classList.toggle('active', match[1] === tabId);
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
function generateOptimizerContent() {
|
|
1951
|
+
const btn = document.getElementById('optimizer-gen-btn');
|
|
1952
|
+
const output = document.getElementById('optimizer-ai-output');
|
|
1953
|
+
if (!btn || !output) return;
|
|
1954
|
+
|
|
1955
|
+
const wasActive = autoRefreshActive;
|
|
1956
|
+
if (autoRefreshActive) pauseAutoRefresh();
|
|
1957
|
+
|
|
1958
|
+
const modelId = selectedModelId;
|
|
1959
|
+
const modelParam = modelId ? `?model=${encodeURIComponent(modelId)}` : '';
|
|
1960
|
+
|
|
1961
|
+
btn.disabled = true;
|
|
1962
|
+
output.innerHTML = `
|
|
1963
|
+
<div class="ai-loading">
|
|
1964
|
+
<div class="ai-spinner"></div>
|
|
1965
|
+
<div>Generating with ${modelId ? modelLabel(modelId) : 'default model'}, please wait...</div>
|
|
1966
|
+
</div>
|
|
1967
|
+
<div class="optimizer-ai-content" id="optimizer-stream-content"></div>`;
|
|
1968
|
+
|
|
1969
|
+
streamOptimizerFetch(modelParam, output, wasActive);
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
async function streamOptimizerFetch(modelParam, output, wasActive) {
|
|
1973
|
+
let fullContent = '';
|
|
1974
|
+
let usedModel = '';
|
|
1975
|
+
|
|
1976
|
+
try {
|
|
1977
|
+
const res = await fetch('/api/ai-suggestions' + modelParam, { method: 'POST' });
|
|
1978
|
+
const reader = res.body.getReader();
|
|
1979
|
+
const decoder = new TextDecoder();
|
|
1980
|
+
let buffer = '';
|
|
1981
|
+
|
|
1982
|
+
while (true) {
|
|
1983
|
+
const { done, value } = await reader.read();
|
|
1984
|
+
if (done) break;
|
|
1985
|
+
|
|
1986
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1987
|
+
const lines = buffer.split('\n');
|
|
1988
|
+
buffer = lines.pop();
|
|
1989
|
+
|
|
1990
|
+
let currentEvent = '';
|
|
1991
|
+
for (const line of lines) {
|
|
1992
|
+
if (line.startsWith('event: ')) {
|
|
1993
|
+
currentEvent = line.slice(7);
|
|
1994
|
+
} else if (line.startsWith('data: ')) {
|
|
1995
|
+
const data = line.slice(6);
|
|
1996
|
+
if (currentEvent === 'model') {
|
|
1997
|
+
usedModel = JSON.parse(data);
|
|
1998
|
+
const loading = output.querySelector('.ai-loading div:last-child');
|
|
1999
|
+
if (loading) loading.textContent = `Generating with ${modelLabel(usedModel)}, please wait...`;
|
|
2000
|
+
} else if (currentEvent === 'chunk') {
|
|
2001
|
+
fullContent += JSON.parse(data);
|
|
2002
|
+
const el = document.getElementById('optimizer-stream-content');
|
|
2003
|
+
if (el) el.innerHTML = renderOptimizerMarkdown(escHtml(fullContent));
|
|
2004
|
+
} else if (currentEvent === 'done') {
|
|
2005
|
+
const { generatedAt, model } = JSON.parse(data);
|
|
2006
|
+
if (model) usedModel = model;
|
|
2007
|
+
const loading = output.querySelector('.ai-loading');
|
|
2008
|
+
if (loading) loading.remove();
|
|
2009
|
+
renderOptimizerAIComplete(fullContent, generatedAt, usedModel);
|
|
2010
|
+
if (wasActive) resumeAutoRefreshIfPaused();
|
|
2011
|
+
} else if (currentEvent === 'error') {
|
|
2012
|
+
const { message } = JSON.parse(data);
|
|
2013
|
+
const btn = document.getElementById('optimizer-gen-btn');
|
|
2014
|
+
if (btn) btn.disabled = false;
|
|
2015
|
+
output.innerHTML = `<div class="ai-error">${escHtml(message)}</div>
|
|
2016
|
+
<button class="optimizer-generate-btn" style="margin-top:8px" onclick="generateOptimizerContent()">Retry</button>`;
|
|
2017
|
+
if (wasActive) resumeAutoRefreshIfPaused();
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
} catch (err) {
|
|
2023
|
+
const btn = document.getElementById('optimizer-gen-btn');
|
|
2024
|
+
if (btn) btn.disabled = false;
|
|
2025
|
+
output.innerHTML = `<div class="ai-error">Failed to connect to server</div>
|
|
2026
|
+
<button class="optimizer-generate-btn" style="margin-top:8px" onclick="generateOptimizerContent()">Retry</button>`;
|
|
2027
|
+
if (wasActive) resumeAutoRefreshIfPaused();
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
function renderOptimizerMarkdown(text) {
|
|
2032
|
+
// Handle code blocks first to protect their content
|
|
2033
|
+
const codeBlocks = [];
|
|
2034
|
+
text = text.replace(/```[\s\S]*?```/g, (match) => {
|
|
2035
|
+
const idx = codeBlocks.length;
|
|
2036
|
+
codeBlocks.push(match);
|
|
2037
|
+
return `__CODEBLOCK_${idx}__`;
|
|
2038
|
+
});
|
|
2039
|
+
|
|
2040
|
+
text = text
|
|
2041
|
+
.replace(/^### (.+)$/gm, (_, t) =>
|
|
2042
|
+
`<h3>${t} <button class="copy-btn" onclick="copySection(this)">copy</button></h3>`)
|
|
2043
|
+
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
|
2044
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
2045
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
2046
|
+
.replace(/^\- (.+)$/gm, '<li>$1</li>')
|
|
2047
|
+
.replace(/(<li>.*<\/li>\n?)+/gs, m => '<ul>' + m + '</ul>')
|
|
2048
|
+
.replace(/^---$/gm, '<hr>')
|
|
2049
|
+
.replace(/\n{2,}/g, '</p><p>')
|
|
2050
|
+
.replace(/^(?!<[hul])/gm, s => s ? '<p>' + s : '')
|
|
2051
|
+
.replace(/<p><\/p>/g, '')
|
|
2052
|
+
.replace(/<p>(<[hul])/g, '$1');
|
|
2053
|
+
|
|
2054
|
+
// Restore code blocks as <pre>
|
|
2055
|
+
text = text.replace(/__CODEBLOCK_(\d+)__/g, (_, i) => {
|
|
2056
|
+
const raw = codeBlocks[parseInt(i)];
|
|
2057
|
+
const inner = raw.replace(/^```[^\n]*\n?/, '').replace(/```$/, '');
|
|
2058
|
+
return `<pre>${escHtml(inner)}<button class="copy-btn" style="position:absolute;top:6px;right:6px" onclick="copyPre(this)">copy</button></pre>`;
|
|
2059
|
+
});
|
|
2060
|
+
|
|
2061
|
+
return text;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
function renderOptimizerAIComplete(content, generatedAt, model) {
|
|
2065
|
+
cachedOptimizerAI = { content, generatedAt, model };
|
|
2066
|
+
const output = document.getElementById('optimizer-ai-output');
|
|
2067
|
+
const btn = document.getElementById('optimizer-gen-btn');
|
|
2068
|
+
if (!output) return;
|
|
2069
|
+
if (btn) btn.disabled = false;
|
|
2070
|
+
output.innerHTML = `
|
|
2071
|
+
<div class="optimizer-ai-content">${renderOptimizerMarkdown(escHtml(content))}</div>
|
|
2072
|
+
<div class="optimizer-timestamp">
|
|
2073
|
+
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>
|
|
2075
|
+
<button class="optimizer-generate-btn" style="padding:4px 12px;font-size:11px;background:var(--surface2);color:var(--text2)" onclick="clearOptimizerAI()">Clear</button>
|
|
2076
|
+
</div>`;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
function clearOptimizerAI() {
|
|
2080
|
+
cachedOptimizerAI = null;
|
|
2081
|
+
fetch('/api/ai-suggestions', { method: 'DELETE' }).catch(() => {});
|
|
2082
|
+
const output = document.getElementById('optimizer-ai-output');
|
|
2083
|
+
const btn = document.getElementById('optimizer-gen-btn');
|
|
2084
|
+
if (output) output.innerHTML = '';
|
|
2085
|
+
if (btn) btn.disabled = false;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
function copySection(btn) {
|
|
2089
|
+
const h3 = btn.closest('h3');
|
|
2090
|
+
const section = [];
|
|
2091
|
+
let el = h3?.nextElementSibling;
|
|
2092
|
+
while (el && el.tagName !== 'H3' && el.tagName !== 'HR') {
|
|
2093
|
+
section.push(el.innerText || el.textContent);
|
|
2094
|
+
el = el.nextElementSibling;
|
|
2095
|
+
}
|
|
2096
|
+
const text = section.join('\n\n').trim();
|
|
2097
|
+
if (text) copyToClipboard(btn, text);
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
function copyPre(btn) {
|
|
2101
|
+
const pre = btn.closest('pre');
|
|
2102
|
+
if (pre) copyToClipboard(btn, pre.textContent.replace('copy', '').trim());
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
function copyToClipboard(btn, text) {
|
|
2106
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
2107
|
+
btn.textContent = 'copied!';
|
|
2108
|
+
btn.classList.add('copied');
|
|
2109
|
+
setTimeout(() => { btn.textContent = 'copy'; btn.classList.remove('copied'); }, 2000);
|
|
2110
|
+
}).catch(() => {});
|
|
2111
|
+
}
|
|
2112
|
+
|
|
1593
2113
|
// --- Share as PNG ---
|
|
1594
2114
|
|
|
1595
2115
|
const BADGE_EMOJI = BADGE_ICONS;
|
|
@@ -1998,15 +2518,46 @@ function resumeAutoRefreshIfPaused() {
|
|
|
1998
2518
|
|
|
1999
2519
|
document.addEventListener('keydown', e => {
|
|
2000
2520
|
if (e.key === 'Escape' && currentView === 'session') closeSessionDrawer();
|
|
2521
|
+
if (e.key === 'Escape' && document.getElementById('whatsnew-overlay')?.classList.contains('open')) closeWhatsNew(false);
|
|
2001
2522
|
});
|
|
2002
2523
|
|
|
2003
2524
|
fetchData().then(data => {
|
|
2004
2525
|
currentData = data;
|
|
2005
2526
|
renderDashboard(data);
|
|
2527
|
+
maybeShowWhatsNew();
|
|
2006
2528
|
loadCachedAIInsights();
|
|
2529
|
+
loadSuggestions();
|
|
2007
2530
|
if (autoRefreshActive) startAutoRefresh();
|
|
2008
2531
|
});
|
|
2009
2532
|
|
|
2533
|
+
// --- What's New ---
|
|
2534
|
+
|
|
2535
|
+
const WHATS_NEW_VERSION = '0.4.0';
|
|
2536
|
+
|
|
2537
|
+
function closeWhatsNew(persist) {
|
|
2538
|
+
document.getElementById('whatsnew-overlay').classList.remove('open');
|
|
2539
|
+
if (persist) localStorage.setItem('seenWhatsNew', WHATS_NEW_VERSION);
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
function showMeWhatsNew() {
|
|
2543
|
+
closeWhatsNew(false);
|
|
2544
|
+
setTimeout(() => {
|
|
2545
|
+
const el = document.getElementById('optimizer-section');
|
|
2546
|
+
if (el) {
|
|
2547
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
2548
|
+
el.style.transition = 'box-shadow 0.3s';
|
|
2549
|
+
el.style.boxShadow = '0 0 0 2px var(--accent)';
|
|
2550
|
+
setTimeout(() => { el.style.boxShadow = ''; }, 1800);
|
|
2551
|
+
}
|
|
2552
|
+
}, 200);
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
function maybeShowWhatsNew() {
|
|
2556
|
+
if (localStorage.getItem('seenWhatsNew') !== WHATS_NEW_VERSION) {
|
|
2557
|
+
document.getElementById('whatsnew-overlay').classList.add('open');
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2010
2561
|
// Live reload via SSE
|
|
2011
2562
|
const sse = new EventSource('/api/reload');
|
|
2012
2563
|
sse.onmessage = async (e) => {
|
package/src/ai-analyze.js
CHANGED
|
@@ -124,7 +124,79 @@ Keep the entire response under 400 words. Use markdown formatting.`;
|
|
|
124
124
|
|
|
125
125
|
export { buildPrompt, buildDataSnapshot };
|
|
126
126
|
|
|
127
|
+
function buildSuggestionsPrompt(scoredData, suggestions) {
|
|
128
|
+
const { sessions, badges, overallScore } = scoredData;
|
|
129
|
+
const totalCost = sessions.reduce((s, x) => s + x.totals.estimatedCost, 0);
|
|
130
|
+
const projectNames = [...new Set(sessions.map((s) => s.project).filter(Boolean))].slice(0, 5);
|
|
131
|
+
const badgeNames = badges.map((b) => `${b.negative ? "⚠ " : "✓ "}${b.name}`).join(", ");
|
|
132
|
+
|
|
133
|
+
const topSkill = suggestions.skills[0];
|
|
134
|
+
const topClaudeMd = suggestions.claudeMd[0];
|
|
135
|
+
const topAgent = suggestions.agents[0];
|
|
136
|
+
const topPlugin = suggestions.plugins[0];
|
|
137
|
+
|
|
138
|
+
const parts = [];
|
|
139
|
+
|
|
140
|
+
if (topSkill) {
|
|
141
|
+
parts.push(`SKILL: "${topSkill.trigger}" — ${topSkill.title}
|
|
142
|
+
Template hint: ${topSkill.templateHint}`);
|
|
143
|
+
}
|
|
144
|
+
if (topClaudeMd) {
|
|
145
|
+
parts.push(`CLAUDE.MD (${topClaudeMd.scope}${topClaudeMd.projectName ? ` for ${topClaudeMd.projectName}` : ""}):
|
|
146
|
+
Sections to include: ${topClaudeMd.sections.join(", ")}`);
|
|
147
|
+
}
|
|
148
|
+
if (topAgent) {
|
|
149
|
+
parts.push(`AGENT: ${topAgent.name} — ${topAgent.use_case}
|
|
150
|
+
Example usage: ${topAgent.example || "n/a"}`);
|
|
151
|
+
}
|
|
152
|
+
if (topPlugin) {
|
|
153
|
+
parts.push(`PLUGIN: ${topPlugin.name}
|
|
154
|
+
Install: ${topPlugin.installCmd}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return `You are helping a developer improve their Claude Code workflow. Based on their session data, generate ready-to-use artifact content for the following recommendations.
|
|
158
|
+
|
|
159
|
+
## Their Usage Context
|
|
160
|
+
- ${sessions.length} total sessions across ${projectNames.length} project(s): ${projectNames.join(", ")}
|
|
161
|
+
- Overall efficiency score: ${overallScore}/100
|
|
162
|
+
- Total spend: $${totalCost.toFixed(2)}
|
|
163
|
+
- Badges: ${badgeNames || "none"}
|
|
164
|
+
|
|
165
|
+
## Recommendations to generate content for
|
|
166
|
+
|
|
167
|
+
${parts.join("\n\n")}
|
|
168
|
+
|
|
169
|
+
## Your output
|
|
170
|
+
|
|
171
|
+
For each recommendation above, generate the actual content the user can copy and use immediately. Use this exact structure:
|
|
172
|
+
|
|
173
|
+
${topSkill ? `### Skill: ${topSkill.trigger}
|
|
174
|
+
Write the full skill prompt body (2-4 paragraphs). This is what Claude reads when the user types "${topSkill.trigger}". Make it specific, actionable, and tailored to the usage patterns above. Include what information the user should provide.
|
|
175
|
+
|
|
176
|
+
---` : ""}
|
|
177
|
+
|
|
178
|
+
${topClaudeMd ? `### CLAUDE.md${topClaudeMd.projectName ? ` (${topClaudeMd.projectName})` : " (global)"}
|
|
179
|
+
Write a complete, ready-to-use CLAUDE.md file. Include all the sections listed. Be specific about the patterns implied by their badges and session data. Use markdown formatting.
|
|
180
|
+
|
|
181
|
+
---` : ""}
|
|
182
|
+
|
|
183
|
+
${topAgent ? `### Agent: ${topAgent.name}
|
|
184
|
+
Describe exactly how to configure and invoke this agent in Claude Code. Include a sample prompt to use it effectively.
|
|
185
|
+
|
|
186
|
+
---` : ""}
|
|
187
|
+
|
|
188
|
+
${topPlugin ? `### Plugin: ${topPlugin.name}
|
|
189
|
+
Explain the 2-3 best use cases for this plugin given their workflow, and give a sample prompt that takes advantage of it.
|
|
190
|
+
|
|
191
|
+
---` : ""}
|
|
192
|
+
|
|
193
|
+
Keep each artifact concise and immediately usable. No filler or generic advice.`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export { buildSuggestionsPrompt };
|
|
197
|
+
|
|
127
198
|
let cachedResult = null;
|
|
199
|
+
let cachedSuggestionResult = null;
|
|
128
200
|
const activeChildren = new Set();
|
|
129
201
|
|
|
130
202
|
/**
|
|
@@ -275,3 +347,114 @@ export function getAvailableModels() {
|
|
|
275
347
|
defaultModelLabel: prettyModelName(detectedDefaultModel),
|
|
276
348
|
};
|
|
277
349
|
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Stream AI-generated suggestion content (skills, CLAUDE.md, agents, plugins).
|
|
353
|
+
* Same SSE protocol as streamAIAnalysis.
|
|
354
|
+
*/
|
|
355
|
+
export function streamAISuggestions(scoredData, suggestions, res, modelId) {
|
|
356
|
+
const prompt = buildSuggestionsPrompt(scoredData, suggestions);
|
|
357
|
+
|
|
358
|
+
const args = ["-p", "-", "--output-format", "stream-json", "--verbose"];
|
|
359
|
+
if (modelId) args.push("--model", modelId);
|
|
360
|
+
|
|
361
|
+
const resolvedModel = modelId || "default";
|
|
362
|
+
console.log(`[ai-suggestions] Starting (model: ${resolvedModel})`);
|
|
363
|
+
res.write(`event: model\ndata: ${JSON.stringify(resolvedModel)}\n\n`);
|
|
364
|
+
|
|
365
|
+
const child = spawn("claude", args, {
|
|
366
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
367
|
+
env: cleanEnv,
|
|
368
|
+
});
|
|
369
|
+
activeChildren.add(child);
|
|
370
|
+
|
|
371
|
+
child.stdin.write(prompt);
|
|
372
|
+
child.stdin.end();
|
|
373
|
+
|
|
374
|
+
let fullContent = "";
|
|
375
|
+
let errOutput = "";
|
|
376
|
+
let detectedModel = modelId || detectedDefaultModel || null;
|
|
377
|
+
let lineBuf = "";
|
|
378
|
+
|
|
379
|
+
child.stdout.on("data", (buf) => {
|
|
380
|
+
lineBuf += buf.toString();
|
|
381
|
+
const lines = lineBuf.split("\n");
|
|
382
|
+
lineBuf = lines.pop();
|
|
383
|
+
for (const line of lines) {
|
|
384
|
+
if (!line.trim()) continue;
|
|
385
|
+
try {
|
|
386
|
+
const obj = JSON.parse(line);
|
|
387
|
+
if (obj.type === "system" || obj.type === "rate_limit_event") continue;
|
|
388
|
+
if (obj.type === "result") {
|
|
389
|
+
const models = Object.keys(obj.modelUsage || {});
|
|
390
|
+
if (models.length > 0) {
|
|
391
|
+
detectedModel = models[0];
|
|
392
|
+
if (!modelId) detectedDefaultModel = models[0];
|
|
393
|
+
}
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (obj.type === "assistant" && obj.message?.content) {
|
|
397
|
+
for (const block of obj.message.content) {
|
|
398
|
+
if (block.type === "text" && block.text) {
|
|
399
|
+
fullContent += block.text;
|
|
400
|
+
res.write(`event: chunk\ndata: ${JSON.stringify(block.text)}\n\n`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (obj.message.model && !detectedModel) {
|
|
404
|
+
detectedModel = obj.message.model;
|
|
405
|
+
if (!modelId) detectedDefaultModel = obj.message.model;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
} catch {
|
|
409
|
+
if (line.trim()) {
|
|
410
|
+
fullContent += line;
|
|
411
|
+
res.write(`event: chunk\ndata: ${JSON.stringify(line)}\n\n`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
child.stderr.on("data", (buf) => {
|
|
418
|
+
errOutput += buf.toString();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
child.on("error", (err) => {
|
|
422
|
+
const message =
|
|
423
|
+
err.code === "ENOENT"
|
|
424
|
+
? "Claude CLI not found. Make sure `claude` is installed and in your PATH."
|
|
425
|
+
: `Claude CLI error: ${err.message}`;
|
|
426
|
+
res.write(`event: error\ndata: ${JSON.stringify({ message })}\n\n`);
|
|
427
|
+
res.end();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
child.on("close", (code) => {
|
|
431
|
+
activeChildren.delete(child);
|
|
432
|
+
if (code !== 0 && !fullContent) {
|
|
433
|
+
const message = errOutput.trim() || `Claude CLI exited with code ${code}`;
|
|
434
|
+
res.write(`event: error\ndata: ${JSON.stringify({ message })}\n\n`);
|
|
435
|
+
} else {
|
|
436
|
+
const finalModel = detectedModel || resolvedModel;
|
|
437
|
+
cachedSuggestionResult = {
|
|
438
|
+
content: fullContent.trim(),
|
|
439
|
+
generatedAt: new Date().toISOString(),
|
|
440
|
+
model: finalModel,
|
|
441
|
+
};
|
|
442
|
+
res.write(
|
|
443
|
+
`event: done\ndata: ${JSON.stringify({ generatedAt: cachedSuggestionResult.generatedAt, model: finalModel })}\n\n`
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
res.end();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
res.on("close", () => {
|
|
450
|
+
if (child.exitCode === null) child.kill();
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function getCachedSuggestions() {
|
|
455
|
+
return cachedSuggestionResult;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function clearCachedSuggestions() {
|
|
459
|
+
cachedSuggestionResult = null;
|
|
460
|
+
}
|
package/src/server.js
CHANGED
|
@@ -7,7 +7,8 @@ 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 } from "./ai-analyze.js";
|
|
10
|
+
import { streamAIAnalysis, getCachedAnalysis, clearCachedAnalysis, getAvailableModels, killActiveProcesses, detectDefaultModel, buildPrompt, buildDataSnapshot, streamAISuggestions, getCachedSuggestions, clearCachedSuggestions } from "./ai-analyze.js";
|
|
11
|
+
import { generateSuggestions } from "./suggestions.js";
|
|
11
12
|
|
|
12
13
|
const execFileAsync = promisify(execFile);
|
|
13
14
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -177,6 +178,35 @@ export function startServer(port = 6543) {
|
|
|
177
178
|
});
|
|
178
179
|
}
|
|
179
180
|
|
|
181
|
+
if (url.pathname === "/api/suggestions" && req.method === "GET") {
|
|
182
|
+
const data = await getData();
|
|
183
|
+
const suggestions = generateSuggestions(data);
|
|
184
|
+
return json(res, suggestions);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (url.pathname === "/api/ai-suggestions" && req.method === "POST") {
|
|
188
|
+
const data = await getData();
|
|
189
|
+
const suggestions = generateSuggestions(data);
|
|
190
|
+
const modelId = url.searchParams.get("model") || "";
|
|
191
|
+
res.writeHead(200, {
|
|
192
|
+
"Content-Type": "text/event-stream",
|
|
193
|
+
"Cache-Control": "no-cache",
|
|
194
|
+
Connection: "keep-alive",
|
|
195
|
+
});
|
|
196
|
+
streamAISuggestions(data, suggestions, res, modelId || undefined);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (url.pathname === "/api/ai-suggestions" && req.method === "DELETE") {
|
|
201
|
+
clearCachedSuggestions();
|
|
202
|
+
return json(res, { ok: true });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (url.pathname === "/api/ai-suggestions" && req.method === "GET") {
|
|
206
|
+
const cached = getCachedSuggestions();
|
|
207
|
+
return json(res, cached || { content: null });
|
|
208
|
+
}
|
|
209
|
+
|
|
180
210
|
if (url.pathname === "/api/refresh") {
|
|
181
211
|
const data = await getData(true);
|
|
182
212
|
return json(res, { sessions: data.sessions.length, overallScore: data.overallScore });
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// Workflow Optimizer — rule-based suggestion engine
|
|
2
|
+
// Analyzes session patterns and produces suggestions for skills, CLAUDE.md, agents, and plugins.
|
|
3
|
+
|
|
4
|
+
// --- Helpers ---
|
|
5
|
+
|
|
6
|
+
function computeVagueRate(sessions) {
|
|
7
|
+
let total = 0;
|
|
8
|
+
let vague = 0;
|
|
9
|
+
for (const s of sessions) {
|
|
10
|
+
const userTurns = s.turns.filter((t) => t.role === "user");
|
|
11
|
+
total += userTurns.length;
|
|
12
|
+
for (const turn of userTurns) {
|
|
13
|
+
if (turn.promptLength < 30) {
|
|
14
|
+
const idx = s.turns.indexOf(turn);
|
|
15
|
+
const next = s.turns.slice(idx + 1).find((t) => t.role === "assistant");
|
|
16
|
+
if (next && next.tokens.input + next.tokens.output > 50_000) vague++;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return total >= 5 ? vague / total : 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function computeAvgToolRatio(sessions) {
|
|
24
|
+
const qualifying = sessions.filter((s) => s.totals.userMessages > 0);
|
|
25
|
+
if (!qualifying.length) return 0;
|
|
26
|
+
return qualifying.reduce((sum, s) => sum + s.totals.toolCalls / s.totals.userMessages, 0) / qualifying.length;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns a map of tool name → fraction of all tool usages.
|
|
31
|
+
* Only counts turns that actually have tool calls.
|
|
32
|
+
*/
|
|
33
|
+
function detectToolPatterns(sessions) {
|
|
34
|
+
const counts = {};
|
|
35
|
+
let total = 0;
|
|
36
|
+
for (const s of sessions) {
|
|
37
|
+
for (const t of s.turns) {
|
|
38
|
+
for (const tool of t.toolCalls || []) {
|
|
39
|
+
const name = tool.toLowerCase();
|
|
40
|
+
counts[name] = (counts[name] || 0) + 1;
|
|
41
|
+
total++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!total) return {};
|
|
46
|
+
const fractions = {};
|
|
47
|
+
for (const [k, v] of Object.entries(counts)) fractions[k] = v / total;
|
|
48
|
+
return fractions;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const TOPIC_PATTERNS = [
|
|
52
|
+
{ topic: "git", re: /\b(git|github|commit|pull request|pr|merge|branch|rebase|stash)\b/i },
|
|
53
|
+
{ topic: "test", re: /\b(test|spec|jest|vitest|cypress|playwright|coverage|assert)\b/i },
|
|
54
|
+
{ topic: "deploy", re: /\b(deploy|release|build|ci|pipeline|docker|kubernetes|k8s|heroku|vercel)\b/i },
|
|
55
|
+
{ topic: "debug", re: /\b(debug|fix|bug|error|crash|exception|trace|breakpoint)\b/i },
|
|
56
|
+
{ topic: "review", re: /\b(review|refactor|cleanup|lint|format|code review)\b/i },
|
|
57
|
+
{ topic: "api", re: /\b(api|endpoint|rest|graphql|backend|route|controller|service)\b/i },
|
|
58
|
+
{ topic: "frontend", re: /\b(ui|component|css|style|react|vue|svelte|tailwind|design)\b/i },
|
|
59
|
+
{ topic: "database", re: /\b(database|db|sql|query|migration|schema|postgres|mysql|sqlite|prisma|supabase)\b/i },
|
|
60
|
+
{ topic: "docs", re: /\b(doc|readme|documentation|comment|jsdoc|typedoc)\b/i },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Groups sessions by dominant topic, returns sorted by count descending.
|
|
65
|
+
*/
|
|
66
|
+
function detectTitleClusters(sessions) {
|
|
67
|
+
const counts = {};
|
|
68
|
+
for (const s of sessions) {
|
|
69
|
+
const title = (s.title || "") + " " + (s.project || "");
|
|
70
|
+
for (const { topic, re } of TOPIC_PATTERNS) {
|
|
71
|
+
if (re.test(title)) {
|
|
72
|
+
counts[topic] = (counts[topic] || 0) + 1;
|
|
73
|
+
break; // one topic per session
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return Object.entries(counts)
|
|
78
|
+
.map(([topic, count]) => ({ topic, count }))
|
|
79
|
+
.sort((a, b) => b.count - a.count);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Returns sessions-per-project, sorted by count descending.
|
|
84
|
+
*/
|
|
85
|
+
function detectProjectPatterns(sessions) {
|
|
86
|
+
const counts = {};
|
|
87
|
+
for (const s of sessions) {
|
|
88
|
+
const p = s.project || "unknown";
|
|
89
|
+
counts[p] = (counts[p] || 0) + 1;
|
|
90
|
+
}
|
|
91
|
+
return Object.entries(counts)
|
|
92
|
+
.map(([name, count]) => ({ name, count }))
|
|
93
|
+
.sort((a, b) => b.count - a.count);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- Main export ---
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Generates workflow suggestions from scored session data.
|
|
100
|
+
* Returns { skills, claudeMd, agents, plugins } — each an array of suggestion objects.
|
|
101
|
+
*/
|
|
102
|
+
export function generateSuggestions(scoredData) {
|
|
103
|
+
const { sessions, badges } = scoredData;
|
|
104
|
+
if (!sessions || sessions.length === 0) {
|
|
105
|
+
return { skills: [], claudeMd: [], agents: [], plugins: [] };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const badgeIds = new Set(badges.map((b) => b.id));
|
|
109
|
+
const toolPatterns = detectToolPatterns(sessions);
|
|
110
|
+
const titleClusters = detectTitleClusters(sessions);
|
|
111
|
+
const projectPatterns = detectProjectPatterns(sessions);
|
|
112
|
+
const avgToolRatio = computeAvgToolRatio(sessions);
|
|
113
|
+
const vagueRate = computeVagueRate(sessions);
|
|
114
|
+
const topCluster = titleClusters[0] || null;
|
|
115
|
+
|
|
116
|
+
const skills = [];
|
|
117
|
+
const claudeMd = [];
|
|
118
|
+
const agents = [];
|
|
119
|
+
const plugins = [];
|
|
120
|
+
|
|
121
|
+
// --- Skills ---
|
|
122
|
+
|
|
123
|
+
if (badgeIds.has("vague-commander") || vagueRate > 0.2) {
|
|
124
|
+
skills.push({
|
|
125
|
+
id: "spec-skill",
|
|
126
|
+
title: "Create a /spec planning skill",
|
|
127
|
+
trigger: "/spec",
|
|
128
|
+
rationale: `${Math.round(vagueRate * 100)}% of your prompts are very short and trigger expensive, wide-ranging Claude responses. A /spec skill prompts you to write a thorough task spec before Claude starts — reducing ambiguity and costly back-and-forth.`,
|
|
129
|
+
priority: "high",
|
|
130
|
+
templateHint:
|
|
131
|
+
"Before starting any implementation task, define: what to build, constraints, files to change, what to leave alone, and success criteria. Then hand this spec to Claude.",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (badgeIds.has("context-hoarder")) {
|
|
136
|
+
skills.push({
|
|
137
|
+
id: "checkpoint-skill",
|
|
138
|
+
title: "Create a /checkpoint context-reset skill",
|
|
139
|
+
trigger: "/checkpoint",
|
|
140
|
+
rationale:
|
|
141
|
+
"Your sessions frequently experience cost inflection without context resets. A /checkpoint skill saves progress, summarizes state, then clears the context window — keeping sessions lean.",
|
|
142
|
+
priority: "high",
|
|
143
|
+
templateHint:
|
|
144
|
+
"Summarize what has been accomplished so far. List remaining tasks. Note any important decisions made. Then run /clear so the next phase starts fresh with this summary.",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (avgToolRatio > 5 && !badgeIds.has("surgical-prompter")) {
|
|
149
|
+
skills.push({
|
|
150
|
+
id: "focus-skill",
|
|
151
|
+
title: "Create a /focus task-scoping skill",
|
|
152
|
+
trigger: "/focus",
|
|
153
|
+
rationale: `Your average tool ratio is ${avgToolRatio.toFixed(1)}x per message — Claude spends a lot of time searching for files and context. A /focus skill primes your prompts with exact file paths and scope to eliminate unnecessary exploration.`,
|
|
154
|
+
priority: "medium",
|
|
155
|
+
templateHint:
|
|
156
|
+
"For every task, include: exact files to change (with paths), specific line numbers if known, what NOT to modify, and the precise expected output or behavior.",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (topCluster && topCluster.count >= 5) {
|
|
161
|
+
skills.push({
|
|
162
|
+
id: `domain-skill-${topCluster.topic}`,
|
|
163
|
+
title: `Create a /${topCluster.topic} workflow skill`,
|
|
164
|
+
trigger: `/${topCluster.topic}`,
|
|
165
|
+
rationale: `${topCluster.count} of your sessions involve "${topCluster.topic}" tasks. A dedicated skill standardizes how you kick off these workflows and gives Claude the right context from the start.`,
|
|
166
|
+
priority: "medium",
|
|
167
|
+
templateHint: `Standard workflow for ${topCluster.topic} tasks: include relevant context, patterns to follow, and any project-specific conventions.`,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// --- CLAUDE.md ---
|
|
172
|
+
|
|
173
|
+
const heavyProjects = projectPatterns.filter((p) => p.count >= 5 && p.name !== "unknown");
|
|
174
|
+
for (const proj of heavyProjects.slice(0, 2)) {
|
|
175
|
+
claudeMd.push({
|
|
176
|
+
id: `claudemd-project-${proj.name.replace(/[^a-z0-9]/gi, "-").toLowerCase()}`,
|
|
177
|
+
scope: "project",
|
|
178
|
+
projectName: proj.name,
|
|
179
|
+
rationale: `You have ${proj.count} sessions in "${proj.name}". A CLAUDE.md file in that project gives Claude persistent context about your codebase, architecture, and conventions — reducing repeated explanations every session.`,
|
|
180
|
+
priority: "high",
|
|
181
|
+
sections: ["Project overview and purpose", "Key files and architecture", "Coding conventions and patterns", "Common tasks and workflows", "What NOT to do"],
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (badgeIds.has("context-hoarder")) {
|
|
186
|
+
claudeMd.push({
|
|
187
|
+
id: "claudemd-clear-guidance",
|
|
188
|
+
scope: "global",
|
|
189
|
+
projectName: null,
|
|
190
|
+
rationale:
|
|
191
|
+
"Your sessions frequently experience cost inflection without context resets. Adding /clear guidance to your global CLAUDE.md establishes the habit of resetting context after each major task.",
|
|
192
|
+
priority: "medium",
|
|
193
|
+
sections: ["Context management: use /clear after completing each major task to keep sessions efficient"],
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (badgeIds.has("opus-addict")) {
|
|
198
|
+
claudeMd.push({
|
|
199
|
+
id: "claudemd-model-guidance",
|
|
200
|
+
scope: "global",
|
|
201
|
+
projectName: null,
|
|
202
|
+
rationale:
|
|
203
|
+
"You frequently use Opus for tasks where Sonnet performs equally well. Documenting model selection guidelines in CLAUDE.md (or setting a default model via claude config) could significantly reduce cost.",
|
|
204
|
+
priority: "high",
|
|
205
|
+
sections: ["Model selection: default to Sonnet for routine coding tasks, use Opus only for complex architecture decisions or deep reasoning"],
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (avgToolRatio > 5 && !badgeIds.has("surgical-prompter")) {
|
|
210
|
+
claudeMd.push({
|
|
211
|
+
id: "claudemd-prompt-conventions",
|
|
212
|
+
scope: "global",
|
|
213
|
+
projectName: null,
|
|
214
|
+
rationale:
|
|
215
|
+
"Your high tool-call ratio suggests Claude frequently has to search for context you could provide upfront. Adding prompt conventions to your CLAUDE.md establishes expectations for how specific your prompts should be.",
|
|
216
|
+
priority: "medium",
|
|
217
|
+
sections: ["Prompt conventions: always include exact file paths, avoid vague references like 'the function' or 'that thing'"],
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// --- Agents ---
|
|
222
|
+
|
|
223
|
+
const highToolSessions = sessions.filter(
|
|
224
|
+
(s) => s.totals.toolCalls > 20 && s.totals.userMessages > 0
|
|
225
|
+
);
|
|
226
|
+
if (highToolSessions.length >= 3) {
|
|
227
|
+
agents.push({
|
|
228
|
+
id: "explore-agent",
|
|
229
|
+
name: "Explore subagent",
|
|
230
|
+
use_case: "Offload codebase exploration and file discovery",
|
|
231
|
+
rationale: `${highToolSessions.length} of your sessions use 20+ tool calls for file searches and codebase navigation. An Explore subagent handles this research in parallel and in a clean context, keeping your main session focused.`,
|
|
232
|
+
priority: "medium",
|
|
233
|
+
example:
|
|
234
|
+
'Launch with: Agent tool with subagent_type="Explore", prompt="Find all files related to authentication and explain the flow"',
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (topCluster && topCluster.count >= 8) {
|
|
239
|
+
agents.push({
|
|
240
|
+
id: `specialist-agent-${topCluster.topic}`,
|
|
241
|
+
name: `${topCluster.topic} specialist agent`,
|
|
242
|
+
use_case: `Handle ${topCluster.topic} tasks autonomously from a single high-level prompt`,
|
|
243
|
+
rationale: `You have ${topCluster.count} sessions focused on "${topCluster.topic}". A specialist subagent for this workflow could handle the entire task from a single high-level instruction, with expertise baked in.`,
|
|
244
|
+
priority: "low",
|
|
245
|
+
example: `Create a custom agent type optimized for ${topCluster.topic} tasks with relevant tools and context pre-loaded.`,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// --- Plugins ---
|
|
250
|
+
// Note: MCP servers load all their tool definitions into context on every message,
|
|
251
|
+
// adding token overhead even when unused. Only recommend MCPs where they provide
|
|
252
|
+
// a genuine capability gap over CLI tools (i.e., Bash can't do it well).
|
|
253
|
+
|
|
254
|
+
const bashFraction = toolPatterns["bash"] || 0;
|
|
255
|
+
const webSessions = sessions.filter((s) =>
|
|
256
|
+
/browser|playwright|scrape|screenshot|e2e|visual test/i.test(s.title)
|
|
257
|
+
);
|
|
258
|
+
if (bashFraction > 0.3 && webSessions.length >= 3) {
|
|
259
|
+
plugins.push({
|
|
260
|
+
id: "playwright-mcp",
|
|
261
|
+
name: "Playwright MCP",
|
|
262
|
+
rationale: `${webSessions.length} sessions involve browser tasks that curl/bash can't handle well — screenshots, JS-rendered pages, user interaction flows. Playwright MCP is one of the few cases where an MCP genuinely beats the CLI alternative.`,
|
|
263
|
+
installCmd: "claude mcp add playwright npx @playwright/mcp@latest",
|
|
264
|
+
url: "https://github.com/microsoft/playwright-mcp",
|
|
265
|
+
priority: "medium",
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Sort by priority
|
|
270
|
+
const priorityOrder = { high: 0, medium: 1, low: 2 };
|
|
271
|
+
const byPriority = (a, b) =>
|
|
272
|
+
(priorityOrder[a.priority] ?? 1) - (priorityOrder[b.priority] ?? 1);
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
skills: skills.sort(byPriority),
|
|
276
|
+
claudeMd: claudeMd.sort(byPriority),
|
|
277
|
+
agents: agents.sort(byPriority),
|
|
278
|
+
plugins: plugins.sort(byPriority),
|
|
279
|
+
};
|
|
280
|
+
}
|