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,531 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Improvisation Session Manager v2
6
+ *
7
+ * Optimized for fast, direct prompt execution in Improvise mode.
8
+ * For complex multi-part prompts with parallel/sequential movements, use Compose tab instead.
9
+ */
10
+
11
+ import { EventEmitter } from 'node:events';
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import { HeadlessRunner } from './headless/index.js';
15
+
16
+ export interface ImprovisationOptions {
17
+ workingDir: string;
18
+ sessionId: string;
19
+ tokenBudgetThreshold: number;
20
+ maxSessions: number;
21
+ verbose: boolean;
22
+ noColor: boolean;
23
+ }
24
+
25
+ // File attachment for multimodal prompts (images)
26
+ export interface FileAttachment {
27
+ fileName: string; // Display name (e.g., "screenshot.png")
28
+ filePath: string; // Full path on disk (for context)
29
+ content: string; // Base64 for images
30
+ isImage: boolean; // True for image files
31
+ mimeType?: string; // MIME type for images (e.g., "image/png")
32
+ }
33
+
34
+ export interface ToolUseRecord {
35
+ toolName: string;
36
+ toolId: string;
37
+ toolInput: Record<string, unknown>;
38
+ result?: string;
39
+ isError?: boolean;
40
+ duration?: number;
41
+ }
42
+
43
+ export interface MovementRecord {
44
+ id: string;
45
+ sequenceNumber: number;
46
+ userPrompt: string;
47
+ timestamp: string;
48
+ tokensUsed: number;
49
+ summary: string;
50
+ filesModified: string[];
51
+ // NEW: Persisted output fields
52
+ assistantResponse?: string; // Claude's text output
53
+ thinkingOutput?: string; // Extended thinking
54
+ toolUseHistory?: ToolUseRecord[];// Tool invocations + results
55
+ errorOutput?: string; // Any errors
56
+ }
57
+
58
+ export interface SessionHistory {
59
+ sessionId: string;
60
+ startedAt: string;
61
+ lastActivityAt: string;
62
+ totalTokens: number;
63
+ movements: MovementRecord[];
64
+ claudeSessionId?: string;
65
+ }
66
+
67
+ export class ImprovisationSessionManager extends EventEmitter {
68
+ private sessionId: string;
69
+ private improviseDir: string;
70
+ private historyPath: string;
71
+ private history: SessionHistory;
72
+ private currentRunner: HeadlessRunner | null = null;
73
+ private options: ImprovisationOptions;
74
+ private pendingApproval?: {
75
+ plan: any;
76
+ resolve: (approved: boolean) => void;
77
+ };
78
+ private outputQueue: Array<{ text: string; timestamp: number }> = [];
79
+ private queueTimer: NodeJS.Timeout | null = null;
80
+ private isFirstPrompt: boolean = true; // Track if this is the first prompt (no --resume needed)
81
+ private claudeSessionId: string | undefined; // Claude CLI session ID for tab isolation
82
+ private isResumedSession: boolean = false; // Track if this is a resumed historical session
83
+ accumulatedKnowledge: string = '';
84
+
85
+ /**
86
+ * Resume from a historical session.
87
+ * Creates a new session manager that continues the conversation from a previous session.
88
+ * The first prompt will include context from the historical session.
89
+ */
90
+ static resumeFromHistory(workingDir: string, historicalSessionId: string): ImprovisationSessionManager {
91
+ const improviseDir = join(workingDir, '.mstro', 'improvise');
92
+
93
+ // Extract timestamp from session ID (format: improv-1234567890123 or just 1234567890123)
94
+ const timestamp = historicalSessionId.replace('improv-', '');
95
+ const historyPath = join(improviseDir, `history-${timestamp}.json`);
96
+
97
+ if (!existsSync(historyPath)) {
98
+ throw new Error(`Historical session not found: ${historicalSessionId}`);
99
+ }
100
+
101
+ // Read the historical session
102
+ const historyData = JSON.parse(readFileSync(historyPath, 'utf-8')) as SessionHistory;
103
+
104
+ // Create a new session manager with the SAME session ID
105
+ // This ensures we continue writing to the same history file
106
+ const manager = new ImprovisationSessionManager({
107
+ workingDir,
108
+ sessionId: historyData.sessionId
109
+ });
110
+
111
+ // Load the historical data
112
+ manager.history = historyData;
113
+
114
+ // Build accumulated knowledge from historical movements
115
+ manager.accumulatedKnowledge = historyData.movements
116
+ .filter(m => m.summary)
117
+ .map(m => m.summary)
118
+ .join('\n\n');
119
+
120
+ // Restore Claude session ID if available so we can --resume the actual conversation
121
+ // NOTE: Always mark as resumed session so historical context can be injected as fallback
122
+ // if the Claude CLI session has expired (e.g., client was restarted)
123
+ manager.isResumedSession = true;
124
+ manager.isFirstPrompt = true; // Always true so historical context is injected on first prompt
125
+ if (historyData.claudeSessionId) {
126
+ manager.claudeSessionId = historyData.claudeSessionId;
127
+ }
128
+
129
+ return manager;
130
+ }
131
+
132
+ constructor(options: Partial<ImprovisationOptions> = {}) {
133
+ super();
134
+
135
+ this.options = {
136
+ workingDir: options.workingDir || process.cwd(),
137
+ sessionId: options.sessionId || `improv-${Date.now()}`,
138
+ tokenBudgetThreshold: options.tokenBudgetThreshold || 170000,
139
+ maxSessions: options.maxSessions || 10,
140
+ verbose: options.verbose || false,
141
+ noColor: options.noColor || false
142
+ };
143
+
144
+ this.sessionId = this.options.sessionId;
145
+ this.improviseDir = join(this.options.workingDir, '.mstro', 'improvise');
146
+ this.historyPath = join(this.improviseDir, `history-${this.sessionId.replace('improv-', '')}.json`);
147
+
148
+ // Ensure improvise directory exists
149
+ if (!existsSync(this.improviseDir)) {
150
+ mkdirSync(this.improviseDir, { recursive: true });
151
+ }
152
+
153
+ // Load or initialize history
154
+ this.history = this.loadHistory();
155
+
156
+ // Start output queue processor
157
+ this.startQueueProcessor();
158
+ }
159
+
160
+ /**
161
+ * Start background queue processor that flushes output immediately
162
+ */
163
+ private startQueueProcessor(): void {
164
+ this.queueTimer = setInterval(() => {
165
+ this.flushOutputQueue();
166
+ }, 10); // Process queue every 10ms for near-instant output
167
+ }
168
+
169
+ /**
170
+ * Queue output for immediate processing
171
+ */
172
+ private queueOutput(text: string): void {
173
+ this.outputQueue.push({ text, timestamp: Date.now() });
174
+ }
175
+
176
+ /**
177
+ * Flush all queued output immediately
178
+ */
179
+ private flushOutputQueue(): void {
180
+ while (this.outputQueue.length > 0) {
181
+ const item = this.outputQueue.shift();
182
+ if (item) {
183
+ this.emit('onOutput', item.text);
184
+ }
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Build prompt with text file attachments prepended
190
+ * Format: each text file is shown as @path followed by content in code block
191
+ */
192
+ private buildPromptWithAttachments(userPrompt: string, attachments?: FileAttachment[]): string {
193
+ if (!attachments || attachments.length === 0) {
194
+ return userPrompt;
195
+ }
196
+
197
+ // Filter to text files only (non-images)
198
+ const textFiles = attachments.filter(a => !a.isImage);
199
+ if (textFiles.length === 0) {
200
+ return userPrompt;
201
+ }
202
+
203
+ // Build file content blocks
204
+ const fileBlocks = textFiles.map(file => {
205
+ return `@${file.filePath}\n\`\`\`\n${file.content}\n\`\`\``;
206
+ }).join('\n\n');
207
+
208
+ // Prepend file content to user prompt
209
+ return `${fileBlocks}\n\n${userPrompt}`;
210
+ }
211
+
212
+ /**
213
+ * Execute a user prompt directly (Improvise mode - no score decomposition)
214
+ * Uses persistent Claude sessions via --resume <sessionId> for conversation continuity
215
+ * Each tab maintains its own claudeSessionId for proper isolation
216
+ * Supports file attachments: text files prepended to prompt, images via stream-json multimodal
217
+ */
218
+ async executePrompt(userPrompt: string, attachments?: FileAttachment[]): Promise<MovementRecord> {
219
+ const _execStart = Date.now();
220
+
221
+ this.emit('onMovementStart', this.history.movements.length + 1, userPrompt);
222
+
223
+ try {
224
+ const sequenceNumber = this.history.movements.length + 1;
225
+
226
+ // DEBUG: Removed "Executing prompt..." message - it serves no purpose now that system responds fast
227
+ // this.queueOutput(`\n🎵 Executing prompt...\n`);
228
+ // this.flushOutputQueue();
229
+
230
+ // NOTE: Risk analysis removed - now handled by MCP bouncer at tool-use time
231
+ // The MCP bouncer intercepts ALL tool calls (Bash, Write, etc.) and uses
232
+ // Mstro's bouncer-integration.ts for AI-powered approval/denial.
233
+ // This is more effective than analyzing user prompts, which had false positives.
234
+
235
+
236
+ // Build prompt with text file attachments prepended
237
+ const promptWithAttachments = this.buildPromptWithAttachments(userPrompt, attachments);
238
+
239
+ // PERSISTENT SESSION: Use --resume <sessionId> to maintain conversation history per tab
240
+ // CRITICAL FIX: Using claudeSessionId ensures each tab resumes its own Claude session
241
+ // Previously used --continue which resumed the most recent global session, causing cross-tab contamination
242
+ const runner = new HeadlessRunner({
243
+ workingDir: this.options.workingDir,
244
+ tokenBudgetThreshold: this.options.tokenBudgetThreshold,
245
+ maxSessions: this.options.maxSessions,
246
+ verbose: this.options.verbose,
247
+ noColor: this.options.noColor,
248
+ improvisationMode: true,
249
+ movementNumber: sequenceNumber,
250
+ continueSession: !this.isFirstPrompt, // Used as fallback only if claudeSessionId is missing
251
+ claudeSessionId: this.claudeSessionId, // Resume specific session for tab isolation
252
+ outputCallback: (text: string) => {
253
+ this.queueOutput(text);
254
+ this.flushOutputQueue();
255
+ },
256
+ thinkingCallback: (text: string) => {
257
+ this.emit('onThinking', text);
258
+ this.flushOutputQueue();
259
+ },
260
+ toolUseCallback: (event) => {
261
+ this.emit('onToolUse', event);
262
+ this.flushOutputQueue();
263
+ },
264
+ directPrompt: promptWithAttachments,
265
+ // Pass image attachments for multimodal handling via stream-json
266
+ imageAttachments: attachments?.filter(a => a.isImage),
267
+ // Inject historical context on first prompt of a resumed session
268
+ // This serves as both the primary context mechanism (no claudeSessionId)
269
+ // and a fallback if claudeSessionId is stale (client restarted since original session)
270
+ promptContext: (this.isResumedSession && this.isFirstPrompt)
271
+ ? { accumulatedKnowledge: this.buildHistoricalContext(), filesModified: [] }
272
+ : undefined
273
+ });
274
+
275
+ this.currentRunner = runner;
276
+
277
+ const result = await runner.run();
278
+
279
+ this.currentRunner = null;
280
+
281
+ // Capture Claude session ID for future prompts in this tab
282
+ // This is critical for tab isolation - each tab maintains its own Claude session
283
+ if (result.claudeSessionId) {
284
+ this.claudeSessionId = result.claudeSessionId;
285
+ this.history.claudeSessionId = result.claudeSessionId;
286
+ }
287
+
288
+ // Mark that we've executed at least one prompt
289
+ this.isFirstPrompt = false;
290
+
291
+ // Create movement record with accumulated output for persistence
292
+ const movement: MovementRecord = {
293
+ id: `prompt-${sequenceNumber}`,
294
+ sequenceNumber,
295
+ userPrompt,
296
+ timestamp: new Date().toISOString(),
297
+ tokensUsed: result.totalTokens,
298
+ summary: '', // No summary needed - Claude session maintains context
299
+ filesModified: [],
300
+ // Persist accumulated output for history replay
301
+ assistantResponse: result.assistantResponse,
302
+ thinkingOutput: result.thinkingOutput,
303
+ toolUseHistory: result.toolUseHistory?.map(t => ({
304
+ toolName: t.toolName,
305
+ toolId: t.toolId,
306
+ toolInput: t.toolInput,
307
+ result: t.result,
308
+ isError: t.isError,
309
+ duration: t.duration
310
+ })),
311
+ errorOutput: result.error
312
+ };
313
+
314
+ // Handle file conflicts if any
315
+ if (result.conflicts && result.conflicts.length > 0) {
316
+ this.queueOutput(`\n⚠ File conflicts detected: ${result.conflicts.length}`);
317
+ result.conflicts.forEach(c => {
318
+ this.queueOutput(` - ${c.filePath} (modified by: ${c.modifiedBy.join(', ')})`);
319
+ if (c.backupPath) {
320
+ this.queueOutput(` Backup created: ${c.backupPath}`);
321
+ }
322
+ });
323
+ this.flushOutputQueue();
324
+ }
325
+
326
+ this.history.movements.push(movement);
327
+ this.history.totalTokens += movement.tokensUsed;
328
+
329
+ // Save history
330
+ this.saveHistory();
331
+
332
+ // Completion message is now handled by the client-side movementComplete event handler
333
+ // This prevents duplicate completion messages (one white, one green)
334
+ // this.queueOutput(`\n✓ Complete (tokens: ${result.totalTokens.toLocaleString()})\n`);
335
+ // this.flushOutputQueue();
336
+
337
+ this.emit('onMovementComplete', movement);
338
+ this.emit('onSessionUpdate', this.getHistory());
339
+
340
+ return movement;
341
+
342
+ } catch (error: any) {
343
+ this.currentRunner = null;
344
+ this.emit('onMovementError', error);
345
+ this.queueOutput(`\n❌ Error: ${error.message}\n`);
346
+ this.flushOutputQueue();
347
+ throw error;
348
+ } finally {
349
+ // Ensure final flush
350
+ this.flushOutputQueue();
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Build historical context for resuming a session.
356
+ * This creates a summary of the previous conversation that will be injected
357
+ * into the first prompt of a resumed session.
358
+ */
359
+ private buildHistoricalContext(): string {
360
+ if (this.history.movements.length === 0) {
361
+ return '';
362
+ }
363
+
364
+ const contextParts: string[] = [
365
+ '--- CONVERSATION HISTORY (for context, do not repeat these responses) ---',
366
+ ''
367
+ ];
368
+
369
+ // Include each movement as context
370
+ for (const movement of this.history.movements) {
371
+ contextParts.push(`[User Prompt ${movement.sequenceNumber}]:`);
372
+ contextParts.push(movement.userPrompt);
373
+ contextParts.push('');
374
+
375
+ if (movement.assistantResponse) {
376
+ contextParts.push(`[Your Response ${movement.sequenceNumber}]:`);
377
+ // Truncate very long responses to save tokens
378
+ const response = movement.assistantResponse.length > 2000
379
+ ? `${movement.assistantResponse.slice(0, 2000)}\n... (response truncated for context)`
380
+ : movement.assistantResponse;
381
+ contextParts.push(response);
382
+ contextParts.push('');
383
+ }
384
+
385
+ if (movement.toolUseHistory && movement.toolUseHistory.length > 0) {
386
+ contextParts.push(`[Tools Used in Prompt ${movement.sequenceNumber}]:`);
387
+ for (const tool of movement.toolUseHistory) {
388
+ contextParts.push(`- ${tool.toolName}`);
389
+ }
390
+ contextParts.push('');
391
+ }
392
+ }
393
+
394
+ contextParts.push('--- END OF CONVERSATION HISTORY ---');
395
+ contextParts.push('');
396
+ contextParts.push('Continue the conversation from where we left off. The user is now asking:');
397
+ contextParts.push('');
398
+
399
+ return contextParts.join('\n');
400
+ }
401
+
402
+ /**
403
+ * Load history from disk
404
+ */
405
+ private loadHistory(): SessionHistory {
406
+ if (existsSync(this.historyPath)) {
407
+ try {
408
+ const data = readFileSync(this.historyPath, 'utf-8');
409
+ return JSON.parse(data);
410
+ } catch (error) {
411
+ console.error('Failed to load history:', error);
412
+ }
413
+ }
414
+
415
+ return {
416
+ sessionId: this.sessionId,
417
+ startedAt: new Date().toISOString(),
418
+ lastActivityAt: new Date().toISOString(),
419
+ totalTokens: 0,
420
+ movements: []
421
+ };
422
+ }
423
+
424
+ /**
425
+ * Save history to disk
426
+ */
427
+ private saveHistory(): void {
428
+ this.history.lastActivityAt = new Date().toISOString();
429
+ writeFileSync(this.historyPath, JSON.stringify(this.history, null, 2));
430
+ }
431
+
432
+ /**
433
+ * Get session history
434
+ */
435
+ getHistory(): SessionHistory {
436
+ return this.history;
437
+ }
438
+
439
+ /**
440
+ * Cancel current execution
441
+ */
442
+ cancel(): void {
443
+ if (this.currentRunner) {
444
+ this.currentRunner.cleanup();
445
+ this.currentRunner = null;
446
+ this.queueOutput('\n⚠ Execution cancelled\n');
447
+ this.flushOutputQueue();
448
+ }
449
+ }
450
+
451
+ /**
452
+ * Cleanup queue processor on shutdown
453
+ */
454
+ destroy(): void {
455
+ if (this.queueTimer) {
456
+ clearInterval(this.queueTimer);
457
+ this.queueTimer = null;
458
+ }
459
+ this.flushOutputQueue(); // Final flush
460
+ }
461
+
462
+ /**
463
+ * Clear session history and reset to fresh Claude session
464
+ * This resets the isFirstPrompt flag and claudeSessionId so the next prompt starts a new session
465
+ */
466
+ clearHistory(): void {
467
+ this.history.movements = [];
468
+ this.history.totalTokens = 0;
469
+ this.accumulatedKnowledge = '';
470
+ this.isFirstPrompt = true; // Reset to start fresh Claude session
471
+ this.claudeSessionId = undefined; // Clear Claude session ID to start new conversation
472
+ this.saveHistory();
473
+ this.emit('onSessionUpdate', this.getHistory());
474
+ }
475
+
476
+ /**
477
+ * Request user approval for a plan
478
+ * Returns a promise that resolves when the user approves/rejects
479
+ */
480
+ async requestApproval(plan: any): Promise<boolean> {
481
+ return new Promise((resolve) => {
482
+ this.pendingApproval = { plan, resolve };
483
+ this.emit('onApprovalRequired', plan);
484
+ });
485
+ }
486
+
487
+ /**
488
+ * Respond to approval request
489
+ */
490
+ respondToApproval(approved: boolean): void {
491
+ if (this.pendingApproval) {
492
+ this.pendingApproval.resolve(approved);
493
+ this.pendingApproval = undefined;
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Get session metadata
499
+ */
500
+ getSessionInfo() {
501
+ return {
502
+ sessionId: this.sessionId,
503
+ startTime: this.history.startedAt,
504
+ workingDir: this.options.workingDir,
505
+ totalTokens: this.history.totalTokens,
506
+ tokenBudgetThreshold: this.options.tokenBudgetThreshold,
507
+ movementCount: this.history.movements.length
508
+ };
509
+ }
510
+
511
+ /**
512
+ * Start a new session with fresh context
513
+ * Creates a completely new session manager with isFirstPrompt=true and no claudeSessionId,
514
+ * ensuring the next prompt starts a fresh Claude conversation (proper tab isolation)
515
+ */
516
+ startNewSession(): ImprovisationSessionManager {
517
+ // Save current session
518
+ this.saveHistory();
519
+
520
+ // Create new session manager - the new instance has:
521
+ // - isFirstPrompt=true by default
522
+ // - claudeSessionId=undefined by default
523
+ // This means the first prompt will start a completely fresh Claude conversation
524
+ const newSession = new ImprovisationSessionManager({
525
+ ...this.options,
526
+ sessionId: `improv-${Date.now()}`
527
+ });
528
+
529
+ return newSession;
530
+ }
531
+ }