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,426 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Tmux Session Manager - Provides persistent terminal sessions via tmux
6
+ *
7
+ * When enabled, terminals are run inside tmux sessions, which allows:
8
+ * - Sessions to survive client restarts
9
+ * - True process persistence across browser disconnections
10
+ * - Session restoration even after server restart
11
+ *
12
+ * Tmux sessions are named with a prefix to identify them as mstro-managed.
13
+ */
14
+
15
+ import { spawnSync } from 'node:child_process';
16
+ import { EventEmitter } from 'node:events';
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
18
+ import { homedir } from 'node:os';
19
+ import { join } from 'node:path';
20
+
21
+ const MSTRO_TMUX_PREFIX = 'mstro-terminal-';
22
+ const SESSION_REGISTRY_PATH = join(homedir(), '.mstro', 'terminal-sessions.json');
23
+
24
+ export interface TmuxSession {
25
+ terminalId: string;
26
+ tmuxSessionName: string;
27
+ shell: string;
28
+ cwd: string;
29
+ createdAt: number;
30
+ lastAttachedAt: number;
31
+ }
32
+
33
+ interface SessionRegistry {
34
+ sessions: TmuxSession[];
35
+ }
36
+
37
+ /**
38
+ * Check if tmux is available on the system
39
+ */
40
+ export function isTmuxAvailable(): boolean {
41
+ try {
42
+ const result = spawnSync('which', ['tmux'], { encoding: 'utf-8' });
43
+ return result.status === 0 && result.stdout.trim().length > 0;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * TmuxManager - Manages tmux-backed terminal sessions
51
+ */
52
+ export class TmuxManager extends EventEmitter {
53
+ private sessions: Map<string, TmuxSession> = new Map();
54
+ private outputHandlers: Map<string, NodeJS.Timeout> = new Map();
55
+ private tmuxAvailable: boolean;
56
+
57
+ constructor() {
58
+ super();
59
+ this.tmuxAvailable = isTmuxAvailable();
60
+
61
+ if (this.tmuxAvailable) {
62
+ this.loadRegistry();
63
+ this.syncWithTmux();
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Check if tmux persistence is available
69
+ */
70
+ isAvailable(): boolean {
71
+ return this.tmuxAvailable;
72
+ }
73
+
74
+ /**
75
+ * Load session registry from disk
76
+ */
77
+ private loadRegistry(): void {
78
+ try {
79
+ if (existsSync(SESSION_REGISTRY_PATH)) {
80
+ const data = readFileSync(SESSION_REGISTRY_PATH, 'utf-8');
81
+ const registry: SessionRegistry = JSON.parse(data);
82
+ for (const session of registry.sessions) {
83
+ this.sessions.set(session.terminalId, session);
84
+ }
85
+ }
86
+ } catch (error) {
87
+ console.error('[TmuxManager] Failed to load registry:', error);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Save session registry to disk
93
+ */
94
+ private saveRegistry(): void {
95
+ try {
96
+ const dir = join(homedir(), '.mstro');
97
+ if (!existsSync(dir)) {
98
+ mkdirSync(dir, { recursive: true });
99
+ }
100
+
101
+ const registry: SessionRegistry = {
102
+ sessions: Array.from(this.sessions.values()),
103
+ };
104
+ writeFileSync(SESSION_REGISTRY_PATH, JSON.stringify(registry, null, 2));
105
+ } catch (error) {
106
+ console.error('[TmuxManager] Failed to save registry:', error);
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Sync our registry with actual tmux sessions
112
+ * Remove sessions that no longer exist in tmux
113
+ */
114
+ private syncWithTmux(): void {
115
+ if (!this.tmuxAvailable) return;
116
+
117
+ try {
118
+ // List all tmux sessions
119
+ const result = spawnSync('tmux', ['list-sessions', '-F', '#{session_name}'], {
120
+ encoding: 'utf-8',
121
+ });
122
+
123
+ if (result.status !== 0) {
124
+ // No tmux server running - clear all sessions
125
+ this.sessions.clear();
126
+ this.saveRegistry();
127
+ return;
128
+ }
129
+
130
+ const existingSessions = new Set(
131
+ result.stdout
132
+ .trim()
133
+ .split('\n')
134
+ .filter((name) => name.startsWith(MSTRO_TMUX_PREFIX))
135
+ );
136
+
137
+ // Remove sessions that no longer exist
138
+ for (const [terminalId, session] of this.sessions) {
139
+ if (!existingSessions.has(session.tmuxSessionName)) {
140
+ this.sessions.delete(terminalId);
141
+ }
142
+ }
143
+
144
+ this.saveRegistry();
145
+ } catch (error) {
146
+ console.error('[TmuxManager] Failed to sync with tmux:', error);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Check if a session exists
152
+ */
153
+ exists(terminalId: string): boolean {
154
+ const session = this.sessions.get(terminalId);
155
+ if (!session) return false;
156
+
157
+ // Verify it still exists in tmux
158
+ try {
159
+ const result = spawnSync('tmux', ['has-session', '-t', session.tmuxSessionName], {
160
+ encoding: 'utf-8',
161
+ });
162
+ return result.status === 0;
163
+ } catch {
164
+ return false;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Get session info
170
+ */
171
+ getSessionInfo(terminalId: string): TmuxSession | null {
172
+ return this.sessions.get(terminalId) || null;
173
+ }
174
+
175
+ /**
176
+ * Get all active sessions
177
+ */
178
+ getActiveSessions(): TmuxSession[] {
179
+ this.syncWithTmux();
180
+ return Array.from(this.sessions.values());
181
+ }
182
+
183
+ /**
184
+ * Create a new tmux-backed terminal session
185
+ */
186
+ create(
187
+ terminalId: string,
188
+ workingDir: string,
189
+ cols: number = 80,
190
+ rows: number = 24,
191
+ shell?: string
192
+ ): { shell: string; cwd: string; isReconnect: boolean } {
193
+ if (!this.tmuxAvailable) {
194
+ throw new Error('tmux is not available');
195
+ }
196
+
197
+ // Check if session already exists
198
+ if (this.exists(terminalId)) {
199
+ const session = this.sessions.get(terminalId)!;
200
+ session.lastAttachedAt = Date.now();
201
+ this.saveRegistry();
202
+ return {
203
+ shell: session.shell,
204
+ cwd: session.cwd,
205
+ isReconnect: true,
206
+ };
207
+ }
208
+
209
+ const tmuxSessionName = `${MSTRO_TMUX_PREFIX}${terminalId}`;
210
+ const cwd = workingDir || homedir();
211
+ const shellPath = shell || process.env.SHELL || '/bin/bash';
212
+ const shellName = shellPath.split('/').pop() || 'shell';
213
+
214
+
215
+ try {
216
+ // Create new tmux session
217
+ const result = spawnSync(
218
+ 'tmux',
219
+ [
220
+ 'new-session',
221
+ '-d', // Detached
222
+ '-s', tmuxSessionName,
223
+ '-x', cols.toString(),
224
+ '-y', rows.toString(),
225
+ '-c', cwd,
226
+ shellPath,
227
+ ],
228
+ {
229
+ encoding: 'utf-8',
230
+ cwd,
231
+ }
232
+ );
233
+
234
+ if (result.status !== 0) {
235
+ throw new Error(`Failed to create tmux session: ${result.stderr}`);
236
+ }
237
+
238
+ // Store session info
239
+ const session: TmuxSession = {
240
+ terminalId,
241
+ tmuxSessionName,
242
+ shell: shellName,
243
+ cwd,
244
+ createdAt: Date.now(),
245
+ lastAttachedAt: Date.now(),
246
+ };
247
+ this.sessions.set(terminalId, session);
248
+ this.saveRegistry();
249
+
250
+ return { shell: shellName, cwd, isReconnect: false };
251
+ } catch (error: any) {
252
+ console.error(`[TmuxManager] Failed to create session:`, error);
253
+ throw error;
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Attach to a tmux session and stream output
259
+ * Returns a function to detach
260
+ */
261
+ attach(
262
+ terminalId: string,
263
+ onOutput: (data: string) => void,
264
+ onExit: (code: number) => void
265
+ ): { write: (data: string) => void; resize: (cols: number, rows: number) => void; detach: () => void } {
266
+ const session = this.sessions.get(terminalId);
267
+ if (!session) {
268
+ throw new Error(`Session not found: ${terminalId}`);
269
+ }
270
+
271
+ // Update last attached time
272
+ session.lastAttachedAt = Date.now();
273
+ this.saveRegistry();
274
+
275
+ // Create a pipe process to capture tmux output
276
+ // We use 'tmux pipe-pane' to capture output and 'tmux send-keys' for input
277
+ const _pipePath = `/tmp/mstro-tmux-pipe-${terminalId}`;
278
+
279
+ // Start capturing output using tmux's capture-pane in a loop
280
+ let capturing = true;
281
+ let lastCaptureLength = 0;
282
+
283
+ const captureLoop = setInterval(() => {
284
+ if (!capturing) {
285
+ clearInterval(captureLoop);
286
+ return;
287
+ }
288
+
289
+ try {
290
+ // Capture the current pane content
291
+ const result = spawnSync(
292
+ 'tmux',
293
+ ['capture-pane', '-t', session.tmuxSessionName, '-p', '-S', '-100'],
294
+ { encoding: 'utf-8' }
295
+ );
296
+
297
+ if (result.status === 0) {
298
+ const output = result.stdout;
299
+ // Only emit new content
300
+ if (output.length > lastCaptureLength) {
301
+ const newContent = output.slice(lastCaptureLength);
302
+ if (newContent.trim()) {
303
+ onOutput(newContent);
304
+ }
305
+ lastCaptureLength = output.length;
306
+ }
307
+ } else if (result.stderr?.includes('no server running')) {
308
+ // Session ended
309
+ capturing = false;
310
+ clearInterval(captureLoop);
311
+ onExit(0);
312
+ this.sessions.delete(terminalId);
313
+ this.saveRegistry();
314
+ }
315
+ } catch (error) {
316
+ console.error('[TmuxManager] Capture error:', error);
317
+ }
318
+ }, 100); // Poll every 100ms
319
+
320
+ this.outputHandlers.set(terminalId, captureLoop);
321
+
322
+ return {
323
+ write: (data: string) => {
324
+ try {
325
+ // Send keys to tmux session
326
+ spawnSync('tmux', ['send-keys', '-t', session.tmuxSessionName, '-l', data], {
327
+ encoding: 'utf-8',
328
+ });
329
+ } catch (error) {
330
+ console.error('[TmuxManager] Write error:', error);
331
+ }
332
+ },
333
+
334
+ resize: (cols: number, rows: number) => {
335
+ try {
336
+ spawnSync(
337
+ 'tmux',
338
+ ['resize-window', '-t', session.tmuxSessionName, '-x', cols.toString(), '-y', rows.toString()],
339
+ { encoding: 'utf-8' }
340
+ );
341
+ } catch (_error) {
342
+ // Resize errors are not critical
343
+ }
344
+ },
345
+
346
+ detach: () => {
347
+ capturing = false;
348
+ const handler = this.outputHandlers.get(terminalId);
349
+ if (handler) {
350
+ clearInterval(handler);
351
+ this.outputHandlers.delete(terminalId);
352
+ }
353
+ },
354
+ };
355
+ }
356
+
357
+ /**
358
+ * Get scrollback/history from tmux session
359
+ */
360
+ getScrollback(terminalId: string, lines: number = 5000): string[] {
361
+ const session = this.sessions.get(terminalId);
362
+ if (!session) return [];
363
+
364
+ try {
365
+ const result = spawnSync(
366
+ 'tmux',
367
+ ['capture-pane', '-t', session.tmuxSessionName, '-p', '-S', `-${lines}`],
368
+ { encoding: 'utf-8' }
369
+ );
370
+
371
+ if (result.status === 0) {
372
+ return result.stdout.split('\n');
373
+ }
374
+ } catch (error) {
375
+ console.error('[TmuxManager] Failed to get scrollback:', error);
376
+ }
377
+
378
+ return [];
379
+ }
380
+
381
+ /**
382
+ * Close/kill a tmux session
383
+ */
384
+ close(terminalId: string): boolean {
385
+ const session = this.sessions.get(terminalId);
386
+ if (!session) return false;
387
+
388
+ // Stop output capture if running
389
+ const handler = this.outputHandlers.get(terminalId);
390
+ if (handler) {
391
+ clearInterval(handler);
392
+ this.outputHandlers.delete(terminalId);
393
+ }
394
+
395
+ try {
396
+ spawnSync('tmux', ['kill-session', '-t', session.tmuxSessionName], {
397
+ encoding: 'utf-8',
398
+ });
399
+ this.sessions.delete(terminalId);
400
+ this.saveRegistry();
401
+ return true;
402
+ } catch (error) {
403
+ console.error('[TmuxManager] Failed to close session:', error);
404
+ return false;
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Close all mstro-managed tmux sessions
410
+ */
411
+ closeAll(): void {
412
+ for (const terminalId of this.sessions.keys()) {
413
+ this.close(terminalId);
414
+ }
415
+ }
416
+ }
417
+
418
+ // Singleton instance
419
+ let tmuxManagerInstance: TmuxManager | null = null;
420
+
421
+ export function getTmuxManager(): TmuxManager {
422
+ if (!tmuxManagerInstance) {
423
+ tmuxManagerInstance = new TmuxManager();
424
+ }
425
+ return tmuxManagerInstance;
426
+ }