markupr 2.1.8 → 2.5.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 (300) hide show
  1. package/README.md +292 -15
  2. package/dist/cli/index.mjs +3593 -0
  3. package/dist/main/index.mjs +743 -220
  4. package/dist/mcp/index.mjs +4053 -0
  5. package/package.json +32 -7
  6. package/.claude/commands/review-feedback.md +0 -47
  7. package/.eslintrc.json +0 -35
  8. package/.github/CODEOWNERS +0 -16
  9. package/.github/FUNDING.yml +0 -1
  10. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -56
  11. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -54
  12. package/.github/PULL_REQUEST_TEMPLATE.md +0 -89
  13. package/.github/dependabot.yml +0 -70
  14. package/.github/workflows/ci.yml +0 -184
  15. package/.github/workflows/deploy-landing.yml +0 -134
  16. package/.github/workflows/nightly.yml +0 -288
  17. package/.github/workflows/release.yml +0 -318
  18. package/CHANGELOG.md +0 -127
  19. package/CLAUDE.md +0 -137
  20. package/CODE_OF_CONDUCT.md +0 -9
  21. package/CONTRIBUTING.md +0 -390
  22. package/PRODUCT_VISION.md +0 -277
  23. package/SECURITY.md +0 -51
  24. package/SIGNING_INSTRUCTIONS.md +0 -284
  25. package/assets/DMG_BACKGROUND_INSTRUCTIONS.md +0 -130
  26. package/assets/svg-source/dmg-background.svg +0 -70
  27. package/assets/svg-source/icon.svg +0 -20
  28. package/assets/svg-source/tray-icon-processing.svg +0 -7
  29. package/assets/svg-source/tray-icon-recording.svg +0 -7
  30. package/assets/svg-source/tray-icon.svg +0 -6
  31. package/assets/tray-complete.png +0 -0
  32. package/assets/tray-complete@2x.png +0 -0
  33. package/assets/tray-completeTemplate.png +0 -0
  34. package/assets/tray-completeTemplate@2x.png +0 -0
  35. package/assets/tray-error.png +0 -0
  36. package/assets/tray-error@2x.png +0 -0
  37. package/assets/tray-errorTemplate.png +0 -0
  38. package/assets/tray-errorTemplate@2x.png +0 -0
  39. package/assets/tray-icon-processing.png +0 -0
  40. package/assets/tray-icon-processing@2x.png +0 -0
  41. package/assets/tray-icon-processingTemplate.png +0 -0
  42. package/assets/tray-icon-processingTemplate@2x.png +0 -0
  43. package/assets/tray-icon-recording.png +0 -0
  44. package/assets/tray-icon-recording@2x.png +0 -0
  45. package/assets/tray-icon-recordingTemplate.png +0 -0
  46. package/assets/tray-icon-recordingTemplate@2x.png +0 -0
  47. package/assets/tray-icon.png +0 -0
  48. package/assets/tray-icon@2x.png +0 -0
  49. package/assets/tray-iconTemplate.png +0 -0
  50. package/assets/tray-iconTemplate@2x.png +0 -0
  51. package/assets/tray-idle.png +0 -0
  52. package/assets/tray-idle@2x.png +0 -0
  53. package/assets/tray-idleTemplate.png +0 -0
  54. package/assets/tray-idleTemplate@2x.png +0 -0
  55. package/assets/tray-processing-0.png +0 -0
  56. package/assets/tray-processing-0@2x.png +0 -0
  57. package/assets/tray-processing-0Template.png +0 -0
  58. package/assets/tray-processing-0Template@2x.png +0 -0
  59. package/assets/tray-processing-1.png +0 -0
  60. package/assets/tray-processing-1@2x.png +0 -0
  61. package/assets/tray-processing-1Template.png +0 -0
  62. package/assets/tray-processing-1Template@2x.png +0 -0
  63. package/assets/tray-processing-2.png +0 -0
  64. package/assets/tray-processing-2@2x.png +0 -0
  65. package/assets/tray-processing-2Template.png +0 -0
  66. package/assets/tray-processing-2Template@2x.png +0 -0
  67. package/assets/tray-processing-3.png +0 -0
  68. package/assets/tray-processing-3@2x.png +0 -0
  69. package/assets/tray-processing-3Template.png +0 -0
  70. package/assets/tray-processing-3Template@2x.png +0 -0
  71. package/assets/tray-processing.png +0 -0
  72. package/assets/tray-processing@2x.png +0 -0
  73. package/assets/tray-processingTemplate.png +0 -0
  74. package/assets/tray-processingTemplate@2x.png +0 -0
  75. package/assets/tray-recording.png +0 -0
  76. package/assets/tray-recording@2x.png +0 -0
  77. package/assets/tray-recordingTemplate.png +0 -0
  78. package/assets/tray-recordingTemplate@2x.png +0 -0
  79. package/build/DMG_BACKGROUND_SPEC.md +0 -50
  80. package/build/dmg-background.png +0 -0
  81. package/build/dmg-background@2x.png +0 -0
  82. package/build/entitlements.mac.inherit.plist +0 -27
  83. package/build/entitlements.mac.plist +0 -41
  84. package/build/favicon-16.png +0 -0
  85. package/build/favicon-180.png +0 -0
  86. package/build/favicon-192.png +0 -0
  87. package/build/favicon-32.png +0 -0
  88. package/build/favicon-48.png +0 -0
  89. package/build/favicon-512.png +0 -0
  90. package/build/favicon-64.png +0 -0
  91. package/build/icon-128.png +0 -0
  92. package/build/icon-16.png +0 -0
  93. package/build/icon-24.png +0 -0
  94. package/build/icon-256.png +0 -0
  95. package/build/icon-32.png +0 -0
  96. package/build/icon-48.png +0 -0
  97. package/build/icon-64.png +0 -0
  98. package/build/icon.icns +0 -0
  99. package/build/icon.ico +0 -0
  100. package/build/icon.iconset/icon_128x128.png +0 -0
  101. package/build/icon.iconset/icon_128x128@2x.png +0 -0
  102. package/build/icon.iconset/icon_16x16.png +0 -0
  103. package/build/icon.iconset/icon_16x16@2x.png +0 -0
  104. package/build/icon.iconset/icon_256x256.png +0 -0
  105. package/build/icon.iconset/icon_256x256@2x.png +0 -0
  106. package/build/icon.iconset/icon_32x32.png +0 -0
  107. package/build/icon.iconset/icon_32x32@2x.png +0 -0
  108. package/build/icon.iconset/icon_512x512.png +0 -0
  109. package/build/icon.iconset/icon_512x512@2x.png +0 -0
  110. package/build/icon.png +0 -0
  111. package/build/installer-header.bmp +0 -0
  112. package/build/installer-header.png +0 -0
  113. package/build/installer-sidebar.bmp +0 -0
  114. package/build/installer-sidebar.png +0 -0
  115. package/build/installer.nsh +0 -45
  116. package/build/overlay-processing.png +0 -0
  117. package/build/overlay-recording.png +0 -0
  118. package/build/toolbar-record.png +0 -0
  119. package/build/toolbar-screenshot.png +0 -0
  120. package/build/toolbar-settings.png +0 -0
  121. package/build/toolbar-stop.png +0 -0
  122. package/dist/preload/index.mjs +0 -907
  123. package/dist/renderer/assets/index-CCmUjl9K.js +0 -19495
  124. package/dist/renderer/assets/index-CUqz_Gs6.css +0 -2270
  125. package/dist/renderer/index.html +0 -27
  126. package/docs/AI_AGENT_QUICKSTART.md +0 -42
  127. package/docs/AI_PIPELINE_DESIGN.md +0 -595
  128. package/docs/API.md +0 -514
  129. package/docs/ARCHITECTURE.md +0 -460
  130. package/docs/CONFIGURATION.md +0 -336
  131. package/docs/DEVELOPMENT.md +0 -508
  132. package/docs/EXPORT_FORMATS.md +0 -451
  133. package/docs/GETTING_STARTED.md +0 -236
  134. package/docs/KEYBOARD_SHORTCUTS.md +0 -334
  135. package/docs/TROUBLESHOOTING.md +0 -418
  136. package/docs/landing/index.html +0 -672
  137. package/docs/landing/script.js +0 -342
  138. package/docs/landing/styles.css +0 -1543
  139. package/electron-builder.yml +0 -140
  140. package/electron.vite.config.ts +0 -63
  141. package/railway.json +0 -12
  142. package/scripts/build.mjs +0 -51
  143. package/scripts/generate-icons.mjs +0 -314
  144. package/scripts/generate-installer-images.cjs +0 -253
  145. package/scripts/generate-tray-icons.mjs +0 -258
  146. package/scripts/notarize.cjs +0 -180
  147. package/scripts/one-click-clean-test.sh +0 -147
  148. package/scripts/postinstall.mjs +0 -36
  149. package/scripts/setup-markupr.sh +0 -55
  150. package/setup +0 -17
  151. package/site/index.html +0 -1835
  152. package/site/package.json +0 -11
  153. package/site/railway.json +0 -12
  154. package/site/server.js +0 -31
  155. package/src/main/AutoUpdater.ts +0 -392
  156. package/src/main/CrashRecovery.ts +0 -655
  157. package/src/main/ErrorHandler.ts +0 -703
  158. package/src/main/HotkeyManager.ts +0 -399
  159. package/src/main/MenuManager.ts +0 -529
  160. package/src/main/PermissionManager.ts +0 -420
  161. package/src/main/SessionController.ts +0 -1465
  162. package/src/main/TrayManager.ts +0 -540
  163. package/src/main/ai/AIPipelineManager.ts +0 -199
  164. package/src/main/ai/ClaudeAnalyzer.ts +0 -339
  165. package/src/main/ai/ImageOptimizer.ts +0 -176
  166. package/src/main/ai/StructuredMarkdownBuilder.ts +0 -379
  167. package/src/main/ai/index.ts +0 -16
  168. package/src/main/ai/types.ts +0 -258
  169. package/src/main/analysis/ClarificationGenerator.ts +0 -385
  170. package/src/main/analysis/FeedbackAnalyzer.ts +0 -531
  171. package/src/main/analysis/index.ts +0 -19
  172. package/src/main/audio/AudioCapture.ts +0 -978
  173. package/src/main/audio/audioUtils.ts +0 -100
  174. package/src/main/audio/index.ts +0 -20
  175. package/src/main/capture/index.ts +0 -1
  176. package/src/main/index.ts +0 -1693
  177. package/src/main/ipc/captureHandlers.ts +0 -272
  178. package/src/main/ipc/index.ts +0 -45
  179. package/src/main/ipc/outputHandlers.ts +0 -302
  180. package/src/main/ipc/sessionHandlers.ts +0 -56
  181. package/src/main/ipc/settingsHandlers.ts +0 -471
  182. package/src/main/ipc/types.ts +0 -56
  183. package/src/main/ipc/windowHandlers.ts +0 -277
  184. package/src/main/output/ClipboardService.ts +0 -369
  185. package/src/main/output/ExportService.ts +0 -539
  186. package/src/main/output/FileManager.ts +0 -416
  187. package/src/main/output/MarkdownGenerator.ts +0 -791
  188. package/src/main/output/MarkdownPatcher.ts +0 -299
  189. package/src/main/output/index.ts +0 -186
  190. package/src/main/output/sessionAdapter.ts +0 -207
  191. package/src/main/output/templates/html-template.ts +0 -553
  192. package/src/main/pipeline/FrameExtractor.ts +0 -330
  193. package/src/main/pipeline/PostProcessor.ts +0 -399
  194. package/src/main/pipeline/TranscriptAnalyzer.ts +0 -226
  195. package/src/main/pipeline/index.ts +0 -36
  196. package/src/main/platform/WindowsTaskbar.ts +0 -600
  197. package/src/main/platform/index.ts +0 -16
  198. package/src/main/settings/SettingsManager.ts +0 -730
  199. package/src/main/settings/index.ts +0 -19
  200. package/src/main/transcription/ModelDownloadManager.ts +0 -494
  201. package/src/main/transcription/TierManager.ts +0 -219
  202. package/src/main/transcription/TranscriptionRecoveryService.ts +0 -340
  203. package/src/main/transcription/WhisperService.ts +0 -748
  204. package/src/main/transcription/index.ts +0 -56
  205. package/src/main/transcription/types.ts +0 -135
  206. package/src/main/windows/PopoverManager.ts +0 -284
  207. package/src/main/windows/TaskbarIntegration.ts +0 -452
  208. package/src/main/windows/index.ts +0 -23
  209. package/src/preload/index.ts +0 -1047
  210. package/src/renderer/App.tsx +0 -515
  211. package/src/renderer/AppWrapper.tsx +0 -28
  212. package/src/renderer/assets/logo-dark.svg +0 -7
  213. package/src/renderer/assets/logo.svg +0 -7
  214. package/src/renderer/audio/AudioCaptureRenderer.ts +0 -454
  215. package/src/renderer/capture/ScreenRecordingRenderer.ts +0 -492
  216. package/src/renderer/components/AnnotationOverlay.tsx +0 -836
  217. package/src/renderer/components/AudioWaveform.tsx +0 -811
  218. package/src/renderer/components/ClarificationQuestions.tsx +0 -656
  219. package/src/renderer/components/CountdownTimer.tsx +0 -495
  220. package/src/renderer/components/CrashRecoveryDialog.tsx +0 -632
  221. package/src/renderer/components/DonateButton.tsx +0 -127
  222. package/src/renderer/components/ErrorBoundary.tsx +0 -308
  223. package/src/renderer/components/ExportDialog.tsx +0 -872
  224. package/src/renderer/components/HotkeyHint.tsx +0 -261
  225. package/src/renderer/components/KeyboardShortcuts.tsx +0 -787
  226. package/src/renderer/components/ModelDownloadDialog.tsx +0 -844
  227. package/src/renderer/components/Onboarding.tsx +0 -1830
  228. package/src/renderer/components/ProcessingOverlay.tsx +0 -157
  229. package/src/renderer/components/RecordingOverlay.tsx +0 -423
  230. package/src/renderer/components/SessionHistory.tsx +0 -1746
  231. package/src/renderer/components/SessionReview.tsx +0 -1321
  232. package/src/renderer/components/SettingsPanel.tsx +0 -217
  233. package/src/renderer/components/Skeleton.tsx +0 -347
  234. package/src/renderer/components/StatusIndicator.tsx +0 -86
  235. package/src/renderer/components/ThemeProvider.tsx +0 -429
  236. package/src/renderer/components/Tooltip.tsx +0 -370
  237. package/src/renderer/components/TranscriptionPreview.tsx +0 -183
  238. package/src/renderer/components/TranscriptionTierSelector.tsx +0 -640
  239. package/src/renderer/components/UpdateNotification.tsx +0 -377
  240. package/src/renderer/components/WindowSelector.tsx +0 -947
  241. package/src/renderer/components/index.ts +0 -99
  242. package/src/renderer/components/primitives/ApiKeyInput.tsx +0 -98
  243. package/src/renderer/components/primitives/ColorPicker.tsx +0 -65
  244. package/src/renderer/components/primitives/DangerButton.tsx +0 -45
  245. package/src/renderer/components/primitives/DirectoryPicker.tsx +0 -41
  246. package/src/renderer/components/primitives/Dropdown.tsx +0 -34
  247. package/src/renderer/components/primitives/KeyRecorder.tsx +0 -117
  248. package/src/renderer/components/primitives/SettingsSection.tsx +0 -32
  249. package/src/renderer/components/primitives/Slider.tsx +0 -43
  250. package/src/renderer/components/primitives/Toggle.tsx +0 -36
  251. package/src/renderer/components/primitives/index.ts +0 -10
  252. package/src/renderer/components/settings/AdvancedTab.tsx +0 -174
  253. package/src/renderer/components/settings/AppearanceTab.tsx +0 -77
  254. package/src/renderer/components/settings/GeneralTab.tsx +0 -40
  255. package/src/renderer/components/settings/HotkeysTab.tsx +0 -79
  256. package/src/renderer/components/settings/RecordingTab.tsx +0 -84
  257. package/src/renderer/components/settings/index.ts +0 -9
  258. package/src/renderer/components/settings/settingsStyles.ts +0 -673
  259. package/src/renderer/components/settings/tabConfig.tsx +0 -85
  260. package/src/renderer/components/settings/useSettingsPanel.ts +0 -447
  261. package/src/renderer/contexts/ProcessingContext.tsx +0 -227
  262. package/src/renderer/contexts/RecordingContext.tsx +0 -683
  263. package/src/renderer/contexts/UIContext.tsx +0 -326
  264. package/src/renderer/contexts/index.ts +0 -24
  265. package/src/renderer/donateMessages.ts +0 -69
  266. package/src/renderer/hooks/index.ts +0 -75
  267. package/src/renderer/hooks/useAnimation.tsx +0 -544
  268. package/src/renderer/hooks/useTheme.ts +0 -313
  269. package/src/renderer/index.html +0 -26
  270. package/src/renderer/main.tsx +0 -52
  271. package/src/renderer/styles/animations.css +0 -1093
  272. package/src/renderer/styles/app-shell.css +0 -662
  273. package/src/renderer/styles/globals.css +0 -515
  274. package/src/renderer/styles/theme.ts +0 -578
  275. package/src/renderer/types/electron.d.ts +0 -385
  276. package/src/shared/hotkeys.ts +0 -283
  277. package/src/shared/types.ts +0 -809
  278. package/tests/clipboard.test.ts +0 -228
  279. package/tests/e2e/criticalPaths.test.ts +0 -594
  280. package/tests/feedbackAnalyzer.test.ts +0 -303
  281. package/tests/integration/sessionFlow.test.ts +0 -583
  282. package/tests/markdownGenerator.test.ts +0 -418
  283. package/tests/output.test.ts +0 -96
  284. package/tests/setup.ts +0 -486
  285. package/tests/unit/appIntegration.test.ts +0 -676
  286. package/tests/unit/appViewState.test.ts +0 -281
  287. package/tests/unit/audioIpcChannels.test.ts +0 -17
  288. package/tests/unit/exportService.test.ts +0 -492
  289. package/tests/unit/hotkeys.test.ts +0 -92
  290. package/tests/unit/navigationPreload.test.ts +0 -94
  291. package/tests/unit/onboardingFlow.test.ts +0 -345
  292. package/tests/unit/permissionManager.test.ts +0 -175
  293. package/tests/unit/permissionManagerExpanded.test.ts +0 -296
  294. package/tests/unit/screenRecordingRenderer.test.ts +0 -368
  295. package/tests/unit/sessionController.test.ts +0 -515
  296. package/tests/unit/tierManager.test.ts +0 -61
  297. package/tests/unit/tierManagerExpanded.test.ts +0 -142
  298. package/tests/unit/transcriptAnalyzer.test.ts +0 -64
  299. package/tsconfig.json +0 -25
  300. package/vitest.config.ts +0 -46
@@ -1,199 +0,0 @@
1
- /**
2
- * AIPipelineManager - Orchestrator for the AI analysis pipeline
3
- *
4
- * Determines which processing tier to use (free vs BYOK), generates output accordingly,
5
- * and ensures the free-tier safety net is always available as fallback.
6
- *
7
- * Key invariant: session data is NEVER lost. Free-tier output is always generated first,
8
- * and AI enhancement is layered on top only when it succeeds.
9
- */
10
-
11
- import type { Session } from '../SessionController';
12
- import type { MarkdownDocument } from '../output/FileManager';
13
- import type { ISettingsManager } from '../settings/SettingsManager';
14
- import { generateDocumentForFileManager } from '../output/sessionAdapter';
15
- import { ClaudeAnalyzer } from './ClaudeAnalyzer';
16
- import { structuredMarkdownBuilder } from './StructuredMarkdownBuilder';
17
- import type {
18
- AITier,
19
- AIPipelineOutput,
20
- } from './types';
21
-
22
- export interface PipelineProcessOptions {
23
- settingsManager: ISettingsManager;
24
- projectName?: string;
25
- screenshotDir?: string;
26
- hasRecording?: boolean;
27
- recordingFilename?: string;
28
- }
29
-
30
- /**
31
- * Determine which AI tier is available based on stored API keys.
32
- */
33
- async function determineTier(settingsManager: ISettingsManager): Promise<AITier> {
34
- const anthropicKey = await settingsManager.getApiKey('anthropic');
35
- if (anthropicKey && anthropicKey.length > 0) {
36
- console.log('[AIPipelineManager] Tier decision: BYOK (Anthropic API key found in keychain)');
37
- return 'byok';
38
- }
39
- console.log('[AIPipelineManager] Tier decision: FREE (no Anthropic API key configured)');
40
- return 'free';
41
- }
42
-
43
- /**
44
- * Generate a free-tier (rule-based) document. This is the safety net that always works.
45
- */
46
- function generateFreeTierDocument(
47
- session: Session,
48
- projectName: string,
49
- screenshotDir: string,
50
- ): MarkdownDocument {
51
- return generateDocumentForFileManager(session, {
52
- projectName,
53
- screenshotDir,
54
- });
55
- }
56
-
57
- /**
58
- * Process a session through the AI pipeline.
59
- *
60
- * 1. Always generates free-tier output first (safety net)
61
- * 2. If BYOK tier is available, attempts AI enhancement
62
- * 3. On any AI failure, returns the free-tier output
63
- *
64
- * @returns A MarkdownDocument compatible with FileManager.saveSession()
65
- */
66
- export async function processSession(
67
- session: Session,
68
- options: PipelineProcessOptions,
69
- ): Promise<{ document: MarkdownDocument; pipelineOutput: AIPipelineOutput }> {
70
- const startTime = Date.now();
71
- const projectName = options.projectName || session.metadata?.sourceName || 'Feedback Session';
72
- const screenshotDir = options.screenshotDir || './screenshots';
73
-
74
- // ALWAYS generate free-tier output first as safety net
75
- console.log('[AIPipelineManager] Generating free-tier output as safety net...');
76
- const freeTierDoc = generateFreeTierDocument(session, projectName, screenshotDir);
77
- console.log('[AIPipelineManager] Free-tier output ready (rule-based markdown generated)');
78
-
79
- // Determine tier
80
- const tier = await determineTier(options.settingsManager);
81
-
82
- if (tier === 'free') {
83
- console.log(
84
- `[AIPipelineManager] Using free-tier output (no AI enhancement). ` +
85
- `Session had ${session.feedbackItems.length} feedback items, ` +
86
- `${session.transcriptBuffer.length} transcript events. ` +
87
- `Completed in ${Date.now() - startTime}ms.`
88
- );
89
- return {
90
- document: freeTierDoc,
91
- pipelineOutput: {
92
- markdown: freeTierDoc.content,
93
- aiEnhanced: false,
94
- processingTimeMs: Date.now() - startTime,
95
- tier: 'free',
96
- },
97
- };
98
- }
99
-
100
- // BYOK tier: attempt AI enhancement
101
- console.log('[AIPipelineManager] BYOK tier: attempting Claude AI enhancement...');
102
- try {
103
- const apiKey = await options.settingsManager.getApiKey('anthropic');
104
- if (!apiKey) {
105
- // Shouldn't happen since determineTier checked, but be defensive
106
- console.warn('[AIPipelineManager] BYOK -> FREE fallback: API key disappeared between tier check and usage');
107
- return {
108
- document: freeTierDoc,
109
- pipelineOutput: {
110
- markdown: freeTierDoc.content,
111
- aiEnhanced: false,
112
- processingTimeMs: Date.now() - startTime,
113
- tier: 'free',
114
- fallbackReason: 'API key not found after tier selection',
115
- },
116
- };
117
- }
118
-
119
- console.log(
120
- `[AIPipelineManager] Calling Claude API (BYOK) with ` +
121
- `${session.feedbackItems.length} feedback items, ` +
122
- `${session.transcriptBuffer.length} transcript events...`
123
- );
124
-
125
- const analyzer = new ClaudeAnalyzer(apiKey);
126
- const analysis = await analyzer.analyze(session);
127
-
128
- if (!analysis) {
129
- console.warn(
130
- `[AIPipelineManager] BYOK -> FREE fallback: Claude API returned null analysis ` +
131
- `after ${Date.now() - startTime}ms. Using free-tier rule-based output instead.`
132
- );
133
- return {
134
- document: freeTierDoc,
135
- pipelineOutput: {
136
- markdown: freeTierDoc.content,
137
- aiEnhanced: false,
138
- processingTimeMs: Date.now() - startTime,
139
- tier: 'byok',
140
- fallbackReason: 'Claude analysis returned null',
141
- },
142
- };
143
- }
144
-
145
- // Build AI-enhanced markdown
146
- const aiMarkdown = structuredMarkdownBuilder.buildDocument(session, analysis, {
147
- projectName,
148
- screenshotDir,
149
- hasRecording: options.hasRecording,
150
- recordingFilename: options.recordingFilename,
151
- modelId: 'claude-sonnet-4-5-20250929',
152
- });
153
-
154
- // Build a MarkdownDocument compatible with FileManager
155
- const aiDocument: MarkdownDocument = {
156
- content: aiMarkdown,
157
- metadata: {
158
- itemCount: analysis.items.length,
159
- screenshotCount: session.screenshotBuffer.length,
160
- types: [...new Set(analysis.items.map((item) => item.category))],
161
- },
162
- };
163
-
164
- console.log(
165
- `[AIPipelineManager] AI analysis complete: ${analysis.items.length} items, ` +
166
- `${analysis.metadata.criticalCount} critical, ${analysis.metadata.highCount} high ` +
167
- `(${Date.now() - startTime}ms)`,
168
- );
169
-
170
- return {
171
- document: aiDocument,
172
- pipelineOutput: {
173
- markdown: aiMarkdown,
174
- aiEnhanced: true,
175
- analysis,
176
- processingTimeMs: Date.now() - startTime,
177
- tier: 'byok',
178
- },
179
- };
180
- } catch (error) {
181
- // ANY error in the AI path falls back to free tier - never lose the session
182
- const errorMessage = error instanceof Error ? error.message : String(error);
183
- console.error(
184
- `[AIPipelineManager] BYOK -> FREE fallback: AI pipeline threw after ${Date.now() - startTime}ms. ` +
185
- `Error: ${errorMessage}. Using free-tier rule-based output instead.`
186
- );
187
-
188
- return {
189
- document: freeTierDoc,
190
- pipelineOutput: {
191
- markdown: freeTierDoc.content,
192
- aiEnhanced: false,
193
- processingTimeMs: Date.now() - startTime,
194
- tier: 'byok',
195
- fallbackReason: error instanceof Error ? error.message : 'Unknown error',
196
- },
197
- };
198
- }
199
- }
@@ -1,339 +0,0 @@
1
- /**
2
- * ClaudeAnalyzer - Core AI analysis engine for markupr
3
- *
4
- * Takes a session's transcript + screenshots, sends to Claude Sonnet 4.5 with vision,
5
- * and returns structured feedback analysis as AIAnalysisResult.
6
- *
7
- * On any error, returns null so the caller can fall back to the free tier.
8
- */
9
-
10
- import Anthropic from '@anthropic-ai/sdk';
11
- import type { Session } from '../SessionController';
12
- import type {
13
- AIAnalysisResult,
14
- ClaudeAnalyzerOptions,
15
- OptimizedImage,
16
- } from './types';
17
- import {
18
- DEFAULT_CLAUDE_ANALYZER_OPTIONS,
19
- AIPipelineError,
20
- } from './types';
21
- import { optimizeForAPI } from './ImageOptimizer';
22
- import type { ImageOptimizeOptions } from './types';
23
-
24
- // =============================================================================
25
- // System Prompt (from AI_PIPELINE_DESIGN.md)
26
- // =============================================================================
27
-
28
- const SYSTEM_PROMPT = `You are markupr's AI analysis engine. You receive a developer's voice-narrated feedback session: a transcript of everything they said while reviewing software, paired with screenshots captured at natural pause points.
29
-
30
- Your job is to transform this raw narration into a structured, actionable feedback document.
31
-
32
- ## Rules
33
-
34
- 1. **Preserve the user's voice.** Quote their exact words in blockquotes. Never rephrase their observations.
35
- 2. **Group related feedback.** If the user mentions the same area multiple times, combine those into one item.
36
- 3. **Match screenshots to feedback.** Each screenshot was captured during or after the text segment it accompanies. Reference screenshots by their index (e.g., [Screenshot 1]).
37
- 4. **Extract action items.** For each feedback item, write a concrete 1-sentence action item a developer could act on immediately.
38
- 5. **Assign priority.** Use Critical/High/Medium/Low based on the severity of the issue described.
39
- 6. **Categorize.** Use exactly one of: Bug, UX Issue, Performance, Suggestion, Question, Positive Note.
40
- 7. **Write a summary.** 2-3 sentences capturing the most important findings.
41
- 8. **Be concise.** Developers will paste this into AI coding tools. Every word must earn its place.
42
-
43
- ## Output Format
44
-
45
- Respond with ONLY valid JSON matching this schema:
46
-
47
- {
48
- "summary": "2-3 sentence overview of key findings",
49
- "items": [
50
- {
51
- "title": "Short descriptive title (5-10 words)",
52
- "category": "Bug|UX Issue|Performance|Suggestion|Question|Positive Note",
53
- "priority": "Critical|High|Medium|Low",
54
- "quote": "User's exact words (the relevant excerpt)",
55
- "screenshotIndices": [0, 1],
56
- "actionItem": "Concrete 1-sentence action for a developer",
57
- "area": "Component or area of the app this relates to (e.g., 'Navigation', 'Login Form', 'Dashboard')"
58
- }
59
- ],
60
- "themes": ["theme1", "theme2"],
61
- "positiveNotes": ["Things the user explicitly praised"],
62
- "metadata": {
63
- "totalItems": 5,
64
- "criticalCount": 1,
65
- "highCount": 2
66
- }
67
- }`;
68
-
69
- // =============================================================================
70
- // Helpers
71
- // =============================================================================
72
-
73
- /**
74
- * Convert an absolute timestamp to session-relative MM:SS format.
75
- */
76
- function toRelativeTimestamp(timestampMs: number, sessionStartMs: number): string {
77
- const relSec = Math.max(0, Math.floor((timestampMs - sessionStartMs) / 1000));
78
- const mm = Math.floor(relSec / 60).toString().padStart(2, '0');
79
- const ss = (relSec % 60).toString().padStart(2, '0');
80
- return `${mm}:${ss}`;
81
- }
82
-
83
- /**
84
- * Build the transcript portion of the user message from session data.
85
- *
86
- * Uses final transcripts grouped chronologically. Falls back to all transcripts
87
- * if no finals exist (e.g., timer-only tier).
88
- */
89
- function buildTranscriptText(session: Session): string {
90
- const finals = session.transcriptBuffer
91
- .filter((e) => e.isFinal && e.text.trim().length > 0)
92
- .sort((a, b) => a.timestamp - b.timestamp);
93
-
94
- const events = finals.length > 0
95
- ? finals
96
- : session.transcriptBuffer
97
- .filter((e) => e.text.trim().length > 0)
98
- .sort((a, b) => a.timestamp - b.timestamp);
99
-
100
- if (events.length === 0) {
101
- return '[No transcript available]';
102
- }
103
-
104
- return events
105
- .map((e) => {
106
- // TranscriptEvent timestamps are in seconds; convert to ms for relative calc
107
- const tsMs = Math.round(e.timestamp * 1000);
108
- const rel = toRelativeTimestamp(tsMs, session.startTime);
109
- return `[${rel}] ${e.text.trim()}`;
110
- })
111
- .join('\n');
112
- }
113
-
114
- /**
115
- * Build the Claude API message content array with text + image blocks.
116
- */
117
- function buildUserContent(
118
- session: Session,
119
- optimizedImages: OptimizedImage[],
120
- ): Anthropic.Messages.ContentBlockParam[] {
121
- const sourceName = session.metadata?.sourceName || 'Application';
122
- const transcriptText = buildTranscriptText(session);
123
-
124
- // Map optimized images back to their original screenshot timestamps
125
- const screenshotTimestamps = new Map<string, number>();
126
- for (const s of session.screenshotBuffer) {
127
- screenshotTimestamps.set(s.id, s.timestamp);
128
- }
129
-
130
- // Build the text preamble
131
- let textContent = `## Transcript\n\nThe user narrated the following while reviewing the application "${sourceName}":\n\n${transcriptText}`;
132
-
133
- if (optimizedImages.length > 0) {
134
- textContent += `\n\n---\n\n## Screenshots\n\n${optimizedImages.length} screenshots were captured at natural pause points during narration.\nThey are provided as images below in chronological order.`;
135
- }
136
-
137
- const content: Anthropic.Messages.ContentBlockParam[] = [
138
- { type: 'text', text: textContent },
139
- ];
140
-
141
- // Add image blocks
142
- for (let i = 0; i < optimizedImages.length; i++) {
143
- const img = optimizedImages[i];
144
- const originalTs = screenshotTimestamps.get(img.originalScreenshotId) ?? session.startTime;
145
- const rel = toRelativeTimestamp(originalTs, session.startTime);
146
-
147
- content.push({
148
- type: 'text',
149
- text: `Screenshot ${i + 1} (captured at ${rel}):`,
150
- });
151
-
152
- content.push({
153
- type: 'image',
154
- source: {
155
- type: 'base64',
156
- media_type: img.mediaType,
157
- data: img.data.toString('base64'),
158
- },
159
- });
160
- }
161
-
162
- return content;
163
- }
164
-
165
- // =============================================================================
166
- // ClaudeAnalyzer
167
- // =============================================================================
168
-
169
- export class ClaudeAnalyzer {
170
- private client: Anthropic;
171
- private options: ClaudeAnalyzerOptions;
172
-
173
- constructor(apiKey: string, options?: Partial<ClaudeAnalyzerOptions>, baseUrl?: string) {
174
- this.options = { ...DEFAULT_CLAUDE_ANALYZER_OPTIONS, ...options };
175
-
176
- const clientOptions: ConstructorParameters<typeof Anthropic>[0] = { apiKey };
177
- if (baseUrl) {
178
- clientOptions.baseURL = baseUrl;
179
- }
180
- this.client = new Anthropic(clientOptions);
181
- }
182
-
183
- /**
184
- * Analyze a session using Claude's vision API.
185
- *
186
- * @param session - The completed session with transcript and screenshots
187
- * @param imageOptions - Optional image optimization settings
188
- * @returns Structured analysis result, or null on any error
189
- */
190
- async analyze(
191
- session: Session,
192
- imageOptions?: Partial<ImageOptimizeOptions>,
193
- ): Promise<AIAnalysisResult | null> {
194
- try {
195
- // Optimize screenshots for the API
196
- const optimizedImages = optimizeForAPI(session.screenshotBuffer, imageOptions);
197
-
198
- // Build message content
199
- const userContent = buildUserContent(session, optimizedImages);
200
-
201
- // Call Claude API
202
- let timeoutHandle: NodeJS.Timeout | undefined;
203
- const timeoutPromise = new Promise<never>((_, reject) => {
204
- timeoutHandle = setTimeout(() => {
205
- reject(
206
- new AIPipelineError(
207
- `Claude API request timed out after ${this.options.timeoutMs}ms`,
208
- 'API_TIMEOUT',
209
- ),
210
- );
211
- }, this.options.timeoutMs);
212
- });
213
-
214
- const response = await Promise.race([
215
- this.client.messages.create({
216
- model: this.options.model,
217
- max_tokens: this.options.maxTokens,
218
- temperature: this.options.temperature,
219
- system: SYSTEM_PROMPT,
220
- messages: [{ role: 'user', content: userContent }],
221
- }),
222
- timeoutPromise,
223
- ]).finally(() => {
224
- if (timeoutHandle) {
225
- clearTimeout(timeoutHandle);
226
- }
227
- });
228
-
229
- // Extract text from response
230
- const textBlock = response.content.find((block) => block.type === 'text');
231
- if (!textBlock || textBlock.type !== 'text') {
232
- throw new AIPipelineError('No text content in Claude response', 'INVALID_RESPONSE');
233
- }
234
-
235
- // Parse JSON from response
236
- const result = parseAnalysisResult(textBlock.text);
237
- return result;
238
- } catch (error) {
239
- if (error instanceof AIPipelineError) {
240
- console.error(`[ClaudeAnalyzer] Pipeline error (${error.code}):`, error.message);
241
- } else {
242
- console.error('[ClaudeAnalyzer] Unexpected error:', error instanceof Error ? error.message : error);
243
- }
244
- return null;
245
- }
246
- }
247
- }
248
-
249
- // =============================================================================
250
- // Response Parsing
251
- // =============================================================================
252
-
253
- /**
254
- * Parse Claude's JSON response into a validated AIAnalysisResult.
255
- *
256
- * Handles common edge cases:
257
- * - JSON wrapped in markdown code fences
258
- * - Extra whitespace / trailing commas (via lenient extraction)
259
- */
260
- function parseAnalysisResult(text: string): AIAnalysisResult {
261
- // Strip markdown code fences if present
262
- let jsonStr = text.trim();
263
- const fenceMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
264
- if (fenceMatch) {
265
- jsonStr = fenceMatch[1].trim();
266
- }
267
-
268
- let parsed: unknown;
269
- try {
270
- parsed = JSON.parse(jsonStr);
271
- } catch {
272
- throw new AIPipelineError(
273
- `Failed to parse Claude response as JSON: ${jsonStr.slice(0, 200)}...`,
274
- 'INVALID_RESPONSE',
275
- );
276
- }
277
-
278
- // Validate required fields
279
- if (
280
- typeof parsed !== 'object' ||
281
- parsed === null ||
282
- !('summary' in parsed) ||
283
- !('items' in parsed) ||
284
- !Array.isArray((parsed as Record<string, unknown>).items)
285
- ) {
286
- throw new AIPipelineError(
287
- 'Claude response JSON missing required fields (summary, items)',
288
- 'INVALID_RESPONSE',
289
- );
290
- }
291
-
292
- const obj = parsed as Record<string, unknown>;
293
-
294
- // Build items first so we can compute accurate metadata
295
- const items = Array.isArray(obj.items)
296
- ? (obj.items as Record<string, unknown>[]).map(validateFeedbackItem)
297
- : [];
298
-
299
- const result: AIAnalysisResult = {
300
- summary: String(obj.summary ?? ''),
301
- items,
302
- themes: Array.isArray(obj.themes)
303
- ? (obj.themes as unknown[]).map(String)
304
- : [],
305
- positiveNotes: Array.isArray(obj.positiveNotes)
306
- ? (obj.positiveNotes as unknown[]).map(String)
307
- : [],
308
- metadata: {
309
- totalItems: items.length,
310
- criticalCount: items.filter((i) => i.priority === 'Critical').length,
311
- highCount: items.filter((i) => i.priority === 'High').length,
312
- },
313
- };
314
-
315
- return result;
316
- }
317
-
318
- /**
319
- * Validate and coerce a single feedback item from Claude's response.
320
- */
321
- function validateFeedbackItem(raw: Record<string, unknown>): AIAnalysisResult['items'][0] {
322
- const validCategories = ['Bug', 'UX Issue', 'Performance', 'Suggestion', 'Question', 'Positive Note'];
323
- const validPriorities = ['Critical', 'High', 'Medium', 'Low'];
324
-
325
- const category = String(raw.category ?? 'Suggestion');
326
- const priority = String(raw.priority ?? 'Medium');
327
-
328
- return {
329
- title: String(raw.title ?? 'Untitled Feedback'),
330
- category: validCategories.includes(category) ? category as AIAnalysisResult['items'][0]['category'] : 'Suggestion',
331
- priority: validPriorities.includes(priority) ? priority as AIAnalysisResult['items'][0]['priority'] : 'Medium',
332
- quote: String(raw.quote ?? ''),
333
- screenshotIndices: Array.isArray(raw.screenshotIndices)
334
- ? (raw.screenshotIndices as unknown[]).filter((v): v is number => typeof v === 'number')
335
- : [],
336
- actionItem: String(raw.actionItem ?? ''),
337
- area: String(raw.area ?? 'General'),
338
- };
339
- }