@hackerai/local 0.5.1 → 0.6.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.
@@ -7,9 +7,11 @@
7
7
  *
8
8
  * Cross-platform: macOS (forkpty), Linux (forkpty), Windows (conpty).
9
9
  */
10
- var __importDefault = (this && this.__importDefault) || function (mod) {
11
- return (mod && mod.__esModule) ? mod : { "default": mod };
12
- };
10
+ var __importDefault =
11
+ (this && this.__importDefault) ||
12
+ function (mod) {
13
+ return mod && mod.__esModule ? mod : { default: mod };
14
+ };
13
15
  Object.defineProperty(exports, "__esModule", { value: true });
14
16
  exports.PtyManager = void 0;
15
17
  const os_1 = __importDefault(require("os"));
@@ -18,418 +20,433 @@ const path_1 = __importDefault(require("path"));
18
20
  const utils_1 = require("./utils");
19
21
  // Convex function references
20
22
  const api = {
21
- localSandbox: {
22
- activatePtySession: "localSandbox:activatePtySession",
23
- submitPtyOutput: "localSandbox:submitPtyOutput",
24
- updatePtySessionStatus: "localSandbox:updatePtySessionStatus",
25
- markPtyInputConsumed: "localSandbox:markPtyInputConsumed",
26
- subscribeToPtyInput: "localSandbox:subscribeToPtyInput",
27
- getPendingPtySessions: "localSandbox:getPendingPtySessions",
28
- },
23
+ localSandbox: {
24
+ activatePtySession: "localSandbox:activatePtySession",
25
+ submitPtyOutput: "localSandbox:submitPtyOutput",
26
+ updatePtySessionStatus: "localSandbox:updatePtySessionStatus",
27
+ markPtyInputConsumed: "localSandbox:markPtyInputConsumed",
28
+ subscribeToPtyInput: "localSandbox:subscribeToPtyInput",
29
+ getPendingPtySessions: "localSandbox:getPendingPtySessions",
30
+ },
29
31
  };
30
32
  // ANSI color codes
31
33
  const chalk = {
32
- blue: (s) => `\x1b[34m${s}\x1b[0m`,
33
- green: (s) => `\x1b[32m${s}\x1b[0m`,
34
- red: (s) => `\x1b[31m${s}\x1b[0m`,
35
- yellow: (s) => `\x1b[33m${s}\x1b[0m`,
36
- cyan: (s) => `\x1b[36m${s}\x1b[0m`,
37
- gray: (s) => `\x1b[90m${s}\x1b[0m`,
34
+ blue: (s) => `\x1b[34m${s}\x1b[0m`,
35
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
36
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
37
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
38
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
39
+ gray: (s) => `\x1b[90m${s}\x1b[0m`,
38
40
  };
39
41
  // Output batching: buffer for ~75ms before sending to reduce Convex write frequency
40
42
  const OUTPUT_BATCH_INTERVAL_MS = 75;
41
43
  // Max size of a single output chunk (base64 encoded) to stay under Convex limits
42
44
  const MAX_CHUNK_SIZE = 64 * 1024; // 64KB
43
45
  class PtyManager {
44
- convex;
45
- token;
46
- connectionId;
47
- session;
48
- mode;
49
- containerId;
50
- containerShell;
51
- sessions = new Map();
52
- sessionSubscription = null;
53
- ptyAvailable;
54
- constructor(convex, token, connectionId, session, mode, containerId, containerShell) {
55
- this.convex = convex;
56
- this.token = token;
57
- this.connectionId = connectionId;
58
- this.session = session;
59
- this.mode = mode;
60
- this.containerId = containerId;
61
- this.containerShell = containerShell;
62
- // Ensure node-pty's spawn-helper binary is executable (npm may strip the +x bit)
63
- this.fixSpawnHelperPermissions();
64
- this.ptyAvailable = (0, utils_1.isNodePtyAvailable)();
65
- if (!this.ptyAvailable) {
66
- console.log(chalk.yellow("⚠️ node-pty not available - PTY sessions will not be supported"));
67
- }
46
+ convex;
47
+ token;
48
+ connectionId;
49
+ session;
50
+ mode;
51
+ containerId;
52
+ containerShell;
53
+ sessions = new Map();
54
+ sessionSubscription = null;
55
+ ptyAvailable;
56
+ constructor(
57
+ convex,
58
+ token,
59
+ connectionId,
60
+ session,
61
+ mode,
62
+ containerId,
63
+ containerShell,
64
+ ) {
65
+ this.convex = convex;
66
+ this.token = token;
67
+ this.connectionId = connectionId;
68
+ this.session = session;
69
+ this.mode = mode;
70
+ this.containerId = containerId;
71
+ this.containerShell = containerShell;
72
+ // Ensure node-pty's spawn-helper binary is executable (npm may strip the +x bit)
73
+ this.fixSpawnHelperPermissions();
74
+ this.ptyAvailable = (0, utils_1.isNodePtyAvailable)();
75
+ if (!this.ptyAvailable) {
76
+ console.log(
77
+ chalk.yellow(
78
+ "⚠️ node-pty not available - PTY sessions will not be supported",
79
+ ),
80
+ );
68
81
  }
69
- /**
70
- * Fix node-pty spawn-helper permissions.
71
- * npm/pnpm can strip the execute bit from prebuilt binaries during install.
72
- * Without +x, posix_spawnp fails on macOS/Linux.
73
- */
74
- fixSpawnHelperPermissions() {
82
+ }
83
+ /**
84
+ * Fix node-pty spawn-helper permissions.
85
+ * npm/pnpm can strip the execute bit from prebuilt binaries during install.
86
+ * Without +x, posix_spawnp fails on macOS/Linux.
87
+ */
88
+ fixSpawnHelperPermissions() {
89
+ try {
90
+ // Resolve node-pty's prebuilds directory
91
+ const nodePtyDir = path_1.default.dirname(
92
+ require.resolve("node-pty/package.json"),
93
+ );
94
+ const prebuildsDir = path_1.default.join(nodePtyDir, "prebuilds");
95
+ if (!fs_1.default.existsSync(prebuildsDir)) return;
96
+ for (const platformDir of fs_1.default.readdirSync(prebuildsDir)) {
97
+ const helperPath = path_1.default.join(
98
+ prebuildsDir,
99
+ platformDir,
100
+ "spawn-helper",
101
+ );
102
+ if (!fs_1.default.existsSync(helperPath)) continue;
75
103
  try {
76
- // Resolve node-pty's prebuilds directory
77
- const nodePtyDir = path_1.default.dirname(require.resolve("node-pty/package.json"));
78
- const prebuildsDir = path_1.default.join(nodePtyDir, "prebuilds");
79
- if (!fs_1.default.existsSync(prebuildsDir))
80
- return;
81
- for (const platformDir of fs_1.default.readdirSync(prebuildsDir)) {
82
- const helperPath = path_1.default.join(prebuildsDir, platformDir, "spawn-helper");
83
- if (!fs_1.default.existsSync(helperPath))
84
- continue;
85
- try {
86
- const stat = fs_1.default.statSync(helperPath);
87
- // Check if execute bit is missing for owner
88
- if ((stat.mode & 0o100) === 0) {
89
- fs_1.default.chmodSync(helperPath, 0o755);
90
- console.debug(`Fixed spawn-helper permissions: ${platformDir}/spawn-helper`);
91
- }
92
- }
93
- catch {
94
- // Non-critical — may not have permission to chmod
95
- }
96
- }
97
- }
98
- catch {
99
- // Non-critical — node-pty might not be installed
104
+ const stat = fs_1.default.statSync(helperPath);
105
+ // Check if execute bit is missing for owner
106
+ if ((stat.mode & 0o100) === 0) {
107
+ fs_1.default.chmodSync(helperPath, 0o755);
108
+ console.debug(
109
+ `Fixed spawn-helper permissions: ${platformDir}/spawn-helper`,
110
+ );
111
+ }
112
+ } catch {
113
+ // Non-critical — may not have permission to chmod
100
114
  }
115
+ }
116
+ } catch {
117
+ // Non-critical — node-pty might not be installed
101
118
  }
102
- /**
103
- * Update the signed session (called after heartbeat refresh).
104
- */
105
- updateSession(session) {
106
- this.session = session;
107
- // Restart subscription with new session
108
- this.stopSessionSubscription();
109
- this.startSessionSubscription();
110
- }
111
- /**
112
- * Start listening for new PTY session requests from the backend.
113
- */
114
- startSessionSubscription() {
115
- if (!this.ptyAvailable)
116
- return;
117
- if (this.sessionSubscription)
118
- return;
119
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
120
- this.sessionSubscription = this.convex.onUpdate(api.localSandbox.getPendingPtySessions, {
121
- connectionId: this.connectionId,
122
- session: {
123
- userId: this.session.userId,
124
- expiresAt: this.session.expiresAt,
125
- signature: this.session.signature,
126
- },
127
- }, async (data) => {
128
- if (data?.authError) {
129
- console.debug("PTY session subscription: auth error, will refresh on next heartbeat");
130
- return;
131
- }
132
- if (!data?.sessions)
133
- return;
134
- for (const req of data.sessions) {
135
- await this.handleCreateSession(req);
136
- }
137
- });
138
- }
139
- /**
140
- * Stop the session subscription.
141
- */
142
- stopSessionSubscription() {
143
- if (this.sessionSubscription) {
144
- this.sessionSubscription();
145
- this.sessionSubscription = null;
119
+ }
120
+ /**
121
+ * Update the signed session (called after heartbeat refresh).
122
+ */
123
+ updateSession(session) {
124
+ this.session = session;
125
+ // Restart subscription with new session
126
+ this.stopSessionSubscription();
127
+ this.startSessionSubscription();
128
+ }
129
+ /**
130
+ * Start listening for new PTY session requests from the backend.
131
+ */
132
+ startSessionSubscription() {
133
+ if (!this.ptyAvailable) return;
134
+ if (this.sessionSubscription) return;
135
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
136
+ this.sessionSubscription = this.convex.onUpdate(
137
+ api.localSandbox.getPendingPtySessions,
138
+ {
139
+ connectionId: this.connectionId,
140
+ session: {
141
+ userId: this.session.userId,
142
+ expiresAt: this.session.expiresAt,
143
+ signature: this.session.signature,
144
+ },
145
+ },
146
+ async (data) => {
147
+ if (data?.authError) {
148
+ console.debug(
149
+ "PTY session subscription: auth error, will refresh on next heartbeat",
150
+ );
151
+ return;
146
152
  }
147
- }
148
- /**
149
- * Handle a request to create a new PTY session.
150
- */
151
- async handleCreateSession(req) {
152
- if (this.sessions.has(req.session_id))
153
- return; // Already created
154
- console.log(chalk.cyan(`▶ PTY session: ${req.session_id.slice(0, 8)}...`));
155
- try {
156
- // Dynamic import of node-pty to handle cases where it's not available
157
- // eslint-disable-next-line @typescript-eslint/no-var-requires
158
- const nodePty = require("node-pty");
159
- // Validate cwd exists on the host for dangerous mode.
160
- // E2B PTY sessions pass cwd="/home/user" which doesn't exist on macOS/Windows.
161
- // For Docker mode, cwd is passed to `docker exec -w` so it's inside the container.
162
- let effectiveCwd = req.cwd;
163
- if (this.mode === "dangerous" && effectiveCwd) {
164
- try {
165
- if (!fs_1.default.existsSync(effectiveCwd)) {
166
- console.debug(`PTY cwd "${effectiveCwd}" not found, falling back to home dir`);
167
- effectiveCwd = os_1.default.homedir();
168
- }
169
- }
170
- catch {
171
- effectiveCwd = os_1.default.homedir();
172
- }
173
- }
174
- const spawnConfig = (0, utils_1.getPtySpawnConfig)({
175
- platform: os_1.default.platform(),
176
- mode: this.mode,
177
- containerId: this.containerId,
178
- containerShell: this.containerShell,
179
- cols: req.cols,
180
- rows: req.rows,
181
- cwd: effectiveCwd,
182
- env: req.env,
183
- });
184
- const ptyProcess = nodePty.spawn(spawnConfig.file, spawnConfig.args, {
185
- name: "xterm-256color",
186
- ...spawnConfig.options,
187
- });
188
- const activeSession = {
189
- sessionId: req.session_id,
190
- process: ptyProcess,
191
- outputBuffer: "",
192
- outputSequence: 0,
193
- batchTimer: null,
194
- inputSubscription: null,
195
- };
196
- this.sessions.set(req.session_id, activeSession);
197
- // Report PID and activate session
198
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
199
- await this.convex.mutation(api.localSandbox.activatePtySession, {
200
- token: this.token,
201
- sessionId: req.session_id,
202
- pid: ptyProcess.pid,
203
- });
204
- // Set up output handler with batching
205
- ptyProcess.onData((data) => {
206
- this.bufferOutput(activeSession, data);
207
- });
208
- // Set up exit handler
209
- ptyProcess.onExit(async (e) => {
210
- // Flush any remaining output
211
- await this.flushOutput(activeSession);
212
- // Report exit
213
- try {
214
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
215
- await this.convex.mutation(api.localSandbox.updatePtySessionStatus, {
216
- token: this.token,
217
- sessionId: req.session_id,
218
- status: "exited",
219
- exitCode: e.exitCode,
220
- });
221
- }
222
- catch (err) {
223
- console.debug(`Failed to report PTY exit: ${err}`);
224
- }
225
- this.cleanupSession(req.session_id);
226
- console.log(chalk.green(`✓ PTY session ${req.session_id.slice(0, 8)}... exited (code: ${e.exitCode})`));
227
- });
228
- // Start listening for stdin input
229
- this.startInputSubscription(activeSession);
230
- console.log(chalk.green(`✓ PTY session ${req.session_id.slice(0, 8)}... active (PID: ${ptyProcess.pid})`));
231
- }
232
- catch (err) {
233
- console.error(chalk.red(`✗ Failed to create PTY session: ${err}`));
234
- // Report failure
235
- try {
236
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
237
- await this.convex.mutation(api.localSandbox.updatePtySessionStatus, {
238
- token: this.token,
239
- sessionId: req.session_id,
240
- status: "exited",
241
- exitCode: -1,
242
- });
243
- }
244
- catch {
245
- /* ignore */
246
- }
153
+ if (!data?.sessions) return;
154
+ for (const req of data.sessions) {
155
+ await this.handleCreateSession(req);
247
156
  }
157
+ },
158
+ );
159
+ }
160
+ /**
161
+ * Stop the session subscription.
162
+ */
163
+ stopSessionSubscription() {
164
+ if (this.sessionSubscription) {
165
+ this.sessionSubscription();
166
+ this.sessionSubscription = null;
248
167
  }
249
- /**
250
- * Buffer PTY output and flush periodically to reduce Convex writes.
251
- */
252
- bufferOutput(session, data) {
253
- session.outputBuffer += data;
254
- // If buffer is getting large, flush immediately
255
- if (session.outputBuffer.length >= MAX_CHUNK_SIZE) {
256
- this.flushOutput(session);
257
- return;
258
- }
259
- // Otherwise, batch with a short timer
260
- if (!session.batchTimer) {
261
- session.batchTimer = setTimeout(() => {
262
- session.batchTimer = null;
263
- this.flushOutput(session);
264
- }, OUTPUT_BATCH_INTERVAL_MS);
265
- }
266
- }
267
- /**
268
- * Flush buffered output to Convex.
269
- */
270
- async flushOutput(session) {
271
- if (session.batchTimer) {
272
- clearTimeout(session.batchTimer);
273
- session.batchTimer = null;
274
- }
275
- if (!session.outputBuffer)
276
- return;
277
- const data = session.outputBuffer;
278
- session.outputBuffer = "";
279
- // Split into chunks if necessary
280
- const chunks = [];
281
- for (let i = 0; i < data.length; i += MAX_CHUNK_SIZE) {
282
- const chunk = data.slice(i, i + MAX_CHUNK_SIZE);
283
- // Encode as base64 to safely transport binary PTY data through Convex
284
- const encoded = Buffer.from(chunk, "utf-8").toString("base64");
285
- chunks.push({
286
- data: encoded,
287
- sequence: ++session.outputSequence,
288
- });
289
- }
168
+ }
169
+ /**
170
+ * Handle a request to create a new PTY session.
171
+ */
172
+ async handleCreateSession(req) {
173
+ if (this.sessions.has(req.session_id)) return; // Already created
174
+ console.log(chalk.cyan(`▶ PTY session: ${req.session_id.slice(0, 8)}...`));
175
+ try {
176
+ // Dynamic import of node-pty to handle cases where it's not available
177
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
178
+ const nodePty = require("node-pty");
179
+ // Validate cwd exists on the host for dangerous mode.
180
+ // E2B PTY sessions pass cwd="/home/user" which doesn't exist on macOS/Windows.
181
+ // For Docker mode, cwd is passed to `docker exec -w` so it's inside the container.
182
+ let effectiveCwd = req.cwd;
183
+ if (this.mode === "dangerous" && effectiveCwd) {
290
184
  try {
291
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
292
- await this.convex.mutation(api.localSandbox.submitPtyOutput, {
293
- token: this.token,
294
- sessionId: session.sessionId,
295
- chunks,
296
- });
185
+ if (!fs_1.default.existsSync(effectiveCwd)) {
186
+ console.debug(
187
+ `PTY cwd "${effectiveCwd}" not found, falling back to home dir`,
188
+ );
189
+ effectiveCwd = os_1.default.homedir();
190
+ }
191
+ } catch {
192
+ effectiveCwd = os_1.default.homedir();
297
193
  }
298
- catch (err) {
299
- console.debug(`Failed to submit PTY output: ${err}`);
194
+ }
195
+ const spawnConfig = (0, utils_1.getPtySpawnConfig)({
196
+ platform: os_1.default.platform(),
197
+ mode: this.mode,
198
+ containerId: this.containerId,
199
+ containerShell: this.containerShell,
200
+ cols: req.cols,
201
+ rows: req.rows,
202
+ cwd: effectiveCwd,
203
+ env: req.env,
204
+ });
205
+ const ptyProcess = nodePty.spawn(spawnConfig.file, spawnConfig.args, {
206
+ name: "xterm-256color",
207
+ ...spawnConfig.options,
208
+ });
209
+ const activeSession = {
210
+ sessionId: req.session_id,
211
+ process: ptyProcess,
212
+ outputBuffer: "",
213
+ outputSequence: 0,
214
+ batchTimer: null,
215
+ inputSubscription: null,
216
+ };
217
+ this.sessions.set(req.session_id, activeSession);
218
+ // Report PID and activate session
219
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
220
+ await this.convex.mutation(api.localSandbox.activatePtySession, {
221
+ token: this.token,
222
+ sessionId: req.session_id,
223
+ pid: ptyProcess.pid,
224
+ });
225
+ // Set up output handler with batching
226
+ ptyProcess.onData((data) => {
227
+ this.bufferOutput(activeSession, data);
228
+ });
229
+ // Set up exit handler
230
+ ptyProcess.onExit(async (e) => {
231
+ // Flush any remaining output
232
+ await this.flushOutput(activeSession);
233
+ // Report exit
234
+ try {
235
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
236
+ await this.convex.mutation(api.localSandbox.updatePtySessionStatus, {
237
+ token: this.token,
238
+ sessionId: req.session_id,
239
+ status: "exited",
240
+ exitCode: e.exitCode,
241
+ });
242
+ } catch (err) {
243
+ console.debug(`Failed to report PTY exit: ${err}`);
300
244
  }
301
- }
302
- /**
303
- * Subscribe to stdin input from the backend for a session.
304
- */
305
- startInputSubscription(session) {
245
+ this.cleanupSession(req.session_id);
246
+ console.log(
247
+ chalk.green(
248
+ `✓ PTY session ${req.session_id.slice(0, 8)}... exited (code: ${e.exitCode})`,
249
+ ),
250
+ );
251
+ });
252
+ // Start listening for stdin input
253
+ this.startInputSubscription(activeSession);
254
+ console.log(
255
+ chalk.green(
256
+ `✓ PTY session ${req.session_id.slice(0, 8)}... active (PID: ${ptyProcess.pid})`,
257
+ ),
258
+ );
259
+ } catch (err) {
260
+ console.error(chalk.red(`✗ Failed to create PTY session: ${err}`));
261
+ // Report failure
262
+ try {
306
263
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
307
- session.inputSubscription = this.convex.onUpdate(api.localSandbox.subscribeToPtyInput, {
308
- sessionId: session.sessionId,
309
- session: {
310
- userId: this.session.userId,
311
- expiresAt: this.session.expiresAt,
312
- signature: this.session.signature,
313
- },
314
- connectionId: this.connectionId,
315
- }, async (data) => {
316
- if (data?.authError)
317
- return;
318
- if (!data?.inputs?.length)
319
- return;
320
- const inputIds = [];
321
- for (const input of data.inputs) {
322
- // Decode base64 data and write to PTY
323
- const decoded = Buffer.from(input.data, "base64").toString("utf-8");
324
- try {
325
- session.process.write(decoded);
326
- }
327
- catch {
328
- // Process may have exited
329
- }
330
- inputIds.push(input._id);
331
- }
332
- // Mark inputs as consumed
333
- if (inputIds.length > 0) {
334
- try {
335
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
336
- await this.convex.mutation(api.localSandbox.markPtyInputConsumed, {
337
- token: this.token,
338
- inputIds,
339
- });
340
- }
341
- catch {
342
- /* ignore */
343
- }
344
- }
264
+ await this.convex.mutation(api.localSandbox.updatePtySessionStatus, {
265
+ token: this.token,
266
+ sessionId: req.session_id,
267
+ status: "exited",
268
+ exitCode: -1,
345
269
  });
270
+ } catch {
271
+ /* ignore */
272
+ }
346
273
  }
347
- /**
348
- * Handle a kill request for a session (called when backend detects killed status).
349
- */
350
- killSession(sessionId) {
351
- const session = this.sessions.get(sessionId);
352
- if (!session)
353
- return false;
354
- try {
355
- session.process.kill();
356
- }
357
- catch {
358
- /* may already be dead */
359
- }
360
- this.cleanupSession(sessionId);
361
- return true;
274
+ }
275
+ /**
276
+ * Buffer PTY output and flush periodically to reduce Convex writes.
277
+ */
278
+ bufferOutput(session, data) {
279
+ session.outputBuffer += data;
280
+ // If buffer is getting large, flush immediately
281
+ if (session.outputBuffer.length >= MAX_CHUNK_SIZE) {
282
+ this.flushOutput(session);
283
+ return;
362
284
  }
363
- /**
364
- * Resize a PTY session.
365
- */
366
- resizeSession(sessionId, cols, rows) {
367
- const session = this.sessions.get(sessionId);
368
- if (!session)
369
- return false;
370
- try {
371
- session.process.resize(cols, rows);
372
- return true;
373
- }
374
- catch {
375
- return false;
376
- }
285
+ // Otherwise, batch with a short timer
286
+ if (!session.batchTimer) {
287
+ session.batchTimer = setTimeout(() => {
288
+ session.batchTimer = null;
289
+ this.flushOutput(session);
290
+ }, OUTPUT_BATCH_INTERVAL_MS);
291
+ }
292
+ }
293
+ /**
294
+ * Flush buffered output to Convex.
295
+ */
296
+ async flushOutput(session) {
297
+ if (session.batchTimer) {
298
+ clearTimeout(session.batchTimer);
299
+ session.batchTimer = null;
300
+ }
301
+ if (!session.outputBuffer) return;
302
+ const data = session.outputBuffer;
303
+ session.outputBuffer = "";
304
+ // Split into chunks if necessary
305
+ const chunks = [];
306
+ for (let i = 0; i < data.length; i += MAX_CHUNK_SIZE) {
307
+ const chunk = data.slice(i, i + MAX_CHUNK_SIZE);
308
+ // Encode as base64 to safely transport binary PTY data through Convex
309
+ const encoded = Buffer.from(chunk, "utf-8").toString("base64");
310
+ chunks.push({
311
+ data: encoded,
312
+ sequence: ++session.outputSequence,
313
+ });
377
314
  }
378
- /**
379
- * Clean up a session's resources.
380
- */
381
- cleanupSession(sessionId) {
382
- const session = this.sessions.get(sessionId);
383
- if (!session)
384
- return;
385
- if (session.batchTimer) {
386
- clearTimeout(session.batchTimer);
315
+ try {
316
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
317
+ await this.convex.mutation(api.localSandbox.submitPtyOutput, {
318
+ token: this.token,
319
+ sessionId: session.sessionId,
320
+ chunks,
321
+ });
322
+ } catch (err) {
323
+ console.debug(`Failed to submit PTY output: ${err}`);
324
+ }
325
+ }
326
+ /**
327
+ * Subscribe to stdin input from the backend for a session.
328
+ */
329
+ startInputSubscription(session) {
330
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
331
+ session.inputSubscription = this.convex.onUpdate(
332
+ api.localSandbox.subscribeToPtyInput,
333
+ {
334
+ sessionId: session.sessionId,
335
+ session: {
336
+ userId: this.session.userId,
337
+ expiresAt: this.session.expiresAt,
338
+ signature: this.session.signature,
339
+ },
340
+ connectionId: this.connectionId,
341
+ },
342
+ async (data) => {
343
+ if (data?.authError) return;
344
+ if (!data?.inputs?.length) return;
345
+ const inputIds = [];
346
+ for (const input of data.inputs) {
347
+ // Decode base64 data and write to PTY
348
+ const decoded = Buffer.from(input.data, "base64").toString("utf-8");
349
+ try {
350
+ session.process.write(decoded);
351
+ } catch {
352
+ // Process may have exited
353
+ }
354
+ inputIds.push(input._id);
387
355
  }
388
- if (session.inputSubscription) {
389
- session.inputSubscription();
356
+ // Mark inputs as consumed
357
+ if (inputIds.length > 0) {
358
+ try {
359
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
360
+ await this.convex.mutation(api.localSandbox.markPtyInputConsumed, {
361
+ token: this.token,
362
+ inputIds,
363
+ });
364
+ } catch {
365
+ /* ignore */
366
+ }
390
367
  }
391
- this.sessions.delete(sessionId);
368
+ },
369
+ );
370
+ }
371
+ /**
372
+ * Handle a kill request for a session (called when backend detects killed status).
373
+ */
374
+ killSession(sessionId) {
375
+ const session = this.sessions.get(sessionId);
376
+ if (!session) return false;
377
+ try {
378
+ session.process.kill();
379
+ } catch {
380
+ /* may already be dead */
392
381
  }
393
- /**
394
- * Clean up all sessions and stop subscriptions.
395
- */
396
- async cleanup() {
397
- this.stopSessionSubscription();
398
- for (const [sessionId, session] of this.sessions) {
399
- try {
400
- session.process.kill();
401
- }
402
- catch {
403
- /* ignore */
404
- }
405
- // Report exit
406
- try {
407
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
408
- await this.convex.mutation(api.localSandbox.updatePtySessionStatus, {
409
- token: this.token,
410
- sessionId,
411
- status: "exited",
412
- exitCode: -1,
413
- });
414
- }
415
- catch {
416
- /* ignore */
417
- }
418
- this.cleanupSession(sessionId);
419
- }
382
+ this.cleanupSession(sessionId);
383
+ return true;
384
+ }
385
+ /**
386
+ * Resize a PTY session.
387
+ */
388
+ resizeSession(sessionId, cols, rows) {
389
+ const session = this.sessions.get(sessionId);
390
+ if (!session) return false;
391
+ try {
392
+ session.process.resize(cols, rows);
393
+ return true;
394
+ } catch {
395
+ return false;
396
+ }
397
+ }
398
+ /**
399
+ * Clean up a session's resources.
400
+ */
401
+ cleanupSession(sessionId) {
402
+ const session = this.sessions.get(sessionId);
403
+ if (!session) return;
404
+ if (session.batchTimer) {
405
+ clearTimeout(session.batchTimer);
420
406
  }
421
- /**
422
- * Check if PTY support is available.
423
- */
424
- get isAvailable() {
425
- return this.ptyAvailable;
407
+ if (session.inputSubscription) {
408
+ session.inputSubscription();
426
409
  }
427
- /**
428
- * Get the number of active sessions.
429
- */
430
- get activeSessionCount() {
431
- return this.sessions.size;
410
+ this.sessions.delete(sessionId);
411
+ }
412
+ /**
413
+ * Clean up all sessions and stop subscriptions.
414
+ */
415
+ async cleanup() {
416
+ this.stopSessionSubscription();
417
+ for (const [sessionId, session] of this.sessions) {
418
+ try {
419
+ session.process.kill();
420
+ } catch {
421
+ /* ignore */
422
+ }
423
+ // Report exit
424
+ try {
425
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
426
+ await this.convex.mutation(api.localSandbox.updatePtySessionStatus, {
427
+ token: this.token,
428
+ sessionId,
429
+ status: "exited",
430
+ exitCode: -1,
431
+ });
432
+ } catch {
433
+ /* ignore */
434
+ }
435
+ this.cleanupSession(sessionId);
432
436
  }
437
+ }
438
+ /**
439
+ * Check if PTY support is available.
440
+ */
441
+ get isAvailable() {
442
+ return this.ptyAvailable;
443
+ }
444
+ /**
445
+ * Get the number of active sessions.
446
+ */
447
+ get activeSessionCount() {
448
+ return this.sessions.size;
449
+ }
433
450
  }
434
451
  exports.PtyManager = PtyManager;
435
- //# sourceMappingURL=pty-manager.js.map
452
+ //# sourceMappingURL=pty-manager.js.map