@hackerai/local 0.6.0 → 0.7.1

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.
package/dist/utils.d.ts CHANGED
@@ -4,41 +4,11 @@
4
4
  */
5
5
  export declare const MAX_OUTPUT_SIZE = 12288;
6
6
  export declare const TRUNCATION_MARKER = "\n\n[... OUTPUT TRUNCATED - middle content removed to fit context limits ...]\n\n";
7
- /**
8
- * Required Docker capabilities for penetration testing tools.
9
- * - NET_RAW: ping, nmap, masscan, hping3, arp-scan, tcpdump, raw sockets
10
- * - NET_ADMIN: network interface manipulation, arp-scan, netdiscover
11
- * - SYS_PTRACE: gdb, strace, ltrace (debugging tools)
12
- */
13
- export declare const DOCKER_CAPABILITIES: readonly ["NET_RAW", "NET_ADMIN", "SYS_PTRACE"];
14
7
  /**
15
8
  * Truncates output using 25% head + 75% tail strategy.
16
9
  * This preserves both the command start (context) and the end (final results/errors).
17
10
  */
18
11
  export declare function truncateOutput(content: string, maxSize?: number): string;
19
- /**
20
- * Build Docker capability flags for the docker run command.
21
- */
22
- export declare function buildDockerCapabilityFlags(): string;
23
- /**
24
- * Build the full docker run command for creating a container.
25
- */
26
- export declare function buildDockerRunCommand(options: {
27
- image: string;
28
- containerName?: string;
29
- capabilities?: boolean;
30
- }): string;
31
- /**
32
- * Determine the sandbox mode based on configuration.
33
- */
34
- export declare function getSandboxMode(config: {
35
- dangerous?: boolean;
36
- }): "docker" | "dangerous";
37
- /**
38
- * Parse shell detection output to find available shell.
39
- * Returns the first valid shell path found.
40
- */
41
- export declare function parseShellDetectionOutput(output: string): string;
42
12
  export interface ShellConfig {
43
13
  shell: string;
44
14
  shellFlag: string;
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,eAAO,MAAM,eAAe,QAAQ,CAAC;AAGrC,eAAO,MAAM,iBAAiB,sFACuD,CAAC;AAEtF;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,iDAItB,CAAC;AAEX;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,MAAwB,GAChC,MAAM,CAcR;AAED;;GAEG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CAEnD;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,GAAG,MAAM,CAOT;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE;IACrC,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,GAAG,QAAQ,GAAG,WAAW,CAKzB;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAOhE;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAM7D"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,eAAO,MAAM,eAAe,QAAQ,CAAC;AAGrC,eAAO,MAAM,iBAAiB,sFACuD,CAAC;AAEtF;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,MAAwB,GAChC,MAAM,CAcR;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAM7D"}
package/dist/utils.js CHANGED
@@ -4,28 +4,13 @@
4
4
  * Extracted for testability.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.DOCKER_CAPABILITIES = exports.TRUNCATION_MARKER = exports.MAX_OUTPUT_SIZE = void 0;
7
+ exports.TRUNCATION_MARKER = exports.MAX_OUTPUT_SIZE = void 0;
8
8
  exports.truncateOutput = truncateOutput;
9
- exports.buildDockerCapabilityFlags = buildDockerCapabilityFlags;
10
- exports.buildDockerRunCommand = buildDockerRunCommand;
11
- exports.getSandboxMode = getSandboxMode;
12
- exports.parseShellDetectionOutput = parseShellDetectionOutput;
13
9
  exports.getDefaultShell = getDefaultShell;
14
10
  // Align with LLM context limits (~4096 tokens ≈ 12288 chars)
15
11
  exports.MAX_OUTPUT_SIZE = 12288;
16
12
  // Truncation marker for 25% head + 75% tail strategy
17
13
  exports.TRUNCATION_MARKER = "\n\n[... OUTPUT TRUNCATED - middle content removed to fit context limits ...]\n\n";
18
- /**
19
- * Required Docker capabilities for penetration testing tools.
20
- * - NET_RAW: ping, nmap, masscan, hping3, arp-scan, tcpdump, raw sockets
21
- * - NET_ADMIN: network interface manipulation, arp-scan, netdiscover
22
- * - SYS_PTRACE: gdb, strace, ltrace (debugging tools)
23
- */
24
- exports.DOCKER_CAPABILITIES = [
25
- "NET_RAW",
26
- "NET_ADMIN",
27
- "SYS_PTRACE",
28
- ];
29
14
  /**
30
15
  * Truncates output using 25% head + 75% tail strategy.
31
16
  * This preserves both the command start (context) and the end (final results/errors).
@@ -42,42 +27,6 @@ function truncateOutput(content, maxSize = exports.MAX_OUTPUT_SIZE) {
42
27
  const tail = content.slice(-tailBudget);
43
28
  return head + exports.TRUNCATION_MARKER + tail;
44
29
  }
45
- /**
46
- * Build Docker capability flags for the docker run command.
47
- */
48
- function buildDockerCapabilityFlags() {
49
- return exports.DOCKER_CAPABILITIES.map((cap) => `--cap-add=${cap}`).join(" ");
50
- }
51
- /**
52
- * Build the full docker run command for creating a container.
53
- */
54
- function buildDockerRunCommand(options) {
55
- const { image, containerName, capabilities = true } = options;
56
- const nameFlag = containerName ? `--name ${containerName} ` : "";
57
- const capFlags = capabilities ? `${buildDockerCapabilityFlags()} ` : "";
58
- return `docker run -d ${nameFlag}${capFlags}--network host ${image} tail -f /dev/null`;
59
- }
60
- /**
61
- * Determine the sandbox mode based on configuration.
62
- */
63
- function getSandboxMode(config) {
64
- if (config.dangerous) {
65
- return "dangerous";
66
- }
67
- return "docker";
68
- }
69
- /**
70
- * Parse shell detection output to find available shell.
71
- * Returns the first valid shell path found.
72
- */
73
- function parseShellDetectionOutput(output) {
74
- if (!output || !output.trim()) {
75
- return "/bin/sh";
76
- }
77
- // Take first line (first result from 'command -v bash || command -v sh')
78
- const shell = output.trim().split("\n")[0];
79
- return shell || "/bin/sh";
80
- }
81
30
  /**
82
31
  * Get the default shell for a given platform.
83
32
  * On Windows, uses cmd.exe (not PowerShell, which aliases curl to Invoke-WebRequest
package/dist/utils.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAyBH,wCAiBC;AAKD,gEAEC;AAKD,sDAWC;AAKD,wCAOC;AAMD,8DAOC;AAYD,0CAMC;AA1GD,6DAA6D;AAChD,QAAA,eAAe,GAAG,KAAK,CAAC;AAErC,qDAAqD;AACxC,QAAA,iBAAiB,GAC5B,mFAAmF,CAAC;AAEtF;;;;;GAKG;AACU,QAAA,mBAAmB,GAAG;IACjC,SAAS;IACT,WAAW;IACX,YAAY;CACJ,CAAC;AAEX;;;GAGG;AACH,SAAgB,cAAc,CAC5B,OAAe,EACf,UAAkB,uBAAe;IAEjC,IAAI,OAAO,CAAC,MAAM,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC;IAE9C,MAAM,YAAY,GAAG,yBAAiB,CAAC,MAAM,CAAC;IAC9C,MAAM,gBAAgB,GAAG,OAAO,GAAG,YAAY,CAAC;IAEhD,+BAA+B;IAC/B,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IACvD,MAAM,UAAU,GAAG,gBAAgB,GAAG,UAAU,CAAC;IAEjD,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IAC1C,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,CAAC;IAExC,OAAO,IAAI,GAAG,yBAAiB,GAAG,IAAI,CAAC;AACzC,CAAC;AAED;;GAEG;AACH,SAAgB,0BAA0B;IACxC,OAAO,2BAAmB,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,aAAa,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACxE,CAAC;AAED;;GAEG;AACH,SAAgB,qBAAqB,CAAC,OAIrC;IACC,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,YAAY,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;IAE9D,MAAM,QAAQ,GAAG,aAAa,CAAC,CAAC,CAAC,UAAU,aAAa,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACjE,MAAM,QAAQ,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,0BAA0B,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAExE,OAAO,iBAAiB,QAAQ,GAAG,QAAQ,kBAAkB,KAAK,oBAAoB,CAAC;AACzF,CAAC;AAED;;GAEG;AACH,SAAgB,cAAc,CAAC,MAE9B;IACC,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACrB,OAAO,WAAW,CAAC;IACrB,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;GAGG;AACH,SAAgB,yBAAyB,CAAC,MAAc;IACtD,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;QAC9B,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,yEAAyE;IACzE,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3C,OAAO,KAAK,IAAI,SAAS,CAAC;AAC5B,CAAC;AAOD;;;;GAIG;AACH,SAAgB,eAAe,CAAC,QAAgB;IAC9C,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IAC/C,CAAC;IACD,yCAAyC;IACzC,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;AACjD,CAAC"}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAaH,wCAiBC;AAYD,0CAMC;AA9CD,6DAA6D;AAChD,QAAA,eAAe,GAAG,KAAK,CAAC;AAErC,qDAAqD;AACxC,QAAA,iBAAiB,GAC5B,mFAAmF,CAAC;AAEtF;;;GAGG;AACH,SAAgB,cAAc,CAC5B,OAAe,EACf,UAAkB,uBAAe;IAEjC,IAAI,OAAO,CAAC,MAAM,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC;IAE9C,MAAM,YAAY,GAAG,yBAAiB,CAAC,MAAM,CAAC;IAC9C,MAAM,gBAAgB,GAAG,OAAO,GAAG,YAAY,CAAC;IAEhD,+BAA+B;IAC/B,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IACvD,MAAM,UAAU,GAAG,gBAAgB,GAAG,UAAU,CAAC;IAEjD,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IAC1C,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,CAAC;IAExC,OAAO,IAAI,GAAG,yBAAiB,GAAG,IAAI,CAAC;AACzC,CAAC;AAOD;;;;GAIG;AACH,SAAgB,eAAe,CAAC,QAAgB;IAC9C,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IAC/C,CAAC;IACD,yCAAyC;IACzC,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;AACjD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hackerai/local",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "HackerAI Local Sandbox Client - Execute commands on your local machine",
5
5
  "bin": {
6
6
  "hackerai-local": "./dist/index.js"
@@ -1,95 +0,0 @@
1
- /**
2
- * PTY Session Manager for the local sandbox client.
3
- *
4
- * Manages node-pty processes, batches output for Convex relay,
5
- * and handles stdin/resize/kill from the backend.
6
- *
7
- * Cross-platform: macOS (forkpty), Linux (forkpty), Windows (conpty).
8
- */
9
- import type { ConvexClient } from "convex/browser";
10
- interface SignedSession {
11
- userId: string;
12
- expiresAt: number;
13
- signature: string;
14
- }
15
- export declare class PtyManager {
16
- private convex;
17
- private token;
18
- private connectionId;
19
- private session;
20
- private mode;
21
- private containerId?;
22
- private containerShell?;
23
- private sessions;
24
- private sessionSubscription;
25
- private ptyAvailable;
26
- constructor(
27
- convex: ConvexClient,
28
- token: string,
29
- connectionId: string,
30
- session: SignedSession,
31
- mode: "docker" | "dangerous",
32
- containerId?: string | undefined,
33
- containerShell?: string | undefined,
34
- );
35
- /**
36
- * Fix node-pty spawn-helper permissions.
37
- * npm/pnpm can strip the execute bit from prebuilt binaries during install.
38
- * Without +x, posix_spawnp fails on macOS/Linux.
39
- */
40
- private fixSpawnHelperPermissions;
41
- /**
42
- * Update the signed session (called after heartbeat refresh).
43
- */
44
- updateSession(session: SignedSession): void;
45
- /**
46
- * Start listening for new PTY session requests from the backend.
47
- */
48
- startSessionSubscription(): void;
49
- /**
50
- * Stop the session subscription.
51
- */
52
- stopSessionSubscription(): void;
53
- /**
54
- * Handle a request to create a new PTY session.
55
- */
56
- private handleCreateSession;
57
- /**
58
- * Buffer PTY output and flush periodically to reduce Convex writes.
59
- */
60
- private bufferOutput;
61
- /**
62
- * Flush buffered output to Convex.
63
- */
64
- private flushOutput;
65
- /**
66
- * Subscribe to stdin input from the backend for a session.
67
- */
68
- private startInputSubscription;
69
- /**
70
- * Handle a kill request for a session (called when backend detects killed status).
71
- */
72
- killSession(sessionId: string): boolean;
73
- /**
74
- * Resize a PTY session.
75
- */
76
- resizeSession(sessionId: string, cols: number, rows: number): boolean;
77
- /**
78
- * Clean up a session's resources.
79
- */
80
- private cleanupSession;
81
- /**
82
- * Clean up all sessions and stop subscriptions.
83
- */
84
- cleanup(): Promise<void>;
85
- /**
86
- * Check if PTY support is available.
87
- */
88
- get isAvailable(): boolean;
89
- /**
90
- * Get the number of active sessions.
91
- */
92
- get activeSessionCount(): number;
93
- }
94
- export {};
95
- //# sourceMappingURL=pty-manager.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"pty-manager.d.ts","sourceRoot":"","sources":["../src/pty-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAmDnD,UAAU,aAAa;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAUD,qBAAa,UAAU;IAMnB,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,WAAW,CAAC;IACpB,OAAO,CAAC,cAAc,CAAC;IAXzB,OAAO,CAAC,QAAQ,CAAyC;IACzD,OAAO,CAAC,mBAAmB,CAA6B;IACxD,OAAO,CAAC,YAAY,CAAU;gBAGpB,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,QAAQ,GAAG,WAAW,EAC5B,WAAW,CAAC,EAAE,MAAM,YAAA,EACpB,cAAc,CAAC,EAAE,MAAM,YAAA;IAejC;;;;OAIG;IACH,OAAO,CAAC,yBAAyB;IAkCjC;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IAO3C;;OAEG;IACH,wBAAwB,IAAI,IAAI;IA8BhC;;OAEG;IACH,uBAAuB,IAAI,IAAI;IAO/B;;OAEG;YACW,mBAAmB;IAsIjC;;OAEG;IACH,OAAO,CAAC,YAAY;IAkBpB;;OAEG;YACW,WAAW;IAsCzB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAoD9B;;OAEG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAcvC;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAYrE;;OAEG;IACH,OAAO,CAAC,cAAc;IActB;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA8B9B;;OAEG;IACH,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED;;OAEG;IACH,IAAI,kBAAkB,IAAI,MAAM,CAE/B;CACF"}
@@ -1,452 +0,0 @@
1
- "use strict";
2
- /**
3
- * PTY Session Manager for the local sandbox client.
4
- *
5
- * Manages node-pty processes, batches output for Convex relay,
6
- * and handles stdin/resize/kill from the backend.
7
- *
8
- * Cross-platform: macOS (forkpty), Linux (forkpty), Windows (conpty).
9
- */
10
- var __importDefault =
11
- (this && this.__importDefault) ||
12
- function (mod) {
13
- return mod && mod.__esModule ? mod : { default: mod };
14
- };
15
- Object.defineProperty(exports, "__esModule", { value: true });
16
- exports.PtyManager = void 0;
17
- const os_1 = __importDefault(require("os"));
18
- const fs_1 = __importDefault(require("fs"));
19
- const path_1 = __importDefault(require("path"));
20
- const utils_1 = require("./utils");
21
- // Convex function references
22
- const api = {
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
- },
31
- };
32
- // ANSI color codes
33
- const chalk = {
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`,
40
- };
41
- // Output batching: buffer for ~75ms before sending to reduce Convex write frequency
42
- const OUTPUT_BATCH_INTERVAL_MS = 75;
43
- // Max size of a single output chunk (base64 encoded) to stay under Convex limits
44
- const MAX_CHUNK_SIZE = 64 * 1024; // 64KB
45
- class PtyManager {
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
- );
81
- }
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;
103
- try {
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
114
- }
115
- }
116
- } catch {
117
- // Non-critical — node-pty might not be installed
118
- }
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;
152
- }
153
- if (!data?.sessions) return;
154
- for (const req of data.sessions) {
155
- await this.handleCreateSession(req);
156
- }
157
- },
158
- );
159
- }
160
- /**
161
- * Stop the session subscription.
162
- */
163
- stopSessionSubscription() {
164
- if (this.sessionSubscription) {
165
- this.sessionSubscription();
166
- this.sessionSubscription = null;
167
- }
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) {
184
- try {
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();
193
- }
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}`);
244
- }
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 {
263
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
264
- await this.convex.mutation(api.localSandbox.updatePtySessionStatus, {
265
- token: this.token,
266
- sessionId: req.session_id,
267
- status: "exited",
268
- exitCode: -1,
269
- });
270
- } catch {
271
- /* ignore */
272
- }
273
- }
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;
284
- }
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
- });
314
- }
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);
355
- }
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
- }
367
- }
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 */
381
- }
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);
406
- }
407
- if (session.inputSubscription) {
408
- session.inputSubscription();
409
- }
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);
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
- }
450
- }
451
- exports.PtyManager = PtyManager;
452
- //# sourceMappingURL=pty-manager.js.map