@polymorphism-tech/morph-spec 4.2.0 → 4.3.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.
Files changed (140) hide show
  1. package/CLAUDE.md +108 -946
  2. package/bin/morph-spec.js +284 -9
  3. package/bin/task-manager.cjs +102 -14
  4. package/bin/validate.js +4 -4
  5. package/docs/{v3.0 → next-generation}/AGENTS.md +1 -1
  6. package/docs/next-generation/CONTEXT-OPTIMIZATION.md +267 -0
  7. package/docs/next-generation/EXECUTION-FLOW.md +274 -0
  8. package/docs/next-generation/META-PROMPTS.md +235 -0
  9. package/docs/next-generation/MIGRATION-GUIDE.md +253 -0
  10. package/docs/next-generation/THREAD-MANAGEMENT.md +240 -0
  11. package/package.json +5 -5
  12. package/src/commands/agents/agents-fuse.js +97 -0
  13. package/src/commands/agents/micro-agent.js +112 -0
  14. package/src/commands/agents/spawn-team.js +69 -4
  15. package/src/commands/agents/squad-template.js +146 -0
  16. package/src/commands/analytics/analytics.js +176 -0
  17. package/src/commands/context/context-prime.js +63 -0
  18. package/src/commands/context/core-four.js +54 -0
  19. package/src/commands/mcp/mcp.js +102 -0
  20. package/src/commands/project/detect-agents.js +32 -2
  21. package/src/commands/project/detect.js +11 -1
  22. package/src/commands/project/doctor.js +573 -356
  23. package/src/commands/project/init.js +9 -2
  24. package/src/commands/project/update.js +13 -3
  25. package/src/commands/state/advance-phase.js +448 -416
  26. package/src/commands/state/state.js +14 -12
  27. package/src/commands/tasks/task.js +1 -1
  28. package/src/commands/templates/template-render.js +80 -1
  29. package/src/commands/threads/thread-template.js +103 -0
  30. package/src/commands/threads/threads.js +261 -0
  31. package/src/commands/trust/trust.js +205 -0
  32. package/src/{orchestrator.js → core/orchestrator.js} +8 -8
  33. package/src/core/state/state-manager.js +37 -17
  34. package/src/core/workflows/workflow-detector.js +114 -3
  35. package/src/lib/agents/micro-agent-factory.js +161 -0
  36. package/src/lib/analytics/analytics-engine.js +345 -0
  37. package/src/lib/checkpoints/checkpoint-hooks.js +298 -258
  38. package/src/lib/context/context-bundler.js +240 -0
  39. package/src/lib/context/context-optimizer.js +212 -0
  40. package/src/lib/context/context-tracker.js +273 -0
  41. package/src/lib/context/core-four-tracker.js +201 -0
  42. package/src/lib/context/mcp-optimizer.js +200 -0
  43. package/src/lib/detectors/index.js +1 -1
  44. package/src/lib/detectors/standards-generator.js +77 -17
  45. package/src/lib/detectors/structure-detector.js +67 -39
  46. package/src/lib/execution/fusion-executor.js +304 -0
  47. package/src/lib/execution/parallel-executor.js +270 -0
  48. package/src/lib/generators/context-generator.js +3 -3
  49. package/src/lib/generators/recap-generator.js +32 -12
  50. package/src/lib/hooks/hook-executor.js +169 -0
  51. package/src/lib/hooks/stop-hook-executor.js +286 -0
  52. package/src/lib/hops/hop-composer.js +221 -0
  53. package/src/lib/threads/thread-coordinator.js +238 -0
  54. package/src/lib/threads/thread-manager.js +317 -0
  55. package/src/lib/tracking/artifact-trail.js +202 -0
  56. package/src/lib/trust/trust-manager.js +269 -0
  57. package/src/lib/validators/design-system/design-system-validator.js +2 -2
  58. package/src/lib/validators/validation-runner.js +14 -30
  59. package/src/utils/hooks-installer.js +69 -0
  60. package/stacks/blazor-azure/.morph/config/agents.json +72 -3
  61. package/stacks/nextjs-supabase/.morph/config/agents.json +3 -3
  62. package/docs/llm-interaction-config.md +0 -735
  63. package/docs/v3.0/EXECUTION-FLOW.md +0 -1304
  64. package/src/commands/utils/migrate-state.js +0 -158
  65. package/src/commands/utils/upgrade.js +0 -346
  66. package/src/lib/validators/architecture-validator.js +0 -60
  67. package/src/lib/validators/content-validator.js +0 -164
  68. package/src/lib/validators/package-validator.js +0 -61
  69. package/src/lib/validators/ui-contrast-validator.js +0 -44
  70. package/stacks/blazor-azure/.claude/commands/morph-apply.md +0 -221
  71. package/stacks/blazor-azure/.claude/commands/morph-archive.md +0 -79
  72. package/stacks/blazor-azure/.claude/commands/morph-deploy.md +0 -529
  73. package/stacks/blazor-azure/.claude/commands/morph-infra.md +0 -209
  74. package/stacks/blazor-azure/.claude/commands/morph-preflight.md +0 -227
  75. package/stacks/blazor-azure/.claude/commands/morph-proposal.md +0 -122
  76. package/stacks/blazor-azure/.claude/commands/morph-status.md +0 -86
  77. package/stacks/blazor-azure/.claude/commands/morph-troubleshoot.md +0 -122
  78. package/stacks/blazor-azure/.claude/skills/level-0-meta/README.md +0 -7
  79. package/stacks/blazor-azure/.claude/skills/level-0-meta/code-review.md +0 -226
  80. package/stacks/blazor-azure/.claude/skills/level-0-meta/morph-checklist.md +0 -117
  81. package/stacks/blazor-azure/.claude/skills/level-0-meta/simulation-checklist.md +0 -77
  82. package/stacks/blazor-azure/.claude/skills/level-1-workflows/README.md +0 -7
  83. package/stacks/blazor-azure/.claude/skills/level-1-workflows/morph-replicate.md +0 -213
  84. package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-clarify.md +0 -131
  85. package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-design.md +0 -213
  86. package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-setup.md +0 -106
  87. package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-tasks.md +0 -164
  88. package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-uiux.md +0 -169
  89. package/stacks/blazor-azure/.claude/skills/level-2-domains/README.md +0 -14
  90. package/stacks/blazor-azure/.claude/skills/level-2-domains/ai-agents/ai-system-architect.md +0 -192
  91. package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/po-pm-advisor.md +0 -197
  92. package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/prompt-engineer.md +0 -189
  93. package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/seo-growth-hacker.md +0 -320
  94. package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/standards-architect.md +0 -156
  95. package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/api-designer.md +0 -59
  96. package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/dotnet-senior.md +0 -77
  97. package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/ef-modeler.md +0 -58
  98. package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/hangfire-orchestrator.md +0 -126
  99. package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/ms-agent-expert.md +0 -45
  100. package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/blazor-builder.md +0 -210
  101. package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/nextjs-expert.md +0 -154
  102. package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/ui-ux-designer.md +0 -191
  103. package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/azure-architect.md +0 -142
  104. package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/azure-deploy-specialist.md +0 -699
  105. package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/bicep-architect.md +0 -126
  106. package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/container-specialist.md +0 -131
  107. package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/devops-engineer.md +0 -119
  108. package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/asaas-financial.md +0 -130
  109. package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/azure-identity.md +0 -142
  110. package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/clerk-auth.md +0 -108
  111. package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/hangfire-orchestrator.md +0 -64
  112. package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/resend-email.md +0 -119
  113. package/stacks/blazor-azure/.claude/skills/level-2-domains/quality/code-analyzer.md +0 -235
  114. package/stacks/blazor-azure/.claude/skills/level-2-domains/quality/testing-specialist.md +0 -126
  115. package/stacks/blazor-azure/.claude/skills/level-3-technologies/README.md +0 -7
  116. package/stacks/blazor-azure/.claude/skills/level-4-patterns/README.md +0 -7
  117. package/stacks/blazor-azure/.morph/archive/.gitkeep +0 -25
  118. package/stacks/blazor-azure/.morph/features/.gitkeep +0 -25
  119. package/stacks/blazor-azure/.morph/schemas/agent.schema.json +0 -296
  120. package/stacks/blazor-azure/.morph/schemas/tasks.schema.json +0 -220
  121. package/stacks/blazor-azure/.morph/specs/.gitkeep +0 -20
  122. package/stacks/blazor-azure/.morph/test-infra/example.bicep +0 -59
  123. package/stacks/nextjs-supabase/.claude/commands/morph-apply.md +0 -221
  124. package/stacks/nextjs-supabase/.claude/commands/morph-archive.md +0 -79
  125. package/stacks/nextjs-supabase/.claude/commands/morph-deploy.md +0 -529
  126. package/stacks/nextjs-supabase/.claude/commands/morph-infra.md +0 -209
  127. package/stacks/nextjs-supabase/.claude/commands/morph-preflight.md +0 -227
  128. package/stacks/nextjs-supabase/.claude/commands/morph-proposal.md +0 -122
  129. package/stacks/nextjs-supabase/.claude/commands/morph-status.md +0 -86
  130. package/stacks/nextjs-supabase/.claude/commands/morph-troubleshoot.md +0 -122
  131. package/stacks/nextjs-supabase/.claude/settings.local.json +0 -6
  132. package/stacks/nextjs-supabase/.claude/skills/level-2-domains/backend/dotnet-supabase.md +0 -244
  133. package/stacks/nextjs-supabase/.claude/skills/level-2-domains/frontend/nextjs-supabase.md +0 -335
  134. package/stacks/nextjs-supabase/.claude/skills/level-2-domains/infrastructure/easypanel-deployer.md +0 -189
  135. package/stacks/nextjs-supabase/.claude/skills/level-2-domains/integrations/supabase-expert.md +0 -50
  136. /package/docs/{v3.0 → next-generation}/ANALYSIS.md +0 -0
  137. /package/docs/{v3.0 → next-generation}/ARCHITECTURE.md +0 -0
  138. /package/docs/{v3.0 → next-generation}/FEATURES.md +0 -0
  139. /package/docs/{v3.0 → next-generation}/README.md +0 -0
  140. /package/docs/{v3.0 → next-generation}/ROADMAP.md +0 -0
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Context Tracker — Real-time token monitoring
3
+ *
4
+ * Tracks token usage per thread/session, detects peak usage moments,
5
+ * calculates averages, and identifies optimization opportunities.
6
+ * Integrates with analytics-engine to record context events.
7
+ */
8
+
9
+ import { recordContextEvent } from '../analytics/analytics-engine.js';
10
+ import { addThreadEvent } from '../threads/thread-manager.js';
11
+
12
+ // ============================================================================
13
+ // Constants
14
+ // ============================================================================
15
+
16
+ const WARNING_THRESHOLD = 0.75; // 75% of context window
17
+ const CRITICAL_THRESHOLD = 0.90; // 90% of context window
18
+
19
+ export const OPTIMIZATION_STRATEGIES = {
20
+ MCP_HYGIENE: 'mcp_hygiene',
21
+ MICRO_AGENT: 'micro_agent_switch',
22
+ CONTEXT_BUNDLE: 'context_bundle',
23
+ PRIMING_FILE: 'priming_file',
24
+ STANDARDS_SUBSET: 'standards_subset'
25
+ };
26
+
27
+ // In-memory session state (not persisted — resets each session)
28
+ const sessions = new Map();
29
+
30
+ // ============================================================================
31
+ // Session Management
32
+ // ============================================================================
33
+
34
+ /**
35
+ * Start tracking a session
36
+ * @param {Object} opts
37
+ * @param {string} opts.feature - Feature name
38
+ * @param {string} [opts.threadId] - Thread ID
39
+ * @param {number} [opts.contextWindow=200000] - Model context window size
40
+ * @param {string} [opts.model] - Model name
41
+ * @returns {string} Session ID
42
+ */
43
+ export function startSession({ feature, threadId = null, contextWindow = 200000, model = 'claude-sonnet-4-6' }) {
44
+ const sessionId = threadId || `session-${Date.now()}`;
45
+
46
+ sessions.set(sessionId, {
47
+ sessionId,
48
+ feature,
49
+ threadId,
50
+ model,
51
+ contextWindow,
52
+ startedAt: new Date().toISOString(),
53
+ peakUsage: 0,
54
+ currentUsage: 0,
55
+ samples: [],
56
+ events: []
57
+ });
58
+
59
+ recordContextEvent({
60
+ type: 'context_session_started',
61
+ feature,
62
+ threadId,
63
+ data: { model, contextWindow }
64
+ });
65
+
66
+ return sessionId;
67
+ }
68
+
69
+ /**
70
+ * Record a token usage sample for a session
71
+ * @param {string} sessionId - Session ID
72
+ * @param {Object} usage
73
+ * @param {number} usage.tokensUsed - Total tokens in context
74
+ * @param {Object} [usage.breakdown] - Token breakdown by source
75
+ * @returns {Object} Current session state with any optimization recommendations
76
+ */
77
+ export function recordTokenUsage(sessionId, { tokensUsed, breakdown = {} }) {
78
+ const session = sessions.get(sessionId);
79
+ if (!session) {
80
+ throw new Error(`Session not found: ${sessionId}`);
81
+ }
82
+
83
+ const utilization = tokensUsed / session.contextWindow;
84
+ session.currentUsage = tokensUsed;
85
+
86
+ if (tokensUsed > session.peakUsage) {
87
+ session.peakUsage = tokensUsed;
88
+ }
89
+
90
+ const sample = {
91
+ timestamp: new Date().toISOString(),
92
+ tokensUsed,
93
+ utilization: Math.round(utilization * 100) / 100,
94
+ breakdown
95
+ };
96
+ session.samples.push(sample);
97
+
98
+ // Record to analytics
99
+ recordContextEvent({
100
+ type: 'token_usage',
101
+ feature: session.feature,
102
+ threadId: session.threadId,
103
+ data: { tokensUsed, utilization, breakdown }
104
+ });
105
+
106
+ // Check thresholds and identify opportunities
107
+ const recommendations = identifyOptimizations(session, utilization);
108
+
109
+ if (utilization >= CRITICAL_THRESHOLD) {
110
+ const event = { type: 'context_critical', utilization, recommendations };
111
+ session.events.push(event);
112
+
113
+ if (session.threadId) {
114
+ try {
115
+ addThreadEvent(session.threadId, 'context_critical', { utilization });
116
+ } catch {
117
+ // Thread may not exist in state yet
118
+ }
119
+ }
120
+
121
+ recordContextEvent({
122
+ type: 'context_critical',
123
+ feature: session.feature,
124
+ threadId: session.threadId,
125
+ data: { utilization, tokensUsed, recommendations }
126
+ });
127
+ } else if (utilization >= WARNING_THRESHOLD) {
128
+ recordContextEvent({
129
+ type: 'context_warning',
130
+ feature: session.feature,
131
+ threadId: session.threadId,
132
+ data: { utilization, tokensUsed }
133
+ });
134
+ }
135
+
136
+ return { session, recommendations };
137
+ }
138
+
139
+ /**
140
+ * End a session and record summary
141
+ * @param {string} sessionId - Session ID
142
+ * @returns {Object} Session summary
143
+ */
144
+ export function endSession(sessionId) {
145
+ const session = sessions.get(sessionId);
146
+ if (!session) return null;
147
+
148
+ const avgUsage = session.samples.length > 0
149
+ ? Math.round(session.samples.reduce((sum, s) => sum + s.tokensUsed, 0) / session.samples.length)
150
+ : 0;
151
+
152
+ const summary = {
153
+ sessionId,
154
+ feature: session.feature,
155
+ model: session.model,
156
+ startedAt: session.startedAt,
157
+ endedAt: new Date().toISOString(),
158
+ peakUsage: session.peakUsage,
159
+ avgUsage,
160
+ samples: session.samples.length,
161
+ criticalMoments: session.events.filter(e => e.type === 'context_critical').length
162
+ };
163
+
164
+ recordContextEvent({
165
+ type: 'context_session_ended',
166
+ feature: session.feature,
167
+ threadId: session.threadId,
168
+ data: summary
169
+ });
170
+
171
+ sessions.delete(sessionId);
172
+ return summary;
173
+ }
174
+
175
+ // ============================================================================
176
+ // Optimization Analysis
177
+ // ============================================================================
178
+
179
+ /**
180
+ * Identify context optimization opportunities based on current utilization
181
+ * @param {Object} session - Session object
182
+ * @param {number} utilization - Current utilization (0-1)
183
+ * @returns {Array} Array of optimization recommendations
184
+ */
185
+ export function identifyOptimizations(session, utilization) {
186
+ const recommendations = [];
187
+
188
+ if (utilization >= 0.5) {
189
+ recommendations.push({
190
+ strategy: OPTIMIZATION_STRATEGIES.MCP_HYGIENE,
191
+ description: 'Disable unused MCP servers to free ~15-30K tokens',
192
+ estimatedSavings: '15,000-30,000 tokens',
193
+ priority: utilization >= 0.75 ? 'high' : 'medium'
194
+ });
195
+ }
196
+
197
+ if (utilization >= 0.65) {
198
+ recommendations.push({
199
+ strategy: OPTIMIZATION_STRATEGIES.STANDARDS_SUBSET,
200
+ description: 'Load only task-relevant standards instead of all standards',
201
+ estimatedSavings: '10,000-20,000 tokens',
202
+ priority: 'medium'
203
+ });
204
+ }
205
+
206
+ if (utilization >= 0.80) {
207
+ recommendations.push({
208
+ strategy: OPTIMIZATION_STRATEGIES.MICRO_AGENT,
209
+ description: 'Switch to a micro-agent with minimal context for focused tasks',
210
+ estimatedSavings: '50,000-100,000 tokens',
211
+ priority: 'high'
212
+ });
213
+ recommendations.push({
214
+ strategy: OPTIMIZATION_STRATEGIES.PRIMING_FILE,
215
+ description: 'Use a 500-token priming file instead of full CLAUDE.md',
216
+ estimatedSavings: '22,000 tokens',
217
+ priority: 'high'
218
+ });
219
+ }
220
+
221
+ if (utilization >= 0.90) {
222
+ recommendations.push({
223
+ strategy: OPTIMIZATION_STRATEGIES.CONTEXT_BUNDLE,
224
+ description: 'Create a context bundle checkpoint (180K → 15K token compression)',
225
+ estimatedSavings: '165,000 tokens',
226
+ priority: 'critical'
227
+ });
228
+ }
229
+
230
+ return recommendations;
231
+ }
232
+
233
+ /**
234
+ * Get statistics for a session
235
+ * @param {string} sessionId - Session ID
236
+ * @returns {Object|null} Session statistics
237
+ */
238
+ export function getSessionStats(sessionId) {
239
+ const session = sessions.get(sessionId);
240
+ if (!session) return null;
241
+
242
+ const avgUsage = session.samples.length > 0
243
+ ? Math.round(session.samples.reduce((sum, s) => sum + s.tokensUsed, 0) / session.samples.length)
244
+ : 0;
245
+
246
+ return {
247
+ sessionId,
248
+ feature: session.feature,
249
+ model: session.model,
250
+ contextWindow: session.contextWindow,
251
+ currentUsage: session.currentUsage,
252
+ peakUsage: session.peakUsage,
253
+ avgUsage,
254
+ peakUtilization: Math.round(session.peakUsage / session.contextWindow * 100),
255
+ currentUtilization: Math.round(session.currentUsage / session.contextWindow * 100),
256
+ samples: session.samples.length
257
+ };
258
+ }
259
+
260
+ /**
261
+ * Get all active sessions
262
+ * @returns {Array} Active session summaries
263
+ */
264
+ export function getActiveSessions() {
265
+ return Array.from(sessions.values()).map(s => ({
266
+ sessionId: s.sessionId,
267
+ feature: s.feature,
268
+ model: s.model,
269
+ currentUsage: s.currentUsage,
270
+ peakUsage: s.peakUsage,
271
+ utilization: Math.round(s.currentUsage / s.contextWindow * 100)
272
+ }));
273
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Core Four Tracker — Context/Model/Prompt/Tools tracking
3
+ *
4
+ * Tracks the "Core Four" metrics that determine LLM behavior:
5
+ * Context — Token count breakdown by source
6
+ * Model — Model name and context window
7
+ * Prompt — Current request summary
8
+ * Tools — Tool usage counts (Read/Write/Edit/Bash/etc.)
9
+ *
10
+ * Reads from analytics-engine for historical data.
11
+ * Formats output for ASCII terminal display.
12
+ */
13
+
14
+ import { readFileSync, existsSync } from 'fs';
15
+ import { join } from 'path';
16
+ import { recordContextEvent } from '../analytics/analytics-engine.js';
17
+
18
+ // In-memory state for current session
19
+ let currentSession = {
20
+ model: null,
21
+ contextWindow: 200000,
22
+ contextBreakdown: {},
23
+ promptSummary: '',
24
+ toolCalls: {}
25
+ };
26
+
27
+ // ============================================================================
28
+ // Session Initialization
29
+ // ============================================================================
30
+
31
+ /**
32
+ * Initialize Core Four tracking for a session
33
+ * @param {Object} opts
34
+ * @param {string} opts.model - Model name
35
+ * @param {number} [opts.contextWindow] - Context window size
36
+ * @param {string} [opts.feature] - Feature being worked on
37
+ */
38
+ export function initCoreFour({ model, contextWindow = 200000, feature = '' }) {
39
+ currentSession = {
40
+ model,
41
+ contextWindow,
42
+ feature,
43
+ contextBreakdown: {
44
+ 'CLAUDE.md': 0,
45
+ standards: 0,
46
+ templates: 0,
47
+ spec: 0,
48
+ conversation: 0,
49
+ other: 0
50
+ },
51
+ promptSummary: '',
52
+ toolCalls: {
53
+ Read: 0,
54
+ Write: 0,
55
+ Edit: 0,
56
+ Bash: 0,
57
+ Glob: 0,
58
+ Grep: 0,
59
+ Task: 0,
60
+ other: 0
61
+ },
62
+ startedAt: new Date().toISOString()
63
+ };
64
+ }
65
+
66
+ // ============================================================================
67
+ // Tracking Updates
68
+ // ============================================================================
69
+
70
+ /**
71
+ * Update context breakdown by source
72
+ * @param {Object} breakdown - { sourceName: tokenCount }
73
+ */
74
+ export function updateContextBreakdown(breakdown) {
75
+ for (const [source, tokens] of Object.entries(breakdown)) {
76
+ currentSession.contextBreakdown[source] = tokens;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Update current prompt summary
82
+ * @param {string} summary - Brief summary of current request
83
+ */
84
+ export function updatePromptSummary(summary) {
85
+ currentSession.promptSummary = summary;
86
+ }
87
+
88
+ /**
89
+ * Record a tool call
90
+ * @param {string} toolName - Tool name (Read|Write|Edit|Bash|etc.)
91
+ */
92
+ export function recordToolCall(toolName) {
93
+ const key = currentSession.toolCalls.hasOwnProperty(toolName) ? toolName : 'other';
94
+ currentSession.toolCalls[key] = (currentSession.toolCalls[key] || 0) + 1;
95
+
96
+ if (currentSession.feature) {
97
+ recordContextEvent({
98
+ type: 'tool_call',
99
+ feature: currentSession.feature,
100
+ data: { tool: toolName }
101
+ });
102
+ }
103
+ }
104
+
105
+ // ============================================================================
106
+ // Display Formatting
107
+ // ============================================================================
108
+
109
+ /**
110
+ * Get the current Core Four state
111
+ * @returns {Object} Core Four metrics
112
+ */
113
+ export function getCoreFour() {
114
+ const totalContextTokens = Object.values(currentSession.contextBreakdown)
115
+ .reduce((sum, v) => sum + v, 0);
116
+
117
+ const contextUtilization = currentSession.contextWindow > 0
118
+ ? Math.round(totalContextTokens / currentSession.contextWindow * 100)
119
+ : 0;
120
+
121
+ return {
122
+ context: {
123
+ total: totalContextTokens,
124
+ window: currentSession.contextWindow,
125
+ utilization: contextUtilization,
126
+ breakdown: currentSession.contextBreakdown
127
+ },
128
+ model: {
129
+ name: currentSession.model || 'unknown',
130
+ contextWindow: currentSession.contextWindow,
131
+ provider: 'Anthropic'
132
+ },
133
+ prompt: {
134
+ summary: currentSession.promptSummary || '(no active prompt)',
135
+ length: currentSession.promptSummary?.length || 0
136
+ },
137
+ tools: {
138
+ calls: { ...currentSession.toolCalls },
139
+ total: Object.values(currentSession.toolCalls).reduce((sum, v) => sum + v, 0)
140
+ }
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Format Core Four as ASCII display string
146
+ * @returns {string} ASCII formatted Core Four display
147
+ */
148
+ export function formatCoreFour() {
149
+ const { context, model, prompt, tools } = getCoreFour();
150
+ const lines = [];
151
+
152
+ lines.push('╔══════════════════════════════════════════╗');
153
+ lines.push('║ CORE FOUR DASHBOARD ║');
154
+ lines.push('╠══════════════════════════════════════════╣');
155
+
156
+ // Context
157
+ lines.push('║ CONTEXT ║');
158
+ const bar = buildProgressBar(context.utilization, 30);
159
+ lines.push(`║ ${bar} ${String(context.utilization).padStart(3)}% ║`);
160
+ lines.push(`║ Total: ${String(context.total.toLocaleString()).padEnd(10)} Window: ${String(context.window.toLocaleString()).padEnd(8)}║`);
161
+
162
+ // Breakdown
163
+ for (const [src, tokens] of Object.entries(context.breakdown)) {
164
+ if (tokens > 0) {
165
+ const pct = Math.round(tokens / Math.max(context.total, 1) * 100);
166
+ lines.push(`║ ${src.padEnd(14)} ${String(tokens.toLocaleString()).padStart(8)} (${String(pct).padStart(2)}%) ║`);
167
+ }
168
+ }
169
+ lines.push('║ ║');
170
+
171
+ // Model
172
+ lines.push('║ MODEL ║');
173
+ lines.push(`║ ${model.name.padEnd(40)}║`);
174
+ lines.push(`║ Window: ${String(model.contextWindow.toLocaleString()).padEnd(32)}║`);
175
+ lines.push('║ ║');
176
+
177
+ // Prompt
178
+ lines.push('║ PROMPT ║');
179
+ const pStr = (prompt.summary || '—').substring(0, 38);
180
+ lines.push(`║ ${pStr.padEnd(40)}║`);
181
+ lines.push('║ ║');
182
+
183
+ // Tools
184
+ lines.push('║ TOOLS ║');
185
+ lines.push(`║ Total calls: ${String(tools.total).padEnd(27)}║`);
186
+ for (const [tool, count] of Object.entries(tools.calls)) {
187
+ if (count > 0) {
188
+ lines.push(`║ ${tool.padEnd(10)} ${String(count).padStart(4)} calls${' '.repeat(20)}║`);
189
+ }
190
+ }
191
+ lines.push('╚══════════════════════════════════════════╝');
192
+
193
+ return lines.join('\n');
194
+ }
195
+
196
+ function buildProgressBar(pct, width) {
197
+ const filled = Math.round(pct / 100 * width);
198
+ const empty = width - filled;
199
+ const color = pct >= 90 ? '█' : pct >= 75 ? '▓' : '░';
200
+ return '[' + color.repeat(filled) + '░'.repeat(empty) + ']';
201
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * MCP Optimizer — Analyze and optimize MCP server configurations
3
+ *
4
+ * Reads .claude/settings.json (or .claude/settings.local.json),
5
+ * detects unused MCP servers based on analytics,
6
+ * estimates token savings per server, and generates recommendations.
7
+ */
8
+
9
+ import { readFileSync, existsSync } from 'fs';
10
+ import { join } from 'path';
11
+
12
+ const SETTINGS_PATHS = [
13
+ join(process.cwd(), '.claude/settings.local.json'),
14
+ join(process.cwd(), '.claude/settings.json'),
15
+ join(process.env.USERPROFILE || process.env.HOME || '', '.claude/settings.json')
16
+ ];
17
+
18
+ // Estimated token overhead per MCP server in context window
19
+ const MCP_TOKEN_OVERHEAD = {
20
+ default: 3000, // average per server
21
+ heavy: 8000, // known heavy servers (filesystem, database)
22
+ light: 1500 // known light servers (simple tools)
23
+ };
24
+
25
+ const HEAVY_SERVERS = ['filesystem', 'postgres', 'sqlite', 'mysql', 'mongodb'];
26
+ const LIGHT_SERVERS = ['time', 'echo', 'fetch'];
27
+
28
+ // ============================================================================
29
+ // Settings Loading
30
+ // ============================================================================
31
+
32
+ /**
33
+ * Load Claude settings file
34
+ * @returns {Object|null} Settings object or null
35
+ */
36
+ export function loadClaudeSettings() {
37
+ for (const path of SETTINGS_PATHS) {
38
+ if (existsSync(path)) {
39
+ try {
40
+ return { settings: JSON.parse(readFileSync(path, 'utf8')), path };
41
+ } catch {
42
+ continue;
43
+ }
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+
49
+ /**
50
+ * Extract MCP server list from settings
51
+ * @param {Object} settings - Claude settings object
52
+ * @returns {Array} Array of { name, config } objects
53
+ */
54
+ export function extractMCPServers(settings) {
55
+ const servers = [];
56
+
57
+ // Claude settings format: mcpServers: { name: { command, args, env } }
58
+ if (settings.mcpServers) {
59
+ for (const [name, config] of Object.entries(settings.mcpServers)) {
60
+ servers.push({ name, config });
61
+ }
62
+ }
63
+
64
+ return servers;
65
+ }
66
+
67
+ // ============================================================================
68
+ // Usage Analysis
69
+ // ============================================================================
70
+
71
+ /**
72
+ * Detect unused MCP servers by checking analytics logs
73
+ * @param {Array} servers - Array of MCP server descriptors
74
+ * @param {number} [lookbackDays=30] - Days to look back in analytics
75
+ * @returns {Array} servers with usage stats added
76
+ */
77
+ export function detectUnusedServers(servers, lookbackDays = 30) {
78
+ // Read analytics for MCP tool usage patterns
79
+ const analyticsPath = join(process.cwd(), '.morph/analytics/threads-log.jsonl');
80
+ const usedServerNames = new Set();
81
+
82
+ if (existsSync(analyticsPath)) {
83
+ try {
84
+ const cutoff = new Date();
85
+ cutoff.setDate(cutoff.getDate() - lookbackDays);
86
+
87
+ const lines = readFileSync(analyticsPath, 'utf8').trim().split('\n').filter(Boolean);
88
+ for (const line of lines) {
89
+ try {
90
+ const event = JSON.parse(line);
91
+ if (event.timestamp && new Date(event.timestamp) > cutoff) {
92
+ // Check for MCP tool references in event data
93
+ const eventStr = JSON.stringify(event);
94
+ for (const server of servers) {
95
+ if (eventStr.includes(server.name)) {
96
+ usedServerNames.add(server.name);
97
+ }
98
+ }
99
+ }
100
+ } catch {
101
+ // Skip malformed lines
102
+ }
103
+ }
104
+ } catch {
105
+ // Analytics not available — mark all as unknown usage
106
+ }
107
+ }
108
+
109
+ return servers.map(server => {
110
+ const isHeavy = HEAVY_SERVERS.some(h => server.name.toLowerCase().includes(h));
111
+ const isLight = LIGHT_SERVERS.some(l => server.name.toLowerCase().includes(l));
112
+ const overhead = isHeavy ? MCP_TOKEN_OVERHEAD.heavy : isLight ? MCP_TOKEN_OVERHEAD.light : MCP_TOKEN_OVERHEAD.default;
113
+
114
+ const usageSeen = usedServerNames.has(server.name);
115
+
116
+ return {
117
+ ...server,
118
+ overhead,
119
+ usageSeen,
120
+ usageStatus: usedServerNames.size === 0 ? 'unknown' : (usageSeen ? 'used' : 'unused')
121
+ };
122
+ });
123
+ }
124
+
125
+ // ============================================================================
126
+ // Recommendations
127
+ // ============================================================================
128
+
129
+ /**
130
+ * Analyze MCP configuration and generate optimization recommendations
131
+ * @returns {Object} Analysis results with recommendations
132
+ */
133
+ export function analyzeMCP() {
134
+ const result = loadClaudeSettings();
135
+
136
+ if (!result) {
137
+ return {
138
+ found: false,
139
+ message: 'No Claude settings file found. Checked: .claude/settings.json and .claude/settings.local.json',
140
+ servers: [],
141
+ recommendations: []
142
+ };
143
+ }
144
+
145
+ const { settings, path: settingsPath } = result;
146
+ const servers = extractMCPServers(settings);
147
+
148
+ if (servers.length === 0) {
149
+ return {
150
+ found: true,
151
+ settingsPath,
152
+ servers: [],
153
+ totalOverhead: 0,
154
+ recommendations: [],
155
+ message: 'No MCP servers configured.'
156
+ };
157
+ }
158
+
159
+ const analyzedServers = detectUnusedServers(servers);
160
+ const totalOverhead = analyzedServers.reduce((sum, s) => sum + s.overhead, 0);
161
+
162
+ const unusedServers = analyzedServers.filter(s => s.usageStatus === 'unused');
163
+ const unusedOverhead = unusedServers.reduce((sum, s) => sum + s.overhead, 0);
164
+
165
+ const recommendations = [];
166
+
167
+ if (unusedServers.length > 0) {
168
+ recommendations.push({
169
+ type: 'disable_unused',
170
+ title: `Disable ${unusedServers.length} unused MCP server(s)`,
171
+ description: `${unusedServers.map(s => s.name).join(', ')} have not been used recently.`,
172
+ estimatedSavings: unusedOverhead,
173
+ servers: unusedServers.map(s => s.name),
174
+ action: 'Remove from mcpServers in .claude/settings.json'
175
+ });
176
+ }
177
+
178
+ const heavyServers = analyzedServers.filter(s => s.overhead >= MCP_TOKEN_OVERHEAD.heavy && s.usageStatus !== 'unused');
179
+ if (heavyServers.length > 0) {
180
+ recommendations.push({
181
+ type: 'defer_heavy',
182
+ title: `Consider deferring ${heavyServers.length} heavy MCP server(s)`,
183
+ description: `${heavyServers.map(s => s.name).join(', ')} use significant context window space.`,
184
+ estimatedSavings: heavyServers.reduce((sum, s) => sum + s.overhead, 0) / 2,
185
+ servers: heavyServers.map(s => s.name),
186
+ action: 'Only enable when needed for that specific task'
187
+ });
188
+ }
189
+
190
+ return {
191
+ found: true,
192
+ settingsPath,
193
+ servers: analyzedServers,
194
+ totalServers: servers.length,
195
+ totalOverhead,
196
+ unusedCount: unusedServers.length,
197
+ potentialSavings: unusedOverhead,
198
+ recommendations: recommendations.sort((a, b) => b.estimatedSavings - a.estimatedSavings)
199
+ };
200
+ }
@@ -69,7 +69,7 @@ export function getDetectionSummary(results) {
69
69
  '### Stack',
70
70
  `- **Type**: ${structure?.stack || 'unknown'}`,
71
71
  `- **Architecture**: ${structure?.architecture || 'unknown'}`,
72
- `- **UI Library**: ${structure?.uiLibrary || 'none'}`,
72
+ ...(structure?.uiLibrary ? [`- **UI Library**: ${structure.uiLibrary}`] : []),
73
73
  '',
74
74
  '### Technologies',
75
75
  `- **Language**: ${config?.language || 'unknown'}`,