agentfit 0.1.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.
Files changed (107) hide show
  1. package/.claude/settings.local.json +26 -0
  2. package/.prettierignore +7 -0
  3. package/.prettierrc +11 -0
  4. package/CONTRIBUTING.md +209 -0
  5. package/LICENSE +21 -0
  6. package/README.md +109 -0
  7. package/app/(dashboard)/coach/page.tsx +11 -0
  8. package/app/(dashboard)/commands/page.tsx +7 -0
  9. package/app/(dashboard)/community/[slug]/page.tsx +23 -0
  10. package/app/(dashboard)/community/page.tsx +71 -0
  11. package/app/(dashboard)/daily/page.tsx +19 -0
  12. package/app/(dashboard)/images/page.tsx +5 -0
  13. package/app/(dashboard)/layout.tsx +12 -0
  14. package/app/(dashboard)/page.tsx +23 -0
  15. package/app/(dashboard)/personality/page.tsx +11 -0
  16. package/app/(dashboard)/projects/page.tsx +11 -0
  17. package/app/(dashboard)/sessions/page.tsx +11 -0
  18. package/app/(dashboard)/tokens/page.tsx +11 -0
  19. package/app/(dashboard)/tools/page.tsx +11 -0
  20. package/app/api/check/route.ts +13 -0
  21. package/app/api/commands/route.ts +16 -0
  22. package/app/api/images/[...path]/route.ts +33 -0
  23. package/app/api/images-analysis/route.ts +177 -0
  24. package/app/api/sync/route.ts +14 -0
  25. package/app/api/usage/route.ts +117 -0
  26. package/app/favicon.ico +0 -0
  27. package/app/globals.css +144 -0
  28. package/app/icon.svg +3 -0
  29. package/app/layout.tsx +35 -0
  30. package/bin/agentfit.mjs +69 -0
  31. package/components/.gitkeep +0 -0
  32. package/components/agent-coach.tsx +248 -0
  33. package/components/app-sidebar.tsx +161 -0
  34. package/components/command-usage.tsx +294 -0
  35. package/components/daily-chart.tsx +118 -0
  36. package/components/daily-table.tsx +115 -0
  37. package/components/dashboard-shell.tsx +149 -0
  38. package/components/data-provider.tsx +213 -0
  39. package/components/fitness-score.tsx +95 -0
  40. package/components/overview-cards.tsx +198 -0
  41. package/components/pagination-controls.tsx +104 -0
  42. package/components/personality-fit.tsx +446 -0
  43. package/components/projects-table.tsx +70 -0
  44. package/components/screenshots-analysis.tsx +359 -0
  45. package/components/sessions-table.tsx +97 -0
  46. package/components/theme-provider.tsx +71 -0
  47. package/components/token-breakdown.tsx +179 -0
  48. package/components/tool-usage-chart.tsx +63 -0
  49. package/components/ui/badge.tsx +52 -0
  50. package/components/ui/button.tsx +60 -0
  51. package/components/ui/card.tsx +103 -0
  52. package/components/ui/chart.tsx +373 -0
  53. package/components/ui/dialog.tsx +160 -0
  54. package/components/ui/input.tsx +20 -0
  55. package/components/ui/scroll-area.tsx +55 -0
  56. package/components/ui/select.tsx +201 -0
  57. package/components/ui/separator.tsx +25 -0
  58. package/components/ui/sheet.tsx +138 -0
  59. package/components/ui/sidebar.tsx +723 -0
  60. package/components/ui/skeleton.tsx +13 -0
  61. package/components/ui/table.tsx +116 -0
  62. package/components/ui/tabs.tsx +82 -0
  63. package/components/ui/tooltip.tsx +66 -0
  64. package/components.json +25 -0
  65. package/generated/prisma/browser.ts +34 -0
  66. package/generated/prisma/client.ts +58 -0
  67. package/generated/prisma/commonInputTypes.ts +237 -0
  68. package/generated/prisma/enums.ts +15 -0
  69. package/generated/prisma/internal/class.ts +224 -0
  70. package/generated/prisma/internal/prismaNamespace.ts +920 -0
  71. package/generated/prisma/internal/prismaNamespaceBrowser.ts +130 -0
  72. package/generated/prisma/models/Image.ts +1310 -0
  73. package/generated/prisma/models/Session.ts +1695 -0
  74. package/generated/prisma/models/SyncLog.ts +1203 -0
  75. package/generated/prisma/models.ts +14 -0
  76. package/hooks/.gitkeep +0 -0
  77. package/hooks/use-mobile.ts +19 -0
  78. package/hooks/use-pagination.ts +60 -0
  79. package/lib/.gitkeep +0 -0
  80. package/lib/coach.ts +425 -0
  81. package/lib/commands.ts +239 -0
  82. package/lib/db.ts +15 -0
  83. package/lib/format.ts +26 -0
  84. package/lib/parse-codex.ts +201 -0
  85. package/lib/parse-logs.ts +369 -0
  86. package/lib/personality.ts +481 -0
  87. package/lib/plugins.ts +107 -0
  88. package/lib/pricing.ts +112 -0
  89. package/lib/queries-codex.ts +130 -0
  90. package/lib/queries.ts +154 -0
  91. package/lib/resolve-icon.ts +12 -0
  92. package/lib/sync.ts +335 -0
  93. package/lib/utils.ts +6 -0
  94. package/next.config.mjs +4 -0
  95. package/package.json +73 -0
  96. package/plugins/cost-heatmap/component.test.tsx +52 -0
  97. package/plugins/cost-heatmap/component.tsx +227 -0
  98. package/plugins/cost-heatmap/manifest.ts +13 -0
  99. package/plugins/index.ts +18 -0
  100. package/prisma/migrations/20260328152517_init/migration.sql +41 -0
  101. package/prisma/migrations/20260328153801_add_image_model/migration.sql +18 -0
  102. package/prisma/migrations/migration_lock.toml +3 -0
  103. package/prisma/schema.prisma +57 -0
  104. package/prisma.config.ts +14 -0
  105. package/public/.gitkeep +0 -0
  106. package/public/logo.svg +3 -0
  107. package/setup.sh +73 -0
@@ -0,0 +1,481 @@
1
+ // ─── Agent Personality Analysis ─────────────────────────────────────
2
+ // Derives Big Five (OCEAN) and MBTI personality profiles from
3
+ // conversation trace behavioral signals.
4
+
5
+ import type { SessionSummary, UsageData } from './parse-logs'
6
+
7
+ // ─── Types ───────────────────────────────────────────────────────────
8
+
9
+ export interface BigFiveProfile {
10
+ openness: number // 0-100: tool diversity, approach variety
11
+ conscientiousness: number // 0-100: read-before-edit, low error/revert rate
12
+ extraversion: number // 0-100: output verbosity, proactive communication
13
+ agreeableness: number // 0-100: compliance, low friction, follows directions
14
+ neuroticism: number // 0-100: behavioral variance, instability (inverse = stability)
15
+ }
16
+
17
+ export interface MBTIProfile {
18
+ ei: number // -100 (Introversion) to +100 (Extraversion): verbosity ratio
19
+ sn: number // -100 (Sensing) to +100 (iNtuition): concrete vs. abstract tool patterns
20
+ tf: number // -100 (Thinking) to +100 (Feeling): execution vs. explanation ratio
21
+ jp: number // -100 (Judging) to +100 (Perceiving): structured vs. exploratory workflow
22
+ }
23
+
24
+ export interface PersonalityProfile {
25
+ bigFive: BigFiveProfile
26
+ mbti: MBTIProfile
27
+ mbtiType: string // e.g., "ISTJ"
28
+ signals: PersonalitySignals
29
+ fitScores: TaskFitScores
30
+ recommendations: PersonalityRecommendation[]
31
+ systemPrompt: string
32
+ }
33
+
34
+ export interface PersonalitySignals {
35
+ toolDiversity: number // unique tools / total tool calls
36
+ readBeforeEditRatio: number // read calls before edit / total edits
37
+ outputInputRatio: number // output tokens / input tokens
38
+ avgAssistantMsgLength: number // avg output tokens per assistant message
39
+ bashToTotalRatio: number // bash calls / total tool calls
40
+ editToReadRatio: number // edit calls / read calls
41
+ toolTransitionEntropy: number // entropy of tool bigrams
42
+ sessionDurationVariance: number// CV of session durations
43
+ avgToolCallsPerMessage: number // tool calls per assistant message
44
+ contextOverflowRate: number // sessions with high token density
45
+ }
46
+
47
+ export interface TaskFitScores {
48
+ frontendDev: number
49
+ backendDev: number
50
+ dataEngineering: number
51
+ devOps: number
52
+ debugging: number
53
+ refactoring: number
54
+ }
55
+
56
+ export interface PersonalityRecommendation {
57
+ trait: string
58
+ current: string
59
+ ideal: string
60
+ adjustment: string
61
+ promptSnippet: string
62
+ }
63
+
64
+ // ─── Signal Extraction ───────────────────────────────────────────────
65
+
66
+ function extractSignals(data: UsageData): PersonalitySignals {
67
+ const sessions = data.sessions
68
+ const toolUsage = data.toolUsage
69
+
70
+ const totalToolCalls = Object.values(toolUsage).reduce((a, b) => a + b, 0)
71
+ const uniqueTools = Object.keys(toolUsage).length
72
+
73
+ // Tool diversity: unique tools / total calls (normalized)
74
+ const toolDiversity = totalToolCalls > 0
75
+ ? Math.min(1, uniqueTools / Math.sqrt(totalToolCalls))
76
+ : 0
77
+
78
+ // Read-before-Edit ratio from tool patterns
79
+ const readCalls = (toolUsage['Read'] || 0) + (toolUsage['Grep'] || 0) + (toolUsage['Glob'] || 0)
80
+ const editCalls = (toolUsage['Edit'] || 0) + (toolUsage['Write'] || 0)
81
+ const readBeforeEditRatio = editCalls > 0
82
+ ? Math.min(2, readCalls / editCalls)
83
+ : 1
84
+
85
+ // Output/Input token ratio
86
+ const totalOutput = data.overview.totalOutputTokens
87
+ const totalInput = data.overview.totalInputTokens
88
+ const outputInputRatio = totalInput > 0 ? totalOutput / totalInput : 1
89
+
90
+ // Average assistant message length (tokens per message)
91
+ const totalAssistantMsgs = data.overview.totalAssistantMessages
92
+ const avgAssistantMsgLength = totalAssistantMsgs > 0
93
+ ? totalOutput / totalAssistantMsgs
94
+ : 0
95
+
96
+ // Bash to total ratio
97
+ const bashCalls = toolUsage['Bash'] || 0
98
+ const bashToTotalRatio = totalToolCalls > 0 ? bashCalls / totalToolCalls : 0
99
+
100
+ // Edit to Read ratio
101
+ const editToReadRatio = readCalls > 0 ? editCalls / readCalls : 0
102
+
103
+ // Tool transition entropy (approximated from tool distribution)
104
+ const toolProbs = Object.values(toolUsage).map(c => c / totalToolCalls)
105
+ const toolTransitionEntropy = totalToolCalls > 0
106
+ ? -toolProbs.reduce((sum, p) => sum + (p > 0 ? p * Math.log2(p) : 0), 0)
107
+ : 0
108
+
109
+ // Session duration variance (coefficient of variation)
110
+ const durations = sessions.map(s => s.durationMinutes).filter(d => d > 0)
111
+ const meanDuration = durations.length > 0
112
+ ? durations.reduce((a, b) => a + b, 0) / durations.length
113
+ : 0
114
+ const varianceDuration = durations.length > 1
115
+ ? durations.reduce((sum, d) => sum + Math.pow(d - meanDuration, 2), 0) / durations.length
116
+ : 0
117
+ const sessionDurationVariance = meanDuration > 0
118
+ ? Math.sqrt(varianceDuration) / meanDuration
119
+ : 0
120
+
121
+ // Tool calls per assistant message
122
+ const avgToolCallsPerMessage = totalAssistantMsgs > 0
123
+ ? totalToolCalls / totalAssistantMsgs
124
+ : 0
125
+
126
+ // Context overflow rate (sessions where tokens > 100k suggesting long context usage)
127
+ const highTokenSessions = sessions.filter(s => s.totalTokens > 100000).length
128
+ const contextOverflowRate = sessions.length > 0
129
+ ? highTokenSessions / sessions.length
130
+ : 0
131
+
132
+ return {
133
+ toolDiversity,
134
+ readBeforeEditRatio,
135
+ outputInputRatio,
136
+ avgAssistantMsgLength,
137
+ bashToTotalRatio,
138
+ editToReadRatio,
139
+ toolTransitionEntropy,
140
+ sessionDurationVariance,
141
+ avgToolCallsPerMessage,
142
+ contextOverflowRate,
143
+ }
144
+ }
145
+
146
+ // ─── Big Five Derivation ─────────────────────────────────────────────
147
+
148
+ function deriveBigFive(signals: PersonalitySignals): BigFiveProfile {
149
+ // Openness: tool diversity + transition entropy (explores many approaches)
150
+ const openness = clamp(
151
+ (signals.toolDiversity * 60) +
152
+ (Math.min(signals.toolTransitionEntropy / 4, 1) * 40),
153
+ 0, 100
154
+ )
155
+
156
+ // Conscientiousness: read-before-edit ratio + low bash ratio (careful, methodical)
157
+ const conscientiousness = clamp(
158
+ (Math.min(signals.readBeforeEditRatio / 2, 1) * 60) +
159
+ ((1 - signals.bashToTotalRatio) * 20) +
160
+ ((1 - signals.contextOverflowRate) * 20),
161
+ 0, 100
162
+ )
163
+
164
+ // Extraversion: output verbosity + tool calls per message (communicative, active)
165
+ const extraversion = clamp(
166
+ (Math.min(signals.outputInputRatio / 2, 1) * 40) +
167
+ (Math.min(signals.avgAssistantMsgLength / 2000, 1) * 30) +
168
+ (Math.min(signals.avgToolCallsPerMessage / 3, 1) * 30),
169
+ 0, 100
170
+ )
171
+
172
+ // Agreeableness: low edit-to-read ratio (listens more than changes) + consistency
173
+ const agreeableness = clamp(
174
+ ((1 - Math.min(signals.editToReadRatio, 1)) * 40) +
175
+ (Math.min(signals.readBeforeEditRatio / 2, 1) * 40) +
176
+ ((1 - signals.sessionDurationVariance) * 20),
177
+ 0, 100
178
+ )
179
+
180
+ // Neuroticism: high variance + high overflow rate + inconsistent patterns
181
+ const neuroticism = clamp(
182
+ (signals.sessionDurationVariance * 40) +
183
+ (signals.contextOverflowRate * 40) +
184
+ ((1 - signals.toolDiversity) * 20),
185
+ 0, 100
186
+ )
187
+
188
+ return { openness, conscientiousness, extraversion, agreeableness, neuroticism }
189
+ }
190
+
191
+ // ─── MBTI Derivation ─────────────────────────────────────────────────
192
+
193
+ function deriveMBTI(signals: PersonalitySignals): MBTIProfile {
194
+ // E/I: Output verbosity — verbose = Extraversion, concise = Introversion
195
+ const ei = clamp(
196
+ ((signals.outputInputRatio - 0.5) * 100) +
197
+ ((signals.avgAssistantMsgLength / 2000 - 0.5) * 100),
198
+ -100, 100
199
+ )
200
+
201
+ // S/N: Read/Grep heavy (concrete, detail-oriented) = Sensing; diverse/exploratory = iNtuition
202
+ const sn = clamp(
203
+ ((signals.toolDiversity - 0.3) * 150) +
204
+ ((signals.toolTransitionEntropy / 4 - 0.5) * 100),
205
+ -100, 100
206
+ )
207
+
208
+ // T/F: Bash/Edit heavy (execution-focused) = Thinking; Read/explanation = Feeling
209
+ const tf = clamp(
210
+ ((signals.bashToTotalRatio - 0.2) * -200) +
211
+ ((signals.editToReadRatio - 0.5) * -100),
212
+ -100, 100
213
+ )
214
+
215
+ // J/P: Low variance (structured) = Judging; high variance (exploratory) = Perceiving
216
+ const jp = clamp(
217
+ ((signals.sessionDurationVariance - 0.5) * 100) +
218
+ ((signals.contextOverflowRate - 0.2) * 100),
219
+ -100, 100
220
+ )
221
+
222
+ return { ei, sn, tf, jp }
223
+ }
224
+
225
+ function mbtiTypeFromProfile(mbti: MBTIProfile): string {
226
+ return (
227
+ (mbti.ei >= 0 ? 'E' : 'I') +
228
+ (mbti.sn >= 0 ? 'N' : 'S') +
229
+ (mbti.tf >= 0 ? 'F' : 'T') +
230
+ (mbti.jp >= 0 ? 'P' : 'J')
231
+ )
232
+ }
233
+
234
+ // ─── Task Fit Scores ─────────────────────────────────────────────────
235
+ // Ideal personality profiles for different task types
236
+
237
+ const IDEAL_PROFILES: Record<string, BigFiveProfile> = {
238
+ frontendDev: {
239
+ openness: 80, // Creative, explores UI approaches
240
+ conscientiousness: 60, // Moderate — needs iteration
241
+ extraversion: 70, // Communicative about visual choices
242
+ agreeableness: 75, // Responsive to design feedback
243
+ neuroticism: 20, // Stable under visual debugging
244
+ },
245
+ backendDev: {
246
+ openness: 50, // Moderate — proven patterns preferred
247
+ conscientiousness: 90, // Very careful with data/logic
248
+ extraversion: 40, // Concise, efficient
249
+ agreeableness: 60, // Follows specs but pushes back on bad ideas
250
+ neuroticism: 15, // Very stable
251
+ },
252
+ dataEngineering: {
253
+ openness: 45, // Conservative with data pipelines
254
+ conscientiousness: 95, // Extremely careful — data integrity matters
255
+ extraversion: 35, // Quiet, focused
256
+ agreeableness: 55, // Follows standards
257
+ neuroticism: 10, // Rock stable
258
+ },
259
+ devOps: {
260
+ openness: 55, // Moderate exploration
261
+ conscientiousness: 85, // Careful with infrastructure
262
+ extraversion: 50, // Communicates clearly about status
263
+ agreeableness: 65, // Follows runbooks
264
+ neuroticism: 15, // Calm under pressure
265
+ },
266
+ debugging: {
267
+ openness: 70, // Explores multiple hypotheses
268
+ conscientiousness: 80, // Methodical investigation
269
+ extraversion: 55, // Explains findings
270
+ agreeableness: 50, // Challenges assumptions
271
+ neuroticism: 25, // Handles frustration
272
+ },
273
+ refactoring: {
274
+ openness: 65, // Sees better patterns
275
+ conscientiousness: 90, // Doesn't break existing behavior
276
+ extraversion: 45, // Shows what changed, concisely
277
+ agreeableness: 70, // Respects existing code style
278
+ neuroticism: 15, // Steady, incremental
279
+ },
280
+ }
281
+
282
+ function computeFitScores(actual: BigFiveProfile): TaskFitScores {
283
+ const scores: Record<string, number> = {}
284
+ for (const [task, ideal] of Object.entries(IDEAL_PROFILES)) {
285
+ const distance = Math.sqrt(
286
+ Math.pow(actual.openness - ideal.openness, 2) +
287
+ Math.pow(actual.conscientiousness - ideal.conscientiousness, 2) +
288
+ Math.pow(actual.extraversion - ideal.extraversion, 2) +
289
+ Math.pow(actual.agreeableness - ideal.agreeableness, 2) +
290
+ Math.pow(actual.neuroticism - ideal.neuroticism, 2)
291
+ )
292
+ // Max possible distance is sqrt(5 * 100^2) ≈ 223.6
293
+ scores[task] = Math.round(Math.max(0, 100 - (distance / 2.236)))
294
+ }
295
+ return scores as unknown as TaskFitScores
296
+ }
297
+
298
+ // ─── Recommendations & System Prompt Generation ──────────────────────
299
+
300
+ function generateRecommendations(
301
+ actual: BigFiveProfile,
302
+ targetTask: string
303
+ ): PersonalityRecommendation[] {
304
+ const ideal = IDEAL_PROFILES[targetTask] || IDEAL_PROFILES.backendDev
305
+ const recommendations: PersonalityRecommendation[] = []
306
+ const threshold = 15 // Only recommend if gap > threshold
307
+
308
+ const traitMap: { trait: keyof BigFiveProfile; label: string }[] = [
309
+ { trait: 'openness', label: 'Openness' },
310
+ { trait: 'conscientiousness', label: 'Conscientiousness' },
311
+ { trait: 'extraversion', label: 'Extraversion' },
312
+ { trait: 'agreeableness', label: 'Agreeableness' },
313
+ { trait: 'neuroticism', label: 'Neuroticism' },
314
+ ]
315
+
316
+ for (const { trait, label } of traitMap) {
317
+ const gap = ideal[trait] - actual[trait]
318
+ if (Math.abs(gap) <= threshold) continue
319
+
320
+ const direction = gap > 0 ? 'increase' : 'decrease'
321
+ const currentLevel = describeLevel(actual[trait])
322
+ const idealLevel = describeLevel(ideal[trait])
323
+
324
+ recommendations.push({
325
+ trait: label,
326
+ current: `${currentLevel} (${Math.round(actual[trait])})`,
327
+ ideal: `${idealLevel} (${Math.round(ideal[trait])})`,
328
+ adjustment: `${direction} by ${Math.abs(Math.round(gap))} points`,
329
+ promptSnippet: getPromptSnippet(trait, direction, targetTask),
330
+ })
331
+ }
332
+
333
+ return recommendations
334
+ }
335
+
336
+ function describeLevel(score: number): string {
337
+ if (score >= 80) return 'Very High'
338
+ if (score >= 60) return 'High'
339
+ if (score >= 40) return 'Moderate'
340
+ if (score >= 20) return 'Low'
341
+ return 'Very Low'
342
+ }
343
+
344
+ function getPromptSnippet(
345
+ trait: keyof BigFiveProfile,
346
+ direction: string,
347
+ task: string
348
+ ): string {
349
+ const snippets: Record<string, Record<string, string>> = {
350
+ openness: {
351
+ increase: 'Explore multiple alternative approaches before settling on a solution. When faced with a problem, propose at least 2-3 different strategies and explain the trade-offs of each.',
352
+ decrease: 'Prefer established, proven patterns and conventions. Avoid novel approaches when standard solutions exist. Stick to the project\'s existing architectural patterns.',
353
+ },
354
+ conscientiousness: {
355
+ increase: 'Always read and understand existing code before making changes. Verify your changes don\'t break existing functionality. Run tests after every modification. Double-check edge cases.',
356
+ decrease: 'Move faster with less verification. Trust that the existing test suite will catch issues. Focus on getting a working solution rather than perfecting every edge case.',
357
+ },
358
+ extraversion: {
359
+ increase: 'Explain your reasoning and approach before writing code. Provide context for why you chose a particular solution. Proactively flag potential issues or improvements you notice.',
360
+ decrease: 'Be concise. Write code, not explanations. Only comment when the logic is non-obvious. Skip preamble and get straight to implementation.',
361
+ },
362
+ agreeableness: {
363
+ increase: 'Follow the user\'s directions closely. When you disagree with an approach, implement it as requested first, then briefly mention concerns. Adapt to the project\'s existing style.',
364
+ decrease: 'Push back when you see a better approach. If the requested change would introduce technical debt, explain why and propose alternatives. Prioritize code quality over compliance.',
365
+ },
366
+ neuroticism: {
367
+ increase: 'Be more cautious. Flag potential risks and failure modes. Ask clarifying questions before making assumptions. Prefer reversible changes.',
368
+ decrease: 'Stay calm and methodical. Don\'t overthink edge cases that are unlikely to occur. Trust your analysis and commit to your approach confidently.',
369
+ },
370
+ }
371
+
372
+ return snippets[trait]?.[direction] || ''
373
+ }
374
+
375
+ function generateSystemPrompt(
376
+ recommendations: PersonalityRecommendation[],
377
+ targetTask: string,
378
+ mbtiType: string
379
+ ): string {
380
+ if (recommendations.length === 0) {
381
+ return `# Agent Personality Configuration\n\nCurrent personality profile (${mbtiType}) is well-suited for ${formatTaskName(targetTask)}. No adjustments needed.`
382
+ }
383
+
384
+ const taskDescriptions: Record<string, string> = {
385
+ frontendDev: 'frontend development — building UI components, styling, responsive design, and visual debugging',
386
+ backendDev: 'backend development — API design, business logic, database interactions, and server-side architecture',
387
+ dataEngineering: 'data engineering — ETL pipelines, data parsing, web scraping, and data quality assurance',
388
+ devOps: 'DevOps — CI/CD pipelines, infrastructure, deployment, and monitoring',
389
+ debugging: 'debugging — root cause analysis, reproducing issues, and systematic problem investigation',
390
+ refactoring: 'refactoring — improving code structure, reducing duplication, and modernizing patterns without changing behavior',
391
+ }
392
+
393
+ let prompt = `# Agent Personality Configuration for ${formatTaskName(targetTask)}\n\n`
394
+ prompt += `You are assisting with ${taskDescriptions[targetTask] || targetTask}.\n\n`
395
+ prompt += `## Behavioral Guidelines\n\n`
396
+
397
+ for (const rec of recommendations) {
398
+ prompt += `### ${rec.trait} Adjustment\n`
399
+ prompt += `${rec.promptSnippet}\n\n`
400
+ }
401
+
402
+ prompt += `## Task-Specific Instructions\n\n`
403
+
404
+ switch (targetTask) {
405
+ case 'frontendDev':
406
+ prompt += `- When making visual changes, describe what the user should expect to see\n`
407
+ prompt += `- Suggest screenshots for verification after UI modifications\n`
408
+ prompt += `- Consider accessibility and responsive design in every component\n`
409
+ break
410
+ case 'backendDev':
411
+ prompt += `- Validate input at system boundaries, trust internal interfaces\n`
412
+ prompt += `- Consider error handling, but don't over-engineer for impossible scenarios\n`
413
+ prompt += `- Write clear, self-documenting function signatures\n`
414
+ break
415
+ case 'dataEngineering':
416
+ prompt += `- Always validate data at ingestion boundaries\n`
417
+ prompt += `- Log record counts at each pipeline stage for auditability\n`
418
+ prompt += `- Handle malformed data gracefully — skip and log, don't crash\n`
419
+ break
420
+ case 'devOps':
421
+ prompt += `- Prefer idempotent operations\n`
422
+ prompt += `- Always consider rollback paths before making infrastructure changes\n`
423
+ prompt += `- Document any manual steps required\n`
424
+ break
425
+ case 'debugging':
426
+ prompt += `- Form hypotheses before investigating. State what you expect to find\n`
427
+ prompt += `- Narrow down with binary search — don't read everything\n`
428
+ prompt += `- Preserve and explain the root cause, not just the fix\n`
429
+ break
430
+ case 'refactoring':
431
+ prompt += `- Ensure behavior is unchanged — preserve all existing tests\n`
432
+ prompt += `- Make small, incremental changes. Each step should be independently valid\n`
433
+ prompt += `- Don't change interfaces unless necessary for the refactor\n`
434
+ break
435
+ }
436
+
437
+ return prompt
438
+ }
439
+
440
+ function formatTaskName(task: string): string {
441
+ const names: Record<string, string> = {
442
+ frontendDev: 'Frontend Development',
443
+ backendDev: 'Backend Development',
444
+ dataEngineering: 'Data Engineering',
445
+ devOps: 'DevOps',
446
+ debugging: 'Debugging',
447
+ refactoring: 'Refactoring',
448
+ }
449
+ return names[task] || task
450
+ }
451
+
452
+ // ─── Main Analysis Function ──────────────────────────────────────────
453
+
454
+ export function analyzePersonality(
455
+ data: UsageData,
456
+ targetTask: string = 'backendDev'
457
+ ): PersonalityProfile {
458
+ const signals = extractSignals(data)
459
+ const bigFive = deriveBigFive(signals)
460
+ const mbti = deriveMBTI(signals)
461
+ const mbtiType = mbtiTypeFromProfile(mbti)
462
+ const fitScores = computeFitScores(bigFive)
463
+ const recommendations = generateRecommendations(bigFive, targetTask)
464
+ const systemPrompt = generateSystemPrompt(recommendations, targetTask, mbtiType)
465
+
466
+ return {
467
+ bigFive,
468
+ mbti,
469
+ mbtiType,
470
+ signals,
471
+ fitScores,
472
+ recommendations,
473
+ systemPrompt,
474
+ }
475
+ }
476
+
477
+ // ─── Helpers ─────────────────────────────────────────────────────────
478
+
479
+ function clamp(value: number, min: number, max: number): number {
480
+ return Math.max(min, Math.min(max, Math.round(value * 100) / 100))
481
+ }
package/lib/plugins.ts ADDED
@@ -0,0 +1,107 @@
1
+ import type { ComponentType } from 'react'
2
+ import type { UsageData } from './parse-logs'
3
+
4
+ // ─── Plugin Contract ────────────────────────────────────────────────
5
+ // Every community plugin must export a manifest and a default component
6
+ // that conforms to these interfaces.
7
+
8
+ /** Props passed to every community plugin component */
9
+ export interface PluginProps {
10
+ data: UsageData
11
+ }
12
+
13
+ /** Metadata every plugin must declare in its manifest.ts */
14
+ export interface PluginManifest {
15
+ /** Unique URL-safe slug (lowercase, hyphens only). Used as the route segment. */
16
+ slug: string
17
+ /** Human-readable name shown in the sidebar */
18
+ name: string
19
+ /** One-line description shown in tooltips */
20
+ description: string
21
+ /** Author name or GitHub handle */
22
+ author: string
23
+ /** Lucide icon name (e.g. "Flame"). Resolved at render time. */
24
+ icon: string
25
+ /** Semver version string */
26
+ version: string
27
+ /** Optional tags for discoverability */
28
+ tags?: string[]
29
+ /** Set to true if the plugin fetches its own data (hides time-range filter) */
30
+ customDataSource?: boolean
31
+ }
32
+
33
+ /** A fully resolved plugin ready to render */
34
+ export interface ResolvedPlugin {
35
+ manifest: PluginManifest
36
+ component: ComponentType<PluginProps>
37
+ }
38
+
39
+ // ─── Plugin Registry ────────────────────────────────────────────────
40
+ // Plugins register themselves by calling registerPlugin().
41
+ // The registry is populated at import time via plugins/index.ts.
42
+
43
+ const registry = new Map<string, ResolvedPlugin>()
44
+
45
+ /** Register a community plugin. Called from plugins/index.ts. */
46
+ export function registerPlugin(
47
+ manifest: PluginManifest,
48
+ component: ComponentType<PluginProps>,
49
+ ) {
50
+ if (registry.has(manifest.slug)) {
51
+ console.warn(`[AgentFit] Duplicate plugin slug: "${manifest.slug}" — skipping.`)
52
+ return
53
+ }
54
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(manifest.slug)) {
55
+ console.warn(
56
+ `[AgentFit] Invalid plugin slug: "${manifest.slug}". ` +
57
+ 'Use lowercase letters, numbers, and hyphens only.',
58
+ )
59
+ return
60
+ }
61
+ registry.set(manifest.slug, { manifest, component })
62
+ }
63
+
64
+ /** Get all registered plugins (sorted alphabetically by name) */
65
+ export function getPlugins(): ResolvedPlugin[] {
66
+ return Array.from(registry.values()).sort((a, b) =>
67
+ a.manifest.name.localeCompare(b.manifest.name),
68
+ )
69
+ }
70
+
71
+ /** Look up a single plugin by slug */
72
+ export function getPlugin(slug: string): ResolvedPlugin | undefined {
73
+ return registry.get(slug)
74
+ }
75
+
76
+ // ─── Validation helper (used by tests) ──────────────────────────────
77
+
78
+ export function validateManifest(m: unknown): string[] {
79
+ const errors: string[] = []
80
+ if (!m || typeof m !== 'object') {
81
+ return ['Manifest must be an object']
82
+ }
83
+ const obj = m as Record<string, unknown>
84
+
85
+ const requiredStrings = ['slug', 'name', 'description', 'author', 'icon', 'version'] as const
86
+ for (const key of requiredStrings) {
87
+ if (typeof obj[key] !== 'string' || (obj[key] as string).trim() === '') {
88
+ errors.push(`"${key}" must be a non-empty string`)
89
+ }
90
+ }
91
+
92
+ if (typeof obj.slug === 'string' && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(obj.slug)) {
93
+ errors.push('"slug" must be lowercase alphanumeric with hyphens (e.g. "my-plugin")')
94
+ }
95
+
96
+ if (typeof obj.version === 'string' && !/^\d+\.\d+\.\d+/.test(obj.version)) {
97
+ errors.push('"version" must follow semver (e.g. "1.0.0")')
98
+ }
99
+
100
+ if (obj.tags !== undefined) {
101
+ if (!Array.isArray(obj.tags) || !obj.tags.every((t: unknown) => typeof t === 'string')) {
102
+ errors.push('"tags" must be an array of strings')
103
+ }
104
+ }
105
+
106
+ return errors
107
+ }