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,909 @@
1
+ import * as pty from 'node-pty';
2
+ import { CUIError } from '../types/index.js';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+ import { EventEmitter } from 'events';
5
+ import { existsSync, readFileSync } from 'fs';
6
+ import { fileURLToPath } from 'url';
7
+ import { dirname } from 'path';
8
+ import os from 'os';
9
+ import { JsonLinesParser } from './json-lines-parser.js';
10
+ import { createLogger } from './logger.js';
11
+ import path from 'path';
12
+ /**
13
+ * Expand tilde (~) in paths to the user's home directory.
14
+ * Node.js spawn doesn't expand ~ like shells do, causing ENOENT errors.
15
+ */
16
+ function expandTilde(filePath) {
17
+ if (filePath === '~' || filePath.startsWith('~/')) {
18
+ return path.join(os.homedir(), filePath.slice(1));
19
+ }
20
+ return filePath;
21
+ }
22
+ // Get the directory of this module
23
+ const __filename = fileURLToPath(import.meta.url);
24
+ const __dirname = dirname(__filename);
25
+ /**
26
+ * Manages Claude CLI processes and their lifecycle
27
+ */
28
+ export class ClaudeProcessManager extends EventEmitter {
29
+ processes = new Map();
30
+ outputBuffers = new Map();
31
+ timeouts = new Map();
32
+ conversationConfigs = new Map();
33
+ sessionIds = new Map(); // streamingId -> claude session_id
34
+ claudeExecutablePath;
35
+ logger;
36
+ envOverrides;
37
+ historyReader;
38
+ mcpConfigPath;
39
+ statusTracker;
40
+ conversationStatusManager;
41
+ toolMetricsService;
42
+ sessionInfoService;
43
+ fileSystemService;
44
+ notificationService;
45
+ routerService;
46
+ killedProcesses = new Set(); // Track which processes we've killed
47
+ constructor(historyReader, statusTracker, claudeExecutablePath, envOverrides, toolMetricsService, sessionInfoService, fileSystemService) {
48
+ super();
49
+ this.historyReader = historyReader;
50
+ this.statusTracker = statusTracker;
51
+ this.claudeExecutablePath = claudeExecutablePath || this.findClaudeExecutable();
52
+ this.logger = createLogger('ClaudeProcessManager');
53
+ this.envOverrides = envOverrides || {};
54
+ this.toolMetricsService = toolMetricsService;
55
+ this.sessionInfoService = sessionInfoService;
56
+ this.fileSystemService = fileSystemService;
57
+ }
58
+ setRouterService(service) {
59
+ this.routerService = service;
60
+ }
61
+ /**
62
+ * Find the Claude executable - check user-local and system paths
63
+ */
64
+ findClaudeExecutable() {
65
+ // Check common installation paths including user-local installs
66
+ const userLocalPath = path.join(os.homedir(), '.local', 'bin', 'claude');
67
+ const candidatePaths = [
68
+ userLocalPath, // ~/.local/bin/claude (common for user installs)
69
+ '/usr/local/bin/claude',
70
+ '/usr/bin/claude',
71
+ ];
72
+ for (const candidate of candidatePaths) {
73
+ if (existsSync(candidate)) {
74
+ return candidate;
75
+ }
76
+ }
77
+ // Fallback: try PATH, excluding node_modules
78
+ const pathEnv = process.env.PATH || '';
79
+ const pathDirs = pathEnv.split(path.delimiter);
80
+ for (const dir of pathDirs) {
81
+ if (dir.includes('node_modules'))
82
+ continue; // Skip node_modules
83
+ const candidate = path.join(dir, 'claude');
84
+ if (existsSync(candidate)) {
85
+ return candidate;
86
+ }
87
+ }
88
+ throw new Error('Claude executable not found. Ensure Claude CLI is installed.');
89
+ }
90
+ /**
91
+ * Set the MCP config path to be used for all conversations
92
+ */
93
+ setMCPConfigPath(path) {
94
+ this.mcpConfigPath = path;
95
+ this.logger.debug('MCP config path set', { path });
96
+ }
97
+ /**
98
+ * Set the optimistic conversation service
99
+ */
100
+ setConversationStatusManager(service) {
101
+ this.conversationStatusManager = service;
102
+ this.logger.debug('Conversation status manager set');
103
+ }
104
+ /**
105
+ * Set the notification service
106
+ */
107
+ setNotificationService(service) {
108
+ this.notificationService = service;
109
+ this.logger.debug('Notification service set');
110
+ }
111
+ /**
112
+ * Start a new Claude conversation (or resume if resumedSessionId is provided)
113
+ */
114
+ async startConversation(config) {
115
+ const isResume = !!config.resumedSessionId;
116
+ // Guard against duplicate spawns for the same session
117
+ if (isResume && config.resumedSessionId && this.statusTracker.isSessionActive(config.resumedSessionId)) {
118
+ this.logger.warn('Attempted to resume already-active session', {
119
+ sessionId: config.resumedSessionId
120
+ });
121
+ throw new CUIError('SESSION_ALREADY_ACTIVE', `Cannot resume session ${config.resumedSessionId}: already has an active Claude process`, 409 // Conflict
122
+ );
123
+ }
124
+ this.logger.debug('Start conversation requested', {
125
+ hasInitialPrompt: !!config.initialPrompt,
126
+ promptLength: config.initialPrompt?.length,
127
+ workingDirectory: config.workingDirectory,
128
+ model: config.model,
129
+ allowedTools: config.allowedTools,
130
+ disallowedTools: config.disallowedTools,
131
+ hasSystemPrompt: !!config.systemPrompt,
132
+ claudePath: config.claudeExecutablePath || this.claudeExecutablePath,
133
+ isResume,
134
+ resumedSessionId: config.resumedSessionId,
135
+ previousMessageCount: config.previousMessages?.length || 0
136
+ });
137
+ // If resuming and no working directory provided, fetch from original session
138
+ let workingDirectory = config.workingDirectory;
139
+ if (isResume && !workingDirectory && config.resumedSessionId) {
140
+ const fetchedWorkingDirectory = await this.historyReader.getConversationWorkingDirectory(config.resumedSessionId);
141
+ if (!fetchedWorkingDirectory) {
142
+ throw new CUIError('CONVERSATION_NOT_FOUND', `Could not find working directory for session ${config.resumedSessionId}`, 404);
143
+ }
144
+ workingDirectory = fetchedWorkingDirectory;
145
+ this.logger.debug('Found working directory for resume session', {
146
+ sessionId: config.resumedSessionId,
147
+ workingDirectory
148
+ });
149
+ }
150
+ const args = isResume && config.resumedSessionId
151
+ ? this.buildResumeArgs({ sessionId: config.resumedSessionId, message: config.initialPrompt, permissionMode: config.permissionMode })
152
+ : this.buildStartArgs(config);
153
+ const spawnConfig = {
154
+ executablePath: config.claudeExecutablePath || this.claudeExecutablePath,
155
+ cwd: workingDirectory || config.workingDirectory || process.cwd(),
156
+ env: { ...process.env, ...this.envOverrides }
157
+ };
158
+ this.logger.debug('Spawn config prepared', {
159
+ executablePath: spawnConfig.executablePath,
160
+ cwd: spawnConfig.cwd,
161
+ hasEnvOverrides: Object.keys(this.envOverrides).length > 0,
162
+ envOverrideKeys: Object.keys(this.envOverrides),
163
+ isResume
164
+ });
165
+ return this.executeConversationFlow(isResume ? 'resuming' : 'starting', isResume && config.resumedSessionId ? { resumeSessionId: config.resumedSessionId } : {}, config, args, spawnConfig, isResume ? 'PROCESS_RESUME_FAILED' : 'PROCESS_START_FAILED', isResume ? 'Failed to resume Claude process' : 'Failed to start Claude process');
166
+ }
167
+ /**
168
+ * Stop a conversation gracefully (SIGTERM, then SIGKILL after 3s)
169
+ */
170
+ async stopConversation(streamingId) {
171
+ this.logger.debug('Stopping conversation', { streamingId });
172
+ const process = this.processes.get(streamingId);
173
+ if (!process) {
174
+ this.logger.warn('No process found for conversation', { streamingId });
175
+ return false;
176
+ }
177
+ try {
178
+ // Force kill if not already killed
179
+ if (!this.killedProcesses.has(streamingId)) {
180
+ this.logger.info('Sending SIGTERM to process', { streamingId, pid: process.pid });
181
+ process.kill('SIGTERM');
182
+ this.killedProcesses.add(streamingId);
183
+ // If SIGTERM doesn't work, use SIGKILL after 3 seconds
184
+ const killTimeout = setTimeout(() => {
185
+ try {
186
+ if (this.processes.has(streamingId)) {
187
+ this.logger.warn('Process not responding to SIGTERM, sending SIGKILL', { streamingId, pid: process.pid });
188
+ process.kill('SIGKILL');
189
+ }
190
+ }
191
+ catch {
192
+ // Process may have already exited, ignore
193
+ }
194
+ }, 3000);
195
+ // Track timeout for cleanup on process exit
196
+ const sessionTimeouts = this.timeouts.get(streamingId) || [];
197
+ sessionTimeouts.push(killTimeout);
198
+ this.timeouts.set(streamingId, sessionTimeouts);
199
+ }
200
+ // Note: Cleanup happens in handleProcessClose when process actually exits
201
+ // Don't clean up here - the process may still be running
202
+ this.logger.info('Stop signal sent to process', { streamingId });
203
+ return true;
204
+ }
205
+ catch (error) {
206
+ this.logger.error('Error stopping conversation', error, { streamingId });
207
+ return false;
208
+ }
209
+ }
210
+ /**
211
+ * Force kill a conversation immediately (SIGKILL)
212
+ */
213
+ forceKillConversation(streamingId) {
214
+ this.logger.info('Force killing conversation', { streamingId });
215
+ const process = this.processes.get(streamingId);
216
+ if (!process) {
217
+ this.logger.warn('No process found for force kill', { streamingId });
218
+ return false;
219
+ }
220
+ try {
221
+ this.logger.warn('Sending SIGKILL to process', { streamingId, pid: process.pid });
222
+ process.kill('SIGKILL');
223
+ this.killedProcesses.add(streamingId);
224
+ return true;
225
+ }
226
+ catch (error) {
227
+ this.logger.error('Error force killing conversation', error, { streamingId });
228
+ return false;
229
+ }
230
+ }
231
+ /**
232
+ * Get active sessions
233
+ */
234
+ getActiveSessions() {
235
+ const sessions = Array.from(this.processes.keys());
236
+ this.logger.debug('Getting active sessions', { sessionCount: sessions.length });
237
+ return sessions;
238
+ }
239
+ /**
240
+ * Check if a session is active
241
+ */
242
+ isSessionActive(streamingId) {
243
+ const active = this.processes.has(streamingId);
244
+ return active;
245
+ }
246
+ /**
247
+ * Wait for the system init message from Claude CLI
248
+ * This should always be the first message in the stream
249
+ */
250
+ async waitForSystemInit(streamingId) {
251
+ this.logger.debug('Waiting for system init message', { streamingId });
252
+ return new Promise((resolve, reject) => {
253
+ let isResolved = false;
254
+ let stderrOutput = '';
255
+ // Set up timeout (1 minute)
256
+ const timeout = setTimeout(() => {
257
+ if (!isResolved) {
258
+ isResolved = true;
259
+ cleanup();
260
+ this.logger.error('Timeout waiting for system init message', {
261
+ streamingId,
262
+ stderrOutput: stderrOutput || '(no stderr output)'
263
+ });
264
+ // Include stderr output in error message if available
265
+ let errorMessage = 'Timeout waiting for system initialization from Claude CLI';
266
+ if (stderrOutput) {
267
+ errorMessage += `. Error output: ${stderrOutput}`;
268
+ }
269
+ reject(new CUIError('SYSTEM_INIT_TIMEOUT', errorMessage, 500));
270
+ }
271
+ }, 60000);
272
+ // Cleanup function to remove all listeners
273
+ const cleanup = () => {
274
+ clearTimeout(timeout);
275
+ this.removeListener('claude-message', messageHandler);
276
+ this.removeListener('process-closed', processClosedHandler);
277
+ this.removeListener('process-error', processErrorHandler);
278
+ };
279
+ // Register timeout for cleanup on process termination
280
+ const existingTimeouts = this.timeouts.get(streamingId) || [];
281
+ existingTimeouts.push(timeout);
282
+ this.timeouts.set(streamingId, existingTimeouts);
283
+ // Listen for process exit before system init is received
284
+ const processClosedHandler = ({ streamingId: closedStreamingId, code }) => {
285
+ if (closedStreamingId !== streamingId || isResolved) {
286
+ return; // Not our process or already resolved
287
+ }
288
+ isResolved = true;
289
+ cleanup();
290
+ this.logger.error('Claude process exited before system init message', {
291
+ streamingId,
292
+ exitCode: code,
293
+ stderrOutput: stderrOutput || '(no stderr output)'
294
+ });
295
+ // Create error message with Claude CLI output if available
296
+ let errorMessage = 'Claude CLI process exited before sending system initialization message';
297
+ if (stderrOutput) {
298
+ // Extract Claude CLI's actual output from parser errors
299
+ const claudeOutputMatch = stderrOutput.match(/Invalid JSON: (.+)/);
300
+ if (claudeOutputMatch) {
301
+ errorMessage += `. Claude CLI said: "${claudeOutputMatch[1]}"`;
302
+ }
303
+ else {
304
+ errorMessage += `. Error output: ${stderrOutput}`;
305
+ }
306
+ }
307
+ if (code !== null) {
308
+ errorMessage += `. Exit code: ${code}`;
309
+ }
310
+ reject(new CUIError('CLAUDE_PROCESS_EXITED_EARLY', errorMessage, 500));
311
+ };
312
+ // Listen for process errors (including stderr output)
313
+ const processErrorHandler = ({ streamingId: errorStreamingId, error }) => {
314
+ if (errorStreamingId !== streamingId) {
315
+ return; // Not our process
316
+ }
317
+ // Capture stderr output for error context
318
+ stderrOutput += error;
319
+ this.logger.debug('Captured stderr output during system init wait', {
320
+ streamingId,
321
+ errorLength: error.length,
322
+ totalStderrLength: stderrOutput.length
323
+ });
324
+ };
325
+ // Listen for the first claude-message event for this streamingId
326
+ const messageHandler = ({ streamingId: msgStreamingId, message }) => {
327
+ if (msgStreamingId !== streamingId) {
328
+ return; // Not for our session
329
+ }
330
+ if (isResolved) {
331
+ return; // Already resolved
332
+ }
333
+ this.logger.debug('Received message from Claude CLI during init wait', {
334
+ streamingId,
335
+ messageType: message?.type,
336
+ messageSubtype: 'subtype' in message ? message.subtype : undefined,
337
+ hasSessionId: 'session_id' in message ? !!message.session_id : false
338
+ });
339
+ // Skip hook_response messages (Claude v2 sends these before init)
340
+ // Cast to unknown string to compare - StreamEvent type doesn't include all system subtypes
341
+ if (message?.type === 'system' && 'subtype' in message && message.subtype === 'hook_response') {
342
+ this.logger.debug('Skipping hook_response message, waiting for init', { streamingId });
343
+ return; // Keep waiting for init message
344
+ }
345
+ // Now we have a non-hook message, mark as resolved
346
+ isResolved = true;
347
+ cleanup();
348
+ // Validate that this message is a system init message
349
+ if (!message || message.type !== 'system' || !('subtype' in message) || message.subtype !== 'init') {
350
+ this.logger.error('Expected system init message', {
351
+ streamingId,
352
+ actualType: message?.type,
353
+ actualSubtype: 'subtype' in message ? message.subtype : undefined,
354
+ expectedType: 'system',
355
+ expectedSubtype: 'init'
356
+ });
357
+ reject(new CUIError('INVALID_SYSTEM_INIT', `Expected system init message, but got: ${message?.type}/${'subtype' in message ? message.subtype : 'undefined'}`, 500));
358
+ return;
359
+ }
360
+ // At this point, TypeScript knows message is SystemInitMessage
361
+ const systemInitMessage = message;
362
+ // Validate required fields
363
+ const requiredFields = ['session_id', 'cwd', 'tools', 'mcp_servers', 'model', 'permissionMode', 'apiKeySource'];
364
+ const missingFields = requiredFields.filter(field => systemInitMessage[field] === undefined);
365
+ if (missingFields.length > 0) {
366
+ this.logger.error('System init message missing required fields', {
367
+ streamingId,
368
+ missingFields,
369
+ availableFields: Object.keys(systemInitMessage)
370
+ });
371
+ reject(new CUIError('INCOMPLETE_SYSTEM_INIT', `System init message missing required fields: ${missingFields.join(', ')}`, 500));
372
+ return;
373
+ }
374
+ this.logger.debug('Successfully received valid system init message', {
375
+ streamingId,
376
+ sessionId: systemInitMessage.session_id,
377
+ cwd: systemInitMessage.cwd,
378
+ model: systemInitMessage.model,
379
+ toolCount: systemInitMessage.tools?.length || 0,
380
+ mcpServerCount: systemInitMessage.mcp_servers?.length || 0
381
+ });
382
+ // Register active session immediately when we have the session_id
383
+ // Include optimistic context if available
384
+ const config = this.conversationConfigs.get(streamingId);
385
+ if (this.conversationStatusManager && config) {
386
+ const optimisticContext = {
387
+ initialPrompt: config.initialPrompt || '',
388
+ workingDirectory: config.workingDirectory || process.cwd(),
389
+ model: config.model || 'default',
390
+ timestamp: new Date().toISOString(),
391
+ inheritedMessages: config.previousMessages
392
+ };
393
+ this.conversationStatusManager.registerActiveSession(streamingId, systemInitMessage.session_id, optimisticContext);
394
+ this.logger.debug('Registered conversation context', {
395
+ streamingId,
396
+ claudeSessionId: systemInitMessage.session_id,
397
+ inheritedMessageCount: config.previousMessages?.length || 0
398
+ });
399
+ }
400
+ else {
401
+ // Fallback to old behavior if service not set
402
+ this.statusTracker.registerActiveSession(streamingId, systemInitMessage.session_id);
403
+ this.logger.debug('Registered active session with status tracker (no optimistic service)', {
404
+ streamingId,
405
+ claudeSessionId: systemInitMessage.session_id
406
+ });
407
+ }
408
+ resolve(systemInitMessage);
409
+ };
410
+ // Set up all event listeners
411
+ this.on('claude-message', messageHandler);
412
+ this.on('process-closed', processClosedHandler);
413
+ this.on('process-error', processErrorHandler);
414
+ });
415
+ }
416
+ /**
417
+ * Execute common conversation flow for both start and resume operations
418
+ */
419
+ async executeConversationFlow(operation, loggerContext, config, args, spawnConfig, errorCode, errorPrefix) {
420
+ const streamingId = uuidv4(); // CUI's internal streaming identifier
421
+ // Store config for use in waitForSystemInit
422
+ this.conversationConfigs.set(streamingId, config);
423
+ try {
424
+ // Validate Claude executable before proceeding
425
+ if (this.fileSystemService) {
426
+ await this.fileSystemService.validateExecutable(spawnConfig.executablePath);
427
+ }
428
+ this.logger.debug(`${operation.charAt(0).toUpperCase() + operation.slice(1)} conversation`, {
429
+ streamingId,
430
+ operation,
431
+ configKeys: Object.keys(config),
432
+ argCount: args.length,
433
+ ...loggerContext
434
+ });
435
+ this.logger.debug(`Built Claude ${operation} args`, {
436
+ streamingId,
437
+ args,
438
+ argsString: args.join(' '),
439
+ ...loggerContext
440
+ });
441
+ // Set up system init promise before spawning process
442
+ const systemInitPromise = this.waitForSystemInit(streamingId);
443
+ // Add streamingId to environment for MCP server to use
444
+ // Filter out debugging-related environment variables that would cause
445
+ // the VSCode debugger to attach to the Claude CLI child process
446
+ // Also filter CLAUDECODE to prevent nested session detection issues
447
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
448
+ const { NODE_OPTIONS, VSCODE_INSPECTOR_OPTIONS, CLAUDECODE, CLAUDE_CODE_ENTRYPOINT, ...cleanEnv } = spawnConfig.env;
449
+ // Expand tilde to actual home directory path
450
+ const expandedCwd = expandTilde(spawnConfig.cwd);
451
+ const envWithStreamingId = {
452
+ ...cleanEnv,
453
+ CUI_STREAMING_ID: streamingId,
454
+ PWD: expandedCwd,
455
+ INIT_CWD: expandedCwd
456
+ };
457
+ const process = this.spawnProcess({ ...spawnConfig, env: envWithStreamingId }, args, streamingId);
458
+ this.processes.set(streamingId, process);
459
+ this.setupProcessHandlers(streamingId, process);
460
+ // Message is now passed as CLI arg (not via stdin)
461
+ // --input-format stream-json was removed because it causes Claude to wait for input
462
+ // without sending system init message first
463
+ // Handle spawn errors by listening for our custom event
464
+ const spawnErrorPromise = new Promise((_, reject) => {
465
+ this.once('spawn-error', (error) => {
466
+ this.processes.delete(streamingId);
467
+ reject(error);
468
+ });
469
+ });
470
+ // Wait a bit to see if spawn fails immediately
471
+ this.logger.debug('Waiting for spawn validation', { streamingId, ...loggerContext });
472
+ const delayPromise = new Promise(resolve => {
473
+ setTimeout(() => {
474
+ this.logger.debug('Spawn validation period passed, process appears stable', { streamingId, ...loggerContext });
475
+ this.removeAllListeners('spawn-error');
476
+ resolve(streamingId);
477
+ }, 100);
478
+ });
479
+ await Promise.race([spawnErrorPromise, delayPromise]);
480
+ // Now wait for the system init message
481
+ this.logger.debug('Process spawned successfully, waiting for system init message', { streamingId, ...loggerContext });
482
+ const systemInit = await systemInitPromise;
483
+ // Check if cwd is a git repository and set initial_commit_head for new session
484
+ if (this.sessionInfoService && this.fileSystemService) {
485
+ try {
486
+ if (await this.fileSystemService.isGitRepository(systemInit.cwd)) {
487
+ const gitHead = await this.fileSystemService.getCurrentGitHead(systemInit.cwd);
488
+ if (gitHead) {
489
+ await this.sessionInfoService.updateSessionInfo(systemInit.session_id, {
490
+ initial_commit_head: gitHead
491
+ });
492
+ this.logger.debug('Set initial commit head for new session', {
493
+ sessionId: systemInit.session_id,
494
+ gitHead
495
+ });
496
+ }
497
+ }
498
+ }
499
+ catch (error) {
500
+ this.logger.warn('Failed to set initial commit head for new session', {
501
+ sessionId: systemInit.session_id,
502
+ cwd: systemInit.cwd,
503
+ error: error instanceof Error ? error.message : String(error)
504
+ });
505
+ }
506
+ }
507
+ this.logger.debug(`${operation.charAt(0).toUpperCase() + operation.slice(1)} conversation successfully`, {
508
+ streamingId,
509
+ sessionId: systemInit.session_id,
510
+ model: systemInit.model,
511
+ cwd: systemInit.cwd,
512
+ processCount: this.processes.size,
513
+ ...loggerContext
514
+ });
515
+ return { streamingId, systemInit };
516
+ }
517
+ catch (error) {
518
+ this.logger.error(`Error ${operation} conversation`, error, {
519
+ streamingId,
520
+ errorName: error instanceof Error ? error.name : 'Unknown',
521
+ errorMessage: error instanceof Error ? error.message : String(error),
522
+ errorCode: error instanceof CUIError ? error.code : undefined,
523
+ ...loggerContext
524
+ });
525
+ // Clean up any resources if process fails
526
+ const timeouts = this.timeouts.get(streamingId);
527
+ if (timeouts) {
528
+ timeouts.forEach(timeout => clearTimeout(timeout));
529
+ this.timeouts.delete(streamingId);
530
+ }
531
+ this.processes.delete(streamingId);
532
+ this.outputBuffers.delete(streamingId);
533
+ this.conversationConfigs.delete(streamingId);
534
+ if (error instanceof CUIError) {
535
+ throw error;
536
+ }
537
+ throw new CUIError(errorCode, `${errorPrefix}: ${error}`, 500);
538
+ }
539
+ }
540
+ buildBaseArgs() {
541
+ return [
542
+ '-p', // Print mode - required for programmatic use
543
+ ];
544
+ }
545
+ buildResumeArgs(config) {
546
+ this.logger.debug('Building Claude resume args', {
547
+ sessionId: config.sessionId,
548
+ messagePreview: config.message.substring(0, 50) + (config.message.length > 50 ? '...' : '')
549
+ });
550
+ const args = this.buildBaseArgs();
551
+ // Add message as CLI arg - same as start
552
+ // NOTE: For very long messages, this may hit CLI arg length limits
553
+ args.push(config.message);
554
+ args.push('--resume', config.sessionId, '--output-format', 'stream-json', '--input-format', 'stream-json', // Enable stdin for AskUserQuestion responses
555
+ '--verbose');
556
+ // Always use dangerously bypass permissions
557
+ args.push('--dangerously-skip-permissions');
558
+ // NOTE: MCP config disabled for resume - it causes Claude to hang waiting for MCP server init
559
+ // The MCP permission server creates a circular dependency when spawned during resume
560
+ // Since we use --dangerously-skip-permissions, we don't need the permission prompt tool anyway
561
+ this.logger.debug('Built Claude resume args', { args, hasMCPConfig: !!this.mcpConfigPath });
562
+ return args;
563
+ }
564
+ buildStartArgs(config) {
565
+ this.logger.debug('Building Claude start args', {
566
+ hasInitialPrompt: !!config.initialPrompt,
567
+ promptPreview: config.initialPrompt ? config.initialPrompt.substring(0, 50) + (config.initialPrompt.length > 50 ? '...' : '') : null,
568
+ workingDirectory: config.workingDirectory,
569
+ model: config.model
570
+ });
571
+ const args = this.buildBaseArgs();
572
+ // Add initial prompt immediately after -p
573
+ if (config.initialPrompt) {
574
+ args.push(config.initialPrompt);
575
+ }
576
+ args.push('--output-format', 'stream-json', // JSONL output format
577
+ '--input-format', 'stream-json', // Enable stdin for AskUserQuestion responses
578
+ '--verbose' // Required when using stream-json with print mode
579
+ );
580
+ // Add working directory access
581
+ // if (config.workingDirectory) {
582
+ // args.push('--add-dir', config.workingDirectory);
583
+ // }
584
+ // Add model specification
585
+ if (config.model) {
586
+ args.push('--model', config.model);
587
+ }
588
+ // Add allowed tools
589
+ if (config.allowedTools && config.allowedTools.length > 0) {
590
+ args.push('--allowedTools', config.allowedTools.join(','));
591
+ }
592
+ // Add disallowed tools
593
+ if (config.disallowedTools && config.disallowedTools.length > 0) {
594
+ args.push('--disallowedTools', config.disallowedTools.join(','));
595
+ }
596
+ // Add system prompt with Claudia environment info
597
+ const claudiaEnvInfo = `
598
+ ## Claudia Orchestrator Environment
599
+
600
+ You are running inside the Claudia orchestrator dashboard.
601
+
602
+ **Server logs** (for debugging issues):
603
+ - \`~/.claudia/logs/server.log\` - Express server logs
604
+ - \`~/.claudia/logs/daemon.log\` - Process daemon logs
605
+ `;
606
+ const systemPrompt = config.systemPrompt
607
+ ? `${config.systemPrompt}\n${claudiaEnvInfo}`
608
+ : claudiaEnvInfo;
609
+ args.push('--system-prompt', systemPrompt);
610
+ // Add MCP config if available
611
+ if (this.mcpConfigPath) {
612
+ args.push('--mcp-config', this.mcpConfigPath);
613
+ }
614
+ // Always use dangerously bypass permissions
615
+ // Note: When using --dangerously-skip-permissions, we don't need the MCP permission prompt tool
616
+ // Adding --permission-prompt-tool with skip-permissions causes startup failures if MCP server is slow
617
+ args.push('--dangerously-skip-permissions');
618
+ this.logger.debug('Built Claude args', { args, hasMCPConfig: !!this.mcpConfigPath });
619
+ return args;
620
+ }
621
+ /**
622
+ * Consolidated method to spawn Claude processes for both start and resume operations
623
+ * Uses node-pty to provide a pseudo-terminal, which is required for Claude CLI -p mode
624
+ */
625
+ spawnProcess(spawnConfig, args, streamingId) {
626
+ const { executablePath } = spawnConfig;
627
+ // Expand tilde to actual home directory - Node.js spawn doesn't do shell expansion
628
+ const cwd = expandTilde(spawnConfig.cwd);
629
+ let { env } = spawnConfig;
630
+ // Inject router proxy if enabled
631
+ if (this.routerService?.isEnabled()) {
632
+ env = {
633
+ ...env,
634
+ ANTHROPIC_BASE_URL: this.routerService.getProxyUrl(),
635
+ ANTHROPIC_API_KEY: 'router-managed'
636
+ };
637
+ this.logger.info('Using router proxy', {
638
+ streamingId,
639
+ proxyUrl: this.routerService.getProxyUrl()
640
+ });
641
+ }
642
+ // Check if MCP config is in args and validate it
643
+ const mcpConfigIndex = args.indexOf('--mcp-config');
644
+ if (mcpConfigIndex !== -1 && mcpConfigIndex + 1 < args.length) {
645
+ const mcpConfigPath = args[mcpConfigIndex + 1];
646
+ this.logger.debug('MCP config specified', {
647
+ streamingId,
648
+ mcpConfigPath,
649
+ exists: existsSync(mcpConfigPath)
650
+ });
651
+ // Try to read and log the MCP config content
652
+ try {
653
+ const mcpConfigContent = readFileSync(mcpConfigPath, 'utf-8');
654
+ this.logger.debug('MCP config content', {
655
+ streamingId,
656
+ mcpConfig: JSON.parse(mcpConfigContent)
657
+ });
658
+ }
659
+ catch (error) {
660
+ this.logger.error('Failed to read MCP config', { streamingId, error });
661
+ }
662
+ }
663
+ this.logger.debug('Spawning Claude process with PTY', {
664
+ streamingId,
665
+ executablePath,
666
+ args,
667
+ cwd,
668
+ PATH: env.PATH,
669
+ nodeVersion: process.version,
670
+ platform: process.platform
671
+ });
672
+ try {
673
+ // Log the exact command for debugging
674
+ const fullCommand = `${executablePath} ${args.join(' ')}`;
675
+ this.logger.info('SPAWNING CLAUDE COMMAND (PTY): ' + fullCommand, {
676
+ streamingId,
677
+ fullCommand,
678
+ executablePath,
679
+ args,
680
+ cwd,
681
+ env: Object.entries(env).reduce((acc, [key, value]) => {
682
+ acc[key] = value;
683
+ return acc;
684
+ }, {})
685
+ });
686
+ // Use node-pty to spawn with a pseudo-terminal
687
+ // This is required because Claude CLI -p mode only outputs when connected to a TTY
688
+ const claudeProcess = pty.spawn(executablePath, args, {
689
+ name: 'xterm-256color',
690
+ cols: 200,
691
+ rows: 50,
692
+ cwd,
693
+ env: env
694
+ });
695
+ if (!claudeProcess.pid) {
696
+ this.logger.error('Failed to spawn Claude process - no PID assigned', {
697
+ streamingId
698
+ });
699
+ throw new Error('Failed to spawn Claude process - no PID assigned');
700
+ }
701
+ this.logger.info('Claude process spawned successfully (PTY)', {
702
+ streamingId,
703
+ pid: claudeProcess.pid
704
+ });
705
+ return claudeProcess;
706
+ }
707
+ catch (error) {
708
+ this.logger.error('Error in spawnProcess', error, { streamingId });
709
+ if (error instanceof CUIError) {
710
+ throw error;
711
+ }
712
+ const errorMessage = error instanceof Error ? error.message : String(error);
713
+ if (errorMessage.includes('ENOENT') || errorMessage.includes('not found')) {
714
+ throw new CUIError('CLAUDE_NOT_FOUND', 'Claude CLI not found. Please ensure Claude is installed and in PATH.', 500);
715
+ }
716
+ throw new CUIError('PROCESS_SPAWN_FAILED', `Failed to spawn Claude process: ${errorMessage}`, 500);
717
+ }
718
+ }
719
+ setupProcessHandlers(streamingId, process) {
720
+ this.logger.debug('Setting up PTY process handlers', { streamingId, pid: process.pid });
721
+ // Create JSONL parser for Claude output
722
+ const parser = new JsonLinesParser();
723
+ // Initialize output buffer for this session
724
+ this.outputBuffers.set(streamingId, '');
725
+ // Handle parsed JSONL messages from Claude
726
+ parser.on('data', (message) => {
727
+ // Skip echoed stdin messages - PTY echoes our input back
728
+ // BUT: tool_result messages are also type: 'user' (Claude API protocol:
729
+ // user = input TO Claude, assistant = output FROM Claude)
730
+ // We need to let tool_result messages through so the UI can update tool status
731
+ if (message?.type === 'user') {
732
+ const content = message.message?.content;
733
+ const isToolResult = Array.isArray(content) &&
734
+ content.some((block) => block.type === 'tool_result');
735
+ if (!isToolResult) {
736
+ this.logger.debug('Skipping echoed stdin message', {
737
+ streamingId,
738
+ messageType: message?.type
739
+ });
740
+ return;
741
+ }
742
+ // Fall through to emit tool_result messages
743
+ this.logger.debug('Allowing tool_result message through', {
744
+ streamingId,
745
+ toolResultCount: content.filter((b) => b.type === 'tool_result').length
746
+ });
747
+ }
748
+ this.logger.debug('Received Claude message', {
749
+ streamingId,
750
+ messageType: message?.type,
751
+ hasContent: !!message?.content,
752
+ contentLength: message?.content?.length,
753
+ messageKeys: message ? Object.keys(message) : [],
754
+ timestamp: new Date().toISOString()
755
+ });
756
+ this.handleClaudeMessage(streamingId, message);
757
+ });
758
+ parser.on('error', (error) => {
759
+ this.logger.error('Parser error', error, {
760
+ streamingId,
761
+ errorType: error.name,
762
+ errorMessage: error.message,
763
+ bufferState: this.outputBuffers.get(streamingId)?.length || 0
764
+ });
765
+ this.handleProcessError(streamingId, error);
766
+ });
767
+ // Handle PTY data output (combined stdout/stderr from the pseudo-terminal)
768
+ // PTY combines both streams, so we need to parse JSONL from the raw output
769
+ process.onData((data) => {
770
+ // Log raw PTY data for debugging
771
+ this.logger.info('RAW PTY DATA', {
772
+ streamingId,
773
+ dataLength: data.length,
774
+ dataPreview: data.substring(0, 200)
775
+ });
776
+ // Strip ANSI escape sequences that might interfere with JSON parsing
777
+ // Common PTY escape sequences: cursor control, colors, etc.
778
+ const cleanedData = data.replace(/\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07/g, '');
779
+ // Feed cleaned data to the JSONL parser
780
+ if (cleanedData.trim()) {
781
+ parser.write(cleanedData);
782
+ }
783
+ });
784
+ // Handle PTY process exit (node-pty uses onExit, not EventEmitter 'exit')
785
+ process.onExit(({ exitCode, signal }) => {
786
+ this.logger.debug('PTY process exited', {
787
+ streamingId,
788
+ exitCode,
789
+ signal,
790
+ normalExit: exitCode === 0,
791
+ timestamp: new Date().toISOString(),
792
+ outputBuffer: this.outputBuffers.get(streamingId) || 'No output captured'
793
+ });
794
+ this.handleProcessClose(streamingId, exitCode);
795
+ });
796
+ }
797
+ handleClaudeMessage(streamingId, message) {
798
+ this.logger.debug('Handling Claude message', {
799
+ streamingId,
800
+ messageType: message?.type,
801
+ isError: message?.type === 'error',
802
+ isResult: message?.type === 'result'
803
+ });
804
+ this.emit('claude-message', { streamingId, message });
805
+ // When we receive a 'result' message, the conversation turn is complete.
806
+ // For PTY, we don't need to close stdin - the process will exit naturally after result.
807
+ // The PTY's onExit handler will clean up.
808
+ if (message?.type === 'result') {
809
+ this.logger.debug('Received result message, process will exit naturally', {
810
+ streamingId,
811
+ resultSubtype: 'subtype' in message ? message.subtype : undefined
812
+ });
813
+ }
814
+ }
815
+ handleProcessClose(streamingId, code) {
816
+ // Clear any pending timeouts for this session
817
+ const timeouts = this.timeouts.get(streamingId);
818
+ if (timeouts) {
819
+ timeouts.forEach(timeout => clearTimeout(timeout));
820
+ this.timeouts.delete(streamingId);
821
+ }
822
+ this.processes.delete(streamingId);
823
+ this.outputBuffers.delete(streamingId);
824
+ const config = this.conversationConfigs.get(streamingId);
825
+ this.conversationConfigs.delete(streamingId);
826
+ // Send notification if service is available
827
+ if (this.notificationService && config) {
828
+ // Get session ID from conversation status or config
829
+ const sessionId = this.statusTracker.getSessionId(streamingId) || 'unknown';
830
+ // Try to get conversation metadata for summary
831
+ this.historyReader.fetchConversationDirect(sessionId)
832
+ .then(({ metadata }) => {
833
+ if (this.notificationService && metadata) {
834
+ return this.notificationService.sendConversationEndNotification(streamingId, sessionId, metadata.summary);
835
+ }
836
+ })
837
+ .catch((error) => {
838
+ this.logger.error('Failed to send conversation end notification', error);
839
+ });
840
+ }
841
+ this.emit('process-closed', { streamingId, code });
842
+ }
843
+ handleProcessError(streamingId, error) {
844
+ const errorMessage = error.toString();
845
+ const isBuffer = Buffer.isBuffer(error);
846
+ this.logger.error('Process error occurred', {
847
+ streamingId,
848
+ error: errorMessage,
849
+ errorType: isBuffer ? 'stderr-output' : error.constructor.name,
850
+ errorLength: errorMessage.length,
851
+ processStillActive: this.processes.has(streamingId),
852
+ timestamp: new Date().toISOString()
853
+ });
854
+ this.emit('process-error', { streamingId, error: errorMessage });
855
+ }
856
+ /**
857
+ * Send an answer to an AskUserQuestion tool via stdin.
858
+ * The Claude CLI with --input-format stream-json expects JSON messages on stdin.
859
+ */
860
+ sendQuestionAnswer(streamingId, answer) {
861
+ const process = this.processes.get(streamingId);
862
+ if (!process) {
863
+ this.logger.warn('Cannot send question answer: process not found', { streamingId });
864
+ return false;
865
+ }
866
+ const sessionId = this.sessionIds.get(streamingId);
867
+ if (!sessionId) {
868
+ this.logger.warn('Cannot send question answer: session ID not found', { streamingId });
869
+ return false;
870
+ }
871
+ // Format the stdin message per Claude CLI stream-json protocol
872
+ const stdinMessage = JSON.stringify({
873
+ type: 'user',
874
+ message: {
875
+ role: 'user',
876
+ content: answer
877
+ },
878
+ session_id: sessionId
879
+ });
880
+ this.logger.info('Sending question answer via stdin', {
881
+ streamingId,
882
+ sessionId,
883
+ answerLength: answer.length
884
+ });
885
+ // Write to PTY stdin with newline
886
+ process.write(stdinMessage + '\n');
887
+ return true;
888
+ }
889
+ /**
890
+ * Send a raw stdin message to a Claude process.
891
+ * Used by ClaudiaService for sending messages to Claudia's session.
892
+ * The message should already be formatted as a JSON string.
893
+ */
894
+ sendStdinMessage(streamingId, message) {
895
+ const process = this.processes.get(streamingId);
896
+ if (!process) {
897
+ this.logger.warn('Cannot send stdin message: process not found', { streamingId });
898
+ return false;
899
+ }
900
+ this.logger.info('Sending stdin message', {
901
+ streamingId,
902
+ messageLength: message.length
903
+ });
904
+ // Write to PTY stdin with newline
905
+ process.write(message + '\n');
906
+ return true;
907
+ }
908
+ }
909
+ //# sourceMappingURL=claude-process-manager.js.map