mstro-app 0.1.47

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 (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +177 -0
  3. package/bin/commands/config.js +145 -0
  4. package/bin/commands/login.js +313 -0
  5. package/bin/commands/logout.js +75 -0
  6. package/bin/commands/status.js +197 -0
  7. package/bin/commands/whoami.js +161 -0
  8. package/bin/configure-claude.js +298 -0
  9. package/bin/mstro.js +581 -0
  10. package/bin/postinstall.js +45 -0
  11. package/bin/release.sh +110 -0
  12. package/dist/server/cli/headless/claude-invoker.d.ts +17 -0
  13. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -0
  14. package/dist/server/cli/headless/claude-invoker.js +311 -0
  15. package/dist/server/cli/headless/claude-invoker.js.map +1 -0
  16. package/dist/server/cli/headless/index.d.ts +13 -0
  17. package/dist/server/cli/headless/index.d.ts.map +1 -0
  18. package/dist/server/cli/headless/index.js +10 -0
  19. package/dist/server/cli/headless/index.js.map +1 -0
  20. package/dist/server/cli/headless/mcp-config.d.ts +11 -0
  21. package/dist/server/cli/headless/mcp-config.d.ts.map +1 -0
  22. package/dist/server/cli/headless/mcp-config.js +76 -0
  23. package/dist/server/cli/headless/mcp-config.js.map +1 -0
  24. package/dist/server/cli/headless/output-utils.d.ts +33 -0
  25. package/dist/server/cli/headless/output-utils.d.ts.map +1 -0
  26. package/dist/server/cli/headless/output-utils.js +101 -0
  27. package/dist/server/cli/headless/output-utils.js.map +1 -0
  28. package/dist/server/cli/headless/prompt-utils.d.ts +21 -0
  29. package/dist/server/cli/headless/prompt-utils.d.ts.map +1 -0
  30. package/dist/server/cli/headless/prompt-utils.js +84 -0
  31. package/dist/server/cli/headless/prompt-utils.js.map +1 -0
  32. package/dist/server/cli/headless/runner.d.ts +24 -0
  33. package/dist/server/cli/headless/runner.d.ts.map +1 -0
  34. package/dist/server/cli/headless/runner.js +99 -0
  35. package/dist/server/cli/headless/runner.js.map +1 -0
  36. package/dist/server/cli/headless/types.d.ts +106 -0
  37. package/dist/server/cli/headless/types.d.ts.map +1 -0
  38. package/dist/server/cli/headless/types.js +4 -0
  39. package/dist/server/cli/headless/types.js.map +1 -0
  40. package/dist/server/cli/improvisation-session-manager.d.ts +155 -0
  41. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -0
  42. package/dist/server/cli/improvisation-session-manager.js +415 -0
  43. package/dist/server/cli/improvisation-session-manager.js.map +1 -0
  44. package/dist/server/index.d.ts +2 -0
  45. package/dist/server/index.d.ts.map +1 -0
  46. package/dist/server/index.js +386 -0
  47. package/dist/server/index.js.map +1 -0
  48. package/dist/server/mcp/bouncer-cli.d.ts +3 -0
  49. package/dist/server/mcp/bouncer-cli.d.ts.map +1 -0
  50. package/dist/server/mcp/bouncer-cli.js +99 -0
  51. package/dist/server/mcp/bouncer-cli.js.map +1 -0
  52. package/dist/server/mcp/bouncer-integration.d.ts +36 -0
  53. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -0
  54. package/dist/server/mcp/bouncer-integration.js +301 -0
  55. package/dist/server/mcp/bouncer-integration.js.map +1 -0
  56. package/dist/server/mcp/security-audit.d.ts +52 -0
  57. package/dist/server/mcp/security-audit.d.ts.map +1 -0
  58. package/dist/server/mcp/security-audit.js +118 -0
  59. package/dist/server/mcp/security-audit.js.map +1 -0
  60. package/dist/server/mcp/security-patterns.d.ts +73 -0
  61. package/dist/server/mcp/security-patterns.d.ts.map +1 -0
  62. package/dist/server/mcp/security-patterns.js +247 -0
  63. package/dist/server/mcp/security-patterns.js.map +1 -0
  64. package/dist/server/mcp/server.d.ts +3 -0
  65. package/dist/server/mcp/server.d.ts.map +1 -0
  66. package/dist/server/mcp/server.js +146 -0
  67. package/dist/server/mcp/server.js.map +1 -0
  68. package/dist/server/routes/files.d.ts +9 -0
  69. package/dist/server/routes/files.d.ts.map +1 -0
  70. package/dist/server/routes/files.js +24 -0
  71. package/dist/server/routes/files.js.map +1 -0
  72. package/dist/server/routes/improvise.d.ts +3 -0
  73. package/dist/server/routes/improvise.d.ts.map +1 -0
  74. package/dist/server/routes/improvise.js +72 -0
  75. package/dist/server/routes/improvise.js.map +1 -0
  76. package/dist/server/routes/index.d.ts +10 -0
  77. package/dist/server/routes/index.d.ts.map +1 -0
  78. package/dist/server/routes/index.js +12 -0
  79. package/dist/server/routes/index.js.map +1 -0
  80. package/dist/server/routes/instances.d.ts +10 -0
  81. package/dist/server/routes/instances.d.ts.map +1 -0
  82. package/dist/server/routes/instances.js +47 -0
  83. package/dist/server/routes/instances.js.map +1 -0
  84. package/dist/server/routes/notifications.d.ts +3 -0
  85. package/dist/server/routes/notifications.d.ts.map +1 -0
  86. package/dist/server/routes/notifications.js +136 -0
  87. package/dist/server/routes/notifications.js.map +1 -0
  88. package/dist/server/services/analytics.d.ts +56 -0
  89. package/dist/server/services/analytics.d.ts.map +1 -0
  90. package/dist/server/services/analytics.js +240 -0
  91. package/dist/server/services/analytics.js.map +1 -0
  92. package/dist/server/services/auth.d.ts +26 -0
  93. package/dist/server/services/auth.d.ts.map +1 -0
  94. package/dist/server/services/auth.js +71 -0
  95. package/dist/server/services/auth.js.map +1 -0
  96. package/dist/server/services/client-id.d.ts +10 -0
  97. package/dist/server/services/client-id.d.ts.map +1 -0
  98. package/dist/server/services/client-id.js +61 -0
  99. package/dist/server/services/client-id.js.map +1 -0
  100. package/dist/server/services/credentials.d.ts +39 -0
  101. package/dist/server/services/credentials.d.ts.map +1 -0
  102. package/dist/server/services/credentials.js +110 -0
  103. package/dist/server/services/credentials.js.map +1 -0
  104. package/dist/server/services/files.d.ts +119 -0
  105. package/dist/server/services/files.d.ts.map +1 -0
  106. package/dist/server/services/files.js +560 -0
  107. package/dist/server/services/files.js.map +1 -0
  108. package/dist/server/services/instances.d.ts +52 -0
  109. package/dist/server/services/instances.d.ts.map +1 -0
  110. package/dist/server/services/instances.js +241 -0
  111. package/dist/server/services/instances.js.map +1 -0
  112. package/dist/server/services/pathUtils.d.ts +47 -0
  113. package/dist/server/services/pathUtils.d.ts.map +1 -0
  114. package/dist/server/services/pathUtils.js +124 -0
  115. package/dist/server/services/pathUtils.js.map +1 -0
  116. package/dist/server/services/platform.d.ts +72 -0
  117. package/dist/server/services/platform.d.ts.map +1 -0
  118. package/dist/server/services/platform.js +368 -0
  119. package/dist/server/services/platform.js.map +1 -0
  120. package/dist/server/services/sentry.d.ts +5 -0
  121. package/dist/server/services/sentry.d.ts.map +1 -0
  122. package/dist/server/services/sentry.js +71 -0
  123. package/dist/server/services/sentry.js.map +1 -0
  124. package/dist/server/services/terminal/pty-manager.d.ts +149 -0
  125. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -0
  126. package/dist/server/services/terminal/pty-manager.js +377 -0
  127. package/dist/server/services/terminal/pty-manager.js.map +1 -0
  128. package/dist/server/services/terminal/tmux-manager.d.ts +82 -0
  129. package/dist/server/services/terminal/tmux-manager.d.ts.map +1 -0
  130. package/dist/server/services/terminal/tmux-manager.js +352 -0
  131. package/dist/server/services/terminal/tmux-manager.js.map +1 -0
  132. package/dist/server/services/websocket/autocomplete.d.ts +50 -0
  133. package/dist/server/services/websocket/autocomplete.d.ts.map +1 -0
  134. package/dist/server/services/websocket/autocomplete.js +361 -0
  135. package/dist/server/services/websocket/autocomplete.js.map +1 -0
  136. package/dist/server/services/websocket/file-utils.d.ts +44 -0
  137. package/dist/server/services/websocket/file-utils.d.ts.map +1 -0
  138. package/dist/server/services/websocket/file-utils.js +272 -0
  139. package/dist/server/services/websocket/file-utils.js.map +1 -0
  140. package/dist/server/services/websocket/handler.d.ts +246 -0
  141. package/dist/server/services/websocket/handler.d.ts.map +1 -0
  142. package/dist/server/services/websocket/handler.js +1771 -0
  143. package/dist/server/services/websocket/handler.js.map +1 -0
  144. package/dist/server/services/websocket/index.d.ts +11 -0
  145. package/dist/server/services/websocket/index.d.ts.map +1 -0
  146. package/dist/server/services/websocket/index.js +14 -0
  147. package/dist/server/services/websocket/index.js.map +1 -0
  148. package/dist/server/services/websocket/types.d.ts +214 -0
  149. package/dist/server/services/websocket/types.d.ts.map +1 -0
  150. package/dist/server/services/websocket/types.js +4 -0
  151. package/dist/server/services/websocket/types.js.map +1 -0
  152. package/dist/server/utils/agent-manager.d.ts +69 -0
  153. package/dist/server/utils/agent-manager.d.ts.map +1 -0
  154. package/dist/server/utils/agent-manager.js +269 -0
  155. package/dist/server/utils/agent-manager.js.map +1 -0
  156. package/dist/server/utils/paths.d.ts +25 -0
  157. package/dist/server/utils/paths.d.ts.map +1 -0
  158. package/dist/server/utils/paths.js +38 -0
  159. package/dist/server/utils/paths.js.map +1 -0
  160. package/dist/server/utils/port-manager.d.ts +10 -0
  161. package/dist/server/utils/port-manager.d.ts.map +1 -0
  162. package/dist/server/utils/port-manager.js +60 -0
  163. package/dist/server/utils/port-manager.js.map +1 -0
  164. package/dist/server/utils/port.d.ts +26 -0
  165. package/dist/server/utils/port.d.ts.map +1 -0
  166. package/dist/server/utils/port.js +83 -0
  167. package/dist/server/utils/port.js.map +1 -0
  168. package/hooks/bouncer.sh +138 -0
  169. package/package.json +74 -0
  170. package/server/README.md +191 -0
  171. package/server/cli/headless/claude-invoker.ts +415 -0
  172. package/server/cli/headless/index.ts +39 -0
  173. package/server/cli/headless/mcp-config.ts +87 -0
  174. package/server/cli/headless/output-utils.ts +109 -0
  175. package/server/cli/headless/prompt-utils.ts +108 -0
  176. package/server/cli/headless/runner.ts +133 -0
  177. package/server/cli/headless/types.ts +118 -0
  178. package/server/cli/improvisation-session-manager.ts +531 -0
  179. package/server/index.ts +456 -0
  180. package/server/mcp/README.md +122 -0
  181. package/server/mcp/bouncer-cli.ts +127 -0
  182. package/server/mcp/bouncer-integration.ts +430 -0
  183. package/server/mcp/security-audit.ts +180 -0
  184. package/server/mcp/security-patterns.ts +290 -0
  185. package/server/mcp/server.ts +174 -0
  186. package/server/routes/files.ts +29 -0
  187. package/server/routes/improvise.ts +82 -0
  188. package/server/routes/index.ts +13 -0
  189. package/server/routes/instances.ts +54 -0
  190. package/server/routes/notifications.ts +158 -0
  191. package/server/services/analytics.ts +277 -0
  192. package/server/services/auth.ts +80 -0
  193. package/server/services/client-id.ts +68 -0
  194. package/server/services/credentials.ts +134 -0
  195. package/server/services/files.ts +710 -0
  196. package/server/services/instances.ts +275 -0
  197. package/server/services/pathUtils.ts +158 -0
  198. package/server/services/platform.test.ts +1314 -0
  199. package/server/services/platform.ts +435 -0
  200. package/server/services/sentry.ts +81 -0
  201. package/server/services/terminal/pty-manager.ts +464 -0
  202. package/server/services/terminal/tmux-manager.ts +426 -0
  203. package/server/services/websocket/autocomplete.ts +438 -0
  204. package/server/services/websocket/file-utils.ts +305 -0
  205. package/server/services/websocket/handler.test.ts +20 -0
  206. package/server/services/websocket/handler.ts +2047 -0
  207. package/server/services/websocket/index.ts +40 -0
  208. package/server/services/websocket/types.ts +339 -0
  209. package/server/tsconfig.json +19 -0
  210. package/server/utils/agent-manager.ts +323 -0
  211. package/server/utils/paths.ts +45 -0
  212. package/server/utils/port-manager.ts +70 -0
  213. package/server/utils/port.ts +102 -0
@@ -0,0 +1,415 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Claude Invoker
6
+ *
7
+ * Handles spawning and managing Claude CLI processes.
8
+ */
9
+
10
+ import { type ChildProcess, spawn } from 'node:child_process';
11
+ import { generateMcpConfig } from './mcp-config.js';
12
+ import { detectErrorInStderr, } from './output-utils.js';
13
+ import { buildMultimodalMessage } from './prompt-utils.js';
14
+ import type {
15
+ ExecutionResult,
16
+ ResolvedHeadlessConfig,
17
+ ToolUseAccumulator,
18
+ } from './types.js';
19
+
20
+ export interface ClaudeInvokerOptions {
21
+ config: ResolvedHeadlessConfig;
22
+ runningProcesses: Map<number, ChildProcess>;
23
+ }
24
+
25
+ // ========== Stream Event Handlers ==========
26
+
27
+ interface StreamHandlerContext {
28
+ config: ResolvedHeadlessConfig;
29
+ accumulatedAssistantResponse: string;
30
+ accumulatedThinking: string;
31
+ accumulatedToolUse: ToolUseAccumulator[];
32
+ toolInputBuffers: Map<number, { name: string; id: string; inputJson: string; startTime: number }>;
33
+ }
34
+
35
+ function handleSessionCapture(
36
+ parsed: any,
37
+ captured: { claudeSessionId?: string }
38
+ ): void {
39
+ if (parsed.type === 'system' && parsed.subtype === 'init' && parsed.session_id) {
40
+ captured.claudeSessionId = parsed.session_id;
41
+ }
42
+ if (parsed.type === 'result' && parsed.session_id && !captured.claudeSessionId) {
43
+ captured.claudeSessionId = parsed.session_id;
44
+ }
45
+ }
46
+
47
+ function handleThinkingDelta(event: any, ctx: StreamHandlerContext): string {
48
+ if (
49
+ event.type !== 'content_block_delta' ||
50
+ event.delta?.type !== 'thinking_delta' ||
51
+ !event.delta?.thinking
52
+ ) {
53
+ return ctx.accumulatedThinking;
54
+ }
55
+
56
+ const thinking = event.delta.thinking;
57
+ const updated = ctx.accumulatedThinking + thinking;
58
+
59
+ if (ctx.config.thinkingCallback) {
60
+ ctx.config.thinkingCallback(thinking);
61
+ } else if (ctx.config.outputCallback) {
62
+ ctx.config.outputCallback(thinking);
63
+ } else {
64
+ process.stdout.write(thinking);
65
+ }
66
+
67
+ return updated;
68
+ }
69
+
70
+ function handleTextDelta(event: any, ctx: StreamHandlerContext): string {
71
+ if (
72
+ event.type !== 'content_block_delta' ||
73
+ event.delta?.type !== 'text_delta' ||
74
+ !event.delta?.text
75
+ ) {
76
+ return ctx.accumulatedAssistantResponse;
77
+ }
78
+
79
+ const text = event.delta.text;
80
+ const updated = ctx.accumulatedAssistantResponse + text;
81
+
82
+ if (ctx.config.outputCallback) {
83
+ ctx.config.outputCallback(text);
84
+ }
85
+
86
+ return updated;
87
+ }
88
+
89
+ function handleToolStart(event: any, ctx: StreamHandlerContext): void {
90
+ if (
91
+ event.type !== 'content_block_start' ||
92
+ event.content_block?.type !== 'tool_use'
93
+ ) {
94
+ return;
95
+ }
96
+
97
+ const toolName = event.content_block.name;
98
+ const toolId = event.content_block.id;
99
+ const index = event.index;
100
+
101
+ ctx.toolInputBuffers.set(index, {
102
+ name: toolName,
103
+ id: toolId,
104
+ inputJson: '',
105
+ startTime: Date.now()
106
+ });
107
+
108
+ if (ctx.config.toolUseCallback) {
109
+ ctx.config.toolUseCallback({ type: 'tool_start', toolName, toolId, index });
110
+ }
111
+ }
112
+
113
+ function handleToolInputDelta(event: any, ctx: StreamHandlerContext): void {
114
+ if (
115
+ event.type !== 'content_block_delta' ||
116
+ event.delta?.type !== 'input_json_delta'
117
+ ) {
118
+ return;
119
+ }
120
+
121
+ const index = event.index;
122
+ const partialJson = event.delta.partial_json;
123
+
124
+ const toolBuffer = ctx.toolInputBuffers.get(index);
125
+ if (toolBuffer) {
126
+ toolBuffer.inputJson += partialJson;
127
+ }
128
+
129
+ if (ctx.config.toolUseCallback) {
130
+ ctx.config.toolUseCallback({ type: 'tool_input_delta', partialJson, index });
131
+ }
132
+ }
133
+
134
+ function handleToolComplete(event: any, ctx: StreamHandlerContext): void {
135
+ if (event.type !== 'content_block_stop') {
136
+ return;
137
+ }
138
+
139
+ const index = event.index;
140
+ const toolBuffer = ctx.toolInputBuffers.get(index);
141
+ if (!toolBuffer) {
142
+ return;
143
+ }
144
+
145
+ let completeInput: any = {};
146
+ try {
147
+ completeInput = JSON.parse(toolBuffer.inputJson);
148
+ } catch (_e) {
149
+ // Input might not be valid JSON yet
150
+ }
151
+
152
+ ctx.accumulatedToolUse.push({
153
+ toolName: toolBuffer.name,
154
+ toolId: toolBuffer.id,
155
+ toolInput: completeInput,
156
+ startTime: toolBuffer.startTime
157
+ });
158
+
159
+ if (ctx.config.toolUseCallback) {
160
+ ctx.config.toolUseCallback({
161
+ type: 'tool_complete',
162
+ toolName: toolBuffer.name,
163
+ toolId: toolBuffer.id,
164
+ index,
165
+ completeInput
166
+ });
167
+ }
168
+ }
169
+
170
+ function handleToolResult(parsed: any, ctx: StreamHandlerContext): void {
171
+ if (parsed.type !== 'user' || !parsed.message?.content) {
172
+ return;
173
+ }
174
+
175
+ for (const content of parsed.message.content) {
176
+ if (content.type !== 'tool_result') {
177
+ continue;
178
+ }
179
+
180
+ const toolId = content.tool_use_id;
181
+ const result = content.content;
182
+ const isError = content.is_error || false;
183
+ const resultStr = typeof result === 'string' ? result : JSON.stringify(result);
184
+
185
+ const toolEntry = ctx.accumulatedToolUse.find(t => t.toolId === toolId);
186
+ if (toolEntry) {
187
+ toolEntry.result = resultStr;
188
+ toolEntry.isError = isError;
189
+ toolEntry.duration = Date.now() - toolEntry.startTime;
190
+ }
191
+
192
+ if (ctx.config.toolUseCallback) {
193
+ ctx.config.toolUseCallback({ type: 'tool_result', toolId, result: resultStr, isError });
194
+ }
195
+ }
196
+ }
197
+
198
+ function processStreamLines(
199
+ buffer: string,
200
+ sessionCapture: { claudeSessionId?: string },
201
+ ctx: StreamHandlerContext
202
+ ): string {
203
+ const lines = buffer.split('\n');
204
+ const remainder = lines.pop() || '';
205
+
206
+ for (const line of lines) {
207
+ if (!line.trim()) continue;
208
+ try {
209
+ const parsed = JSON.parse(line);
210
+ handleSessionCapture(parsed, sessionCapture);
211
+ processStreamEvent(parsed, ctx);
212
+ } catch (_e) {
213
+ // Ignore parse errors
214
+ }
215
+ }
216
+
217
+ return remainder;
218
+ }
219
+
220
+ function processStreamEvent(parsed: any, ctx: StreamHandlerContext): void {
221
+ if (parsed.type === 'stream_event' && parsed.event) {
222
+ const event = parsed.event;
223
+ ctx.accumulatedThinking = handleThinkingDelta(event, ctx);
224
+ ctx.accumulatedAssistantResponse = handleTextDelta(event, ctx);
225
+ handleToolStart(event, ctx);
226
+ handleToolInputDelta(event, ctx);
227
+ handleToolComplete(event, ctx);
228
+ }
229
+ handleToolResult(parsed, ctx);
230
+ }
231
+
232
+ // ========== Error Handling ==========
233
+
234
+ const SPAWN_ERROR_MAP: Record<string, { code: string; message: string }> = {
235
+ ENOENT: {
236
+ code: 'CLAUDE_NOT_INSTALLED',
237
+ message: 'Claude Code is not installed or not in PATH. Please install Claude Code: npm install -g @anthropic-ai/claude-code'
238
+ },
239
+ EACCES: {
240
+ code: 'PERMISSION_DENIED',
241
+ message: 'Permission denied when running Claude Code. Please check file permissions.'
242
+ }
243
+ };
244
+
245
+ function handleSpawnError(
246
+ error: NodeJS.ErrnoException,
247
+ config: ResolvedHeadlessConfig,
248
+ reject: (reason: Error) => void
249
+ ): void {
250
+ const mapped = error.code ? SPAWN_ERROR_MAP[error.code] : undefined;
251
+ if (!mapped) {
252
+ reject(error);
253
+ return;
254
+ }
255
+
256
+ const formatted = `[[MSTRO_ERROR:${mapped.code}]] ${mapped.message}`;
257
+ if (config.outputCallback) {
258
+ config.outputCallback(`\n${formatted}\n`);
259
+ }
260
+ reject(new Error(formatted));
261
+ }
262
+
263
+ // ========== Argument Building ==========
264
+
265
+ function buildClaudeArgs(
266
+ config: ResolvedHeadlessConfig,
267
+ prompt: string,
268
+ hasImageAttachments: boolean,
269
+ useStreamJson: boolean,
270
+ mcpConfigPath: string | null
271
+ ): string[] {
272
+ const args = ['--print'];
273
+
274
+ if (useStreamJson) {
275
+ args.push('--output-format', 'stream-json', '--include-partial-messages', '--verbose');
276
+ }
277
+
278
+ if (hasImageAttachments) {
279
+ args.push('--input-format', 'stream-json');
280
+ }
281
+
282
+ if (config.claudeSessionId) {
283
+ args.push('--resume', config.claudeSessionId);
284
+ } else if (config.continueSession) {
285
+ args.push('--continue');
286
+ }
287
+
288
+ if (mcpConfigPath) {
289
+ args.push('--mcp-config', mcpConfigPath);
290
+ args.push('--permission-prompt-tool', 'mcp__mstro-bouncer__approval_prompt');
291
+ }
292
+
293
+ if (!hasImageAttachments) {
294
+ args.push(prompt);
295
+ }
296
+
297
+ return args;
298
+ }
299
+
300
+ /**
301
+ * Execute a Claude CLI command for a single movement
302
+ * Supports multimodal prompts via --input-format stream-json when image attachments are present
303
+ */
304
+ export async function executeClaudeCommand(
305
+ prompt: string,
306
+ _movementId: string,
307
+ _sessionNumber: number,
308
+ options: ClaudeInvokerOptions
309
+ ): Promise<ExecutionResult> {
310
+ const { config, runningProcesses } = options;
311
+ const perfStart = Date.now();
312
+ if (config.verbose) {
313
+ console.log(`[PERF] executeMovement started`);
314
+ }
315
+
316
+ const hasImageAttachments = config.imageAttachments && config.imageAttachments.length > 0;
317
+ const useStreamJson = hasImageAttachments || config.thinkingCallback || config.outputCallback || config.toolUseCallback;
318
+ const mcpConfigPath = generateMcpConfig(config.workingDir, config.verbose);
319
+ const args = buildClaudeArgs(config, prompt, !!hasImageAttachments, !!useStreamJson, mcpConfigPath);
320
+
321
+ if (config.verbose) {
322
+ console.log(`[PERF] About to spawn: ${Date.now() - perfStart}ms`);
323
+ console.log(`[PERF] Command: ${config.claudeCommand} ${args.join(' ')}`);
324
+ }
325
+
326
+ const claudeProcess = spawn(config.claudeCommand, args, {
327
+ cwd: config.workingDir,
328
+ env: { ...process.env },
329
+ stdio: [hasImageAttachments ? 'pipe' : 'ignore', 'pipe', 'pipe']
330
+ });
331
+
332
+ if (hasImageAttachments && claudeProcess.stdin) {
333
+ const multimodalMessage = buildMultimodalMessage(prompt, config.imageAttachments!);
334
+ claudeProcess.stdin.write(multimodalMessage);
335
+ claudeProcess.stdin.end();
336
+ }
337
+
338
+ if (claudeProcess.pid) {
339
+ runningProcesses.set(claudeProcess.pid, claudeProcess);
340
+ }
341
+
342
+ if (config.verbose) {
343
+ console.log(`[PERF] Spawned: ${Date.now() - perfStart}ms`);
344
+ }
345
+
346
+ let stdout = '';
347
+ let stderr = '';
348
+ let thinkingBuffer = '';
349
+ let firstStdoutReceived = false;
350
+ let errorAlreadySurfaced = false;
351
+
352
+ const sessionCapture: { claudeSessionId?: string } = {};
353
+ const ctx: StreamHandlerContext = {
354
+ config,
355
+ accumulatedAssistantResponse: '',
356
+ accumulatedThinking: '',
357
+ accumulatedToolUse: [],
358
+ toolInputBuffers: new Map(),
359
+ };
360
+
361
+ claudeProcess.stdout!.on('data', (data) => {
362
+ if (!firstStdoutReceived) {
363
+ firstStdoutReceived = true;
364
+ if (config.verbose) {
365
+ console.log(`[PERF] First stdout data: ${Date.now() - perfStart}ms`);
366
+ }
367
+ }
368
+
369
+ const chunk = data.toString();
370
+ stdout += chunk;
371
+
372
+ if (useStreamJson) {
373
+ thinkingBuffer = processStreamLines(thinkingBuffer + chunk, sessionCapture, ctx);
374
+ }
375
+ });
376
+
377
+ claudeProcess.stderr!.on('data', async (data) => {
378
+ const chunk = data.toString();
379
+ stderr += chunk;
380
+
381
+ if (errorAlreadySurfaced) return;
382
+
383
+ const error = detectErrorInStderr(stderr);
384
+ if (error) {
385
+ errorAlreadySurfaced = true;
386
+ if (config.outputCallback) {
387
+ config.outputCallback(`\n[[MSTRO_ERROR:${error.errorCode}]] ${error.message}\n`);
388
+ }
389
+ }
390
+ });
391
+
392
+ return new Promise((resolve, reject) => {
393
+ claudeProcess.on('close', (code) => {
394
+ if (claudeProcess.pid) {
395
+ runningProcesses.delete(claudeProcess.pid);
396
+ }
397
+ resolve({
398
+ output: stdout,
399
+ error: stderr || undefined,
400
+ exitCode: code || 0,
401
+ assistantResponse: ctx.accumulatedAssistantResponse || undefined,
402
+ thinkingOutput: ctx.accumulatedThinking || undefined,
403
+ toolUseHistory: ctx.accumulatedToolUse.length > 0 ? ctx.accumulatedToolUse : undefined,
404
+ claudeSessionId: sessionCapture.claudeSessionId
405
+ });
406
+ });
407
+
408
+ claudeProcess.on('error', (error: NodeJS.ErrnoException) => {
409
+ if (claudeProcess.pid) {
410
+ runningProcesses.delete(claudeProcess.pid);
411
+ }
412
+ handleSpawnError(error, config, reject);
413
+ });
414
+ });
415
+ }
@@ -0,0 +1,39 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Headless Runner Module
6
+ *
7
+ * Re-exports all headless runner components for backward compatibility.
8
+ */
9
+
10
+ export type { ClaudeInvokerOptions } from './claude-invoker.js';
11
+ export { executeClaudeCommand } from './claude-invoker.js';
12
+
13
+ // Utilities (for advanced usage)
14
+ export { generateMcpConfig } from './mcp-config.js';
15
+ export {
16
+ detectErrorInStderr,
17
+ ERROR_PATTERNS,
18
+ estimateTokensFromOutput,
19
+ extractCleanOutput,
20
+ extractModifiedFiles
21
+ } from './output-utils.js';
22
+ export {
23
+ buildMultimodalMessage,
24
+ enrichPromptWithContext,
25
+ isApprovalPrompt
26
+ } from './prompt-utils.js';
27
+ // Main runner class
28
+ export { HeadlessRunner } from './runner.js';
29
+ // Types
30
+ export type {
31
+ ExecutionResult,
32
+ HeadlessConfig,
33
+ ImageAttachment,
34
+ ResolvedHeadlessConfig,
35
+ SessionResult,
36
+ SessionState,
37
+ ToolUseAccumulator,
38
+ ToolUseEvent
39
+ } from './types.js';
@@ -0,0 +1,87 @@
1
+ /**
2
+ * MCP Configuration Generator
3
+ *
4
+ * Generates MCP config with bouncer + user's MCP servers from ~/.claude.json.
5
+ */
6
+
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { homedir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import { MCP_SERVER_PATH, MSTRO_ROOT } from '../../utils/paths.js';
11
+
12
+ /**
13
+ * Load user's MCP servers from ~/.claude.json (global + project-level)
14
+ */
15
+ function loadUserMcpServers(workingDir: string, verbose: boolean): Record<string, any> {
16
+ const servers: Record<string, any> = {};
17
+ const claudeConfigPath = join(homedir(), '.claude.json');
18
+
19
+ if (!existsSync(claudeConfigPath)) {
20
+ return servers;
21
+ }
22
+
23
+ try {
24
+ const claudeConfig = JSON.parse(readFileSync(claudeConfigPath, 'utf-8'));
25
+
26
+ if (claudeConfig.mcpServers && typeof claudeConfig.mcpServers === 'object') {
27
+ Object.assign(servers, claudeConfig.mcpServers);
28
+ }
29
+
30
+ if (claudeConfig.projects && typeof claudeConfig.projects === 'object') {
31
+ for (const [projectPath, projectConfig] of Object.entries(claudeConfig.projects)) {
32
+ const projectServers = (projectConfig as any)?.mcpServers;
33
+ if (workingDir.startsWith(projectPath) && typeof projectServers === 'object') {
34
+ Object.assign(servers, projectServers);
35
+ }
36
+ }
37
+ }
38
+
39
+ if (verbose) {
40
+ console.log(`[${new Date().toISOString()}] Loaded ${Object.keys(servers).length} user MCP servers from ~/.claude.json`);
41
+ }
42
+ } catch (parseError: any) {
43
+ console.error(`[${new Date().toISOString()}] Failed to parse ~/.claude.json: ${parseError.message}`);
44
+ }
45
+
46
+ return servers;
47
+ }
48
+
49
+ /**
50
+ * Generate MCP config with bouncer + user's MCP servers from ~/.claude.json.
51
+ * Writes to ~/.mstro/mcp-config.json for use with --mcp-config flag.
52
+ */
53
+ export function generateMcpConfig(workingDir: string, verbose: boolean = false): string | null {
54
+ try {
55
+ if (!existsSync(MCP_SERVER_PATH)) {
56
+ console.error(`[${new Date().toISOString()}] MCP server not found at ${MCP_SERVER_PATH}`);
57
+ return null;
58
+ }
59
+
60
+ const mcpServers: Record<string, any> = {
61
+ 'mstro-bouncer': {
62
+ command: 'npx',
63
+ args: ['tsx', MCP_SERVER_PATH],
64
+ description: 'Mstro security bouncer for approving/denying Claude Code tool use',
65
+ env: { BOUNCER_USE_AI: 'true', MSTRO_ROOT: MSTRO_ROOT }
66
+ },
67
+ ...loadUserMcpServers(workingDir, verbose)
68
+ };
69
+
70
+ const configDir = join(homedir(), '.mstro');
71
+ if (!existsSync(configDir)) {
72
+ mkdirSync(configDir, { recursive: true });
73
+ }
74
+
75
+ const configPath = join(configDir, 'mcp-config.json');
76
+ writeFileSync(configPath, JSON.stringify({ mcpServers }, null, 2));
77
+
78
+ if (verbose) {
79
+ console.log(`[${new Date().toISOString()}] Generated MCP config at ${configPath} (${Object.keys(mcpServers).length} servers)`);
80
+ }
81
+
82
+ return configPath;
83
+ } catch (error: any) {
84
+ console.error(`[${new Date().toISOString()}] Failed to generate MCP config: ${error.message}`);
85
+ return null;
86
+ }
87
+ }
@@ -0,0 +1,109 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Output Utilities
6
+ *
7
+ * Utilities for processing and parsing Claude CLI output.
8
+ */
9
+
10
+ /**
11
+ * Extract clean output from Claude response
12
+ */
13
+ export function extractCleanOutput(rawOutput: string): string {
14
+ // When using stream-json format, filter out all JSON lines
15
+ // since we already streamed the actual content via callbacks
16
+ const lines = rawOutput.split('\n');
17
+ const cleanLines = lines.filter(line => {
18
+ const trimmed = line.trim();
19
+ // Filter out JSON lines (system, stream_event, assistant, user, result)
20
+ if (trimmed.startsWith('{') && trimmed.includes('"type"')) {
21
+ return false;
22
+ }
23
+ return trimmed.length > 0;
24
+ });
25
+
26
+ return cleanLines.join('\n')
27
+ .replace(/\x1b\[[0-9;]*m/g, '') // ANSI color codes
28
+ .replace(/\r\n/g, '\n') // Normalize line endings
29
+ .trim();
30
+ }
31
+
32
+ /**
33
+ * Estimate tokens from output
34
+ */
35
+ export function estimateTokensFromOutput(output: string): number {
36
+ return Math.floor(output.length / 4);
37
+ }
38
+
39
+ /**
40
+ * Extract modified files from output (simplified)
41
+ */
42
+ export function extractModifiedFiles(output: string): string[] {
43
+ // This is a simplified version. In production, would parse actual file operations
44
+ const filePattern = /(?:wrote|modified|created|edited)\s+(?:file\s+)?['"]?([^\s'"]+\.[a-z0-9]+)['"]?/gi;
45
+ const matches = output.matchAll(filePattern);
46
+ const files = new Set<string>();
47
+
48
+ for (const match of matches) {
49
+ files.add(match[1]);
50
+ }
51
+
52
+ return Array.from(files);
53
+ }
54
+
55
+ /**
56
+ * Error patterns for detecting Claude Code errors in stderr
57
+ */
58
+ export const ERROR_PATTERNS = [
59
+ // Authentication errors - most common user-facing issue
60
+ { pattern: /not logged in|login required|must be authenticated|session.*expired|token.*invalid|token.*expired/i,
61
+ message: 'Claude Code authentication required. Please run "claude login" in your terminal to authenticate.',
62
+ errorCode: 'AUTH_REQUIRED' },
63
+ { pattern: /account.*not.*found|user.*not.*found/i,
64
+ message: 'Claude account not found. Please verify your account at claude.ai.',
65
+ errorCode: 'ACCOUNT_NOT_FOUND' },
66
+ // API/subscription errors
67
+ { pattern: /api key|invalid key|unauthorized|forbidden.*api/i,
68
+ message: 'API key error. Please check your Claude Code configuration.',
69
+ errorCode: 'API_KEY_INVALID' },
70
+ { pattern: /quota exceeded|usage.*limit|billing.*issue|payment.*required|subscription.*expired/i,
71
+ message: 'Usage quota or subscription issue. Please check your account billing at claude.ai.',
72
+ errorCode: 'QUOTA_EXCEEDED' },
73
+ { pattern: /rate limit|too many requests|429|throttl/i,
74
+ message: 'Rate limit exceeded. Please wait a moment before trying again.',
75
+ errorCode: 'RATE_LIMITED' },
76
+ // Network errors
77
+ { pattern: /no internet|network error|connection refused|ENOTFOUND|ECONNREFUSED|ETIMEDOUT|socket hang up/i,
78
+ message: 'Network error. Please check your internet connection.',
79
+ errorCode: 'NETWORK_ERROR' },
80
+ { pattern: /SSL|certificate|TLS|CERT_/i,
81
+ message: 'SSL/TLS certificate error. Please check your network configuration.',
82
+ errorCode: 'SSL_ERROR' },
83
+ // Model/service errors
84
+ { pattern: /model not found|model unavailable|service unavailable|503|502|504/i,
85
+ message: 'Claude service temporarily unavailable. Please try again in a few moments.',
86
+ errorCode: 'SERVICE_UNAVAILABLE' },
87
+ { pattern: /internal server error|500/i,
88
+ message: 'Claude service encountered an internal error. Please try again.',
89
+ errorCode: 'INTERNAL_ERROR' },
90
+ // Context/session errors
91
+ { pattern: /context.*too.*long|context.*limit|token.*limit.*exceeded/i,
92
+ message: 'Context too long. Please start a new session or reduce prompt size.',
93
+ errorCode: 'CONTEXT_TOO_LONG' },
94
+ { pattern: /session.*not.*found|invalid.*session/i,
95
+ message: 'Session not found. Starting a new session may help.',
96
+ errorCode: 'SESSION_NOT_FOUND' },
97
+ ];
98
+
99
+ /**
100
+ * Check stderr for known error patterns and return the first match
101
+ */
102
+ export function detectErrorInStderr(stderrBuffer: string): { message: string; errorCode: string } | null {
103
+ for (const { pattern, message, errorCode } of ERROR_PATTERNS) {
104
+ if (pattern.test(stderrBuffer)) {
105
+ return { message, errorCode };
106
+ }
107
+ }
108
+ return null;
109
+ }