ai-remote 0.4.13 → 0.4.15

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.
@@ -0,0 +1,63 @@
1
+ // src/cli/viewer-layout.ts
2
+ var TERMINAL_COLUMNS = 80;
3
+ var TERMINAL_CELL_WIDTH = 7.25;
4
+ var TERMINAL_PANE_CHROME = 30;
5
+ var SIDE_PANEL_WIDTH = TERMINAL_COLUMNS * TERMINAL_CELL_WIDTH + TERMINAL_PANE_CHROME;
6
+ var SIDE_PANEL_MIN_WIDTH = 40 * TERMINAL_CELL_WIDTH + TERMINAL_PANE_CHROME;
7
+ var TITLEBAR_HEIGHT = 34;
8
+ var TITLEBAR_TRAFFIC_LIGHTS_WIDTH = 72;
9
+ var TITLEBAR_CONTROLS_WIDTH = 154;
10
+ function isTitlebarDragPoint(point, innerWidth, scaleFactor) {
11
+ const scale = Number.isFinite(scaleFactor) && scaleFactor > 0 ? scaleFactor : 1;
12
+ return point.y >= 0 && point.y < TITLEBAR_HEIGHT * scale && point.x >= TITLEBAR_TRAFFIC_LIGHTS_WIDTH * scale && point.x < innerWidth - TITLEBAR_CONTROLS_WIDTH * scale;
13
+ }
14
+ function logicalDisplaySize(physical, scaleFactor) {
15
+ const scale = Number.isFinite(scaleFactor) && scaleFactor > 0 ? scaleFactor : 1;
16
+ return {
17
+ width: Math.max(1, Math.round(physical.width / scale)),
18
+ height: Math.max(1, Math.round(physical.height / scale))
19
+ };
20
+ }
21
+ function fullscreenRdpSize(display, sidePanel) {
22
+ const availableWidth = Math.max(640, Math.floor(display.width) - (sidePanel ? SIDE_PANEL_WIDTH : 0));
23
+ const availableHeight = Math.max(480, Math.floor(display.height));
24
+ const scale = Math.min(1, 4096 / availableWidth, 2048 / availableHeight);
25
+ return {
26
+ width: Math.max(640, Math.floor(availableWidth * scale)) & ~3,
27
+ height: Math.max(480, Math.floor(availableHeight * scale)) & ~1
28
+ };
29
+ }
30
+
31
+ // src/cli/paths.ts
32
+ import { mkdirSync } from "node:fs";
33
+ import { homedir } from "node:os";
34
+ import { join } from "node:path";
35
+ var ROOT = join(homedir(), ".ai-remote");
36
+ var RUN_DIR = join(ROOT, "run");
37
+ function ensureRunDir() {
38
+ mkdirSync(RUN_DIR, { recursive: true, mode: 448 });
39
+ return RUN_DIR;
40
+ }
41
+ function sessionName(host, port, explicit) {
42
+ if (explicit) return explicit.replace(/[^\w.-]/g, "_");
43
+ return `${host}_${port}`.replace(/[^\w.-]/g, "_");
44
+ }
45
+ var socketPath = (name) => join(RUN_DIR, `${name}.sock`);
46
+ var metaPath = (name) => join(RUN_DIR, `${name}.json`);
47
+ var logPath = (name) => join(RUN_DIR, `${name}.log`);
48
+ var currentPath = () => join(ROOT, "current");
49
+
50
+ export {
51
+ SIDE_PANEL_WIDTH,
52
+ isTitlebarDragPoint,
53
+ logicalDisplaySize,
54
+ fullscreenRdpSize,
55
+ ROOT,
56
+ RUN_DIR,
57
+ ensureRunDir,
58
+ sessionName,
59
+ socketPath,
60
+ metaPath,
61
+ logPath,
62
+ currentPath
63
+ };
@@ -0,0 +1,173 @@
1
+ import {
2
+ SshSession,
3
+ TcpTransport
4
+ } from "./cli-chunk-EYQCDSPT.mjs";
5
+
6
+ // src/cli/shell.ts
7
+ function bareUsername(username) {
8
+ const withoutDomain = username.includes("\\") ? username.split("\\").pop() : username;
9
+ return withoutDomain.split("@")[0].trim();
10
+ }
11
+ var Shell = class {
12
+ options;
13
+ session;
14
+ ready = false;
15
+ /** True once the connection has ended. A closed shell never reopens; a new one is made. */
16
+ closed = false;
17
+ lastError = "";
18
+ #sinks = /* @__PURE__ */ new Set();
19
+ /** What the shell has printed, so a viewer attaching late sees the scrollback. */
20
+ #scrollback = [];
21
+ #scrollbackBytes = 0;
22
+ constructor(options) {
23
+ this.options = options;
24
+ this.session = new SshSession(`tcp://${options.host}:${options.port}`, {
25
+ username: options.username,
26
+ password: options.password ?? "",
27
+ identities: options.identities ?? [],
28
+ columns: options.columns ?? 120,
29
+ rows: options.rows ?? 30,
30
+ // Every host this reaches was named by the person running the command,
31
+ // on their own network. There is no stored known_hosts to compare with
32
+ // yet, so nothing useful can be decided here.
33
+ verifyHost: async () => true,
34
+ openTransport: () => new TcpTransport(options.host, options.port)
35
+ });
36
+ this.session.addEventListener("data", (event) => {
37
+ const bytes = event.detail.display ?? event.detail.bytes;
38
+ if (!bytes?.length) return;
39
+ this.#remember(bytes);
40
+ for (const sink of this.#sinks) sink(bytes);
41
+ });
42
+ this.session.addEventListener("ready", () => {
43
+ this.ready = true;
44
+ });
45
+ this.session.addEventListener("error", (event) => {
46
+ this.lastError = event.detail?.message || "";
47
+ });
48
+ this.session.addEventListener("close", () => {
49
+ this.ready = false;
50
+ this.closed = true;
51
+ });
52
+ }
53
+ /** Everything printed so far, capped so a long-running shell cannot grow forever. */
54
+ get scrollback() {
55
+ const total = this.#scrollback.reduce((sum, chunk) => sum + chunk.length, 0);
56
+ const out = new Uint8Array(total);
57
+ let offset = 0;
58
+ for (const chunk of this.#scrollback) {
59
+ out.set(chunk, offset);
60
+ offset += chunk.length;
61
+ }
62
+ return out;
63
+ }
64
+ /** Called when the connection ends, however it ends. Returns an unsubscribe. */
65
+ onClose(listener) {
66
+ if (this.closed) {
67
+ queueMicrotask(listener);
68
+ return () => {
69
+ };
70
+ }
71
+ this.session.addEventListener("close", listener);
72
+ return () => this.session.removeEventListener("close", listener);
73
+ }
74
+ onData(sink) {
75
+ this.#sinks.add(sink);
76
+ return () => this.#sinks.delete(sink);
77
+ }
78
+ connect(timeoutMs = 3e4) {
79
+ return new Promise((resolve, reject) => {
80
+ let settled = false;
81
+ const finish = (error) => {
82
+ if (settled) return;
83
+ settled = true;
84
+ clearTimeout(timer);
85
+ if (error) reject(error);
86
+ else resolve();
87
+ };
88
+ const timer = setTimeout(
89
+ () => finish(new Error(`The SSH host did not open a shell within ${Math.round(timeoutMs / 1e3)}s.`)),
90
+ timeoutMs
91
+ );
92
+ this.session.addEventListener("ready", () => finish());
93
+ this.session.addEventListener("close", (event) => {
94
+ finish(new Error(this.lastError || event.detail?.message || "The SSH connection closed during sign-in."));
95
+ });
96
+ this.session.connect();
97
+ });
98
+ }
99
+ /**
100
+ * Run one command and wait for its output and exit status.
101
+ *
102
+ * The first command against a Windows host usually fails: the engine frames
103
+ * commands POSIX-style to find where the output starts and ends, cmd.exe
104
+ * rejects that, and the engine notes the shell family and asks to be called
105
+ * again. That is a detail of how the shell was discovered, not something a
106
+ * caller did wrong, so the retry happens here instead of reaching them.
107
+ *
108
+ * The wait in front is the same idea one step earlier. "Connected" and "able
109
+ * to take a command" are not the same moment on Windows: OpenSSH hands out
110
+ * cmd.exe, the engine swaps it for PowerShell, and readiness is withdrawn in
111
+ * between. A caller arriving in that window used to be told the shell was
112
+ * still starting -- true, and useless, because the only thing to do about it
113
+ * is wait. So it waits.
114
+ */
115
+ async runCommand(command) {
116
+ await this.#waitForPrompt();
117
+ let result;
118
+ try {
119
+ result = await this.session.runCommand(command);
120
+ } catch (error) {
121
+ const message = error instanceof Error ? error.message : String(error);
122
+ if (!/run the command again|not a POSIX shell/i.test(message)) throw error;
123
+ await this.#waitForPrompt();
124
+ result = await this.session.runCommand(command);
125
+ }
126
+ return {
127
+ output: result.output ?? "",
128
+ exitStatus: result.exitStatus ?? null,
129
+ timedOut: Boolean(result.timedOut),
130
+ durationMs: result.durationMs ?? null
131
+ };
132
+ }
133
+ /**
134
+ * Wait until the shell will accept a framed command.
135
+ *
136
+ * Returns on the first check when the shell is already settled, which is the
137
+ * usual case, so this costs a waiting caller nothing.
138
+ */
139
+ async #waitForPrompt(timeoutMs = 15e3) {
140
+ const deadline = Date.now() + timeoutMs;
141
+ while (Date.now() < deadline) {
142
+ if (this.session.shellOpen && this.session.readySignaled !== false) return;
143
+ await new Promise((resolve) => setTimeout(resolve, 100));
144
+ }
145
+ throw new Error("The SSH shell did not become ready for a command.");
146
+ }
147
+ /** Raw keystrokes, for an interactive terminal. */
148
+ write(text) {
149
+ this.session.write(text);
150
+ }
151
+ resize(columns, rows) {
152
+ this.session.resize?.(columns, rows);
153
+ }
154
+ disconnect() {
155
+ try {
156
+ this.session.disconnect();
157
+ } catch {
158
+ }
159
+ }
160
+ #remember(bytes) {
161
+ const CAP = 256 * 1024;
162
+ this.#scrollback.push(bytes);
163
+ this.#scrollbackBytes += bytes.length;
164
+ while (this.#scrollbackBytes > CAP && this.#scrollback.length > 1) {
165
+ this.#scrollbackBytes -= this.#scrollback.shift().length;
166
+ }
167
+ }
168
+ };
169
+
170
+ export {
171
+ bareUsername,
172
+ Shell
173
+ };
@@ -0,0 +1,51 @@
1
+ // src/cli/ipc.ts
2
+ import net from "node:net";
3
+ import { StringDecoder } from "node:string_decoder";
4
+ function readMessages(socket, onMessage) {
5
+ const decoder = new StringDecoder("utf8");
6
+ let buffer = "";
7
+ socket.on("data", (chunk) => {
8
+ buffer += decoder.write(chunk);
9
+ for (; ; ) {
10
+ const newline = buffer.indexOf("\n");
11
+ if (newline === -1) return;
12
+ const line = buffer.slice(0, newline);
13
+ buffer = buffer.slice(newline + 1);
14
+ if (!line.trim()) continue;
15
+ try {
16
+ onMessage(JSON.parse(line));
17
+ } catch {
18
+ }
19
+ }
20
+ });
21
+ }
22
+ var writeMessage = (socket, value) => {
23
+ socket.write(`${JSON.stringify(value)}
24
+ `);
25
+ };
26
+ function request(path, op, args = {}, timeoutMs = 12e4) {
27
+ return new Promise((resolve, reject) => {
28
+ const socket = net.connect(path);
29
+ let settled = false;
30
+ const finish = (error, response) => {
31
+ if (settled) return;
32
+ settled = true;
33
+ clearTimeout(timer);
34
+ socket.destroy();
35
+ if (error) reject(error);
36
+ else resolve(response);
37
+ };
38
+ const timer = setTimeout(() => finish(new Error(`The session did not answer "${op}" in time.`)), timeoutMs);
39
+ socket.on("connect", () => writeMessage(socket, { id: 1, op, args }));
40
+ socket.on("error", (error) => {
41
+ finish(Object.assign(error, { notRunning: error.code === "ENOENT" || error.code === "ECONNREFUSED" }));
42
+ });
43
+ readMessages(socket, (message) => finish(null, message));
44
+ });
45
+ }
46
+
47
+ export {
48
+ readMessages,
49
+ writeMessage,
50
+ request
51
+ };
@@ -0,0 +1,201 @@
1
+ // src/protocols/ssh/wire.ts
2
+ var textEncoder = new TextEncoder();
3
+ var textDecoder = new TextDecoder();
4
+ function encodeUtf8(text) {
5
+ return textEncoder.encode(text);
6
+ }
7
+ function decodeUtf8(bytes) {
8
+ return textDecoder.decode(bytes);
9
+ }
10
+ function concatBytes(...parts) {
11
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
12
+ const out = new Uint8Array(total);
13
+ let offset = 0;
14
+ for (const part of parts) {
15
+ out.set(part, offset);
16
+ offset += part.length;
17
+ }
18
+ return out;
19
+ }
20
+ function toBase64(bytes) {
21
+ let binary = "";
22
+ for (const byte of bytes) binary += String.fromCharCode(byte);
23
+ return btoa(binary);
24
+ }
25
+ function fromBase64(text) {
26
+ const binary = atob(text);
27
+ const bytes = new Uint8Array(binary.length);
28
+ for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
29
+ return bytes;
30
+ }
31
+ function toBase64Url(bytes) {
32
+ return toBase64(bytes).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
33
+ }
34
+ var SshWriter = class {
35
+ bytes;
36
+ view;
37
+ offset = 0;
38
+ constructor(capacity = 256) {
39
+ this.bytes = new Uint8Array(new ArrayBuffer(capacity));
40
+ this.view = new DataView(this.bytes.buffer);
41
+ }
42
+ #ensure(extra) {
43
+ if (this.offset + extra <= this.bytes.length) return;
44
+ let capacity = Math.max(this.bytes.length * 2, 64);
45
+ while (capacity < this.offset + extra) capacity *= 2;
46
+ const grown = new Uint8Array(new ArrayBuffer(capacity));
47
+ grown.set(this.bytes.subarray(0, this.offset));
48
+ this.bytes = grown;
49
+ this.view = new DataView(grown.buffer);
50
+ }
51
+ u8(value) {
52
+ this.#ensure(1);
53
+ this.view.setUint8(this.offset, value);
54
+ this.offset += 1;
55
+ return this;
56
+ }
57
+ boolean(value) {
58
+ return this.u8(value ? 1 : 0);
59
+ }
60
+ u32(value) {
61
+ this.#ensure(4);
62
+ this.view.setUint32(this.offset, value >>> 0, false);
63
+ this.offset += 4;
64
+ return this;
65
+ }
66
+ /**
67
+ * A 64-bit unsigned integer, written from a JavaScript number.
68
+ *
69
+ * SFTP measures files and offsets in these. Written as two 32-bit halves
70
+ * rather than through a BigInt, because the values are byte counts that come
71
+ * from and go back to `number` at every other layer, and converting twice per
72
+ * chunk to satisfy a type is work with nothing to show for it. Above
73
+ * `Number.MAX_SAFE_INTEGER` -- eight petabytes -- this is wrong, and so is
74
+ * every other size in this codebase.
75
+ */
76
+ u64(value) {
77
+ const whole = Math.floor(value);
78
+ return this.u32(Math.floor(whole / 4294967296)).u32(whole % 4294967296);
79
+ }
80
+ raw(bytes) {
81
+ this.#ensure(bytes.length);
82
+ this.bytes.set(bytes, this.offset);
83
+ this.offset += bytes.length;
84
+ return this;
85
+ }
86
+ /** A length-prefixed string. Text is encoded as UTF-8. */
87
+ string(value) {
88
+ const bytes = typeof value === "string" ? encodeUtf8(value) : value;
89
+ return this.u32(bytes.length).raw(bytes);
90
+ }
91
+ nameList(names) {
92
+ return this.string(names.join(","));
93
+ }
94
+ /**
95
+ * An mpint: two's complement, big endian, with no leading zero bytes except
96
+ * the one that keeps a positive number from looking negative.
97
+ */
98
+ mpint(bytes) {
99
+ let start = 0;
100
+ while (start < bytes.length && bytes[start] === 0) start++;
101
+ const magnitude = bytes.subarray(start);
102
+ if (magnitude.length === 0) return this.u32(0);
103
+ if ((magnitude[0] ?? 0) & 128) {
104
+ this.u32(magnitude.length + 1).u8(0);
105
+ return this.raw(magnitude);
106
+ }
107
+ return this.u32(magnitude.length).raw(magnitude);
108
+ }
109
+ get length() {
110
+ return this.offset;
111
+ }
112
+ take() {
113
+ return this.bytes.slice(0, this.offset);
114
+ }
115
+ };
116
+ var SshReader = class {
117
+ bytes;
118
+ view;
119
+ offset;
120
+ constructor(bytes, offset = 0) {
121
+ this.bytes = bytes;
122
+ this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
123
+ this.offset = offset;
124
+ }
125
+ get remaining() {
126
+ return this.bytes.length - this.offset;
127
+ }
128
+ #need(count) {
129
+ if (count < 0 || this.remaining < count) {
130
+ throw new Error(`SSH: truncated packet (needed ${count} bytes, ${this.remaining} left)`);
131
+ }
132
+ }
133
+ u8() {
134
+ this.#need(1);
135
+ return this.view.getUint8(this.offset++);
136
+ }
137
+ boolean() {
138
+ return this.u8() !== 0;
139
+ }
140
+ u32() {
141
+ this.#need(4);
142
+ const value = this.view.getUint32(this.offset, false);
143
+ this.offset += 4;
144
+ return value;
145
+ }
146
+ /** A 64-bit unsigned integer as a number; see SshWriter#u64 on the range. */
147
+ u64() {
148
+ return this.u32() * 4294967296 + this.u32();
149
+ }
150
+ raw(count) {
151
+ this.#need(count);
152
+ const slice = this.bytes.subarray(this.offset, this.offset + count);
153
+ this.offset += count;
154
+ return slice;
155
+ }
156
+ /** The bytes of a length-prefixed string. */
157
+ stringBytes() {
158
+ return this.raw(this.u32());
159
+ }
160
+ string() {
161
+ return decodeUtf8(this.stringBytes());
162
+ }
163
+ nameList() {
164
+ const value = this.string();
165
+ return value ? value.split(",") : [];
166
+ }
167
+ /** An mpint's magnitude, with the sign byte removed. */
168
+ mpint() {
169
+ const bytes = this.stringBytes();
170
+ let start = 0;
171
+ while (start < bytes.length && bytes[start] === 0) start++;
172
+ return bytes.subarray(start);
173
+ }
174
+ skip(count) {
175
+ this.#need(count);
176
+ this.offset += count;
177
+ return this;
178
+ }
179
+ rest() {
180
+ return this.bytes.subarray(this.offset);
181
+ }
182
+ };
183
+ function padStart(bytes, width) {
184
+ if (bytes.length === width) return bytes;
185
+ if (bytes.length > width) return bytes.subarray(bytes.length - width);
186
+ const out = new Uint8Array(width);
187
+ out.set(bytes, width - bytes.length);
188
+ return out;
189
+ }
190
+
191
+ export {
192
+ encodeUtf8,
193
+ decodeUtf8,
194
+ concatBytes,
195
+ toBase64,
196
+ fromBase64,
197
+ toBase64Url,
198
+ SshWriter,
199
+ SshReader,
200
+ padStart
201
+ };