@indianic/dialout 1.10.0 → 1.12.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.
Files changed (117) hide show
  1. package/README.md +14 -0
  2. package/dist/ai-adapters/claude.d.ts.map +1 -1
  3. package/dist/ai-adapters/claude.js +18 -1
  4. package/dist/ai-adapters/claude.js.map +1 -1
  5. package/dist/ai-sessions.d.ts +26 -1
  6. package/dist/ai-sessions.d.ts.map +1 -1
  7. package/dist/ai-sessions.js +43 -7
  8. package/dist/ai-sessions.js.map +1 -1
  9. package/dist/cli-client.d.ts +60 -0
  10. package/dist/cli-client.d.ts.map +1 -0
  11. package/dist/cli-client.js +246 -0
  12. package/dist/cli-client.js.map +1 -0
  13. package/dist/cli-terminal.d.ts +101 -0
  14. package/dist/cli-terminal.d.ts.map +1 -0
  15. package/dist/cli-terminal.js +1121 -0
  16. package/dist/cli-terminal.js.map +1 -0
  17. package/dist/cli.js +84 -0
  18. package/dist/cli.js.map +1 -1
  19. package/dist/file-transfer.d.ts +18 -0
  20. package/dist/file-transfer.d.ts.map +1 -0
  21. package/dist/file-transfer.js +266 -0
  22. package/dist/file-transfer.js.map +1 -0
  23. package/dist/fs-browser.d.ts +3 -1
  24. package/dist/fs-browser.d.ts.map +1 -1
  25. package/dist/fs-browser.js +35 -6
  26. package/dist/fs-browser.js.map +1 -1
  27. package/dist/mcp-stdio.d.ts +10 -0
  28. package/dist/mcp-stdio.d.ts.map +1 -0
  29. package/dist/mcp-stdio.js +199 -0
  30. package/dist/mcp-stdio.js.map +1 -0
  31. package/dist/shared-shell.d.ts +77 -0
  32. package/dist/shared-shell.d.ts.map +1 -0
  33. package/dist/shared-shell.js +282 -0
  34. package/dist/shared-shell.js.map +1 -0
  35. package/dist/websocket.d.ts.map +1 -1
  36. package/dist/websocket.js +413 -1
  37. package/dist/websocket.js.map +1 -1
  38. package/node_modules/@indianic/dialout-shared/dist/ai-chat-prefs.test.d.ts +2 -0
  39. package/node_modules/@indianic/dialout-shared/dist/ai-chat-prefs.test.d.ts.map +1 -0
  40. package/node_modules/@indianic/dialout-shared/dist/ai-chat-prefs.test.js +38 -0
  41. package/node_modules/@indianic/dialout-shared/dist/attachments.test.d.ts +2 -0
  42. package/node_modules/@indianic/dialout-shared/dist/attachments.test.d.ts.map +1 -0
  43. package/node_modules/@indianic/dialout-shared/dist/attachments.test.js +31 -0
  44. package/node_modules/@indianic/dialout-shared/dist/captured.d.ts +27 -0
  45. package/node_modules/@indianic/dialout-shared/dist/captured.d.ts.map +1 -0
  46. package/node_modules/@indianic/dialout-shared/dist/captured.js +23 -0
  47. package/node_modules/@indianic/dialout-shared/dist/captured.test.d.ts +2 -0
  48. package/node_modules/@indianic/dialout-shared/dist/captured.test.d.ts.map +1 -0
  49. package/node_modules/@indianic/dialout-shared/dist/captured.test.js +27 -0
  50. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.d.ts +6 -0
  51. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.d.ts.map +1 -1
  52. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.js +15 -4
  53. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.test.d.ts +2 -0
  54. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.test.d.ts.map +1 -0
  55. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.test.js +81 -0
  56. package/node_modules/@indianic/dialout-shared/dist/command-filter.test.d.ts +2 -0
  57. package/node_modules/@indianic/dialout-shared/dist/command-filter.test.d.ts.map +1 -0
  58. package/node_modules/@indianic/dialout-shared/dist/command-filter.test.js +63 -0
  59. package/node_modules/@indianic/dialout-shared/dist/composer-behaviour.test.d.ts +2 -0
  60. package/node_modules/@indianic/dialout-shared/dist/composer-behaviour.test.d.ts.map +1 -0
  61. package/node_modules/@indianic/dialout-shared/dist/composer-behaviour.test.js +27 -0
  62. package/node_modules/@indianic/dialout-shared/dist/event-merge.d.ts +7 -0
  63. package/node_modules/@indianic/dialout-shared/dist/event-merge.d.ts.map +1 -1
  64. package/node_modules/@indianic/dialout-shared/dist/event-merge.test.d.ts +2 -0
  65. package/node_modules/@indianic/dialout-shared/dist/event-merge.test.d.ts.map +1 -0
  66. package/node_modules/@indianic/dialout-shared/dist/event-merge.test.js +66 -0
  67. package/node_modules/@indianic/dialout-shared/dist/events.d.ts +1 -0
  68. package/node_modules/@indianic/dialout-shared/dist/events.d.ts.map +1 -1
  69. package/node_modules/@indianic/dialout-shared/dist/index.d.ts +6 -1
  70. package/node_modules/@indianic/dialout-shared/dist/index.d.ts.map +1 -1
  71. package/node_modules/@indianic/dialout-shared/dist/index.js +15 -1
  72. package/node_modules/@indianic/dialout-shared/dist/notices.d.ts +37 -0
  73. package/node_modules/@indianic/dialout-shared/dist/notices.d.ts.map +1 -0
  74. package/node_modules/@indianic/dialout-shared/dist/notices.js +124 -0
  75. package/node_modules/@indianic/dialout-shared/dist/notices.test.d.ts +2 -0
  76. package/node_modules/@indianic/dialout-shared/dist/notices.test.d.ts.map +1 -0
  77. package/node_modules/@indianic/dialout-shared/dist/notices.test.js +92 -0
  78. package/node_modules/@indianic/dialout-shared/dist/notify.test.d.ts +2 -0
  79. package/node_modules/@indianic/dialout-shared/dist/notify.test.d.ts.map +1 -0
  80. package/node_modules/@indianic/dialout-shared/dist/notify.test.js +51 -0
  81. package/node_modules/@indianic/dialout-shared/dist/pane-prompt.d.ts +17 -0
  82. package/node_modules/@indianic/dialout-shared/dist/pane-prompt.d.ts.map +1 -1
  83. package/node_modules/@indianic/dialout-shared/dist/pane-prompt.js +27 -0
  84. package/node_modules/@indianic/dialout-shared/dist/pane-prompt.test.d.ts +2 -0
  85. package/node_modules/@indianic/dialout-shared/dist/pane-prompt.test.d.ts.map +1 -0
  86. package/node_modules/@indianic/dialout-shared/dist/pane-prompt.test.js +184 -0
  87. package/node_modules/@indianic/dialout-shared/dist/scroll-pin.test.d.ts +2 -0
  88. package/node_modules/@indianic/dialout-shared/dist/scroll-pin.test.d.ts.map +1 -0
  89. package/node_modules/@indianic/dialout-shared/dist/scroll-pin.test.js +21 -0
  90. package/node_modules/@indianic/dialout-shared/dist/thinking.d.ts +9 -0
  91. package/node_modules/@indianic/dialout-shared/dist/thinking.d.ts.map +1 -0
  92. package/node_modules/@indianic/dialout-shared/dist/thinking.js +38 -0
  93. package/node_modules/@indianic/dialout-shared/dist/thinking.test.d.ts +2 -0
  94. package/node_modules/@indianic/dialout-shared/dist/thinking.test.d.ts.map +1 -0
  95. package/node_modules/@indianic/dialout-shared/dist/thinking.test.js +30 -0
  96. package/node_modules/@indianic/dialout-shared/dist/tmux-prefs.test.d.ts +2 -0
  97. package/node_modules/@indianic/dialout-shared/dist/tmux-prefs.test.d.ts.map +1 -0
  98. package/node_modules/@indianic/dialout-shared/dist/tmux-prefs.test.js +95 -0
  99. package/node_modules/@indianic/dialout-shared/dist/tool-appearance.test.d.ts +2 -0
  100. package/node_modules/@indianic/dialout-shared/dist/tool-appearance.test.d.ts.map +1 -0
  101. package/node_modules/@indianic/dialout-shared/dist/tool-appearance.test.js +38 -0
  102. package/node_modules/@indianic/dialout-shared/package.json +2 -2
  103. package/node_modules/@indianic/dialout-shared/src/captured.test.ts +29 -0
  104. package/node_modules/@indianic/dialout-shared/src/captured.ts +46 -0
  105. package/node_modules/@indianic/dialout-shared/src/chat-blocks.test.ts +14 -0
  106. package/node_modules/@indianic/dialout-shared/src/chat-blocks.ts +14 -5
  107. package/node_modules/@indianic/dialout-shared/src/event-merge.test.ts +27 -0
  108. package/node_modules/@indianic/dialout-shared/src/event-merge.ts +11 -0
  109. package/node_modules/@indianic/dialout-shared/src/events.ts +5 -1
  110. package/node_modules/@indianic/dialout-shared/src/index.ts +8 -1
  111. package/node_modules/@indianic/dialout-shared/src/notices.test.ts +102 -0
  112. package/node_modules/@indianic/dialout-shared/src/notices.ts +159 -0
  113. package/node_modules/@indianic/dialout-shared/src/pane-prompt.test.ts +23 -1
  114. package/node_modules/@indianic/dialout-shared/src/pane-prompt.ts +27 -0
  115. package/node_modules/@indianic/dialout-shared/src/thinking.test.ts +32 -0
  116. package/node_modules/@indianic/dialout-shared/src/thinking.ts +32 -0
  117. package/package.json +1 -1
@@ -0,0 +1,1121 @@
1
+ "use strict";
2
+ // User CLI session: login, the machine list, a remote shell, and one-file
3
+ // copy. config.json's apiKey and WS_INTERNAL_TOKEN are never read. The
4
+ // session file is the user JWT only. Leaving this process detaches the
5
+ // view; it must not end the remote tmux session.
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ var __importDefault = (this && this.__importDefault) || function (mod) {
40
+ return (mod && mod.__esModule) ? mod : { "default": mod };
41
+ };
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.login = login;
44
+ exports.logout = logout;
45
+ exports.printMachines = printMachines;
46
+ exports.machineMatchError = machineMatchError;
47
+ exports.connectMachine = connectMachine;
48
+ exports.fatalTerminalFrame = fatalTerminalFrame;
49
+ exports.bridgeTerminal = bridgeTerminal;
50
+ exports.sharedControlMessage = sharedControlMessage;
51
+ exports.controlWaitMsFor = controlWaitMsFor;
52
+ exports.runTerminalControl = runTerminalControl;
53
+ exports.transferCopy = transferCopy;
54
+ exports.copyCommand = copyCommand;
55
+ exports.copyFile = copyFile;
56
+ exports.serveMcp = serveMcp;
57
+ const crypto_1 = require("crypto");
58
+ const http_1 = require("http");
59
+ const https_1 = require("https");
60
+ const fs_1 = require("fs");
61
+ const os = __importStar(require("os"));
62
+ const path = __importStar(require("path"));
63
+ const ws_1 = __importDefault(require("ws"));
64
+ const config_1 = require("./config");
65
+ const cli_client_1 = require("./cli-client");
66
+ const mcp_stdio_1 = require("./mcp-stdio");
67
+ const LOGIN_EXPIRED = 'dialout: login expired, run dialout login\n';
68
+ const ORIGIN_TEXT = 'dialout: server URL could not be turned into an API origin';
69
+ const ORIGIN_FAILED = `${ORIGIN_TEXT}\n`;
70
+ const ENROLL_MSG = 'dialout: finish two-factor enrollment in the browser, then run dialout login\n';
71
+ const EXISTS_CONFLICT = 'exists (use overwrite)';
72
+ const FILE_TOO_BIG = 'file exceeds 10 MB';
73
+ // Stat stays on the JSON budget. File bytes wait long enough that a relay
74
+ // which answers at 70s can still return 504 instead of a client timeout.
75
+ const JSON_TIMEOUT_MS = 20_000;
76
+ const FILE_BYTE_TIMEOUT_MS = 90_000;
77
+ // The server fires "Agent did not answer" at this pad past the caller's
78
+ // deadline. The client pad is on top of that, so the sentence wins the race.
79
+ const SERVER_SILENCE_PAD_MS = 5_000;
80
+ const CLIENT_WAIT_PAD_MS = 5_000;
81
+ const DEFAULT_CONTROL_MS = 120_000;
82
+ function sessionFile() {
83
+ return path.join((0, config_1.getConfigDir)(), 'cli-session.json');
84
+ }
85
+ function unlisten(emitter, event, listener) {
86
+ if (typeof emitter.off === 'function')
87
+ emitter.off(event, listener);
88
+ else
89
+ emitter.removeListener?.(event, listener);
90
+ }
91
+ function chunkText(chunk) {
92
+ return typeof chunk === 'string' ? chunk : chunk.toString('utf8');
93
+ }
94
+ // Bytes left after a newline when one read delivered two prompts. A pipe
95
+ // can do that; a tty usually does not.
96
+ let carry = '';
97
+ // PIN is the only hidden prompt. A tty in raw mode does not echo. Cooked
98
+ // mode already echoes, and a pipe has no echo to suppress.
99
+ function readLine(io, label, hidden) {
100
+ io.stdout.write(label);
101
+ const stdin = io.stdin;
102
+ const raw = hidden && stdin.isTTY === true && typeof stdin.setRawMode === 'function';
103
+ if (raw)
104
+ stdin.setRawMode(true);
105
+ return new Promise((resolve) => {
106
+ let buf = '';
107
+ const seeded = carry;
108
+ carry = '';
109
+ const cleanup = () => {
110
+ unlisten(stdin, 'data', onData);
111
+ if (raw) {
112
+ try {
113
+ stdin.setRawMode(false);
114
+ }
115
+ catch { /* tty already gone */ }
116
+ }
117
+ stdin.pause?.();
118
+ };
119
+ const finish = (line) => {
120
+ cleanup();
121
+ if (raw)
122
+ io.stdout.write('\n');
123
+ resolve(line);
124
+ };
125
+ const push = (text) => {
126
+ for (let i = 0; i < text.length; i++) {
127
+ const ch = text[i] ?? '';
128
+ if (ch === '\u0003') {
129
+ cleanup();
130
+ process.exit(130);
131
+ }
132
+ if (ch === '\n' || ch === '\r') {
133
+ if (ch === '\r' && text[i + 1] === '\n')
134
+ i += 1;
135
+ carry = text.slice(i + 1);
136
+ finish(buf);
137
+ return true;
138
+ }
139
+ if (raw && (ch === '\u007f' || ch === '\b')) {
140
+ buf = buf.slice(0, -1);
141
+ continue;
142
+ }
143
+ buf += ch;
144
+ }
145
+ return false;
146
+ };
147
+ const onData = (chunk) => {
148
+ push(chunkText(chunk));
149
+ };
150
+ if (seeded && push(seeded))
151
+ return;
152
+ stdin.on('data', onData);
153
+ stdin.resume?.();
154
+ });
155
+ }
156
+ function requestBytes(method, target, headers, body, timeoutMs) {
157
+ return new Promise((resolve, reject) => {
158
+ let settled = false;
159
+ const fail = (err) => {
160
+ if (settled)
161
+ return;
162
+ settled = true;
163
+ reject(err);
164
+ };
165
+ const ok = (status, buf) => {
166
+ if (settled)
167
+ return;
168
+ settled = true;
169
+ resolve({ status, body: buf });
170
+ };
171
+ const url = new URL(target);
172
+ const lib = url.protocol === 'https:' ? https_1.request : http_1.request;
173
+ const reqHeaders = { ...headers };
174
+ if (body !== undefined)
175
+ reqHeaders['Content-Length'] = String(body.length);
176
+ const req = lib(url, { method, headers: reqHeaders }, (res) => {
177
+ const chunks = [];
178
+ res.on('data', (chunk) => {
179
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
180
+ });
181
+ res.on('end', () => {
182
+ ok(res.statusCode ?? 0, Buffer.concat(chunks));
183
+ });
184
+ });
185
+ // A stuck API call should not hold the prompt open forever.
186
+ req.setTimeout(timeoutMs, () => {
187
+ req.destroy(new Error('timed out'));
188
+ });
189
+ req.on('error', (err) => fail(err));
190
+ if (body !== undefined)
191
+ req.write(body);
192
+ req.end();
193
+ });
194
+ }
195
+ function parseJsonBody(body) {
196
+ const text = body.toString('utf8');
197
+ if (!text)
198
+ return null;
199
+ try {
200
+ return JSON.parse(text);
201
+ }
202
+ catch {
203
+ return null;
204
+ }
205
+ }
206
+ function requestJson(method, target, headers, body) {
207
+ return requestBytes(method, target, headers, body !== undefined ? Buffer.from(body, 'utf8') : undefined, JSON_TIMEOUT_MS).then(({ status, body: buf }) => ({ status, json: parseJsonBody(buf) }));
208
+ }
209
+ async function postAuth(origin, body) {
210
+ const { json } = await requestJson('POST', `${origin}/api/auth`, {
211
+ 'Content-Type': 'application/json',
212
+ 'X-DevDash-Client': 'native',
213
+ }, JSON.stringify(body));
214
+ return json;
215
+ }
216
+ function fail(io, message) {
217
+ io.stderr.write(message.endsWith('\n') ? message : `${message}\n`);
218
+ process.exitCode = 1;
219
+ }
220
+ async function login(io) {
221
+ const { serverUrl } = (0, config_1.loadConfig)();
222
+ const origin = (0, cli_client_1.apiOriginFromServerUrl)(serverUrl);
223
+ if (!origin) {
224
+ fail(io, ORIGIN_FAILED);
225
+ return;
226
+ }
227
+ const email = (await readLine(io, 'Email: ', false)).trim();
228
+ const otpCode = (await readLine(io, 'PIN: ', true)).trim();
229
+ let first;
230
+ try {
231
+ const loginBody = {
232
+ action: 'login',
233
+ email,
234
+ otpCode,
235
+ };
236
+ first = await postAuth(origin, loginBody);
237
+ }
238
+ catch (err) {
239
+ fail(io, `dialout: ${err instanceof Error ? err.message : 'request failed'}`);
240
+ return;
241
+ }
242
+ let step = (0, cli_client_1.interpretAuthBody)(first);
243
+ if (step.step === 'totp') {
244
+ const code = (await readLine(io, 'Authenticator code: ', false)).trim();
245
+ const pendingToken = step.pendingToken;
246
+ try {
247
+ // No trustDevice: this client has no browser trust cookie to set.
248
+ const verifyBody = {
249
+ action: 'verify-2fa',
250
+ code,
251
+ pendingToken,
252
+ };
253
+ step = (0, cli_client_1.interpretAuthBody)(await postAuth(origin, verifyBody));
254
+ }
255
+ catch (err) {
256
+ fail(io, `dialout: ${err instanceof Error ? err.message : 'request failed'}`);
257
+ return;
258
+ }
259
+ }
260
+ if (step.step === 'enroll') {
261
+ fail(io, ENROLL_MSG);
262
+ return;
263
+ }
264
+ if (step.step === 'error') {
265
+ fail(io, step.message);
266
+ return;
267
+ }
268
+ if (step.step !== 'store') {
269
+ fail(io, 'unexpected login response');
270
+ return;
271
+ }
272
+ (0, cli_client_1.writeCliSession)(sessionFile(), { token: step.token, serverUrl });
273
+ }
274
+ function logout() {
275
+ const file = sessionFile();
276
+ if (!(0, fs_1.existsSync)(file))
277
+ return;
278
+ try {
279
+ (0, fs_1.unlinkSync)(file);
280
+ }
281
+ catch (err) {
282
+ const code = err.code;
283
+ if (code !== 'ENOENT')
284
+ throw err;
285
+ }
286
+ }
287
+ function readMachineRows(json) {
288
+ if (!Array.isArray(json))
289
+ return null;
290
+ const rows = [];
291
+ for (const item of json) {
292
+ if (typeof item !== 'object' || item === null)
293
+ return null;
294
+ const rec = item;
295
+ if (typeof rec.id !== 'number' || !Number.isInteger(rec.id))
296
+ return null;
297
+ if (typeof rec.name !== 'string' || rec.name.length === 0)
298
+ return null;
299
+ rows.push({ id: rec.id, name: rec.name, isOnline: rec.isOnline === true });
300
+ }
301
+ return rows;
302
+ }
303
+ async function fetchMachines(session, http) {
304
+ const origin = (0, cli_client_1.apiOriginFromServerUrl)(session.serverUrl);
305
+ if (!origin)
306
+ return { ok: false, kind: 'origin' };
307
+ try {
308
+ const headers = { Authorization: `Bearer ${session.token}` };
309
+ const target = `${origin}/api/machines`;
310
+ let status;
311
+ let json;
312
+ if (http) {
313
+ const res = await http({ method: 'GET', url: target, headers, timeoutMs: JSON_TIMEOUT_MS });
314
+ status = res.status;
315
+ json = parseJsonBody(res.body);
316
+ }
317
+ else {
318
+ const got = await requestJson('GET', target, headers);
319
+ status = got.status;
320
+ json = got.json;
321
+ }
322
+ if (status === 401)
323
+ return { ok: false, kind: 'expired' };
324
+ if (status < 200 || status >= 300)
325
+ return { ok: false, kind: 'http', detail: String(status) };
326
+ const rows = readMachineRows(json);
327
+ if (!rows)
328
+ return { ok: false, kind: 'http' };
329
+ return { ok: true, rows };
330
+ }
331
+ catch (err) {
332
+ return { ok: false, kind: 'network', detail: err instanceof Error ? err.message : 'request failed' };
333
+ }
334
+ }
335
+ function reportFetchFailure(result) {
336
+ if (result.kind === 'expired') {
337
+ process.stderr.write(LOGIN_EXPIRED);
338
+ return;
339
+ }
340
+ if (result.kind === 'origin') {
341
+ process.stderr.write(ORIGIN_FAILED);
342
+ return;
343
+ }
344
+ if (result.kind === 'network') {
345
+ process.stderr.write(`dialout: ${result.detail ?? 'request failed'}\n`);
346
+ return;
347
+ }
348
+ const suffix = result.detail ? ` (${result.detail})` : '';
349
+ process.stderr.write(`dialout: could not list machines${suffix}\n`);
350
+ }
351
+ async function printMachines() {
352
+ const session = (0, cli_client_1.readCliSession)(sessionFile());
353
+ if (!session) {
354
+ process.stderr.write(LOGIN_EXPIRED);
355
+ process.exitCode = 1;
356
+ return;
357
+ }
358
+ const result = await fetchMachines(session);
359
+ if (!result.ok) {
360
+ reportFetchFailure(result);
361
+ process.exitCode = 1;
362
+ return;
363
+ }
364
+ for (const row of result.rows) {
365
+ process.stdout.write(`${row.id} ${row.name} ${row.isOnline ? 'online' : 'offline'}\n`);
366
+ }
367
+ }
368
+ function machineMatchError(input, matched) {
369
+ if (matched.reason === 'none')
370
+ return `dialout: no machine named "${input}"`;
371
+ if (matched.reason === 'offline') {
372
+ const label = matched.matches[0]?.name ?? input;
373
+ return `dialout: ${label} is offline`;
374
+ }
375
+ return matched.matches
376
+ .map((row) => ` ${row.id} ${row.name} ${row.isOnline ? 'online' : 'offline'}`)
377
+ .join('\n');
378
+ }
379
+ function reportMatch(name, matched) {
380
+ if (matched.ok)
381
+ return true;
382
+ process.stderr.write(`${machineMatchError(name, matched)}\n`);
383
+ return false;
384
+ }
385
+ async function connectMachine(name) {
386
+ const session = (0, cli_client_1.readCliSession)(sessionFile());
387
+ if (!session) {
388
+ process.stderr.write(LOGIN_EXPIRED);
389
+ return 1;
390
+ }
391
+ // The token was issued for the server URL stored beside it. A later
392
+ // profile switch must not reuse it against a different server.
393
+ const { serverUrl } = (0, config_1.loadConfig)();
394
+ if (session.serverUrl !== serverUrl) {
395
+ process.stderr.write(LOGIN_EXPIRED);
396
+ return 1;
397
+ }
398
+ const result = await fetchMachines(session);
399
+ if (!result.ok) {
400
+ reportFetchFailure(result);
401
+ return 1;
402
+ }
403
+ const matched = (0, cli_client_1.matchMachine)(result.rows, name);
404
+ if (!reportMatch(name, matched))
405
+ return 1;
406
+ if (!matched.ok)
407
+ return 1;
408
+ const url = (0, cli_client_1.terminalSocketUrl)(session.serverUrl, matched.machine.id, session.token);
409
+ if (!url) {
410
+ process.stderr.write(ORIGIN_FAILED);
411
+ return 1;
412
+ }
413
+ return bridgeTerminal({
414
+ url,
415
+ machineName: matched.machine.name,
416
+ machineId: matched.machine.id,
417
+ pid: process.pid,
418
+ stdin: process.stdin,
419
+ stdout: process.stdout,
420
+ signals: process,
421
+ });
422
+ }
423
+ function termSize(stdout) {
424
+ const cols = typeof stdout.columns === 'number' && stdout.columns > 0 ? stdout.columns : 80;
425
+ const rows = typeof stdout.rows === 'number' && stdout.rows > 0 ? stdout.rows : 24;
426
+ return { cols, rows };
427
+ }
428
+ function messageText(raw) {
429
+ if (typeof raw === 'string')
430
+ return raw;
431
+ if (Buffer.isBuffer(raw))
432
+ return raw.toString('utf8');
433
+ if (raw instanceof ArrayBuffer)
434
+ return Buffer.from(raw).toString('utf8');
435
+ if (Array.isArray(raw) && raw.every((part) => Buffer.isBuffer(part))) {
436
+ return Buffer.concat(raw).toString('utf8');
437
+ }
438
+ return '';
439
+ }
440
+ function fatalSentence(error) {
441
+ return error.endsWith('\n') ? error : `${error}\n`;
442
+ }
443
+ // Frames that must settle the local tty. `unauthorized` is not one of them:
444
+ // that sentence is the login-expired path, not the server's text.
445
+ function fatalTerminalFrame(msg, sessionId) {
446
+ if (typeof msg !== 'object' || msg === null || Array.isArray(msg))
447
+ return null;
448
+ const rec = msg;
449
+ if (rec.type === 'pty_error') {
450
+ if (rec.id !== sessionId || typeof rec.error !== 'string')
451
+ return null;
452
+ return { sentence: fatalSentence(rec.error) };
453
+ }
454
+ if (rec.type === 'error' && typeof rec.error === 'string' && rec.error !== 'unauthorized') {
455
+ return { sentence: fatalSentence(rec.error) };
456
+ }
457
+ return null;
458
+ }
459
+ function bridgeTerminal(opts) {
460
+ // A new id per process, so this attach does not steal another viewer's
461
+ // socket. The tmux name stays one shell per machine.
462
+ const id = `cli-m${opts.machineId}-${opts.pid}`;
463
+ const tmuxName = `dd-m${opts.machineId}`;
464
+ const socket = opts.socket ?? new ws_1.default(opts.url);
465
+ let rawOn = false;
466
+ let settled = false;
467
+ const restoreTty = () => {
468
+ if (!rawOn)
469
+ return;
470
+ rawOn = false;
471
+ try {
472
+ opts.stdin.setRawMode?.(false);
473
+ }
474
+ catch { /* tty already restored */ }
475
+ };
476
+ // The hint is a normal line. Raw mode is what hands the screen to the remote shell.
477
+ process.stderr.write(`dialout: ${opts.machineName} \u2014 exit the shell to return here\n`);
478
+ if (opts.stdin.isTTY === true && typeof opts.stdin.setRawMode === 'function') {
479
+ opts.stdin.setRawMode(true);
480
+ rawOn = true;
481
+ }
482
+ return new Promise((resolve) => {
483
+ let atLineStart = true;
484
+ let pendingTilde = false;
485
+ let opened = false;
486
+ const finish = (code) => {
487
+ if (settled)
488
+ return;
489
+ settled = true;
490
+ unlisten(opts.stdin, 'data', onStdin);
491
+ unlisten(opts.signals, 'SIGTERM', onTerm);
492
+ unlisten(opts.signals, 'SIGHUP', onHang);
493
+ unlisten(opts.signals, 'SIGWINCH', onResize);
494
+ unlisten(socket, 'message', onMessage);
495
+ unlisten(socket, 'close', onClose);
496
+ unlisten(socket, 'error', onError);
497
+ unlisten(socket, 'open', onOpen);
498
+ unlisten(socket, 'unexpected-response', onUpgradeRejected);
499
+ restoreTty();
500
+ try {
501
+ socket.close?.();
502
+ }
503
+ catch { /* already closed */ }
504
+ resolve(code);
505
+ };
506
+ const backOn = () => {
507
+ process.stderr.write(`dialout: back on ${os.hostname()}\n`);
508
+ };
509
+ const send = (msg) => {
510
+ try {
511
+ socket.send(JSON.stringify(msg));
512
+ return true;
513
+ }
514
+ catch {
515
+ return false;
516
+ }
517
+ };
518
+ const lost = () => {
519
+ if (settled)
520
+ return;
521
+ // Only while the socket can still take a frame. A close that already
522
+ // landed does not get a second detach, and never a remote-shell end.
523
+ if (socket.readyState === ws_1.default.OPEN) {
524
+ send({ type: 'pty_detach', id });
525
+ }
526
+ process.stderr.write('dialout: connection lost\n');
527
+ finish(1);
528
+ };
529
+ const expired = () => {
530
+ if (settled)
531
+ return;
532
+ process.stderr.write(LOGIN_EXPIRED);
533
+ finish(1);
534
+ };
535
+ const onStdin = (chunk) => {
536
+ if (settled)
537
+ return;
538
+ const result = (0, cli_client_1.consumeEscape)(atLineStart, chunkText(chunk), pendingTilde);
539
+ atLineStart = result.atLineStart;
540
+ pendingTilde = result.pendingTilde;
541
+ if (result.forward.length > 0) {
542
+ send({ type: 'pty_data', id, data: result.forward });
543
+ }
544
+ if (!result.detach)
545
+ return;
546
+ send({ type: 'pty_detach', id });
547
+ backOn();
548
+ finish(0);
549
+ };
550
+ const onResize = () => {
551
+ if (settled)
552
+ return;
553
+ const { cols, rows } = termSize(opts.stdout);
554
+ send({ type: 'pty_resize', id, cols, rows });
555
+ };
556
+ const onTerm = () => lost();
557
+ const onHang = () => lost();
558
+ const onMessage = (raw) => {
559
+ if (settled)
560
+ return;
561
+ let msg;
562
+ try {
563
+ const parsed = JSON.parse(messageText(raw));
564
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
565
+ return;
566
+ msg = parsed;
567
+ }
568
+ catch {
569
+ return;
570
+ }
571
+ if (msg.type === 'pty_data' && msg.id === id && typeof msg.data === 'string') {
572
+ opts.stdout.write(msg.data);
573
+ return;
574
+ }
575
+ if (msg.type === 'pty_exit' && (msg.id === undefined || msg.id === id)) {
576
+ const code = typeof msg.code === 'number' ? msg.code : 0;
577
+ backOn();
578
+ finish(code);
579
+ return;
580
+ }
581
+ // pty_error and "Machine is offline" never used to settle, so finish
582
+ // never restored the tty and Ctrl-C was not SIGINT. Do not pty_detach
583
+ // or pty_close: this frame means the shell was not opened.
584
+ const fatal = fatalTerminalFrame(msg, id);
585
+ if (fatal) {
586
+ process.stderr.write(fatal.sentence);
587
+ finish(1);
588
+ return;
589
+ }
590
+ if (msg.type === 'error' && msg.error === 'unauthorized') {
591
+ expired();
592
+ }
593
+ };
594
+ const onClose = (code) => {
595
+ if (settled)
596
+ return;
597
+ if (code === 1008) {
598
+ expired();
599
+ return;
600
+ }
601
+ lost();
602
+ };
603
+ const onError = () => {
604
+ if (settled)
605
+ return;
606
+ lost();
607
+ };
608
+ // The server rejects a bad or expired JWT with an HTTP upgrade failure,
609
+ // before any pty frame. That is a login problem, not a dropped shell.
610
+ const onUpgradeRejected = (_req, res) => {
611
+ try {
612
+ res?.resume?.();
613
+ res?.destroy?.();
614
+ }
615
+ catch { /* response already gone */ }
616
+ expired();
617
+ };
618
+ const onOpen = () => {
619
+ if (opened || settled)
620
+ return;
621
+ opened = true;
622
+ const { cols, rows } = termSize(opts.stdout);
623
+ send({
624
+ type: 'pty_open',
625
+ id,
626
+ ensureSession: tmuxName,
627
+ tmuxSession: tmuxName,
628
+ cols,
629
+ rows,
630
+ });
631
+ };
632
+ opts.stdin.on('data', onStdin);
633
+ opts.signals.on('SIGTERM', onTerm);
634
+ opts.signals.on('SIGHUP', onHang);
635
+ opts.signals.on('SIGWINCH', onResize);
636
+ socket.on('message', onMessage);
637
+ socket.on('close', onClose);
638
+ socket.on('error', onError);
639
+ socket.on('unexpected-response', onUpgradeRejected);
640
+ opts.stdin.resume?.();
641
+ if (socket.readyState === ws_1.default.OPEN)
642
+ onOpen();
643
+ else
644
+ socket.on('open', onOpen);
645
+ }).finally(() => {
646
+ restoreTty();
647
+ });
648
+ }
649
+ const CONTROL_FIELDS = ['command', 'cwd', 'timeoutMs', 'lines', 'text', 'keys'];
650
+ function sharedControlMessage(type, requestId, args) {
651
+ const msg = { type, requestId };
652
+ const rec = args;
653
+ for (const key of CONTROL_FIELDS) {
654
+ if (rec[key] !== undefined)
655
+ msg[key] = rec[key];
656
+ }
657
+ return msg;
658
+ }
659
+ function controlWaitMsFor(type, args) {
660
+ // Read and keys have no caller timeout, so they use the 120s default.
661
+ // A non-finite exec timeout does too: the server's timer treats it the same way.
662
+ const requested = type === 'tmux_exec' ? args.timeoutMs : undefined;
663
+ const base = typeof requested === 'number' && Number.isFinite(requested) ? requested : DEFAULT_CONTROL_MS;
664
+ return base + SERVER_SILENCE_PAD_MS + CLIENT_WAIT_PAD_MS;
665
+ }
666
+ function controlResultValue(msg) {
667
+ const rest = {};
668
+ for (const [key, value] of Object.entries(msg)) {
669
+ if (key === 'type' || key === 'requestId' || key === 'ok')
670
+ continue;
671
+ rest[key] = value;
672
+ }
673
+ if (Object.keys(rest).length === 0)
674
+ return 'ok';
675
+ return rest;
676
+ }
677
+ function runTerminalControl(opts) {
678
+ const requestId = (0, crypto_1.randomBytes)(16).toString('hex');
679
+ const payload = sharedControlMessage(opts.type, requestId, opts.args);
680
+ const waitMs = controlWaitMsFor(opts.type, opts.args);
681
+ const resultType = `${opts.type}_result`;
682
+ const connect = opts.connect ?? ((url) => new ws_1.default(url));
683
+ return new Promise((resolve, reject) => {
684
+ let socket;
685
+ try {
686
+ socket = connect(opts.url);
687
+ }
688
+ catch (err) {
689
+ reject(err instanceof Error ? err : new Error('Machine offline'));
690
+ return;
691
+ }
692
+ let settled = false;
693
+ let timer;
694
+ const finish = (fn) => {
695
+ if (settled)
696
+ return;
697
+ settled = true;
698
+ if (timer !== undefined)
699
+ clearTimeout(timer);
700
+ unlisten(socket, 'message', onMessage);
701
+ unlisten(socket, 'close', onClose);
702
+ unlisten(socket, 'error', onError);
703
+ unlisten(socket, 'open', onOpen);
704
+ unlisten(socket, 'unexpected-response', onUpgradeRejected);
705
+ try {
706
+ socket.close();
707
+ }
708
+ catch { /* already closed */ }
709
+ fn();
710
+ };
711
+ const fail = (err) => finish(() => reject(err));
712
+ const succeed = (value) => finish(() => resolve(value));
713
+ const onMessage = (raw) => {
714
+ if (settled)
715
+ return;
716
+ let msg;
717
+ try {
718
+ const parsed = JSON.parse(messageText(raw));
719
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
720
+ return;
721
+ msg = parsed;
722
+ }
723
+ catch {
724
+ return;
725
+ }
726
+ if (msg.type !== resultType || msg.requestId !== requestId)
727
+ return;
728
+ // A late "Agent did not answer" is the server's sentence. Do not
729
+ // replace it with Machine offline — that sentence means the socket died.
730
+ if (msg.ok === false || typeof msg.error === 'string') {
731
+ const error = typeof msg.error === 'string' ? msg.error : 'request failed';
732
+ fail(new Error(error));
733
+ return;
734
+ }
735
+ succeed(controlResultValue(msg));
736
+ };
737
+ const onClose = () => fail(new Error('Machine offline'));
738
+ const onError = () => {
739
+ // Upgrade failure and a dropped socket both emit error. close and
740
+ // unexpected-response pick the sentence.
741
+ };
742
+ const onUpgradeRejected = (_req, res) => {
743
+ fail(noSession());
744
+ try {
745
+ res?.resume?.();
746
+ res?.destroy?.();
747
+ }
748
+ catch { /* response already gone */ }
749
+ };
750
+ const onOpen = () => {
751
+ if (settled)
752
+ return;
753
+ try {
754
+ socket.send(JSON.stringify(payload));
755
+ }
756
+ catch (err) {
757
+ fail(err instanceof Error ? err : new Error('Machine offline'));
758
+ }
759
+ };
760
+ timer = setTimeout(() => fail(new Error('Machine offline')), waitMs);
761
+ socket.on('message', onMessage);
762
+ socket.on('close', onClose);
763
+ socket.on('error', onError);
764
+ socket.on('unexpected-response', onUpgradeRejected);
765
+ if (socket.readyState === ws_1.default.OPEN)
766
+ onOpen();
767
+ else
768
+ socket.on('open', onOpen);
769
+ });
770
+ }
771
+ function noSession() {
772
+ const err = new Error('dialout: login expired, run dialout login');
773
+ err.code = 'NO_SESSION';
774
+ return err;
775
+ }
776
+ function notDeployed() {
777
+ const err = new Error('file transfer is not on this server yet');
778
+ err.code = 'NOT_DEPLOYED';
779
+ // MCP maps this code to one sentence. A missing file is 400 ENOENT, not this.
780
+ err.limit = cli_client_1.MAX_COPY_BYTES;
781
+ return err;
782
+ }
783
+ function requireSession() {
784
+ const session = (0, cli_client_1.readCliSession)(sessionFile());
785
+ if (!session)
786
+ throw noSession();
787
+ // Same rule as connect: a token belongs to the server URL stored with it.
788
+ if (session.serverUrl !== (0, config_1.loadConfig)().serverUrl)
789
+ throw noSession();
790
+ return session;
791
+ }
792
+ function throwFetchFailure(result) {
793
+ if (result.kind === 'expired')
794
+ throw noSession();
795
+ if (result.kind === 'origin')
796
+ throw new Error(ORIGIN_TEXT);
797
+ if (result.kind === 'network')
798
+ throw new Error(`dialout: ${result.detail ?? 'request failed'}`);
799
+ const suffix = result.detail ? ` (${result.detail})` : '';
800
+ throw new Error(`dialout: could not list machines${suffix}`);
801
+ }
802
+ async function listMachines() {
803
+ const session = requireSession();
804
+ const listed = await fetchMachines(session);
805
+ if (!listed.ok)
806
+ throwFetchFailure(listed);
807
+ return listed.rows.map((row) => ({ name: row.name, id: row.id, online: row.isOnline }));
808
+ }
809
+ async function runMachineControl(type, args) {
810
+ const session = requireSession();
811
+ const listed = await fetchMachines(session);
812
+ if (!listed.ok)
813
+ throwFetchFailure(listed);
814
+ const machine = typeof args.machine === 'string'
815
+ ? args.machine
816
+ : '';
817
+ const matched = (0, cli_client_1.matchMachine)(listed.rows, machine);
818
+ // Match failures are the tool text. The socket stays closed.
819
+ if (!matched.ok)
820
+ throw new Error(machineMatchError(machine, matched));
821
+ const url = (0, cli_client_1.terminalSocketUrl)(session.serverUrl, matched.machine.id, session.token);
822
+ if (!url)
823
+ throw new Error(ORIGIN_TEXT);
824
+ return runTerminalControl({ url, type, args });
825
+ }
826
+ function defaultCopyHttp(req) {
827
+ return requestBytes(req.method, req.url, req.headers, req.body, req.timeoutMs);
828
+ }
829
+ // URLSearchParams encodes a space as '+'. The path has to keep a real space
830
+ // as %20, and a leading '~' has to stay so the agent can expand it.
831
+ function fileApiUrl(origin, kind, machineId, filePath, overwrite) {
832
+ const pathname = kind === 'stat' ? '/api/files/stat' : '/api/files';
833
+ let url = `${origin}${pathname}?machineId=${encodeURIComponent(String(machineId))}&path=${encodeURIComponent(filePath)}`;
834
+ if (overwrite !== undefined)
835
+ url += `&overwrite=${overwrite}`;
836
+ return url;
837
+ }
838
+ function lastSegment(filePath) {
839
+ const trimmed = filePath.endsWith('/') ? filePath.slice(0, -1) : filePath;
840
+ const slash = trimmed.lastIndexOf('/');
841
+ return slash === -1 ? trimmed : trimmed.slice(slash + 1);
842
+ }
843
+ function isLatin1(value) {
844
+ for (let i = 0; i < value.length; i++) {
845
+ if (value.charCodeAt(i) > 255)
846
+ return false;
847
+ }
848
+ return true;
849
+ }
850
+ function errorString(body, status) {
851
+ const json = parseJsonBody(body);
852
+ if (typeof json === 'object' && json !== null && !Array.isArray(json)) {
853
+ const error = json.error;
854
+ if (typeof error === 'string')
855
+ return error;
856
+ }
857
+ return `dialout: request failed (${status})`;
858
+ }
859
+ function offlineLine(name) {
860
+ return machineMatchError(name, {
861
+ ok: false,
862
+ reason: 'offline',
863
+ matches: [{ id: 0, name, isOnline: false }],
864
+ });
865
+ }
866
+ // 404 is an old server with no file route. Every other failure keeps the
867
+ // server's sentence, except the two the CLI names itself.
868
+ function statusError(status, body, machineName) {
869
+ if (status >= 200 && status < 300)
870
+ return null;
871
+ if (status === 404)
872
+ return notDeployed();
873
+ if (status === 503)
874
+ return new Error(offlineLine(machineName));
875
+ if (status === 413)
876
+ return new Error(FILE_TOO_BIG);
877
+ return new Error(errorString(body, status));
878
+ }
879
+ function readUpload(filePath) {
880
+ let st;
881
+ try {
882
+ st = (0, fs_1.statSync)(filePath);
883
+ }
884
+ catch (err) {
885
+ throw new Error(err instanceof Error ? err.message : 'request failed');
886
+ }
887
+ if (!st.isFile())
888
+ throw new Error('not a file');
889
+ if (st.size > cli_client_1.MAX_COPY_BYTES)
890
+ throw new Error(FILE_TOO_BIG);
891
+ const bytes = (0, fs_1.readFileSync)(filePath);
892
+ if (bytes.length > cli_client_1.MAX_COPY_BYTES)
893
+ throw new Error(FILE_TOO_BIG);
894
+ return bytes;
895
+ }
896
+ function readStat(body) {
897
+ const json = parseJsonBody(body);
898
+ if (typeof json !== 'object' || json === null || Array.isArray(json))
899
+ return null;
900
+ const type = json.type;
901
+ if (typeof type !== 'string')
902
+ return null;
903
+ return { type };
904
+ }
905
+ async function statRemote(http, origin, machineId, machineName, filePath, headers) {
906
+ const res = await http({
907
+ method: 'GET',
908
+ url: fileApiUrl(origin, 'stat', machineId, filePath),
909
+ headers,
910
+ timeoutMs: JSON_TIMEOUT_MS,
911
+ });
912
+ if (res.status === 400) {
913
+ const error = errorString(res.body, 400);
914
+ if (error.startsWith('ENOENT'))
915
+ return { kind: 'missing', error };
916
+ return { kind: 'fail', error: new Error(error) };
917
+ }
918
+ const failed = statusError(res.status, res.body, machineName);
919
+ if (failed)
920
+ return { kind: 'fail', error: failed };
921
+ const stat = readStat(res.body);
922
+ if (!stat)
923
+ return { kind: 'fail', error: new Error('dialout: unexpected stat response') };
924
+ return { kind: 'stat', stat };
925
+ }
926
+ function promptLine(stdin, stdout, label) {
927
+ stdout.write(label);
928
+ return new Promise((resolve) => {
929
+ let buf = '';
930
+ let done = false;
931
+ const finish = (line) => {
932
+ if (done)
933
+ return;
934
+ done = true;
935
+ unlisten(stdin, 'data', onData);
936
+ stdin.pause?.();
937
+ resolve(line);
938
+ };
939
+ const onData = (chunk) => {
940
+ const text = chunkText(chunk);
941
+ for (let i = 0; i < text.length; i++) {
942
+ const ch = text[i] ?? '';
943
+ if (ch === '\n' || ch === '\r') {
944
+ if (ch === '\r' && text[i + 1] === '\n')
945
+ i += 1;
946
+ finish(buf);
947
+ return;
948
+ }
949
+ buf += ch;
950
+ }
951
+ };
952
+ stdin.on('data', onData);
953
+ stdin.resume?.();
954
+ });
955
+ }
956
+ // MCP passes prompt false. Its stdin is the JSON-RPC session, so a y/N
957
+ // question there would consume the next client frame.
958
+ async function confirmOverwrite(prompt, io, destPath) {
959
+ if (!prompt)
960
+ throw new Error(EXISTS_CONFLICT);
961
+ if (io.stdin.isTTY !== true)
962
+ throw new Error(EXISTS_CONFLICT);
963
+ const answer = (await promptLine(io.stdin, io.stdout, `overwrite ${destPath}? [y/N] `)).trim();
964
+ if (answer !== 'y' && answer !== 'Y')
965
+ throw new Error(EXISTS_CONFLICT);
966
+ }
967
+ function isExactExists(status, body) {
968
+ return status === 400 && errorString(body, 400) === EXISTS_CONFLICT;
969
+ }
970
+ async function transferCopy(opts) {
971
+ const parsed = (0, cli_client_1.parseCopyPair)(opts.source, opts.dest);
972
+ if (!parsed.ok)
973
+ throw new Error(parsed.error);
974
+ const homedir = opts.hooks?.homedir ?? os.homedir();
975
+ const localExpanded = (0, cli_client_1.expandLocalHome)(parsed.local, homedir);
976
+ // Size and "not a file" are local. Do not ask the network about them.
977
+ const uploadBytes = parsed.upload ? readUpload(localExpanded) : undefined;
978
+ const session = opts.hooks?.session ?? requireSession();
979
+ const http = opts.hooks?.http ?? defaultCopyHttp;
980
+ const listed = await fetchMachines(session, opts.hooks?.http);
981
+ if (!listed.ok)
982
+ throwFetchFailure(listed);
983
+ const matched = (0, cli_client_1.matchMachine)(listed.rows, parsed.machine);
984
+ if (!matched.ok)
985
+ throw new Error(machineMatchError(parsed.machine, matched));
986
+ const origin = (0, cli_client_1.apiOriginFromServerUrl)(session.serverUrl);
987
+ if (!origin)
988
+ throw new Error(ORIGIN_TEXT);
989
+ const headers = { Authorization: `Bearer ${session.token}` };
990
+ const machineName = matched.machine.name;
991
+ const machineId = matched.machine.id;
992
+ const io = opts.hooks?.io ?? {
993
+ stdin: process.stdin,
994
+ stdout: process.stdout,
995
+ stderr: process.stderr,
996
+ };
997
+ const looked = await statRemote(http, origin, machineId, machineName, parsed.remote, headers);
998
+ if (parsed.upload) {
999
+ if (looked.kind === 'fail')
1000
+ throw looked.error;
1001
+ if (looked.kind === 'missing' && parsed.remote.endsWith('/'))
1002
+ throw new Error(looked.error);
1003
+ const remoteIsDir = looked.kind === 'stat' && looked.stat.type === 'directory';
1004
+ const remoteDest = remoteIsDir || parsed.remote.endsWith('/')
1005
+ ? (0, cli_client_1.resolveRemoteDest)(parsed.remote, true, lastSegment(localExpanded))
1006
+ : parsed.remote;
1007
+ const base = lastSegment(localExpanded);
1008
+ const putHeaders = {
1009
+ ...headers,
1010
+ 'Content-Type': 'application/octet-stream',
1011
+ };
1012
+ if (base.length > 0 && isLatin1(base))
1013
+ putHeaders['X-File-Name'] = base;
1014
+ const send = (overwrite) => http({
1015
+ method: 'PUT',
1016
+ url: fileApiUrl(origin, 'bytes', machineId, remoteDest, overwrite ? '1' : '0'),
1017
+ headers: putHeaders,
1018
+ body: uploadBytes,
1019
+ timeoutMs: FILE_BYTE_TIMEOUT_MS,
1020
+ });
1021
+ let res = await send(opts.force);
1022
+ if (isExactExists(res.status, res.body) && !opts.force) {
1023
+ await confirmOverwrite(opts.prompt, io, remoteDest);
1024
+ res = await send(true);
1025
+ }
1026
+ const failed = statusError(res.status, res.body, machineName);
1027
+ if (failed)
1028
+ throw failed;
1029
+ return `${uploadBytes.length} bytes → ${parsed.machine}:${remoteDest}`;
1030
+ }
1031
+ if (looked.kind === 'fail')
1032
+ throw looked.error;
1033
+ if (looked.kind === 'missing')
1034
+ throw new Error(looked.error);
1035
+ if (looked.stat.type === 'directory' || parsed.remote.endsWith('/'))
1036
+ throw new Error('not a file');
1037
+ const res = await http({
1038
+ method: 'GET',
1039
+ url: fileApiUrl(origin, 'bytes', machineId, parsed.remote),
1040
+ headers,
1041
+ timeoutMs: FILE_BYTE_TIMEOUT_MS,
1042
+ });
1043
+ const failed = statusError(res.status, res.body, machineName);
1044
+ if (failed)
1045
+ throw failed;
1046
+ const localIsDir = parsed.local.endsWith('/') || localPathIsDir(localExpanded);
1047
+ const localDest = (0, cli_client_1.resolveLocalDest)(localExpanded, localIsDir, lastSegment(parsed.remote));
1048
+ // Stat before opening. A refused overwrite must not truncate the file.
1049
+ if (localFileExists(localDest)) {
1050
+ if (!opts.force)
1051
+ await confirmOverwrite(opts.prompt, io, localDest);
1052
+ }
1053
+ try {
1054
+ (0, fs_1.writeFileSync)(localDest, res.body);
1055
+ }
1056
+ catch (err) {
1057
+ throw new Error(err instanceof Error ? err.message : 'request failed');
1058
+ }
1059
+ return `${res.body.length} bytes → ${localDest}`;
1060
+ }
1061
+ function localPathIsDir(filePath) {
1062
+ try {
1063
+ return (0, fs_1.statSync)(filePath).isDirectory();
1064
+ }
1065
+ catch {
1066
+ return false;
1067
+ }
1068
+ }
1069
+ function localFileExists(filePath) {
1070
+ try {
1071
+ const st = (0, fs_1.statSync)(filePath);
1072
+ if (st.isDirectory())
1073
+ throw new Error('not a file');
1074
+ return st.isFile();
1075
+ }
1076
+ catch (err) {
1077
+ if (err instanceof Error && err.message === 'not a file')
1078
+ throw err;
1079
+ const code = err.code;
1080
+ if (code === 'ENOENT')
1081
+ return false;
1082
+ throw new Error(err instanceof Error ? err.message : 'request failed');
1083
+ }
1084
+ }
1085
+ async function copyCommand(source, dest, force) {
1086
+ try {
1087
+ const line = await transferCopy({ source, dest, force, prompt: true });
1088
+ process.stdout.write(`${line}\n`);
1089
+ return 0;
1090
+ }
1091
+ catch (err) {
1092
+ const message = err instanceof Error ? err.message : 'request failed';
1093
+ process.stderr.write(message.endsWith('\n') ? message : `${message}\n`);
1094
+ return 1;
1095
+ }
1096
+ }
1097
+ async function copyFile(args, hooks) {
1098
+ const source = typeof args.source === 'string' ? args.source : '';
1099
+ const dest = typeof args.dest === 'string' ? args.dest : '';
1100
+ return transferCopy({ source, dest, force: false, prompt: false, hooks });
1101
+ }
1102
+ async function serveMcp() {
1103
+ const session = (0, cli_client_1.readCliSession)(sessionFile());
1104
+ if (!session) {
1105
+ process.stderr.write(LOGIN_EXPIRED);
1106
+ process.exit(1);
1107
+ }
1108
+ if (session.serverUrl !== (0, config_1.loadConfig)().serverUrl) {
1109
+ process.stderr.write(LOGIN_EXPIRED);
1110
+ process.exit(1);
1111
+ }
1112
+ const deps = {
1113
+ listMachines,
1114
+ exec: (args) => runMachineControl('tmux_exec', args),
1115
+ readScreen: (args) => runMachineControl('tmux_read', args),
1116
+ writeKeys: (args) => runMachineControl('tmux_keys', args),
1117
+ copyFile: (args) => copyFile(args),
1118
+ };
1119
+ await (0, mcp_stdio_1.runMcpStdio)(deps);
1120
+ }
1121
+ //# sourceMappingURL=cli-terminal.js.map