@alfe.ai/terminal 0.1.2 → 0.1.4

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/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # `@alfe.ai/terminal`
2
+
3
+ PTY terminal surface for Alfe's interactive remote-control relay. Each viewer
4
+ session owns a separate shell process in the agent workspace; terminal frames
5
+ travel over the agent's existing outbound WebSocket, with no inbound port or
6
+ SSH daemon.
7
+
8
+ Part of [Alfe](https://alfe.ai), the operating system for AI agents.
9
+
10
+ ## Runtime contract
11
+
12
+ - One numeric remote session owns one exact PTY instance. Late data/exit events
13
+ from an older process cannot affect a reopened session with the same ID.
14
+ - Initial and resize dimensions are integers from 1 to 1,000. Terminal input is
15
+ valid UTF-8 capped at 64 KiB per frame.
16
+ - PTY output is split into UTF-8-safe frames of at most 64 KiB so terminal
17
+ floods cannot create relay-sized single messages.
18
+ - If the outbound relay already has 4 MiB queued, output delivery fails closed
19
+ and the owning PTY is terminated instead of growing an unbounded buffer.
20
+ - Session count defaults to 8 and may be configured from 1 to 64.
21
+ - Shutdown retries a PTY once when its native `kill()` call fails instead of
22
+ immediately forgetting the still-running process.
23
+
24
+ ## Usage
25
+
26
+ ```ts
27
+ import { TerminalSurface } from "@alfe.ai/terminal";
28
+
29
+ const surface = new TerminalSurface(
30
+ { cwd: "/path/to/agent/workspace" },
31
+ (frame) => relay.sendFrame(frame),
32
+ );
33
+ ```
34
+
35
+ The package uses `@lydell/node-pty`, which publishes matching native binaries
36
+ for supported macOS, Linux, and Windows architectures.
package/dist/index.cjs CHANGED
@@ -12,6 +12,16 @@ let _alfe_ai_remote = require("@alfe.ai/remote");
12
12
  const DEFAULT_COLS = 80;
13
13
  const DEFAULT_ROWS = 24;
14
14
  const DEFAULT_MAX_SESSIONS = 8;
15
+ const MAX_SESSIONS = 64;
16
+ const MAX_DIMENSION = 1e3;
17
+ const MAX_TERMINAL_INPUT_BYTES = 64 * 1024;
18
+ const MAX_TERMINAL_OUTPUT_FRAME_BYTES = 64 * 1024;
19
+ const MAX_SHELL_CHARS = 4096;
20
+ const MAX_CWD_CHARS = 8192;
21
+ const MAX_ENV_VALUE_CHARS = 64 * 1024;
22
+ const MAX_ENV_OVERRIDES = 256;
23
+ const MAX_ENV_OVERRIDE_BYTES = 512 * 1024;
24
+ const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
15
25
  const noopLogger = {
16
26
  info: () => {},
17
27
  warn: () => {},
@@ -27,29 +37,49 @@ var TerminalSurface = class {
27
37
  maxSessions;
28
38
  log;
29
39
  constructor(options, sendFrame) {
30
- this.options = options;
31
40
  this.sendFrame = sendFrame;
32
41
  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 = {};
42
+ this.shell = requirePlainString(options.shell ?? process.env.SHELL ?? "/bin/bash", "terminal shell", MAX_SHELL_CHARS);
43
+ this.cwd = requirePlainString(options.cwd ?? process.cwd(), "terminal working directory", MAX_CWD_CHARS);
44
+ this.maxSessions = optionalBoundedInteger(options.maxSessions, DEFAULT_MAX_SESSIONS, 1, MAX_SESSIONS, "terminal maxSessions");
45
+ const baseEnv = Object.create(null);
37
46
  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
- };
47
+ const environmentOverrides = Object.entries(options.env ?? {});
48
+ if (environmentOverrides.length > MAX_ENV_OVERRIDES) throw new TypeError("terminal environment contains too many variables");
49
+ let environmentBytes = 0;
50
+ for (const [key, value] of environmentOverrides) {
51
+ if (key.length > 256 || !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) throw new TypeError("terminal environment contains an invalid variable name");
52
+ const admittedValue = requirePlainString(value, "terminal environment value", MAX_ENV_VALUE_CHARS, true);
53
+ environmentBytes += Buffer.byteLength(key, "utf8") + Buffer.byteLength(admittedValue, "utf8");
54
+ if (environmentBytes > MAX_ENV_OVERRIDE_BYTES) throw new TypeError("terminal environment exceeds the byte limit");
55
+ baseEnv[key] = admittedValue;
56
+ }
57
+ baseEnv.TERM = "xterm-256color";
58
+ this.env = baseEnv;
43
59
  }
44
60
  openSession(sessionId, open) {
61
+ if (!isSessionId(sessionId)) {
62
+ this.log.warn("Terminal session ID is invalid");
63
+ return;
64
+ }
45
65
  if (this.ptys.has(sessionId)) return;
66
+ if (open.surface !== "terminal") {
67
+ this.log.warn("Terminal session surface is invalid");
68
+ this.sendClose(sessionId);
69
+ return;
70
+ }
46
71
  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));
72
+ this.log.warn("Terminal session limit reached");
73
+ this.sendClose(sessionId);
74
+ return;
75
+ }
76
+ const cols = optionalDimension(open.cols, DEFAULT_COLS);
77
+ const rows = optionalDimension(open.rows, DEFAULT_ROWS);
78
+ if (cols === void 0 || rows === void 0) {
79
+ this.log.warn("Terminal session dimensions are invalid");
80
+ this.sendClose(sessionId);
49
81
  return;
50
82
  }
51
- const cols = open.cols ?? DEFAULT_COLS;
52
- const rows = open.rows ?? DEFAULT_ROWS;
53
83
  let ptyProcess;
54
84
  try {
55
85
  ptyProcess = (0, _lydell_node_pty.spawn)(this.shell, [], {
@@ -59,33 +89,55 @@ var TerminalSurface = class {
59
89
  cwd: this.cwd,
60
90
  env: this.env
61
91
  });
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));
92
+ } catch {
93
+ this.log.error("Could not spawn terminal session");
94
+ this.sendClose(sessionId);
65
95
  return;
66
96
  }
67
97
  this.ptys.set(sessionId, ptyProcess);
68
- this.log.info(`Terminal session ${String(sessionId)} started (${this.shell})`);
98
+ this.log.info("Terminal session started");
69
99
  ptyProcess.onData((data) => {
70
- this.sendFrame((0, _alfe_ai_remote.encodeFrame)(_alfe_ai_remote.RemoteFrameType.TERMINAL_DATA, sessionId, Buffer.from(data, "utf-8")));
100
+ if (this.ptys.get(sessionId) !== ptyProcess) return;
101
+ for (const chunk of chunkUtf8(data, MAX_TERMINAL_OUTPUT_FRAME_BYTES)) if (!this.send((0, _alfe_ai_remote.encodeFrame)(_alfe_ai_remote.RemoteFrameType.TERMINAL_DATA, sessionId, chunk))) {
102
+ this.closeSession(sessionId);
103
+ return;
104
+ }
71
105
  });
72
106
  ptyProcess.onExit(() => {
73
- this.log.info(`Terminal session ${String(sessionId)} exited`);
107
+ if (this.ptys.get(sessionId) !== ptyProcess) return;
108
+ this.log.info("Terminal session exited");
74
109
  this.ptys.delete(sessionId);
75
- this.sendFrame((0, _alfe_ai_remote.encodeFrame)(_alfe_ai_remote.RemoteFrameType.SESSION_CLOSE, sessionId));
110
+ this.sendClose(sessionId);
76
111
  });
77
112
  }
78
113
  handleFrame(frame) {
79
114
  const ptyProcess = this.ptys.get(frame.sessionId);
80
115
  if (!ptyProcess) return;
81
116
  switch (frame.type) {
82
- case _alfe_ai_remote.RemoteFrameType.TERMINAL_INPUT:
83
- ptyProcess.write(frame.payload.toString("utf-8"));
117
+ case _alfe_ai_remote.RemoteFrameType.TERMINAL_INPUT: {
118
+ if (frame.payload.length > MAX_TERMINAL_INPUT_BYTES) {
119
+ this.log.warn("Terminal input exceeded the byte limit");
120
+ return;
121
+ }
122
+ let input;
123
+ try {
124
+ input = utf8Decoder.decode(frame.payload);
125
+ } catch {
126
+ this.log.warn("Terminal input was invalid");
127
+ return;
128
+ }
129
+ try {
130
+ ptyProcess.write(input);
131
+ } catch {
132
+ this.log.warn("Could not write terminal input");
133
+ this.closeSession(frame.sessionId);
134
+ }
84
135
  break;
136
+ }
85
137
  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);
138
+ const resize = (0, _alfe_ai_remote.decodeTerminalResizePayload)(frame.payload);
139
+ if (resize) try {
140
+ ptyProcess.resize(resize.cols, resize.rows);
89
141
  } catch {}
90
142
  break;
91
143
  }
@@ -95,20 +147,66 @@ var TerminalSurface = class {
95
147
  closeSession(sessionId) {
96
148
  const ptyProcess = this.ptys.get(sessionId);
97
149
  if (!ptyProcess) return;
98
- this.ptys.delete(sessionId);
99
150
  try {
100
151
  ptyProcess.kill();
101
- } catch {}
152
+ if (this.ptys.get(sessionId) === ptyProcess) this.ptys.delete(sessionId);
153
+ } catch {
154
+ this.log.warn("Could not close terminal session");
155
+ }
102
156
  }
103
157
  /** Kill every live PTY (plugin shutdown). */
104
158
  shutdown() {
105
- for (const [sessionId, ptyProcess] of this.ptys) {
106
- try {
107
- ptyProcess.kill();
108
- } catch {}
109
- this.ptys.delete(sessionId);
159
+ for (const sessionId of [...this.ptys.keys()]) {
160
+ this.closeSession(sessionId);
161
+ if (this.ptys.has(sessionId)) this.closeSession(sessionId);
162
+ }
163
+ }
164
+ sendClose(sessionId) {
165
+ this.send((0, _alfe_ai_remote.encodeFrame)(_alfe_ai_remote.RemoteFrameType.SESSION_CLOSE, sessionId));
166
+ }
167
+ send(frame) {
168
+ try {
169
+ this.sendFrame(frame);
170
+ return true;
171
+ } catch {
172
+ this.log.warn("Could not send terminal frame");
173
+ return false;
110
174
  }
111
175
  }
112
176
  };
177
+ function isSessionId(value) {
178
+ return Number.isInteger(value) && value >= 0 && value <= 4294967295;
179
+ }
180
+ function optionalDimension(value, fallback) {
181
+ if (value === void 0) return fallback;
182
+ return Number.isInteger(value) && value >= 1 && value <= MAX_DIMENSION ? value : void 0;
183
+ }
184
+ function optionalBoundedInteger(value, fallback, minimum, maximum, label) {
185
+ if (value === void 0) return fallback;
186
+ if (!Number.isInteger(value) || value < minimum || value > maximum) throw new TypeError(`${label} is invalid`);
187
+ return value;
188
+ }
189
+ function requirePlainString(value, label, maxChars, allowEmpty = false) {
190
+ if (typeof value !== "string" || !allowEmpty && value.length === 0 || value.length > maxChars || value.includes("\0")) throw new TypeError(`${label} is invalid`);
191
+ return value;
192
+ }
193
+ function chunkUtf8(value, maxBytes) {
194
+ if (value.length === 0) return [];
195
+ const chunks = [];
196
+ let characters = [];
197
+ let bytes = 0;
198
+ for (const character of value) {
199
+ const size = Buffer.byteLength(character, "utf8");
200
+ if (bytes + size > maxBytes && characters.length > 0) {
201
+ chunks.push(Buffer.from(characters.join(""), "utf8"));
202
+ characters = [];
203
+ bytes = 0;
204
+ }
205
+ characters.push(character);
206
+ bytes += size;
207
+ }
208
+ if (characters.length > 0) chunks.push(Buffer.from(characters.join(""), "utf8"));
209
+ return chunks;
210
+ }
113
211
  //#endregion
114
212
  exports.TerminalSurface = TerminalSurface;
package/dist/index.d.cts CHANGED
@@ -83,7 +83,6 @@ interface TerminalSurfaceOptions {
83
83
  //#endregion
84
84
  //#region src/terminal-surface.d.ts
85
85
  declare class TerminalSurface implements SurfaceHandler {
86
- private readonly options;
87
86
  private readonly sendFrame;
88
87
  readonly surface: "terminal";
89
88
  private readonly ptys;
@@ -98,6 +97,8 @@ declare class TerminalSurface implements SurfaceHandler {
98
97
  closeSession(sessionId: number): void;
99
98
  /** Kill every live PTY (plugin shutdown). */
100
99
  shutdown(): void;
100
+ private sendClose;
101
+ private send;
101
102
  }
102
103
  //#endregion
103
104
  export { type Logger, TerminalSurface, type TerminalSurfaceOptions };
package/dist/index.d.ts CHANGED
@@ -83,7 +83,6 @@ interface TerminalSurfaceOptions {
83
83
  //#endregion
84
84
  //#region src/terminal-surface.d.ts
85
85
  declare class TerminalSurface implements SurfaceHandler {
86
- private readonly options;
87
86
  private readonly sendFrame;
88
87
  readonly surface: "terminal";
89
88
  private readonly ptys;
@@ -98,6 +97,8 @@ declare class TerminalSurface implements SurfaceHandler {
98
97
  closeSession(sessionId: number): void;
99
98
  /** Kill every live PTY (plugin shutdown). */
100
99
  shutdown(): void;
100
+ private sendClose;
101
+ private send;
101
102
  }
102
103
  //#endregion
103
104
  export { type Logger, TerminalSurface, type TerminalSurfaceOptions };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "@lydell/node-pty";
2
- import { RemoteFrameType, decodeJson, encodeFrame } from "@alfe.ai/remote";
2
+ import { RemoteFrameType, decodeTerminalResizePayload, encodeFrame } from "@alfe.ai/remote";
3
3
  //#region src/terminal-surface.ts
4
4
  /**
5
5
  * TerminalSurface — the `SurfaceHandler` for the web terminal. Each viewer
@@ -11,6 +11,16 @@ import { RemoteFrameType, decodeJson, encodeFrame } from "@alfe.ai/remote";
11
11
  const DEFAULT_COLS = 80;
12
12
  const DEFAULT_ROWS = 24;
13
13
  const DEFAULT_MAX_SESSIONS = 8;
14
+ const MAX_SESSIONS = 64;
15
+ const MAX_DIMENSION = 1e3;
16
+ const MAX_TERMINAL_INPUT_BYTES = 64 * 1024;
17
+ const MAX_TERMINAL_OUTPUT_FRAME_BYTES = 64 * 1024;
18
+ const MAX_SHELL_CHARS = 4096;
19
+ const MAX_CWD_CHARS = 8192;
20
+ const MAX_ENV_VALUE_CHARS = 64 * 1024;
21
+ const MAX_ENV_OVERRIDES = 256;
22
+ const MAX_ENV_OVERRIDE_BYTES = 512 * 1024;
23
+ const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
14
24
  const noopLogger = {
15
25
  info: () => {},
16
26
  warn: () => {},
@@ -26,29 +36,49 @@ var TerminalSurface = class {
26
36
  maxSessions;
27
37
  log;
28
38
  constructor(options, sendFrame) {
29
- this.options = options;
30
39
  this.sendFrame = sendFrame;
31
40
  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 = {};
41
+ this.shell = requirePlainString(options.shell ?? process.env.SHELL ?? "/bin/bash", "terminal shell", MAX_SHELL_CHARS);
42
+ this.cwd = requirePlainString(options.cwd ?? process.cwd(), "terminal working directory", MAX_CWD_CHARS);
43
+ this.maxSessions = optionalBoundedInteger(options.maxSessions, DEFAULT_MAX_SESSIONS, 1, MAX_SESSIONS, "terminal maxSessions");
44
+ const baseEnv = Object.create(null);
36
45
  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
- };
46
+ const environmentOverrides = Object.entries(options.env ?? {});
47
+ if (environmentOverrides.length > MAX_ENV_OVERRIDES) throw new TypeError("terminal environment contains too many variables");
48
+ let environmentBytes = 0;
49
+ for (const [key, value] of environmentOverrides) {
50
+ if (key.length > 256 || !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) throw new TypeError("terminal environment contains an invalid variable name");
51
+ const admittedValue = requirePlainString(value, "terminal environment value", MAX_ENV_VALUE_CHARS, true);
52
+ environmentBytes += Buffer.byteLength(key, "utf8") + Buffer.byteLength(admittedValue, "utf8");
53
+ if (environmentBytes > MAX_ENV_OVERRIDE_BYTES) throw new TypeError("terminal environment exceeds the byte limit");
54
+ baseEnv[key] = admittedValue;
55
+ }
56
+ baseEnv.TERM = "xterm-256color";
57
+ this.env = baseEnv;
42
58
  }
43
59
  openSession(sessionId, open) {
60
+ if (!isSessionId(sessionId)) {
61
+ this.log.warn("Terminal session ID is invalid");
62
+ return;
63
+ }
44
64
  if (this.ptys.has(sessionId)) return;
65
+ if (open.surface !== "terminal") {
66
+ this.log.warn("Terminal session surface is invalid");
67
+ this.sendClose(sessionId);
68
+ return;
69
+ }
45
70
  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));
71
+ this.log.warn("Terminal session limit reached");
72
+ this.sendClose(sessionId);
73
+ return;
74
+ }
75
+ const cols = optionalDimension(open.cols, DEFAULT_COLS);
76
+ const rows = optionalDimension(open.rows, DEFAULT_ROWS);
77
+ if (cols === void 0 || rows === void 0) {
78
+ this.log.warn("Terminal session dimensions are invalid");
79
+ this.sendClose(sessionId);
48
80
  return;
49
81
  }
50
- const cols = open.cols ?? DEFAULT_COLS;
51
- const rows = open.rows ?? DEFAULT_ROWS;
52
82
  let ptyProcess;
53
83
  try {
54
84
  ptyProcess = spawn(this.shell, [], {
@@ -58,33 +88,55 @@ var TerminalSurface = class {
58
88
  cwd: this.cwd,
59
89
  env: this.env
60
90
  });
61
- } catch (err) {
62
- this.log.error(`Failed to spawn PTY: ${err.message}`);
63
- this.sendFrame(encodeFrame(RemoteFrameType.SESSION_CLOSE, sessionId));
91
+ } catch {
92
+ this.log.error("Could not spawn terminal session");
93
+ this.sendClose(sessionId);
64
94
  return;
65
95
  }
66
96
  this.ptys.set(sessionId, ptyProcess);
67
- this.log.info(`Terminal session ${String(sessionId)} started (${this.shell})`);
97
+ this.log.info("Terminal session started");
68
98
  ptyProcess.onData((data) => {
69
- this.sendFrame(encodeFrame(RemoteFrameType.TERMINAL_DATA, sessionId, Buffer.from(data, "utf-8")));
99
+ if (this.ptys.get(sessionId) !== ptyProcess) return;
100
+ for (const chunk of chunkUtf8(data, MAX_TERMINAL_OUTPUT_FRAME_BYTES)) if (!this.send(encodeFrame(RemoteFrameType.TERMINAL_DATA, sessionId, chunk))) {
101
+ this.closeSession(sessionId);
102
+ return;
103
+ }
70
104
  });
71
105
  ptyProcess.onExit(() => {
72
- this.log.info(`Terminal session ${String(sessionId)} exited`);
106
+ if (this.ptys.get(sessionId) !== ptyProcess) return;
107
+ this.log.info("Terminal session exited");
73
108
  this.ptys.delete(sessionId);
74
- this.sendFrame(encodeFrame(RemoteFrameType.SESSION_CLOSE, sessionId));
109
+ this.sendClose(sessionId);
75
110
  });
76
111
  }
77
112
  handleFrame(frame) {
78
113
  const ptyProcess = this.ptys.get(frame.sessionId);
79
114
  if (!ptyProcess) return;
80
115
  switch (frame.type) {
81
- case RemoteFrameType.TERMINAL_INPUT:
82
- ptyProcess.write(frame.payload.toString("utf-8"));
116
+ case RemoteFrameType.TERMINAL_INPUT: {
117
+ if (frame.payload.length > MAX_TERMINAL_INPUT_BYTES) {
118
+ this.log.warn("Terminal input exceeded the byte limit");
119
+ return;
120
+ }
121
+ let input;
122
+ try {
123
+ input = utf8Decoder.decode(frame.payload);
124
+ } catch {
125
+ this.log.warn("Terminal input was invalid");
126
+ return;
127
+ }
128
+ try {
129
+ ptyProcess.write(input);
130
+ } catch {
131
+ this.log.warn("Could not write terminal input");
132
+ this.closeSession(frame.sessionId);
133
+ }
83
134
  break;
135
+ }
84
136
  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);
137
+ const resize = decodeTerminalResizePayload(frame.payload);
138
+ if (resize) try {
139
+ ptyProcess.resize(resize.cols, resize.rows);
88
140
  } catch {}
89
141
  break;
90
142
  }
@@ -94,20 +146,66 @@ var TerminalSurface = class {
94
146
  closeSession(sessionId) {
95
147
  const ptyProcess = this.ptys.get(sessionId);
96
148
  if (!ptyProcess) return;
97
- this.ptys.delete(sessionId);
98
149
  try {
99
150
  ptyProcess.kill();
100
- } catch {}
151
+ if (this.ptys.get(sessionId) === ptyProcess) this.ptys.delete(sessionId);
152
+ } catch {
153
+ this.log.warn("Could not close terminal session");
154
+ }
101
155
  }
102
156
  /** Kill every live PTY (plugin shutdown). */
103
157
  shutdown() {
104
- for (const [sessionId, ptyProcess] of this.ptys) {
105
- try {
106
- ptyProcess.kill();
107
- } catch {}
108
- this.ptys.delete(sessionId);
158
+ for (const sessionId of [...this.ptys.keys()]) {
159
+ this.closeSession(sessionId);
160
+ if (this.ptys.has(sessionId)) this.closeSession(sessionId);
161
+ }
162
+ }
163
+ sendClose(sessionId) {
164
+ this.send(encodeFrame(RemoteFrameType.SESSION_CLOSE, sessionId));
165
+ }
166
+ send(frame) {
167
+ try {
168
+ this.sendFrame(frame);
169
+ return true;
170
+ } catch {
171
+ this.log.warn("Could not send terminal frame");
172
+ return false;
109
173
  }
110
174
  }
111
175
  };
176
+ function isSessionId(value) {
177
+ return Number.isInteger(value) && value >= 0 && value <= 4294967295;
178
+ }
179
+ function optionalDimension(value, fallback) {
180
+ if (value === void 0) return fallback;
181
+ return Number.isInteger(value) && value >= 1 && value <= MAX_DIMENSION ? value : void 0;
182
+ }
183
+ function optionalBoundedInteger(value, fallback, minimum, maximum, label) {
184
+ if (value === void 0) return fallback;
185
+ if (!Number.isInteger(value) || value < minimum || value > maximum) throw new TypeError(`${label} is invalid`);
186
+ return value;
187
+ }
188
+ function requirePlainString(value, label, maxChars, allowEmpty = false) {
189
+ if (typeof value !== "string" || !allowEmpty && value.length === 0 || value.length > maxChars || value.includes("\0")) throw new TypeError(`${label} is invalid`);
190
+ return value;
191
+ }
192
+ function chunkUtf8(value, maxBytes) {
193
+ if (value.length === 0) return [];
194
+ const chunks = [];
195
+ let characters = [];
196
+ let bytes = 0;
197
+ for (const character of value) {
198
+ const size = Buffer.byteLength(character, "utf8");
199
+ if (bytes + size > maxBytes && characters.length > 0) {
200
+ chunks.push(Buffer.from(characters.join(""), "utf8"));
201
+ characters = [];
202
+ bytes = 0;
203
+ }
204
+ characters.push(character);
205
+ bytes += size;
206
+ }
207
+ if (characters.length > 0) chunks.push(Buffer.from(characters.join(""), "utf8"));
208
+ return chunks;
209
+ }
112
210
  //#endregion
113
211
  export { TerminalSurface };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/terminal",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
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
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -13,11 +13,12 @@
13
13
  }
14
14
  },
15
15
  "files": [
16
- "dist"
16
+ "dist",
17
+ "README.md"
17
18
  ],
18
19
  "dependencies": {
19
20
  "@lydell/node-pty": "1.2.0-beta.3",
20
- "@alfe.ai/remote": "^0.1.0"
21
+ "@alfe.ai/remote": "^0.2.0"
21
22
  },
22
23
  "license": "UNLICENSED",
23
24
  "homepage": "https://alfe.ai",