@alfe.ai/terminal 0.0.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,114 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let node_pty = require("node-pty");
3
+ let _alfe_ai_remote = require("@alfe.ai/remote");
4
+ //#region src/terminal-surface.ts
5
+ /**
6
+ * TerminalSurface — the `SurfaceHandler` for the web terminal. Each viewer
7
+ * session gets its own PTY (unlike the browser surface, where viewers share
8
+ * one Chrome). The PTY runs as the process's own user — i.e. the non-root
9
+ * agent runtime user — scoped to the agent workspace. No inbound port and no
10
+ * SSH daemon: output rides the agent's existing outbound WS to the relay.
11
+ */
12
+ const DEFAULT_COLS = 80;
13
+ const DEFAULT_ROWS = 24;
14
+ const DEFAULT_MAX_SESSIONS = 8;
15
+ const noopLogger = {
16
+ info: () => {},
17
+ warn: () => {},
18
+ error: () => {},
19
+ debug: () => {}
20
+ };
21
+ var TerminalSurface = class {
22
+ surface = "terminal";
23
+ ptys = /* @__PURE__ */ new Map();
24
+ shell;
25
+ cwd;
26
+ env;
27
+ maxSessions;
28
+ log;
29
+ constructor(options, sendFrame) {
30
+ this.options = options;
31
+ this.sendFrame = sendFrame;
32
+ this.log = options.logger ?? noopLogger;
33
+ this.shell = options.shell ?? process.env.SHELL ?? "/bin/bash";
34
+ this.cwd = options.cwd ?? process.cwd();
35
+ this.maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS;
36
+ const baseEnv = {};
37
+ for (const [k, v] of Object.entries(process.env)) if (v !== void 0) baseEnv[k] = v;
38
+ this.env = {
39
+ ...baseEnv,
40
+ ...options.env ?? {},
41
+ TERM: "xterm-256color"
42
+ };
43
+ }
44
+ openSession(sessionId, open) {
45
+ if (this.ptys.has(sessionId)) return;
46
+ if (this.ptys.size >= this.maxSessions) {
47
+ this.log.warn(`Terminal session limit (${String(this.maxSessions)}) reached — refusing`);
48
+ this.sendFrame((0, _alfe_ai_remote.encodeFrame)(_alfe_ai_remote.RemoteFrameType.SESSION_CLOSE, sessionId));
49
+ return;
50
+ }
51
+ const cols = open.cols ?? DEFAULT_COLS;
52
+ const rows = open.rows ?? DEFAULT_ROWS;
53
+ let ptyProcess;
54
+ try {
55
+ ptyProcess = (0, node_pty.spawn)(this.shell, [], {
56
+ name: "xterm-256color",
57
+ cols,
58
+ rows,
59
+ cwd: this.cwd,
60
+ env: this.env
61
+ });
62
+ } catch (err) {
63
+ this.log.error(`Failed to spawn PTY: ${err.message}`);
64
+ this.sendFrame((0, _alfe_ai_remote.encodeFrame)(_alfe_ai_remote.RemoteFrameType.SESSION_CLOSE, sessionId));
65
+ return;
66
+ }
67
+ this.ptys.set(sessionId, ptyProcess);
68
+ this.log.info(`Terminal session ${String(sessionId)} started (${this.shell})`);
69
+ ptyProcess.onData((data) => {
70
+ this.sendFrame((0, _alfe_ai_remote.encodeFrame)(_alfe_ai_remote.RemoteFrameType.TERMINAL_DATA, sessionId, Buffer.from(data, "utf-8")));
71
+ });
72
+ ptyProcess.onExit(() => {
73
+ this.log.info(`Terminal session ${String(sessionId)} exited`);
74
+ this.ptys.delete(sessionId);
75
+ this.sendFrame((0, _alfe_ai_remote.encodeFrame)(_alfe_ai_remote.RemoteFrameType.SESSION_CLOSE, sessionId));
76
+ });
77
+ }
78
+ handleFrame(frame) {
79
+ const ptyProcess = this.ptys.get(frame.sessionId);
80
+ if (!ptyProcess) return;
81
+ switch (frame.type) {
82
+ case _alfe_ai_remote.RemoteFrameType.TERMINAL_INPUT:
83
+ ptyProcess.write(frame.payload.toString("utf-8"));
84
+ break;
85
+ case _alfe_ai_remote.RemoteFrameType.TERMINAL_RESIZE: {
86
+ const r = (0, _alfe_ai_remote.decodeJson)(frame.payload);
87
+ if (r && r.cols > 0 && r.rows > 0) try {
88
+ ptyProcess.resize(r.cols, r.rows);
89
+ } catch {}
90
+ break;
91
+ }
92
+ default: break;
93
+ }
94
+ }
95
+ closeSession(sessionId) {
96
+ const ptyProcess = this.ptys.get(sessionId);
97
+ if (!ptyProcess) return;
98
+ this.ptys.delete(sessionId);
99
+ try {
100
+ ptyProcess.kill();
101
+ } catch {}
102
+ }
103
+ /** Kill every live PTY (plugin shutdown). */
104
+ shutdown() {
105
+ for (const [sessionId, ptyProcess] of this.ptys) {
106
+ try {
107
+ ptyProcess.kill();
108
+ } catch {}
109
+ this.ptys.delete(sessionId);
110
+ }
111
+ }
112
+ };
113
+ //#endregion
114
+ exports.TerminalSurface = TerminalSurface;
@@ -0,0 +1,103 @@
1
+ //#region ../remote/dist/index.d.ts
2
+
3
+ declare const RemoteFrameType: {
4
+ /** viewer→plugin: a viewer attached. Payload: SessionOpenPayload. */
5
+ readonly SESSION_OPEN: 1;
6
+ /** either direction: a viewer detached / session torn down. */
7
+ readonly SESSION_CLOSE: 2;
8
+ /** plugin→viewer: current surface state. Payload: SessionStatePayload. */
9
+ readonly SESSION_STATE: 3;
10
+ /** plugin→viewer (browser): the page navigated. Payload: { url }. */
11
+ readonly NAVIGATION: 4;
12
+ /** plugin→viewer: [metaLen:u16BE][meta JSON][raw JPEG]. */
13
+ readonly SCREENCAST_FRAME: 16;
14
+ /** viewer→plugin: { frameSeq } — drives ack-gated backpressure. */
15
+ readonly SCREENCAST_ACK: 17;
16
+ readonly INPUT_MOUSE: 32;
17
+ readonly INPUT_WHEEL: 33;
18
+ readonly INPUT_KEY: 34;
19
+ /** viewer→plugin: { width, height, dpr }. */
20
+ readonly RESIZE: 35;
21
+ readonly TAKEOVER_REQUEST: 48;
22
+ readonly TAKEOVER_GRANTED: 49;
23
+ readonly TAKEOVER_DENIED: 50;
24
+ readonly RELEASE_CONTROL: 51;
25
+ readonly CONTROL_REVOKED: 52;
26
+ /** plugin→viewer: raw PTY output bytes. */
27
+ readonly TERMINAL_DATA: 64;
28
+ /** viewer→plugin: raw keystroke bytes. */
29
+ readonly TERMINAL_INPUT: 65;
30
+ /** viewer→plugin: { cols, rows }. */
31
+ readonly TERMINAL_RESIZE: 66;
32
+ };
33
+ type RemoteFrameType = (typeof RemoteFrameType)[keyof typeof RemoteFrameType];
34
+ type RemoteSurface = "browser" | "terminal";
35
+ interface RemoteFrame {
36
+ type: RemoteFrameType;
37
+ sessionId: number;
38
+ payload: Buffer;
39
+ }
40
+ interface SessionOpenPayload {
41
+ surface: RemoteSurface;
42
+ /** Browser: initial viewport in device pixels. */
43
+ width?: number;
44
+ height?: number;
45
+ dpr?: number;
46
+ /** Terminal: initial PTY dimensions. */
47
+ cols?: number;
48
+ rows?: number;
49
+ }
50
+ //#endregion
51
+ //#region src/surface.d.ts
52
+ interface SurfaceHandler {
53
+ /** Which surface this handler serves. */
54
+ readonly surface: RemoteSurface;
55
+ /** A viewer attached — open the surface for this session. */
56
+ openSession(sessionId: number, open: SessionOpenPayload): void | Promise<void>;
57
+ /** A subsequent frame for one of this handler's sessions (not SESSION_OPEN). */
58
+ handleFrame(frame: RemoteFrame): void;
59
+ /** The viewer detached — tear down this session's resources. */
60
+ closeSession(sessionId: number): void;
61
+ }
62
+ //#endregion
63
+ //#region src/types.d.ts
64
+ interface Logger {
65
+ info(msg: string, ...args: unknown[]): void;
66
+ warn(msg: string, ...args: unknown[]): void;
67
+ error(msg: string, ...args: unknown[]): void;
68
+ debug(msg: string, ...args: unknown[]): void;
69
+ }
70
+ //#endregion
71
+ //#region src/types.d.ts
72
+ interface TerminalSurfaceOptions {
73
+ /** Shell to spawn. Defaults to $SHELL or /bin/bash. */
74
+ shell?: string;
75
+ /** Working directory for the shell (the agent workspace). Defaults to cwd. */
76
+ cwd?: string;
77
+ /** Extra env for the shell. Merged over process.env. */
78
+ env?: Record<string, string>;
79
+ /** Max concurrent terminal sessions (default 8). */
80
+ maxSessions?: number;
81
+ logger?: Logger;
82
+ }
83
+ //#endregion
84
+ //#region src/terminal-surface.d.ts
85
+ declare class TerminalSurface implements SurfaceHandler {
86
+ private readonly options;
87
+ private readonly sendFrame;
88
+ readonly surface: "terminal";
89
+ private readonly ptys;
90
+ private readonly shell;
91
+ private readonly cwd;
92
+ private readonly env;
93
+ private readonly maxSessions;
94
+ private readonly log;
95
+ constructor(options: TerminalSurfaceOptions, sendFrame: (buf: Buffer) => void);
96
+ openSession(sessionId: number, open: SessionOpenPayload): void;
97
+ handleFrame(frame: RemoteFrame): void;
98
+ closeSession(sessionId: number): void;
99
+ /** Kill every live PTY (plugin shutdown). */
100
+ shutdown(): void;
101
+ }
102
+ //#endregion
103
+ export { type Logger, TerminalSurface, type TerminalSurfaceOptions };
@@ -0,0 +1,103 @@
1
+ //#region ../remote/dist/index.d.ts
2
+
3
+ declare const RemoteFrameType: {
4
+ /** viewer→plugin: a viewer attached. Payload: SessionOpenPayload. */
5
+ readonly SESSION_OPEN: 1;
6
+ /** either direction: a viewer detached / session torn down. */
7
+ readonly SESSION_CLOSE: 2;
8
+ /** plugin→viewer: current surface state. Payload: SessionStatePayload. */
9
+ readonly SESSION_STATE: 3;
10
+ /** plugin→viewer (browser): the page navigated. Payload: { url }. */
11
+ readonly NAVIGATION: 4;
12
+ /** plugin→viewer: [metaLen:u16BE][meta JSON][raw JPEG]. */
13
+ readonly SCREENCAST_FRAME: 16;
14
+ /** viewer→plugin: { frameSeq } — drives ack-gated backpressure. */
15
+ readonly SCREENCAST_ACK: 17;
16
+ readonly INPUT_MOUSE: 32;
17
+ readonly INPUT_WHEEL: 33;
18
+ readonly INPUT_KEY: 34;
19
+ /** viewer→plugin: { width, height, dpr }. */
20
+ readonly RESIZE: 35;
21
+ readonly TAKEOVER_REQUEST: 48;
22
+ readonly TAKEOVER_GRANTED: 49;
23
+ readonly TAKEOVER_DENIED: 50;
24
+ readonly RELEASE_CONTROL: 51;
25
+ readonly CONTROL_REVOKED: 52;
26
+ /** plugin→viewer: raw PTY output bytes. */
27
+ readonly TERMINAL_DATA: 64;
28
+ /** viewer→plugin: raw keystroke bytes. */
29
+ readonly TERMINAL_INPUT: 65;
30
+ /** viewer→plugin: { cols, rows }. */
31
+ readonly TERMINAL_RESIZE: 66;
32
+ };
33
+ type RemoteFrameType = (typeof RemoteFrameType)[keyof typeof RemoteFrameType];
34
+ type RemoteSurface = "browser" | "terminal";
35
+ interface RemoteFrame {
36
+ type: RemoteFrameType;
37
+ sessionId: number;
38
+ payload: Buffer;
39
+ }
40
+ interface SessionOpenPayload {
41
+ surface: RemoteSurface;
42
+ /** Browser: initial viewport in device pixels. */
43
+ width?: number;
44
+ height?: number;
45
+ dpr?: number;
46
+ /** Terminal: initial PTY dimensions. */
47
+ cols?: number;
48
+ rows?: number;
49
+ }
50
+ //#endregion
51
+ //#region src/surface.d.ts
52
+ interface SurfaceHandler {
53
+ /** Which surface this handler serves. */
54
+ readonly surface: RemoteSurface;
55
+ /** A viewer attached — open the surface for this session. */
56
+ openSession(sessionId: number, open: SessionOpenPayload): void | Promise<void>;
57
+ /** A subsequent frame for one of this handler's sessions (not SESSION_OPEN). */
58
+ handleFrame(frame: RemoteFrame): void;
59
+ /** The viewer detached — tear down this session's resources. */
60
+ closeSession(sessionId: number): void;
61
+ }
62
+ //#endregion
63
+ //#region src/types.d.ts
64
+ interface Logger {
65
+ info(msg: string, ...args: unknown[]): void;
66
+ warn(msg: string, ...args: unknown[]): void;
67
+ error(msg: string, ...args: unknown[]): void;
68
+ debug(msg: string, ...args: unknown[]): void;
69
+ }
70
+ //#endregion
71
+ //#region src/types.d.ts
72
+ interface TerminalSurfaceOptions {
73
+ /** Shell to spawn. Defaults to $SHELL or /bin/bash. */
74
+ shell?: string;
75
+ /** Working directory for the shell (the agent workspace). Defaults to cwd. */
76
+ cwd?: string;
77
+ /** Extra env for the shell. Merged over process.env. */
78
+ env?: Record<string, string>;
79
+ /** Max concurrent terminal sessions (default 8). */
80
+ maxSessions?: number;
81
+ logger?: Logger;
82
+ }
83
+ //#endregion
84
+ //#region src/terminal-surface.d.ts
85
+ declare class TerminalSurface implements SurfaceHandler {
86
+ private readonly options;
87
+ private readonly sendFrame;
88
+ readonly surface: "terminal";
89
+ private readonly ptys;
90
+ private readonly shell;
91
+ private readonly cwd;
92
+ private readonly env;
93
+ private readonly maxSessions;
94
+ private readonly log;
95
+ constructor(options: TerminalSurfaceOptions, sendFrame: (buf: Buffer) => void);
96
+ openSession(sessionId: number, open: SessionOpenPayload): void;
97
+ handleFrame(frame: RemoteFrame): void;
98
+ closeSession(sessionId: number): void;
99
+ /** Kill every live PTY (plugin shutdown). */
100
+ shutdown(): void;
101
+ }
102
+ //#endregion
103
+ export { type Logger, TerminalSurface, type TerminalSurfaceOptions };
package/dist/index.js ADDED
@@ -0,0 +1,113 @@
1
+ import { spawn } from "node-pty";
2
+ import { RemoteFrameType, decodeJson, encodeFrame } from "@alfe.ai/remote";
3
+ //#region src/terminal-surface.ts
4
+ /**
5
+ * TerminalSurface — the `SurfaceHandler` for the web terminal. Each viewer
6
+ * session gets its own PTY (unlike the browser surface, where viewers share
7
+ * one Chrome). The PTY runs as the process's own user — i.e. the non-root
8
+ * agent runtime user — scoped to the agent workspace. No inbound port and no
9
+ * SSH daemon: output rides the agent's existing outbound WS to the relay.
10
+ */
11
+ const DEFAULT_COLS = 80;
12
+ const DEFAULT_ROWS = 24;
13
+ const DEFAULT_MAX_SESSIONS = 8;
14
+ const noopLogger = {
15
+ info: () => {},
16
+ warn: () => {},
17
+ error: () => {},
18
+ debug: () => {}
19
+ };
20
+ var TerminalSurface = class {
21
+ surface = "terminal";
22
+ ptys = /* @__PURE__ */ new Map();
23
+ shell;
24
+ cwd;
25
+ env;
26
+ maxSessions;
27
+ log;
28
+ constructor(options, sendFrame) {
29
+ this.options = options;
30
+ this.sendFrame = sendFrame;
31
+ this.log = options.logger ?? noopLogger;
32
+ this.shell = options.shell ?? process.env.SHELL ?? "/bin/bash";
33
+ this.cwd = options.cwd ?? process.cwd();
34
+ this.maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS;
35
+ const baseEnv = {};
36
+ for (const [k, v] of Object.entries(process.env)) if (v !== void 0) baseEnv[k] = v;
37
+ this.env = {
38
+ ...baseEnv,
39
+ ...options.env ?? {},
40
+ TERM: "xterm-256color"
41
+ };
42
+ }
43
+ openSession(sessionId, open) {
44
+ if (this.ptys.has(sessionId)) return;
45
+ if (this.ptys.size >= this.maxSessions) {
46
+ this.log.warn(`Terminal session limit (${String(this.maxSessions)}) reached — refusing`);
47
+ this.sendFrame(encodeFrame(RemoteFrameType.SESSION_CLOSE, sessionId));
48
+ return;
49
+ }
50
+ const cols = open.cols ?? DEFAULT_COLS;
51
+ const rows = open.rows ?? DEFAULT_ROWS;
52
+ let ptyProcess;
53
+ try {
54
+ ptyProcess = spawn(this.shell, [], {
55
+ name: "xterm-256color",
56
+ cols,
57
+ rows,
58
+ cwd: this.cwd,
59
+ env: this.env
60
+ });
61
+ } catch (err) {
62
+ this.log.error(`Failed to spawn PTY: ${err.message}`);
63
+ this.sendFrame(encodeFrame(RemoteFrameType.SESSION_CLOSE, sessionId));
64
+ return;
65
+ }
66
+ this.ptys.set(sessionId, ptyProcess);
67
+ this.log.info(`Terminal session ${String(sessionId)} started (${this.shell})`);
68
+ ptyProcess.onData((data) => {
69
+ this.sendFrame(encodeFrame(RemoteFrameType.TERMINAL_DATA, sessionId, Buffer.from(data, "utf-8")));
70
+ });
71
+ ptyProcess.onExit(() => {
72
+ this.log.info(`Terminal session ${String(sessionId)} exited`);
73
+ this.ptys.delete(sessionId);
74
+ this.sendFrame(encodeFrame(RemoteFrameType.SESSION_CLOSE, sessionId));
75
+ });
76
+ }
77
+ handleFrame(frame) {
78
+ const ptyProcess = this.ptys.get(frame.sessionId);
79
+ if (!ptyProcess) return;
80
+ switch (frame.type) {
81
+ case RemoteFrameType.TERMINAL_INPUT:
82
+ ptyProcess.write(frame.payload.toString("utf-8"));
83
+ break;
84
+ case RemoteFrameType.TERMINAL_RESIZE: {
85
+ const r = decodeJson(frame.payload);
86
+ if (r && r.cols > 0 && r.rows > 0) try {
87
+ ptyProcess.resize(r.cols, r.rows);
88
+ } catch {}
89
+ break;
90
+ }
91
+ default: break;
92
+ }
93
+ }
94
+ closeSession(sessionId) {
95
+ const ptyProcess = this.ptys.get(sessionId);
96
+ if (!ptyProcess) return;
97
+ this.ptys.delete(sessionId);
98
+ try {
99
+ ptyProcess.kill();
100
+ } catch {}
101
+ }
102
+ /** Kill every live PTY (plugin shutdown). */
103
+ shutdown() {
104
+ for (const [sessionId, ptyProcess] of this.ptys) {
105
+ try {
106
+ ptyProcess.kill();
107
+ } catch {}
108
+ this.ptys.delete(sessionId);
109
+ }
110
+ }
111
+ };
112
+ //#endregion
113
+ export { TerminalSurface };
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@alfe.ai/terminal",
3
+ "version": "0.0.0",
4
+ "description": "PTY terminal surface for the Alfe interactive remote-control relay — a web shell over the agent's outbound WS (no inbound port, no SSH daemon)",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "require": "./dist/index.cjs",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "node-pty": "^1.0.0",
20
+ "@alfe.ai/remote": "^0.0.0"
21
+ },
22
+ "license": "UNLICENSED",
23
+ "scripts": {
24
+ "build": "tsdown",
25
+ "dev": "tsdown --watch",
26
+ "test": "vitest run --passWithNoTests",
27
+ "typecheck": "tsc --noEmit",
28
+ "lint": "eslint ."
29
+ }
30
+ }