claudia-orchestrator 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 (296) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +109 -0
  3. package/dist/cli-parser.d.ts +11 -0
  4. package/dist/cli-parser.d.ts.map +1 -0
  5. package/dist/cli-parser.js +57 -0
  6. package/dist/cli-parser.js.map +1 -0
  7. package/dist/cui-server.d.ts +69 -0
  8. package/dist/cui-server.d.ts.map +1 -0
  9. package/dist/cui-server.js +705 -0
  10. package/dist/cui-server.js.map +1 -0
  11. package/dist/mcp-server/claudia-tools.d.ts +15 -0
  12. package/dist/mcp-server/claudia-tools.d.ts.map +1 -0
  13. package/dist/mcp-server/claudia-tools.js +366 -0
  14. package/dist/mcp-server/claudia-tools.js.map +1 -0
  15. package/dist/mcp-server/index.d.ts +3 -0
  16. package/dist/mcp-server/index.d.ts.map +1 -0
  17. package/dist/mcp-server/index.js +176 -0
  18. package/dist/mcp-server/index.js.map +1 -0
  19. package/dist/middleware/auth.d.ts +18 -0
  20. package/dist/middleware/auth.d.ts.map +1 -0
  21. package/dist/middleware/auth.js +136 -0
  22. package/dist/middleware/auth.js.map +1 -0
  23. package/dist/middleware/cors-setup.d.ts +7 -0
  24. package/dist/middleware/cors-setup.d.ts.map +1 -0
  25. package/dist/middleware/cors-setup.js +8 -0
  26. package/dist/middleware/cors-setup.js.map +1 -0
  27. package/dist/middleware/error-handler.d.ts +4 -0
  28. package/dist/middleware/error-handler.d.ts.map +1 -0
  29. package/dist/middleware/error-handler.js +27 -0
  30. package/dist/middleware/error-handler.js.map +1 -0
  31. package/dist/middleware/query-parser.d.ts +11 -0
  32. package/dist/middleware/query-parser.d.ts.map +1 -0
  33. package/dist/middleware/query-parser.js +68 -0
  34. package/dist/middleware/query-parser.js.map +1 -0
  35. package/dist/middleware/request-logger.d.ts +4 -0
  36. package/dist/middleware/request-logger.d.ts.map +1 -0
  37. package/dist/middleware/request-logger.js +29 -0
  38. package/dist/middleware/request-logger.js.map +1 -0
  39. package/dist/process-daemon/index.d.ts +14 -0
  40. package/dist/process-daemon/index.d.ts.map +1 -0
  41. package/dist/process-daemon/index.js +51 -0
  42. package/dist/process-daemon/index.js.map +1 -0
  43. package/dist/process-daemon/process-daemon.d.ts +78 -0
  44. package/dist/process-daemon/process-daemon.d.ts.map +1 -0
  45. package/dist/process-daemon/process-daemon.js +568 -0
  46. package/dist/process-daemon/process-daemon.js.map +1 -0
  47. package/dist/process-daemon/process-manager-client.d.ts +108 -0
  48. package/dist/process-daemon/process-manager-client.d.ts.map +1 -0
  49. package/dist/process-daemon/process-manager-client.js +314 -0
  50. package/dist/process-daemon/process-manager-client.js.map +1 -0
  51. package/dist/process-daemon/process-manager-interface.d.ts +47 -0
  52. package/dist/process-daemon/process-manager-interface.d.ts.map +1 -0
  53. package/dist/process-daemon/process-manager-interface.js +8 -0
  54. package/dist/process-daemon/process-manager-interface.js.map +1 -0
  55. package/dist/process-daemon/test-daemon.d.ts +12 -0
  56. package/dist/process-daemon/test-daemon.d.ts.map +1 -0
  57. package/dist/process-daemon/test-daemon.js +81 -0
  58. package/dist/process-daemon/test-daemon.js.map +1 -0
  59. package/dist/process-daemon/types.d.ts +85 -0
  60. package/dist/process-daemon/types.d.ts.map +1 -0
  61. package/dist/process-daemon/types.js +8 -0
  62. package/dist/process-daemon/types.js.map +1 -0
  63. package/dist/routes/claudia.routes.d.ts +10 -0
  64. package/dist/routes/claudia.routes.d.ts.map +1 -0
  65. package/dist/routes/claudia.routes.js +123 -0
  66. package/dist/routes/claudia.routes.js.map +1 -0
  67. package/dist/routes/config.routes.d.ts +4 -0
  68. package/dist/routes/config.routes.d.ts.map +1 -0
  69. package/dist/routes/config.routes.js +27 -0
  70. package/dist/routes/config.routes.js.map +1 -0
  71. package/dist/routes/conversation.routes.d.ts +8 -0
  72. package/dist/routes/conversation.routes.d.ts.map +1 -0
  73. package/dist/routes/conversation.routes.js +870 -0
  74. package/dist/routes/conversation.routes.js.map +1 -0
  75. package/dist/routes/filesystem.routes.d.ts +4 -0
  76. package/dist/routes/filesystem.routes.d.ts.map +1 -0
  77. package/dist/routes/filesystem.routes.js +86 -0
  78. package/dist/routes/filesystem.routes.js.map +1 -0
  79. package/dist/routes/gemini.routes.d.ts +4 -0
  80. package/dist/routes/gemini.routes.d.ts.map +1 -0
  81. package/dist/routes/gemini.routes.js +93 -0
  82. package/dist/routes/gemini.routes.js.map +1 -0
  83. package/dist/routes/insights.routes.d.ts +17 -0
  84. package/dist/routes/insights.routes.d.ts.map +1 -0
  85. package/dist/routes/insights.routes.js +417 -0
  86. package/dist/routes/insights.routes.js.map +1 -0
  87. package/dist/routes/license.routes.d.ts +3 -0
  88. package/dist/routes/license.routes.d.ts.map +1 -0
  89. package/dist/routes/license.routes.js +111 -0
  90. package/dist/routes/license.routes.js.map +1 -0
  91. package/dist/routes/log.routes.d.ts +3 -0
  92. package/dist/routes/log.routes.d.ts.map +1 -0
  93. package/dist/routes/log.routes.js +65 -0
  94. package/dist/routes/log.routes.js.map +1 -0
  95. package/dist/routes/notifications.routes.d.ts +4 -0
  96. package/dist/routes/notifications.routes.d.ts.map +1 -0
  97. package/dist/routes/notifications.routes.js +71 -0
  98. package/dist/routes/notifications.routes.js.map +1 -0
  99. package/dist/routes/permission.routes.d.ts +4 -0
  100. package/dist/routes/permission.routes.d.ts.map +1 -0
  101. package/dist/routes/permission.routes.js +116 -0
  102. package/dist/routes/permission.routes.js.map +1 -0
  103. package/dist/routes/question.routes.d.ts +8 -0
  104. package/dist/routes/question.routes.d.ts.map +1 -0
  105. package/dist/routes/question.routes.js +82 -0
  106. package/dist/routes/question.routes.js.map +1 -0
  107. package/dist/routes/streaming.routes.d.ts +4 -0
  108. package/dist/routes/streaming.routes.d.ts.map +1 -0
  109. package/dist/routes/streaming.routes.js +28 -0
  110. package/dist/routes/streaming.routes.js.map +1 -0
  111. package/dist/routes/system.routes.d.ts +5 -0
  112. package/dist/routes/system.routes.d.ts.map +1 -0
  113. package/dist/routes/system.routes.js +103 -0
  114. package/dist/routes/system.routes.js.map +1 -0
  115. package/dist/routes/working-directories.routes.d.ts +4 -0
  116. package/dist/routes/working-directories.routes.d.ts.map +1 -0
  117. package/dist/routes/working-directories.routes.js +25 -0
  118. package/dist/routes/working-directories.routes.js.map +1 -0
  119. package/dist/server.d.ts +3 -0
  120. package/dist/server.d.ts.map +1 -0
  121. package/dist/server.js +34 -0
  122. package/dist/server.js.map +1 -0
  123. package/dist/services/ToolMetricsService.d.ts +53 -0
  124. package/dist/services/ToolMetricsService.d.ts.map +1 -0
  125. package/dist/services/ToolMetricsService.js +230 -0
  126. package/dist/services/ToolMetricsService.js.map +1 -0
  127. package/dist/services/anthropic-service.d.ts +186 -0
  128. package/dist/services/anthropic-service.d.ts.map +1 -0
  129. package/dist/services/anthropic-service.js +1132 -0
  130. package/dist/services/anthropic-service.js.map +1 -0
  131. package/dist/services/claude-history-reader.d.ts +126 -0
  132. package/dist/services/claude-history-reader.d.ts.map +1 -0
  133. package/dist/services/claude-history-reader.js +717 -0
  134. package/dist/services/claude-history-reader.js.map +1 -0
  135. package/dist/services/claude-process-manager.d.ts +108 -0
  136. package/dist/services/claude-process-manager.d.ts.map +1 -0
  137. package/dist/services/claude-process-manager.js +909 -0
  138. package/dist/services/claude-process-manager.js.map +1 -0
  139. package/dist/services/claude-router-service.d.ts +19 -0
  140. package/dist/services/claude-router-service.d.ts.map +1 -0
  141. package/dist/services/claude-router-service.js +160 -0
  142. package/dist/services/claude-router-service.js.map +1 -0
  143. package/dist/services/claudia-service.d.ts +77 -0
  144. package/dist/services/claudia-service.d.ts.map +1 -0
  145. package/dist/services/claudia-service.js +194 -0
  146. package/dist/services/claudia-service.js.map +1 -0
  147. package/dist/services/commands-service.d.ts +18 -0
  148. package/dist/services/commands-service.d.ts.map +1 -0
  149. package/dist/services/commands-service.js +76 -0
  150. package/dist/services/commands-service.js.map +1 -0
  151. package/dist/services/config-service.d.ts +68 -0
  152. package/dist/services/config-service.d.ts.map +1 -0
  153. package/dist/services/config-service.js +429 -0
  154. package/dist/services/config-service.js.map +1 -0
  155. package/dist/services/conversation-cache.d.ts +86 -0
  156. package/dist/services/conversation-cache.d.ts.map +1 -0
  157. package/dist/services/conversation-cache.js +235 -0
  158. package/dist/services/conversation-cache.js.map +1 -0
  159. package/dist/services/conversation-status-manager.d.ts +98 -0
  160. package/dist/services/conversation-status-manager.d.ts.map +1 -0
  161. package/dist/services/conversation-status-manager.js +295 -0
  162. package/dist/services/conversation-status-manager.js.map +1 -0
  163. package/dist/services/cost-tracker.d.ts +87 -0
  164. package/dist/services/cost-tracker.d.ts.map +1 -0
  165. package/dist/services/cost-tracker.js +335 -0
  166. package/dist/services/cost-tracker.js.map +1 -0
  167. package/dist/services/file-system-service.d.ts +61 -0
  168. package/dist/services/file-system-service.d.ts.map +1 -0
  169. package/dist/services/file-system-service.js +348 -0
  170. package/dist/services/file-system-service.js.map +1 -0
  171. package/dist/services/gemini-service.d.ts +72 -0
  172. package/dist/services/gemini-service.d.ts.map +1 -0
  173. package/dist/services/gemini-service.js +431 -0
  174. package/dist/services/gemini-service.js.map +1 -0
  175. package/dist/services/insight-queue.d.ts +99 -0
  176. package/dist/services/insight-queue.d.ts.map +1 -0
  177. package/dist/services/insight-queue.js +277 -0
  178. package/dist/services/insight-queue.js.map +1 -0
  179. package/dist/services/insights-service.d.ts +102 -0
  180. package/dist/services/insights-service.d.ts.map +1 -0
  181. package/dist/services/insights-service.js +1152 -0
  182. package/dist/services/insights-service.js.map +1 -0
  183. package/dist/services/json-lines-parser.d.ts +19 -0
  184. package/dist/services/json-lines-parser.d.ts.map +1 -0
  185. package/dist/services/json-lines-parser.js +56 -0
  186. package/dist/services/json-lines-parser.js.map +1 -0
  187. package/dist/services/license-service.d.ts +69 -0
  188. package/dist/services/license-service.d.ts.map +1 -0
  189. package/dist/services/license-service.js +330 -0
  190. package/dist/services/license-service.js.map +1 -0
  191. package/dist/services/log-formatter.d.ts +5 -0
  192. package/dist/services/log-formatter.d.ts.map +1 -0
  193. package/dist/services/log-formatter.js +77 -0
  194. package/dist/services/log-formatter.js.map +1 -0
  195. package/dist/services/log-stream-buffer.d.ts +11 -0
  196. package/dist/services/log-stream-buffer.d.ts.map +1 -0
  197. package/dist/services/log-stream-buffer.js +36 -0
  198. package/dist/services/log-stream-buffer.js.map +1 -0
  199. package/dist/services/logger.d.ts +71 -0
  200. package/dist/services/logger.d.ts.map +1 -0
  201. package/dist/services/logger.js +215 -0
  202. package/dist/services/logger.js.map +1 -0
  203. package/dist/services/mcp-config-generator.d.ts +32 -0
  204. package/dist/services/mcp-config-generator.d.ts.map +1 -0
  205. package/dist/services/mcp-config-generator.js +126 -0
  206. package/dist/services/mcp-config-generator.js.map +1 -0
  207. package/dist/services/message-filter.d.ts +22 -0
  208. package/dist/services/message-filter.d.ts.map +1 -0
  209. package/dist/services/message-filter.js +57 -0
  210. package/dist/services/message-filter.js.map +1 -0
  211. package/dist/services/notification-service.d.ts +45 -0
  212. package/dist/services/notification-service.d.ts.map +1 -0
  213. package/dist/services/notification-service.js +184 -0
  214. package/dist/services/notification-service.js.map +1 -0
  215. package/dist/services/permission-tracker.d.ts +67 -0
  216. package/dist/services/permission-tracker.d.ts.map +1 -0
  217. package/dist/services/permission-tracker.js +161 -0
  218. package/dist/services/permission-tracker.js.map +1 -0
  219. package/dist/services/process-manager-factory.d.ts +81 -0
  220. package/dist/services/process-manager-factory.d.ts.map +1 -0
  221. package/dist/services/process-manager-factory.js +211 -0
  222. package/dist/services/process-manager-factory.js.map +1 -0
  223. package/dist/services/question-tracker.d.ts +47 -0
  224. package/dist/services/question-tracker.d.ts.map +1 -0
  225. package/dist/services/question-tracker.js +105 -0
  226. package/dist/services/question-tracker.js.map +1 -0
  227. package/dist/services/session-activity-watcher.d.ts +33 -0
  228. package/dist/services/session-activity-watcher.d.ts.map +1 -0
  229. package/dist/services/session-activity-watcher.js +194 -0
  230. package/dist/services/session-activity-watcher.js.map +1 -0
  231. package/dist/services/session-info-service.d.ts +228 -0
  232. package/dist/services/session-info-service.d.ts.map +1 -0
  233. package/dist/services/session-info-service.js +920 -0
  234. package/dist/services/session-info-service.js.map +1 -0
  235. package/dist/services/session-insights-service.d.ts +119 -0
  236. package/dist/services/session-insights-service.d.ts.map +1 -0
  237. package/dist/services/session-insights-service.js +889 -0
  238. package/dist/services/session-insights-service.js.map +1 -0
  239. package/dist/services/stream-manager.d.ts +62 -0
  240. package/dist/services/stream-manager.d.ts.map +1 -0
  241. package/dist/services/stream-manager.js +239 -0
  242. package/dist/services/stream-manager.js.map +1 -0
  243. package/dist/services/web-push-service.d.ts +48 -0
  244. package/dist/services/web-push-service.d.ts.map +1 -0
  245. package/dist/services/web-push-service.js +186 -0
  246. package/dist/services/web-push-service.js.map +1 -0
  247. package/dist/services/working-directories-service.d.ts +19 -0
  248. package/dist/services/working-directories-service.d.ts.map +1 -0
  249. package/dist/services/working-directories-service.js +103 -0
  250. package/dist/services/working-directories-service.js.map +1 -0
  251. package/dist/types/config.d.ts +111 -0
  252. package/dist/types/config.d.ts.map +1 -0
  253. package/dist/types/config.js +14 -0
  254. package/dist/types/config.js.map +1 -0
  255. package/dist/types/express.d.ts +5 -0
  256. package/dist/types/express.d.ts.map +1 -0
  257. package/dist/types/express.js +2 -0
  258. package/dist/types/express.js.map +1 -0
  259. package/dist/types/index.d.ts +325 -0
  260. package/dist/types/index.d.ts.map +1 -0
  261. package/dist/types/index.js +18 -0
  262. package/dist/types/index.js.map +1 -0
  263. package/dist/types/insights.d.ts +99 -0
  264. package/dist/types/insights.d.ts.map +1 -0
  265. package/dist/types/insights.js +7 -0
  266. package/dist/types/insights.js.map +1 -0
  267. package/dist/types/license.d.ts +70 -0
  268. package/dist/types/license.d.ts.map +1 -0
  269. package/dist/types/license.js +5 -0
  270. package/dist/types/license.js.map +1 -0
  271. package/dist/types/router-config.d.ts +13 -0
  272. package/dist/types/router-config.d.ts.map +1 -0
  273. package/dist/types/router-config.js +2 -0
  274. package/dist/types/router-config.js.map +1 -0
  275. package/dist/utils/constants.d.ts +26 -0
  276. package/dist/utils/constants.d.ts.map +1 -0
  277. package/dist/utils/constants.js +28 -0
  278. package/dist/utils/constants.js.map +1 -0
  279. package/dist/utils/machine-id.d.ts +7 -0
  280. package/dist/utils/machine-id.d.ts.map +1 -0
  281. package/dist/utils/machine-id.js +76 -0
  282. package/dist/utils/machine-id.js.map +1 -0
  283. package/dist/utils/server-startup.d.ts +13 -0
  284. package/dist/utils/server-startup.d.ts.map +1 -0
  285. package/dist/utils/server-startup.js +20 -0
  286. package/dist/utils/server-startup.js.map +1 -0
  287. package/dist/web/assets/main-DAc2rjJ2.css +1 -0
  288. package/dist/web/assets/main-DvlZ02mT.js +137 -0
  289. package/dist/web/favicon.png +0 -0
  290. package/dist/web/favicon.svg +22 -0
  291. package/dist/web/icon-192x192.png +0 -0
  292. package/dist/web/icon-512x512.png +0 -0
  293. package/dist/web/index.html +36 -0
  294. package/dist/web/manifest.json +61 -0
  295. package/package.json +174 -0
  296. package/scripts/postinstall.js +30 -0
@@ -0,0 +1,1132 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import { CUIError, } from '../types/index.js';
3
+ import { createLogger } from '../services/logger.js';
4
+ import { ConfigService } from './config-service.js';
5
+ import { getCostTracker } from './cost-tracker.js';
6
+ const DEFAULT_RETRY_CONFIG = {
7
+ maxRetries: 3,
8
+ baseDelayMs: 1000,
9
+ maxDelayMs: 10000,
10
+ };
11
+ /**
12
+ * Determines if an error is retryable (transient network/rate limit issues).
13
+ * Non-retryable: auth errors, credit exhaustion, invalid requests.
14
+ */
15
+ function isRetryableError(error) {
16
+ const message = error instanceof Error ? error.message : String(error);
17
+ const errorName = error instanceof Error ? error.name : '';
18
+ // Non-retryable errors - don't waste retries on these
19
+ if (message.includes('credit balance') || message.includes('billing'))
20
+ return false;
21
+ if (message.includes('invalid_api_key') || message.includes('authentication'))
22
+ return false;
23
+ if (message.includes('invalid_request_error'))
24
+ return false;
25
+ // Retryable: rate limits, overloaded, server errors, network issues
26
+ if (message.includes('rate_limit') || message.includes('overloaded'))
27
+ return true;
28
+ if (message.includes('529') || message.includes('503') || message.includes('500'))
29
+ return true;
30
+ if (errorName === 'APIConnectionError' || message.includes('ECONNRESET'))
31
+ return true;
32
+ if (errorName === 'RateLimitError' || errorName === 'InternalServerError')
33
+ return true;
34
+ // Default: retry on unknown errors (might be transient)
35
+ return true;
36
+ }
37
+ /**
38
+ * Calculates exponential backoff delay with jitter.
39
+ */
40
+ function calculateBackoff(attempt, config) {
41
+ const exponentialDelay = config.baseDelayMs * Math.pow(2, attempt);
42
+ const jitter = Math.random() * 0.3 * exponentialDelay; // 0-30% jitter
43
+ return Math.min(exponentialDelay + jitter, config.maxDelayMs);
44
+ }
45
+ /**
46
+ * Sleep for a specified duration.
47
+ */
48
+ function sleep(ms) {
49
+ return new Promise(resolve => setTimeout(resolve, ms));
50
+ }
51
+ // ============================================================================
52
+ // Model Configuration
53
+ // ============================================================================
54
+ // Centralized model IDs - update these when Anthropic releases new versions.
55
+ // These are defaults; can be overridden via config.anthropic.models.*
56
+ const DEFAULT_MODELS = {
57
+ /** Full insight generation - Sonnet balances quality/cost well for structured extraction */
58
+ generation: 'claude-sonnet-4-5-20250929',
59
+ /** Quick staleness checks - needs speed over capability */
60
+ quickCheck: 'claude-haiku-4-5-20251001',
61
+ /** Fast patch generation - speed over quality */
62
+ patch: 'claude-haiku-4-5-20251001',
63
+ /** Full patch generation (fallback) - for complex patches when fast path fails */
64
+ fullPatch: 'claude-haiku-4-5-20251001',
65
+ /** Proposition extraction - factual extraction task */
66
+ propositions: 'claude-haiku-4-5-20251001',
67
+ };
68
+ // ============================================================================
69
+ // Shared Prompt Components
70
+ // ============================================================================
71
+ // Extracted prompt sections that are reused across multiple LLM calls.
72
+ // Single source of truth for consistent behavior.
73
+ /**
74
+ * CurrentState decision logic - guides the LLM on how to determine session state.
75
+ * Used in: extractSessionInsights, generateCurrentStateOnly, generateInsightsPatch
76
+ */
77
+ const CURRENT_STATE_DECISION_PROMPT = `Think carefully about what someone monitoring this session needs to know.
78
+
79
+ Before deciding the current state, ask yourself:
80
+
81
+ 1. "What is the actual situation right now?"
82
+ - Is the agent actively working on something?
83
+ - Is the agent idle, waiting for the user?
84
+ - Is something blocking progress?
85
+
86
+ 2. "What's the last thing Claude communicated to the user?"
87
+ - Did Claude make a request or ask a question?
88
+ - Did Claude present a summary and stop?
89
+ - Did Claude describe next steps that require user action?
90
+
91
+ 3. "Is there unfinished business?"
92
+ - Even if implementation is "done", is validation/testing/verification still needed?
93
+ - Did Claude say "ready to test" or "after you do X" - meaning X hasn't happened yet?
94
+ - Or is everything truly complete with no pending actions?
95
+
96
+ 4. "What does the dashboard viewer need to understand?"
97
+ - If they're checking in on this session, what's the most accurate summary?
98
+ - What's the one thing they need to know about where this session stands?
99
+
100
+ IMPORTANT: When in doubt between "completed" vs "waiting", err on the side of "waiting".
101
+ It's much worse to say everything is done when user action is still needed, than to imply
102
+ there's pending work when things are actually complete. If there's ANY hint of testing,
103
+ validation, verification, or "after you do X" - that's waiting, not completed.
104
+
105
+ Choose the format and wording that communicates most clearly:
106
+ - Single line summary, or
107
+ - Bullet points (with or without icons)
108
+
109
+ Color guidance:
110
+ - **amber**: waiting for user action, pending decision, question asked
111
+ - **red**: blocked, error preventing progress
112
+ - **emerald**: truly complete - implemented, validated, no pending actions
113
+ - **sky**: actively working, in progress
114
+
115
+ Choose an icon that fits the situation.
116
+
117
+ The right state is the one that accurately communicates the session's reality to someone monitoring from the dashboard.`;
118
+ export class AnthropicService {
119
+ logger;
120
+ client = null;
121
+ // Model configuration - populated from config or defaults
122
+ // Uses explicit mutable type rather than `typeof DEFAULT_MODELS` which is readonly due to `as const`
123
+ models;
124
+ constructor() {
125
+ this.logger = createLogger('AnthropicService');
126
+ this.models = { ...DEFAULT_MODELS };
127
+ }
128
+ async initialize() {
129
+ const config = ConfigService.getInstance().getConfig();
130
+ const apiKey = config.anthropic?.apiKey || process.env.ANTHROPIC_API_KEY;
131
+ if (!apiKey) {
132
+ this.logger.warn('Anthropic API key not configured');
133
+ return;
134
+ }
135
+ try {
136
+ this.client = new Anthropic({ apiKey });
137
+ // Load model overrides from config
138
+ const configModels = config.anthropic?.models;
139
+ if (configModels) {
140
+ if (configModels.generation)
141
+ this.models.generation = configModels.generation;
142
+ if (configModels.quickCheck)
143
+ this.models.quickCheck = configModels.quickCheck;
144
+ if (configModels.patch)
145
+ this.models.patch = configModels.patch;
146
+ if (configModels.fullPatch)
147
+ this.models.fullPatch = configModels.fullPatch;
148
+ if (configModels.propositions)
149
+ this.models.propositions = configModels.propositions;
150
+ }
151
+ // Also support legacy config.anthropic.model for generation
152
+ if (config.anthropic?.model) {
153
+ this.models.generation = config.anthropic.model;
154
+ }
155
+ this.logger.info('Anthropic service initialized', { models: this.models });
156
+ }
157
+ catch (error) {
158
+ this.logger.error('Failed to initialize Anthropic service', { error });
159
+ throw new CUIError('ANTHROPIC_INIT_ERROR', 'Failed to initialize Anthropic service', 500);
160
+ }
161
+ }
162
+ /**
163
+ * Execute a function with exponential backoff retry on transient errors.
164
+ */
165
+ async withRetry(operation, fn, config = DEFAULT_RETRY_CONFIG) {
166
+ let lastError;
167
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
168
+ try {
169
+ return await fn();
170
+ }
171
+ catch (error) {
172
+ lastError = error;
173
+ // Don't retry non-retryable errors
174
+ if (!isRetryableError(error)) {
175
+ throw error;
176
+ }
177
+ // Don't retry after max attempts
178
+ if (attempt >= config.maxRetries) {
179
+ this.logger.warn(`${operation}: All ${config.maxRetries} retries exhausted`, {
180
+ error: error instanceof Error ? error.message : String(error)
181
+ });
182
+ throw error;
183
+ }
184
+ const delayMs = calculateBackoff(attempt, config);
185
+ this.logger.info(`${operation}: Retry ${attempt + 1}/${config.maxRetries} after ${Math.round(delayMs)}ms`, {
186
+ error: error instanceof Error ? error.message : String(error)
187
+ });
188
+ await sleep(delayMs);
189
+ }
190
+ }
191
+ throw lastError;
192
+ }
193
+ async checkHealth() {
194
+ if (!this.client) {
195
+ return {
196
+ status: 'unhealthy',
197
+ message: 'Anthropic API key not configured',
198
+ apiKeyValid: false
199
+ };
200
+ }
201
+ try {
202
+ const response = await this.withRetry('checkHealth', () => this.client.messages.create({
203
+ model: this.models.generation,
204
+ max_tokens: 50,
205
+ messages: [{ role: 'user', content: 'Say "ok" and nothing else.' }]
206
+ }));
207
+ const text = response.content[0]?.type === 'text' ? response.content[0].text : null;
208
+ if (text) {
209
+ return {
210
+ status: 'healthy',
211
+ message: 'Anthropic API is accessible',
212
+ apiKeyValid: true
213
+ };
214
+ }
215
+ return {
216
+ status: 'unhealthy',
217
+ message: 'Unexpected response from Anthropic API',
218
+ apiKeyValid: true
219
+ };
220
+ }
221
+ catch (error) {
222
+ this.logger.error('Health check failed', { error });
223
+ return {
224
+ status: 'unhealthy',
225
+ message: error instanceof Error ? error.message : 'Unknown error',
226
+ apiKeyValid: false
227
+ };
228
+ }
229
+ }
230
+ /**
231
+ * Log cost for an LLM API call.
232
+ * Call this after each API call to track costs.
233
+ */
234
+ logCost(response, operation, model, durationMs, sessionId) {
235
+ try {
236
+ const costTracker = getCostTracker();
237
+ costTracker.log({
238
+ sessionId: sessionId || 'unknown',
239
+ operation,
240
+ model,
241
+ inputTokens: response.usage?.input_tokens || 0,
242
+ outputTokens: response.usage?.output_tokens || 0,
243
+ durationMs,
244
+ });
245
+ }
246
+ catch (error) {
247
+ // Don't let cost logging failures break the main flow
248
+ this.logger.debug('Failed to log cost', { error, operation });
249
+ }
250
+ }
251
+ /**
252
+ * Extract rich, structured session insights using Opus 4.5
253
+ */
254
+ async extractSessionInsights(conversationText) {
255
+ if (!this.client) {
256
+ throw new CUIError('ANTHROPIC_API_KEY_MISSING', 'Anthropic API key not configured', 400);
257
+ }
258
+ const systemPrompt = `You are analyzing a coding session transcript to extract structured information for a dashboard display.
259
+
260
+ The goal: Someone glancing at this dashboard should immediately understand what this session is about, where it's at, and what's notable.
261
+
262
+ Extract the following:
263
+
264
+ 1. CONTEXT - Identity of the session:
265
+ - project: The codebase/repo name
266
+ - area: The specific component/module/domain if applicable (null if general)
267
+ - mission: The overarching goal in one sentence - be specific
268
+ - scope: "minor" (quick fix), "feature" (meaningful addition), "major" (significant change)
269
+
270
+ 2. CURRENT STATE - Describe where this coding session stands right now.
271
+
272
+ This is the most important indicator for deciding whether to jump back into a session.
273
+
274
+ ${CURRENT_STATE_DECISION_PROMPT.split('\n').map(line => ' ' + line).join('\n')}
275
+
276
+ LANGUAGE RULES:
277
+ - Use "we" to mean the session participants (user + Claude working together)
278
+ - NEVER use "you" or "your" - the person reading this dashboard is NOT the session participant
279
+ - Bad: "your edit", "you found", "waiting for your input"
280
+ - Good: "We made an edit", "We found the issue", "Waiting for input on X"
281
+
282
+ Each currentState has:
283
+ - icon: emoji that fits the situation
284
+ - color: semantic color (amber/red/emerald/sky) based on the guidance above
285
+ - content: Single line summary OR bullet points (with or without icons)
286
+
287
+ 3. THEME - A single word capturing the session's current mode/vibe:
288
+ Examples: "debugging", "building", "exploring", "firefighting", "refactoring", "designing", "investigating", "polishing"
289
+
290
+ 4. TAGS - Quick categorization:
291
+ - workType: ["implementing", "debugging", "refactoring", "exploring", "designing"]
292
+ - domain: Technical domains touched, e.g., ["UI", "database"], ["API", "auth"]
293
+ - collaboration: "autonomous" | "iterative" | "guided"
294
+ - complexity: "routine" | "tricky" | "gnarly"
295
+
296
+ 5. PANELS - Design 3-4 stable semantic panels that capture the essence of this session.
297
+
298
+ PANEL PHILOSOPHY:
299
+ - Panels should be STABLE - they define the semantic shape of the session
300
+ - Update the VALUE when facts change, not the panel itself
301
+ - Only change panels if the session fundamentally pivots
302
+ - Use expressive emoji icons that capture meaning at a glance
303
+
304
+ Each panel has:
305
+ - label: Short semantic label (e.g., "Goal", "Key Constraint", "Approach", "Target User")
306
+ - icon: Single emoji that captures the panel meaning (๐ŸŽฏ ๐Ÿง  ๐Ÿ”ฌ โš ๏ธ ๐Ÿ“Š ๐Ÿ’ก ๐Ÿ”ง ๐Ÿš€ etc.)
307
+ - value: A single concise sentence capturing the current state
308
+ - color: One of: "sky", "purple", "rose", "orange", "emerald", "amber", "red", "teal", "blue", "fuchsia", "slate"
309
+
310
+ Pick colors that match semantic meaning:
311
+ - sky/blue: informational, context, status
312
+ - emerald/teal: success, progress, done
313
+ - amber/orange: warnings, blockers, pending
314
+ - red/rose: errors, critical issues
315
+ - purple/fuchsia: decisions, architecture, design
316
+ - slate: neutral, reference
317
+
318
+ 6. MILESTONES - The roadmap of the session:
319
+
320
+ Milestones show the journey: what's done, what's active, AND what's coming.
321
+
322
+ CRITICAL: ALWAYS return 3-5 milestones. Think like a project manager - decompose the mission into steps:
323
+ - What has been accomplished? (done)
324
+ - What's actively being worked on? (in_progress)
325
+ - What logically comes NEXT? (pending) โ† This is the most valuable part!
326
+
327
+ FORWARD-LOOKING IS KEY:
328
+ - Don't just report what happened - anticipate what's needed to complete the mission
329
+ - Break down the user's goal into logical steps, even if they weren't explicitly stated
330
+ - Example: If mission is "add dark mode", milestones might be:
331
+ โœ“ [done] Create theme context
332
+ โ†’ [in_progress] Update components to use theme
333
+ โ—‹ [pending] Add theme toggle UI
334
+ โ—‹ [pending] Persist theme preference
335
+ โ—‹ [pending] Test theme switching
336
+
337
+ MILESTONE PHILOSOPHY:
338
+ - Milestones tell the story of intent: past, present, AND planned
339
+ - When work completes: mark "done"
340
+ - When new objectives emerge: ADD new milestones (don't remove old ones)
341
+ - When the user pivots away from something incomplete: mark it "on_hold" (might return)
342
+ - When an approach is abandoned for a better one: mark it "superseded" (won't return)
343
+ - Available statuses (use exactly these strings): "pending", "in_progress", "done", "on_hold", "superseded"
344
+ - Keep 5-7 visible milestones. Oldest completed items can be dropped if needed to make room.
345
+
346
+ 7. NOTABLE - Cumulative event log with rich emojis:
347
+
348
+ Each event has:
349
+ - icon: Expressive emoji matching event type (๐Ÿ’ก ๐Ÿ”„ ๐Ÿ”ง โœจ ๐ŸŽฏ โœ… โš ๏ธ ๐Ÿ› ๐ŸŽจ ๐Ÿ“ ๐Ÿงช ๐Ÿš€ etc.)
350
+ - type: "insight" | "pivot" | "breakthrough" | "infrastructure" | "refinement" | "validation" | "error" | "fix" | "blocker" | "decision" | "iteration"
351
+ - text: What happened (1 sentence)
352
+
353
+ NOTABLE PHILOSOPHY:
354
+ - Events ACCUMULATE over time - don't remove old events
355
+ - New events get added to the list as they occur
356
+ - Use diverse, expressive emojis to make scanning easy
357
+
358
+ 8. RECENT ACTIONS - 3-4 most recent concrete actions
359
+
360
+ CRITICAL OUTPUT INSTRUCTIONS:
361
+ - Respond with ONLY valid JSON
362
+ - Do NOT include explanatory text before or after the JSON
363
+ - Do NOT include markdown code blocks or formatting
364
+ - Start your response with { and end with }
365
+ - No prose, no commentary, ONLY the JSON object
366
+
367
+ JSON Structure:
368
+ {
369
+ "context": { "project": "string", "area": "string|null", "mission": "string", "scope": "minor|feature|major" },
370
+ "currentState": { "icon": "emoji (required)", "color": "sky|emerald|amber|red|...", "content": "sentence or bullet points" },
371
+ "theme": "string",
372
+ "tags": { "workType": [...], "domain": [...], "collaboration": "...", "complexity": "..." },
373
+ "panels": [ { "label": "string", "icon": "emoji", "color": "sky|purple|rose|...", "value": "string" } ],
374
+ "milestones": [ { "label": "string", "status": "pending|in_progress|done|on_hold|superseded" } ],
375
+ "notable": [ { "icon": "emoji", "type": "...", "text": "string" } ],
376
+ "recentActions": ["string", ...]
377
+ }`;
378
+ let responseText = null;
379
+ const startTime = Date.now();
380
+ try {
381
+ const response = await this.withRetry('extractSessionInsights', () => this.client.messages.create({
382
+ model: this.models.generation,
383
+ max_tokens: 2000,
384
+ system: systemPrompt,
385
+ messages: [
386
+ {
387
+ role: 'user',
388
+ content: `Analyze this coding session and extract structured insights:\n\n${conversationText}`
389
+ },
390
+ {
391
+ role: 'assistant',
392
+ content: '{' // Prefill to force JSON output
393
+ }
394
+ ]
395
+ }));
396
+ const durationMs = Date.now() - startTime;
397
+ // Log cost for GENERATE operation
398
+ this.logCost(response, 'GENERATE', this.models.generation, durationMs);
399
+ responseText = response.content[0]?.type === 'text' ? response.content[0].text : null;
400
+ if (!responseText) {
401
+ throw new CUIError('ANTHROPIC_INSIGHTS_ERROR', 'No response text returned', 500);
402
+ }
403
+ // Prepend the '{' we used for prefill
404
+ responseText = '{' + responseText;
405
+ // Extract JSON from response with multiple fallback strategies
406
+ let jsonText = responseText.trim();
407
+ // Strategy 1: Try to extract from markdown code blocks
408
+ const codeBlockMatch = responseText.match(/```(?:json)?\s*([\s\S]*?)```/);
409
+ if (codeBlockMatch) {
410
+ jsonText = codeBlockMatch[1].trim();
411
+ }
412
+ else {
413
+ // Strategy 2: Look for content between first { and last }
414
+ const firstBrace = jsonText.indexOf('{');
415
+ const lastBrace = jsonText.lastIndexOf('}');
416
+ if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
417
+ jsonText = jsonText.substring(firstBrace, lastBrace + 1);
418
+ }
419
+ }
420
+ // Log the extracted JSON for debugging
421
+ this.logger.debug('Extracted JSON from LLM response', {
422
+ responsePreview: responseText.slice(0, 100),
423
+ extractedPreview: jsonText.slice(0, 100),
424
+ usedCodeBlock: !!codeBlockMatch
425
+ });
426
+ const result = JSON.parse(jsonText);
427
+ // Validate structure - be lenient, provide defaults
428
+ if (!result.context || !result.tags) {
429
+ throw new CUIError('ANTHROPIC_INSIGHTS_ERROR', 'Invalid response structure', 500);
430
+ }
431
+ // Ensure arrays exist with defaults
432
+ result.milestones = result.milestones || [];
433
+ result.notable = result.notable || [];
434
+ result.recentActions = result.recentActions || [];
435
+ result.panels = result.panels || [];
436
+ result.theme = result.theme || 'working';
437
+ result.currentState = result.currentState || { icon: '๐Ÿ”„', color: 'sky', content: 'Session in progress' };
438
+ this.logger.info('Session insights extracted (Opus)', {
439
+ model: this.models.generation,
440
+ inputTokens: response.usage?.input_tokens,
441
+ outputTokens: response.usage?.output_tokens,
442
+ project: result.context.project,
443
+ theme: result.theme,
444
+ currentState: result.currentState.content?.slice(0, 50),
445
+ panelCount: result.panels.length,
446
+ panelLabels: result.panels.map(p => p.label || p.title || 'untitled'),
447
+ panelValues: result.panels.map(p => p.value ? `${p.value.slice(0, 50)}...` : p.items ? `${p.items.length} items` : 'empty'),
448
+ milestoneCount: result.milestones.length,
449
+ notableCount: result.notable.length
450
+ });
451
+ return result;
452
+ }
453
+ catch (error) {
454
+ if (error instanceof CUIError) {
455
+ throw error;
456
+ }
457
+ // Check for credit/billing errors and make them LOUD
458
+ const errorMessage = error instanceof Error ? error.message : String(error);
459
+ if (errorMessage.includes('credit balance') || errorMessage.includes('billing')) {
460
+ this.logger.error('๐Ÿšจ ANTHROPIC API CREDITS EXHAUSTED ๐Ÿšจ', {
461
+ errorMessage,
462
+ action: 'ADD CREDITS AT https://console.anthropic.com/settings/billing'
463
+ });
464
+ throw new CUIError('ANTHROPIC_NO_CREDITS', '๐Ÿšจ Anthropic API credits exhausted. Add credits at https://console.anthropic.com/settings/billing', 402);
465
+ }
466
+ // Log error with proper serialization AND the response that failed to parse
467
+ this.logger.error('Session insights extraction failed', {
468
+ errorMessage,
469
+ errorStack: error instanceof Error ? error.stack : undefined,
470
+ errorType: error?.constructor?.name,
471
+ // Include the actual response for debugging
472
+ responsePreview: responseText?.slice(0, 500) || 'no response'
473
+ });
474
+ throw new CUIError('ANTHROPIC_INSIGHTS_ERROR', 'Failed to extract session insights', 500);
475
+ }
476
+ }
477
+ /**
478
+ * Generate a session name (simpler extraction for naming)
479
+ */
480
+ async generateSessionName(conversationText) {
481
+ if (!this.client) {
482
+ throw new CUIError('ANTHROPIC_API_KEY_MISSING', 'Anthropic API key not configured', 400);
483
+ }
484
+ try {
485
+ const response = await this.withRetry('generateSessionName', () => this.client.messages.create({
486
+ model: this.models.generation,
487
+ max_tokens: 100,
488
+ messages: [{
489
+ role: 'user',
490
+ content: `Generate a brief, descriptive name (3-6 words) for this coding session. Include the project/area context. Focus on the main task or feature being worked on. Do not include generic words like "session", "conversation", or "chat".
491
+
492
+ Examples of good names:
493
+ - "CUI: Session insights UI redesign"
494
+ - "Auth: Fix redirect loop bug"
495
+ - "API: Add rate limiting middleware"
496
+
497
+ Respond with ONLY the name, nothing else.
498
+
499
+ Conversation:
500
+ ${conversationText}`
501
+ }]
502
+ }));
503
+ const name = response.content[0]?.type === 'text' ? response.content[0].text.trim() : null;
504
+ if (!name) {
505
+ throw new CUIError('ANTHROPIC_NAME_ERROR', 'No name returned', 500);
506
+ }
507
+ return { name };
508
+ }
509
+ catch (error) {
510
+ if (error instanceof CUIError) {
511
+ throw error;
512
+ }
513
+ this.logger.error('Session name generation failed', { error });
514
+ throw new CUIError('ANTHROPIC_NAME_ERROR', 'Failed to generate session name', 500);
515
+ }
516
+ }
517
+ isConfigured() {
518
+ return this.client !== null;
519
+ }
520
+ /**
521
+ * Quick check using Haiku to determine if insights need patching.
522
+ *
523
+ * This is a fast, cheap call that just answers "has anything meaningful changed?"
524
+ * based on recent actions. If yes, we'll follow up with Sonnet for actual patching.
525
+ */
526
+ async quickCheckInsightsStale(currentInsights, recentActions) {
527
+ if (!this.client) {
528
+ return { needsPatch: false, reason: 'Anthropic not configured' };
529
+ }
530
+ const startTime = Date.now();
531
+ try {
532
+ const milestonesStr = currentInsights.milestones
533
+ .map(m => `[${m.status}] ${m.label}`)
534
+ .join(', ');
535
+ const panelsStr = currentInsights.panels
536
+ .map(p => p.title)
537
+ .join(', ');
538
+ const actionsStr = recentActions.join('\n');
539
+ const prompt = `Current session insights (a living summary of the conversation):
540
+ - Mission: "${currentInsights.mission}"
541
+ - Milestones: ${milestonesStr || '(none)'}
542
+ - Panels: ${panelsStr || '(none)'}
543
+
544
+ Recent activity:
545
+ ${actionsStr}
546
+
547
+ Should these insights be updated to reflect the recent activity?
548
+
549
+ Say YES (needsPatch: true) if ANY of these apply:
550
+ - Work was done that should be reflected in a panel (new file, code change, finding, etc.)
551
+ - Progress was made on any task or milestone
552
+ - Focus shifted to a new area or topic
553
+ - Something notable happened worth capturing
554
+ - The summary feels stale compared to what just happened
555
+
556
+ Say NO only if the recent actions are trivial or already reflected in current insights.
557
+
558
+ Err on the side of updating - the insights should feel LIVE and current.
559
+
560
+ Respond with JSON only:
561
+ {"needsPatch": true/false, "reason": "brief explanation"}`;
562
+ const response = await this.withRetry('quickCheckInsightsStale', () => this.client.messages.create({
563
+ model: this.models.quickCheck,
564
+ max_tokens: 100,
565
+ messages: [{ role: 'user', content: prompt }]
566
+ }));
567
+ const duration = Date.now() - startTime;
568
+ const text = response.content[0]?.type === 'text' ? response.content[0].text : '';
569
+ // Log cost for QUICK_CHECK operation
570
+ this.logCost(response, 'QUICK_CHECK', this.models.quickCheck, duration);
571
+ // Log for observability
572
+ this.logger.info('Quick check (Haiku) completed', {
573
+ durationMs: duration,
574
+ inputTokens: response.usage?.input_tokens,
575
+ outputTokens: response.usage?.output_tokens,
576
+ recentActionsCount: recentActions.length,
577
+ responsePreview: text.slice(0, 100)
578
+ });
579
+ try {
580
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
581
+ const result = JSON.parse(jsonMatch?.[0] || text);
582
+ return {
583
+ needsPatch: result.needsPatch === true,
584
+ reason: result.reason || 'No reason provided'
585
+ };
586
+ }
587
+ catch {
588
+ this.logger.debug('Failed to parse quick check response', { text });
589
+ return { needsPatch: false, reason: 'Parse error - keeping cached' };
590
+ }
591
+ }
592
+ catch (error) {
593
+ // Check for credit/billing errors and make them LOUD
594
+ const errorMessage = error instanceof Error ? error.message : String(error);
595
+ if (errorMessage.includes('credit balance') || errorMessage.includes('billing')) {
596
+ this.logger.error('๐Ÿšจ ANTHROPIC API CREDITS EXHAUSTED ๐Ÿšจ', {
597
+ errorMessage,
598
+ action: 'ADD CREDITS AT https://console.anthropic.com/settings/billing',
599
+ context: 'Quick check failed due to billing issue'
600
+ });
601
+ }
602
+ else {
603
+ this.logger.error('Quick check failed', { error });
604
+ }
605
+ return { needsPatch: false, reason: 'Check error - keeping cached' };
606
+ }
607
+ }
608
+ /**
609
+ * Decompose recent activity into exhaustive propositional facts.
610
+ * Separates "what happened" (facts) from "what does this mean" (interpretation).
611
+ * Returns structured bullet points of all tool uses, user messages, and Claude messages.
612
+ */
613
+ async decomposeActivityIntoPropositions(previousState, recentActivity) {
614
+ if (!this.client) {
615
+ return [];
616
+ }
617
+ const previousStateStr = previousState
618
+ ? `${previousState.icon} [${previousState.color}] ${previousState.content}`
619
+ : '(no previous state)';
620
+ const activityStr = recentActivity
621
+ .map(a => `[${a.type}] ${a.content}`)
622
+ .join('\n\n');
623
+ const prompt = `You are analyzing a transcript of a coding session between a user and Claude (an AI assistant).
624
+
625
+ **Previous Current State:**
626
+ ${previousStateStr}
627
+
628
+ **Recent Activity:**
629
+ ${activityStr}
630
+
631
+ **Your Task:**
632
+ Create an exhaustive list of propositional facts about what happened in this activity.
633
+ Be thorough and explicit - list EVERYTHING that was done and said.
634
+
635
+ Format as bullet points:
636
+ - Tool uses: "Claude used [tool] to [action]"
637
+ - Claude statements: "Claude said/stated/explained [fact]"
638
+ - User messages: "User asked/requested/said [fact]"
639
+
640
+ Be exhaustive - every action, every statement, every request should become a bullet point.
641
+ Keep each bullet point factual and specific, not interpretive.
642
+
643
+ Examples:
644
+ โœ“ "Claude used Grep to search for 'validateUser' in the codebase"
645
+ โœ“ "Claude stated that the bug was caused by missing null checks"
646
+ โœ“ "Claude said the fix is ready to test after server restart"
647
+ โœ“ "User asked about the performance impact"
648
+ โœ— "Claude made progress on the bug" (too vague)
649
+ โœ— "The session is going well" (interpretive, not factual)
650
+
651
+ Return ONLY a JSON array of strings, one per proposition:
652
+ ["proposition 1", "proposition 2", ...]`;
653
+ let rawResponse = '';
654
+ const startTime = Date.now();
655
+ try {
656
+ const response = await this.withRetry('decomposeActivityIntoPropositions', () => this.client.messages.create({
657
+ model: this.models.propositions, // Use Haiku - faster for factual extraction
658
+ max_tokens: 2000,
659
+ temperature: 0,
660
+ messages: [{
661
+ role: 'user',
662
+ content: prompt
663
+ }]
664
+ }));
665
+ const durationMs = Date.now() - startTime;
666
+ // Log cost for PROPOSITIONS operation (Haiku)
667
+ this.logCost(response, 'PROPOSITIONS', this.models.propositions, durationMs);
668
+ const textContent = response.content.find(block => block.type === 'text');
669
+ if (!textContent || textContent.type !== 'text') {
670
+ return [];
671
+ }
672
+ rawResponse = textContent.text;
673
+ let jsonText = textContent.text.trim();
674
+ // Strip markdown code blocks if present (```json ... ```)
675
+ if (jsonText.startsWith('```')) {
676
+ jsonText = jsonText.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '').trim();
677
+ }
678
+ // Strip markdown headings and prose before JSON (e.g., "# Summary\nHere are...")
679
+ // Always try to extract JSON array first, even if text looks like it starts with [
680
+ const jsonMatch = jsonText.match(/\[[\s\S]*?\](?=\s*$)/);
681
+ if (jsonMatch) {
682
+ jsonText = jsonMatch[0];
683
+ }
684
+ else if (!jsonText.startsWith('[')) {
685
+ // No JSON array found and doesn't start with [
686
+ this.logger.debug('Decomposition response is not JSON', { preview: jsonText.slice(0, 200) });
687
+ return [];
688
+ }
689
+ const propositions = JSON.parse(jsonText);
690
+ // Log token usage for cost tracking
691
+ this.logger.info('Propositional decomposition (Haiku)', {
692
+ model: this.models.propositions,
693
+ inputTokens: response.usage?.input_tokens,
694
+ outputTokens: response.usage?.output_tokens,
695
+ propositionCount: Array.isArray(propositions) ? propositions.length : 0,
696
+ activityCount: recentActivity.length
697
+ });
698
+ return Array.isArray(propositions) ? propositions : [];
699
+ }
700
+ catch (error) {
701
+ this.logger.debug('Failed to decompose activity into propositions', {
702
+ error: error instanceof Error ? error.message : String(error),
703
+ responsePreview: rawResponse.slice(0, 200),
704
+ });
705
+ return [];
706
+ }
707
+ }
708
+ /**
709
+ * Fast currentState-only update using Haiku.
710
+ * Bypasses full patch generation when only currentState needs updating.
711
+ * Much faster (~3-4s) than full patch pipeline (~12s).
712
+ */
713
+ async generateCurrentStateOnly(currentState, propositions) {
714
+ if (!this.client || propositions.length === 0) {
715
+ return null;
716
+ }
717
+ const previousStateStr = currentState
718
+ ? `${currentState.icon} [${currentState.color}] ${currentState.content}`
719
+ : '(no previous state)';
720
+ const propositionsStr = propositions.map((p, i) => `${i + 1}. ${p}`).join('\n');
721
+ const prompt = `You are updating the "current state" for a coding session dashboard.
722
+
723
+ **Previous Current State:**
724
+ ${previousStateStr}
725
+
726
+ **What Happened Since Then (propositional facts):**
727
+ ${propositionsStr}
728
+
729
+ **Your Task:**
730
+ Decide the new current state for the dashboard. This is what someone monitoring the session needs to know RIGHT NOW.
731
+
732
+ ${CURRENT_STATE_DECISION_PROMPT}
733
+
734
+ LANGUAGE:
735
+ - Use "we" (session participants working together)
736
+ - NEVER "you"/"your" (dashboard reader is NOT a participant)
737
+ - You're an OBSERVER analyzing their session
738
+
739
+ Respond with ONLY valid JSON:
740
+ {
741
+ "icon": "emoji",
742
+ "color": "amber|red|emerald|sky",
743
+ "content": "single line or bullet points"
744
+ }`;
745
+ let rawResponse = '';
746
+ try {
747
+ const response = await this.withRetry('generateCurrentStateOnly', () => this.client.messages.create({
748
+ model: this.models.patch, // Use Haiku for fast currentState-only updates
749
+ max_tokens: 300,
750
+ temperature: 0,
751
+ messages: [{
752
+ role: 'user',
753
+ content: prompt
754
+ }]
755
+ }));
756
+ const textContent = response.content.find(block => block.type === 'text');
757
+ if (!textContent || textContent.type !== 'text') {
758
+ return null;
759
+ }
760
+ rawResponse = textContent.text;
761
+ let jsonText = textContent.text.trim();
762
+ // Strip markdown code blocks if present (```json ... ```)
763
+ if (jsonText.startsWith('```')) {
764
+ jsonText = jsonText.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '').trim();
765
+ }
766
+ // Strip markdown headings and prose before JSON (e.g., "# Summary\nHere is...")
767
+ // Always try to extract JSON object first, even if text looks like it starts with {
768
+ const jsonMatch = jsonText.match(/\{[\s\S]*?\}(?=\s*$)/);
769
+ if (jsonMatch) {
770
+ jsonText = jsonMatch[0];
771
+ }
772
+ else if (!jsonText.startsWith('{')) {
773
+ // No JSON object found and doesn't start with {
774
+ this.logger.debug('CurrentState response is not JSON', { preview: jsonText.slice(0, 200) });
775
+ return null;
776
+ }
777
+ const result = JSON.parse(jsonText);
778
+ return result;
779
+ }
780
+ catch (error) {
781
+ this.logger.debug('Failed to generate currentState', {
782
+ error: error instanceof Error ? error.message : String(error),
783
+ responsePreview: rawResponse.slice(0, 200),
784
+ });
785
+ return null;
786
+ }
787
+ }
788
+ /**
789
+ * Generate a fast patch using Haiku that updates:
790
+ * - currentState
791
+ * - milestones (status updates + new ones)
792
+ * - recentActions
793
+ *
794
+ * This is the new "Haiku-heavy" patch strategy from Architecture V2.
795
+ * Much faster (~1-2s) than full Sonnet patch (~12s).
796
+ * Panels, notable, and context are handled by REFRESH (Opus).
797
+ */
798
+ async generateFastPatch(currentInsights, propositions) {
799
+ if (!this.client || propositions.length === 0) {
800
+ return null;
801
+ }
802
+ const previousStateStr = currentInsights.currentState
803
+ ? `${currentInsights.currentState.icon} [${currentInsights.currentState.color}] ${currentInsights.currentState.content}`
804
+ : '(no previous state)';
805
+ const milestonesStr = currentInsights.milestones.length > 0
806
+ ? currentInsights.milestones.map(m => `[${m.status}] ${m.label}`).join('\n')
807
+ : '(none)';
808
+ const propositionsStr = propositions.map((p, i) => `${i + 1}. ${p}`).join('\n');
809
+ const prompt = `You are updating a coding session dashboard based on recent activity.
810
+
811
+ CURRENT STATE:
812
+ ${previousStateStr}
813
+
814
+ CURRENT MILESTONES (Progress):
815
+ ${milestonesStr}
816
+
817
+ RECENT ACTIVITY (propositional facts):
818
+ ${propositionsStr}
819
+
820
+ Update the dashboard to reflect what happened. Return JSON with:
821
+
822
+ 1. **currentState**: What's the situation right now?
823
+ - icon: emoji
824
+ - color: amber (waiting), red (blocked), emerald (done), sky (working)
825
+ - content: single sentence or bullet points
826
+
827
+ 2. **milestones**: Update statuses AND add forward-looking milestones
828
+ - Mark completed work as "done"
829
+ - Mark active work as "current" (only ONE at a time)
830
+ - IMPORTANT: Add pending milestones for what logically comes NEXT
831
+ - Think ahead: what steps remain to complete the session's goal?
832
+ - Keep labels stable when possible (don't rephrase existing milestones)
833
+ - Use {"action": "update", "label": "exact existing label", "status": "done|current|pending"}
834
+ - Use {"action": "add", "label": "New milestone", "status": "pending"} for upcoming steps
835
+
836
+ 3. **recentActions**: 3-4 most recent concrete actions (tool names, file edits, etc.)
837
+ - Keep these short: "Read config.ts", "Edit auth.ts", "Ran tests"
838
+ - Most recent last
839
+
840
+ LANGUAGE:
841
+ - Use "we" (session participants)
842
+ - Never "you/your" (observer, not participant)
843
+
844
+ Respond with ONLY valid JSON:
845
+ {
846
+ "currentState": { "icon": "...", "color": "...", "content": "..." },
847
+ "milestones": [
848
+ { "action": "update", "label": "exact label", "status": "done" },
849
+ { "action": "add", "label": "New objective", "status": "pending" }
850
+ ],
851
+ "recentActions": ["Action 1", "Action 2", "Action 3"]
852
+ }`;
853
+ let rawResponse = '';
854
+ const startTime = Date.now();
855
+ try {
856
+ const response = await this.withRetry('generateFastPatch', () => this.client.messages.create({
857
+ model: this.models.patch,
858
+ max_tokens: 600,
859
+ temperature: 0,
860
+ messages: [{
861
+ role: 'user',
862
+ content: prompt
863
+ }]
864
+ }));
865
+ const durationMs = Date.now() - startTime;
866
+ // Log cost for FAST_PATCH operation (Haiku)
867
+ this.logCost(response, 'FAST_PATCH', this.models.patch, durationMs);
868
+ const textContent = response.content.find(block => block.type === 'text');
869
+ if (!textContent || textContent.type !== 'text') {
870
+ return null;
871
+ }
872
+ rawResponse = textContent.text;
873
+ let jsonText = textContent.text.trim();
874
+ // Strip markdown code blocks if present
875
+ if (jsonText.startsWith('```')) {
876
+ jsonText = jsonText.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '').trim();
877
+ }
878
+ // Extract JSON object
879
+ const jsonMatch = jsonText.match(/\{[\s\S]*\}(?=\s*$)/);
880
+ if (jsonMatch) {
881
+ jsonText = jsonMatch[0];
882
+ }
883
+ else if (!jsonText.startsWith('{')) {
884
+ this.logger.debug('FastPatch response is not JSON', { preview: jsonText.slice(0, 200) });
885
+ return null;
886
+ }
887
+ const result = JSON.parse(jsonText);
888
+ // Validate structure
889
+ if (!result.currentState || !Array.isArray(result.milestones) || !Array.isArray(result.recentActions)) {
890
+ this.logger.debug('FastPatch invalid response structure', { keys: Object.keys(result) });
891
+ return null;
892
+ }
893
+ return result;
894
+ }
895
+ catch (error) {
896
+ this.logger.debug('FastPatch generation failed', {
897
+ error: error instanceof Error ? error.message : String(error),
898
+ responsePreview: rawResponse.slice(0, 200),
899
+ });
900
+ return null;
901
+ }
902
+ }
903
+ /**
904
+ * Generate targeted patches to insights using Sonnet.
905
+ *
906
+ * Called when quickCheckInsightsStale returns needsPatch=true.
907
+ * Returns specific patches to apply rather than regenerating everything.
908
+ *
909
+ * @param useFastPath - If true, only generate currentState using fast Haiku-only pipeline (~5s vs ~12s)
910
+ */
911
+ async generateInsightsPatch(currentInsights, recentActivity, useFastPath = true // Default to fast path for better UX
912
+ ) {
913
+ if (!this.client) {
914
+ return { patches: {}, reason: 'Anthropic not configured', propositions: [] };
915
+ }
916
+ const startTime = Date.now();
917
+ try {
918
+ // Step 1: Decompose activity into propositional facts
919
+ const decompositionStart = Date.now();
920
+ const propositions = await this.decomposeActivityIntoPropositions(currentInsights.currentState, recentActivity);
921
+ const decompositionTime = Date.now() - decompositionStart;
922
+ this.logger.debug('Propositional decomposition completed', {
923
+ durationMs: decompositionTime,
924
+ propositionCount: propositions.length,
925
+ });
926
+ // Fast path: Generate currentState + milestones + recentActions using Haiku (~1-2s vs ~12s full patch)
927
+ // This is the "Haiku-heavy" strategy from Architecture V2
928
+ if (useFastPath) {
929
+ this.logger.debug('Using fast path for patch generation');
930
+ const fastPatch = await this.generateFastPatch({
931
+ currentState: currentInsights.currentState,
932
+ milestones: currentInsights.milestones,
933
+ recentActions: currentInsights.recentActions,
934
+ }, propositions);
935
+ const totalTime = Date.now() - startTime;
936
+ if (fastPatch) {
937
+ const patches = {};
938
+ // Always include currentState if present
939
+ if (fastPatch.currentState) {
940
+ patches.currentState = fastPatch.currentState;
941
+ }
942
+ // Include milestones if there are any changes
943
+ if (fastPatch.milestones && fastPatch.milestones.length > 0) {
944
+ patches.milestones = fastPatch.milestones;
945
+ }
946
+ // Include recentActions if present
947
+ if (fastPatch.recentActions && fastPatch.recentActions.length > 0) {
948
+ patches.recentActions = fastPatch.recentActions;
949
+ }
950
+ const patchedFields = Object.keys(patches);
951
+ this.logger.info('Fast path patch completed', {
952
+ durationMs: totalTime,
953
+ patchedFields,
954
+ });
955
+ return {
956
+ patches,
957
+ reason: `Fast path: Updated ${patchedFields.join(', ')} based on propositional analysis`,
958
+ propositions
959
+ };
960
+ }
961
+ // Fall through to full path if fast path fails
962
+ this.logger.debug('Fast path returned null, falling back to full patch generation');
963
+ }
964
+ const milestonesStr = currentInsights.milestones
965
+ .map((m, i) => ` ${i}: [${m.status}] ${m.label}`)
966
+ .join('\n');
967
+ const panelsStr = currentInsights.panels
968
+ .map(p => {
969
+ // Handle both V8 format (label/value) and old format (title/items)
970
+ const panelLabel = p.label || p.title || 'Untitled';
971
+ const panelContent = p.value
972
+ ? p.value // Show full value, not truncated
973
+ : p.items?.map(item => typeof item === 'string' ? item : item.text).join(', ') || '(empty)'; // Show all items, not just first 3
974
+ return ` - "${panelLabel}": ${panelContent}`;
975
+ })
976
+ .join('\n');
977
+ // Use propositions if available, fallback to raw activity
978
+ const activityStr = propositions.length > 0
979
+ ? propositions.map(p => `โ€ข ${p}`).join('\n')
980
+ : recentActivity.slice(-10).map(a => `[${a.type}] ${a.content}`).join('\n');
981
+ const currentStateStr = currentInsights.currentState
982
+ ? `${currentInsights.currentState.icon} [${currentInsights.currentState.color}] ${currentInsights.currentState.content}`
983
+ : '(none)';
984
+ // Format previous propositions if available
985
+ const previousPropsStr = (currentInsights.previousPropositions && currentInsights.previousPropositions.length > 0)
986
+ ? currentInsights.previousPropositions.map(p => `โ€ข ${p}`).join('\n')
987
+ : null;
988
+ const prompt = `You are analyzing a coding session transcript to update a dashboard display.
989
+
990
+ IMPORTANT CONTEXT:
991
+ - You are an OBSERVER looking at a transcript of a human ("the user") working with Claude ("the assistant")
992
+ - The "RECENT ACTIVITY" below shows what happened in THEIR session
993
+ - Your job is to update the dashboard to reflect what's happening in their session
994
+ - NEVER ask questions or request clarification - just analyze what happened and update accordingly
995
+ - The "current state" should describe THEIR session state, not ask meta-questions about the insights
996
+
997
+ CURRENT SESSION INSIGHTS:
998
+ Mission: "${currentInsights.mission}"
999
+ Theme: ${currentInsights.theme}
1000
+ Current State: ${currentStateStr}
1001
+
1002
+ Milestones:
1003
+ ${milestonesStr || ' (none)'}
1004
+
1005
+ Panels:
1006
+ ${panelsStr || ' (none)'}
1007
+
1008
+ Notable events:
1009
+ ${currentInsights.notable.map(n => ` โ€ข ${n.icon || 'โ€ข'} ${n.text}`).join('\n') || ' (none)'}
1010
+
1011
+ ${previousPropsStr ? `PROPOSITIONS THAT GENERATED CURRENT INSIGHTS:
1012
+ ${previousPropsStr}
1013
+
1014
+ ` : ''}WHAT HAPPENED SINCE LAST UPDATE (exhaustive propositional facts):
1015
+ ${activityStr}
1016
+
1017
+ The above is an exhaustive, factual breakdown of everything that was done and said since the last current state update.
1018
+ Each bullet point is a specific, verifiable fact extracted from the session transcript.
1019
+
1020
+ Generate PATCHES to update these insights based on what happened.
1021
+ Only include fields that need changing. Be surgical - don't regenerate what's still accurate.
1022
+
1023
+ PANEL RULES (STRICT - NEVER VIOLATE):
1024
+ - Sessions have EXACTLY 4 panels - this is a hard limit
1025
+ - CURRENT PANELS: ${currentInsights.panels.map(p => `"${p.label || p.title}"`).join(', ')}
1026
+ - To change a panel's content: use {"action": "update", "label": "exact label from above", "value": "new content"}
1027
+ - To completely replace a panel: use {"action": "remove", "label": "old label"} then {"action": "add", "label": "New Label", ...}
1028
+ - You can ONLY use "add" if there are fewer than 4 panels total after removals
1029
+ - When in doubt, UPDATE existing panels rather than adding new ones
1030
+ - Match panel labels EXACTLY as shown above
1031
+
1032
+ NOTABLE EVENT GUIDELINES:
1033
+ - Only add truly SIGNIFICANT events: breakthroughs, blockers, major decisions, errors, fixes, pivots, insights
1034
+ - Do NOT add routine actions like "added logging" or "updated tests"
1035
+ - Each event should be meaningful enough that someone reviewing the session would want to know about it
1036
+ - Use specific event types: "insight", "pivot", "breakthrough", "infrastructure", "refinement", "validation", "error", "fix", "blocker", "decision", "iteration", "correction"
1037
+ - Choose expressive emojis that capture the meaning: ๐Ÿ’ก๐Ÿ”„๐Ÿ”งโœจ๐ŸŽฏโœ…โš ๏ธ๐Ÿ›๐ŸŽจ๐Ÿ“๐Ÿงช๐Ÿš€๐Ÿ”ฅโšก๐Ÿ› ๏ธ๐Ÿ—๏ธ
1038
+ - Keep text to one concise sentence describing what happened and why it matters
1039
+
1040
+ CURRENT STATE - ALWAYS UPDATE THIS (HIGHEST PRIORITY):
1041
+
1042
+ ${CURRENT_STATE_DECISION_PROMPT}
1043
+
1044
+ Response format (JSON only):
1045
+ {
1046
+ "reason": "brief explanation of changes",
1047
+ "patches": {
1048
+ "mission": "new mission if changed",
1049
+ "description": "new description if changed",
1050
+ "theme": "new theme if changed",
1051
+ "currentState": {"icon": "emoji", "color": "sky|emerald|amber|red|...", "content": "short description of current state"},
1052
+ "milestones": [
1053
+ {"action": "update", "label": "exact label", "status": "done|in_progress|pending|on_hold|superseded"},
1054
+ {"action": "add", "label": "new milestone", "status": "pending"}
1055
+ ],
1056
+ "panels": [
1057
+ {"action": "update", "label": "exact label", "value": "updated single sentence"},
1058
+ {"action": "add", "label": "New Panel", "icon": "emoji", "color": "sky|purple|rose|orange|emerald|amber|red|teal|blue|fuchsia|slate", "value": "one sentence"},
1059
+ {"action": "remove", "label": "Obsolete Panel"}
1060
+ ],
1061
+ "notable": [
1062
+ {"action": "add", "type": "breakthrough|insight|fix|error|blocker|decision|pivot|iteration|infrastructure|refinement|validation|correction", "text": "What happened and why it matters", "icon": "๐Ÿš€"}
1063
+ ],
1064
+ "recentActions": ["Action 1", "Action 2"],
1065
+ "tags": ["tag1", "tag2"]
1066
+ }
1067
+ }
1068
+
1069
+ Only include patches object keys that need updating. Omit unchanged fields (except currentState which should usually be updated).`;
1070
+ const patchGenStart = Date.now();
1071
+ const response = await this.withRetry('generateInsightsPatch', () => this.client.messages.create({
1072
+ model: this.models.fullPatch,
1073
+ max_tokens: 800,
1074
+ temperature: 0, // Lower temperature for more consistent currentState decisions
1075
+ messages: [{ role: 'user', content: prompt }]
1076
+ }));
1077
+ const patchGenTime = Date.now() - patchGenStart;
1078
+ const duration = Date.now() - startTime;
1079
+ const text = response.content[0]?.type === 'text' ? response.content[0].text : '';
1080
+ // Log cost for PATCH operation (full path)
1081
+ this.logCost(response, 'PATCH', this.models.fullPatch, duration);
1082
+ // Log for observability
1083
+ this.logger.info('Patch generation (Sonnet) completed', {
1084
+ durationMs: duration,
1085
+ inputTokens: response.usage?.input_tokens,
1086
+ outputTokens: response.usage?.output_tokens,
1087
+ activityCount: recentActivity.length,
1088
+ responseLength: text.length
1089
+ });
1090
+ try {
1091
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
1092
+ const result = JSON.parse(jsonMatch?.[0] || text);
1093
+ const patches = result.patches || {};
1094
+ // Log what's being patched for observability
1095
+ const patchedFields = Object.keys(patches).filter(k => patches[k] !== undefined);
1096
+ this.logger.info('Patch fields identified', {
1097
+ patchedFields,
1098
+ milestonesPatched: patches.milestones?.length || 0,
1099
+ panelsPatched: patches.panels?.length || 0,
1100
+ notableAdded: patches.notable?.length || 0
1101
+ });
1102
+ return {
1103
+ patches,
1104
+ reason: result.reason || 'Patches generated',
1105
+ propositions
1106
+ };
1107
+ }
1108
+ catch {
1109
+ this.logger.debug('Failed to parse patch response', { text });
1110
+ return { patches: {}, reason: 'Parse error - no patches applied', propositions };
1111
+ }
1112
+ }
1113
+ catch (error) {
1114
+ // Check for credit/billing errors and make them LOUD
1115
+ const errorMessage = error instanceof Error ? error.message : String(error);
1116
+ if (errorMessage.includes('credit balance') || errorMessage.includes('billing')) {
1117
+ this.logger.error('๐Ÿšจ ANTHROPIC API CREDITS EXHAUSTED ๐Ÿšจ', {
1118
+ errorMessage,
1119
+ action: 'ADD CREDITS AT https://console.anthropic.com/settings/billing',
1120
+ context: 'Patch generation failed due to billing issue'
1121
+ });
1122
+ }
1123
+ else {
1124
+ this.logger.error('Patch generation failed', { error });
1125
+ }
1126
+ return { patches: {}, reason: 'Generation error - no patches applied', propositions: [] };
1127
+ }
1128
+ }
1129
+ }
1130
+ // Export singleton instance
1131
+ export const anthropicService = new AnthropicService();
1132
+ //# sourceMappingURL=anthropic-service.js.map