@cli-remote/local 0.0.2 → 0.0.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.
Files changed (2) hide show
  1. package/dist/index.cjs +461 -88
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -3214,7 +3214,7 @@ var require_stream = __commonJS({
3214
3214
  };
3215
3215
  duplex._final = function(callback) {
3216
3216
  if (ws.readyState === ws.CONNECTING) {
3217
- ws.once("open", function open() {
3217
+ ws.once("open", function open2() {
3218
3218
  duplex._final(callback);
3219
3219
  });
3220
3220
  return;
@@ -3235,7 +3235,7 @@ var require_stream = __commonJS({
3235
3235
  };
3236
3236
  duplex._write = function(chunk, encoding, callback) {
3237
3237
  if (ws.readyState === ws.CONNECTING) {
3238
- ws.once("open", function open() {
3238
+ ws.once("open", function open2() {
3239
3239
  duplex._write(chunk, encoding, callback);
3240
3240
  });
3241
3241
  return;
@@ -6683,11 +6683,11 @@ var require_dijkstra = __commonJS({
6683
6683
  var predecessors = {};
6684
6684
  var costs = {};
6685
6685
  costs[s] = 0;
6686
- var open = dijkstra.PriorityQueue.make();
6687
- open.push(s, 0);
6686
+ var open2 = dijkstra.PriorityQueue.make();
6687
+ open2.push(s, 0);
6688
6688
  var closest, u, v, cost_of_s_to_u, adjacent_nodes, cost_of_e, cost_of_s_to_u_plus_cost_of_e, cost_of_s_to_v, first_visit;
6689
- while (!open.empty()) {
6690
- closest = open.pop();
6689
+ while (!open2.empty()) {
6690
+ closest = open2.pop();
6691
6691
  u = closest.value;
6692
6692
  cost_of_s_to_u = closest.cost;
6693
6693
  adjacent_nodes = graph[u] || {};
@@ -6699,7 +6699,7 @@ var require_dijkstra = __commonJS({
6699
6699
  first_visit = typeof costs[v] === "undefined";
6700
6700
  if (first_visit || cost_of_s_to_v > cost_of_s_to_u_plus_cost_of_e) {
6701
6701
  costs[v] = cost_of_s_to_u_plus_cost_of_e;
6702
- open.push(v, cost_of_s_to_u_plus_cost_of_e);
6702
+ open2.push(v, cost_of_s_to_u_plus_cost_of_e);
6703
6703
  predecessors[v] = u;
6704
6704
  }
6705
6705
  }
@@ -9786,10 +9786,10 @@ var require_browser = __commonJS({
9786
9786
  text = canvas;
9787
9787
  canvas = void 0;
9788
9788
  }
9789
- return new Promise(function(resolve, reject) {
9789
+ return new Promise(function(resolve2, reject) {
9790
9790
  try {
9791
9791
  const data = QRCode2.create(text, opts);
9792
- resolve(renderFunc(data, canvas, opts));
9792
+ resolve2(renderFunc(data, canvas, opts));
9793
9793
  } catch (e) {
9794
9794
  reject(e);
9795
9795
  }
@@ -9870,11 +9870,11 @@ var require_server = __commonJS({
9870
9870
  }
9871
9871
  function render(renderFunc, text, params) {
9872
9872
  if (!params.cb) {
9873
- return new Promise(function(resolve, reject) {
9873
+ return new Promise(function(resolve2, reject) {
9874
9874
  try {
9875
9875
  const data = QRCode2.create(text, params.opts);
9876
9876
  return renderFunc(data, params.opts, function(err, data2) {
9877
- return err ? reject(err) : resolve(data2);
9877
+ return err ? reject(err) : resolve2(data2);
9878
9878
  });
9879
9879
  } catch (e) {
9880
9880
  reject(e);
@@ -9939,8 +9939,10 @@ var require_lib = __commonJS({
9939
9939
  });
9940
9940
 
9941
9941
  // src/index.ts
9942
- var import_node_fs2 = require("node:fs");
9943
- var import_node_path5 = require("node:path");
9942
+ var import_node_fs3 = require("node:fs");
9943
+ var import_node_child_process2 = require("node:child_process");
9944
+ var import_node_os6 = require("node:os");
9945
+ var import_node_path6 = require("node:path");
9944
9946
  var import_node_url = require("node:url");
9945
9947
 
9946
9948
  // ../../node_modules/.pnpm/ws@8.21.3/node_modules/ws/wrapper.mjs
@@ -9956,9 +9958,9 @@ var wrapper_default = import_websocket.default;
9956
9958
 
9957
9959
  // src/client.ts
9958
9960
  var import_node_readline = require("node:readline");
9959
- var import_promises2 = require("node:fs/promises");
9960
- var import_node_os4 = require("node:os");
9961
- var import_node_path4 = require("node:path");
9961
+ var import_promises3 = require("node:fs/promises");
9962
+ var import_node_os5 = require("node:os");
9963
+ var import_node_path5 = require("node:path");
9962
9964
 
9963
9965
  // ../shared/src/codec.ts
9964
9966
  var import_msgpack = __toESM(require_dist(), 1);
@@ -9978,6 +9980,10 @@ var ACCESS_TOKEN_TTL_MS = 60 * 6e4;
9978
9980
  var REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 6e4;
9979
9981
  var REFRESH_MARGIN_MS = 5 * 6e4;
9980
9982
  var OUTPUT_BUFFER_SIZE = 500;
9983
+ var FILE_CHUNK_SIZE = 64 * 1024;
9984
+ var MAX_TRANSFER_BYTES = 500 * 1024 * 1024;
9985
+ var UPLOAD_DIR = "uploads";
9986
+ var RECONNECT_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
9981
9987
  var DEFAULT_CONFIG_DIR = ".cli-remote";
9982
9988
  var CREDENTIALS_FILE = "credentials.json";
9983
9989
  var CONFIG_FILE = "config.json";
@@ -10211,6 +10217,10 @@ var SessionLogger = class {
10211
10217
  if (this.closed) return;
10212
10218
  this.write({ ts: Date.now(), event: "rejected", cmd, reason, dangerous });
10213
10219
  }
10220
+ logForced(cmd) {
10221
+ if (this.closed) return;
10222
+ this.write({ ts: Date.now(), event: "forced", cmd });
10223
+ }
10214
10224
  logExit(code) {
10215
10225
  if (this.closed) return;
10216
10226
  this.write({ ts: Date.now(), event: "exit", code });
@@ -10320,19 +10330,196 @@ function checkCommand(line, whitelist, dangerousPatterns = DANGEROUS_PATTERNS) {
10320
10330
  return { allowed: true };
10321
10331
  }
10322
10332
 
10323
- // src/auth.ts
10324
- var import_node_child_process = require("node:child_process");
10325
- var import_node_crypto = require("node:crypto");
10333
+ // src/file-transfer.ts
10334
+ var import_node_fs2 = require("node:fs");
10326
10335
  var import_promises = require("node:fs/promises");
10327
10336
  var import_node_os3 = require("node:os");
10328
10337
  var import_node_path3 = require("node:path");
10338
+ var FileTransferManager = class _FileTransferManager {
10339
+ constructor(send, log) {
10340
+ this.send = send;
10341
+ this.log = log;
10342
+ }
10343
+ uploads = /* @__PURE__ */ new Map();
10344
+ uploadDir() {
10345
+ return (0, import_node_path3.join)((0, import_node_os3.homedir)(), DEFAULT_CONFIG_DIR, UPLOAD_DIR);
10346
+ }
10347
+ expandPath(p) {
10348
+ const home = process.env.PTY_HOME ?? (0, import_node_os3.homedir)();
10349
+ if (p === "~") return home;
10350
+ if (p.startsWith("~/")) return (0, import_node_path3.join)(home, p.slice(2));
10351
+ return (0, import_node_path3.resolve)(p);
10352
+ }
10353
+ /** Strip any directory components and control chars from a client filename. */
10354
+ static sanitizeFilename(name) {
10355
+ const base = (0, import_node_path3.basename)(name).replace(/[\u0000-\u001f\u007f]/g, "");
10356
+ return base && base !== "." && base !== ".." ? base : "upload.bin";
10357
+ }
10358
+ async handleUploadStart(frame) {
10359
+ const { sessionId, uploadId, filename, size } = frame;
10360
+ if (this.uploads.has(uploadId)) return;
10361
+ if (size > MAX_TRANSFER_BYTES) {
10362
+ this.emit({
10363
+ t: "upload-err",
10364
+ sessionId,
10365
+ uploadId,
10366
+ reason: `\u6587\u4EF6\u8FC7\u5927\uFF08>${Math.floor(MAX_TRANSFER_BYTES / 1024 / 1024)}MB\uFF09`
10367
+ });
10368
+ return;
10369
+ }
10370
+ try {
10371
+ const dir = this.uploadDir();
10372
+ await (0, import_promises.mkdir)(dir, { recursive: true });
10373
+ const target = await this.uniqPath(dir, _FileTransferManager.sanitizeFilename(filename));
10374
+ const stream = (0, import_node_fs2.createWriteStream)(target, { mode: 384 });
10375
+ this.uploads.set(uploadId, {
10376
+ uploadId,
10377
+ sessionId,
10378
+ path: target,
10379
+ size,
10380
+ received: 0,
10381
+ lastSeq: 0,
10382
+ stream
10383
+ });
10384
+ this.emit({ t: "upload-ok", sessionId, uploadId, path: target });
10385
+ this.log(`upload start ${uploadId} \u2192 ${target} (${size}B)`);
10386
+ } catch (e) {
10387
+ this.emit({ t: "upload-err", sessionId, uploadId, reason: e.message });
10388
+ }
10389
+ }
10390
+ async uniqPath(dir, name) {
10391
+ let candidate = (0, import_node_path3.join)(dir, name);
10392
+ let i = 1;
10393
+ while (await exists(candidate)) {
10394
+ const dot = name.lastIndexOf(".");
10395
+ const stem = dot > 0 ? name.slice(0, dot) : name;
10396
+ const ext = dot > 0 ? name.slice(dot) : "";
10397
+ candidate = (0, import_node_path3.join)(dir, `${stem} (${i++})${ext}`);
10398
+ }
10399
+ return candidate;
10400
+ }
10401
+ handleUploadChunk(frame) {
10402
+ const up = this.uploads.get(frame.uploadId);
10403
+ if (!up) return;
10404
+ if (frame.seq <= up.lastSeq) return;
10405
+ up.lastSeq = frame.seq;
10406
+ up.received += frame.data.byteLength;
10407
+ up.stream.write(Buffer.from(frame.data.buffer, frame.data.byteOffset, frame.data.byteLength));
10408
+ }
10409
+ async handleUploadEnd(frame) {
10410
+ const up = this.uploads.get(frame.uploadId);
10411
+ if (!up) return;
10412
+ this.uploads.delete(frame.uploadId);
10413
+ await new Promise((res) => up.stream.end(res));
10414
+ const sizeOk = up.received === up.size;
10415
+ this.log(`upload done ${frame.uploadId}: ${up.received}B \u2192 ${up.path}`);
10416
+ if (!sizeOk) {
10417
+ this.emit({
10418
+ t: "upload-err",
10419
+ sessionId: up.sessionId,
10420
+ uploadId: frame.uploadId,
10421
+ reason: `\u5927\u5C0F\u4E0D\u5339\u914D\uFF08\u58F0\u660E ${up.size}B\uFF0C\u5B9E\u6536 ${up.received}B\uFF09`
10422
+ });
10423
+ }
10424
+ }
10425
+ abortUpload(uploadId) {
10426
+ const up = this.uploads.get(uploadId);
10427
+ if (!up) return;
10428
+ this.uploads.delete(uploadId);
10429
+ up.stream.destroy();
10430
+ this.log(`upload aborted ${uploadId}`);
10431
+ }
10432
+ /** Stream a file back to the web client with mild backpressure via `send`. */
10433
+ async handleDownloadReq(frame, isBusy) {
10434
+ const sessionId = frame.sessionId;
10435
+ const downloadId = "dl_" + Math.random().toString(36).slice(2, 10);
10436
+ let abs;
10437
+ try {
10438
+ abs = this.expandPath(frame.path);
10439
+ const st = await (0, import_promises.stat)(abs);
10440
+ if (!st.isFile()) throw new Error("\u4E0D\u662F\u666E\u901A\u6587\u4EF6");
10441
+ if (st.size > MAX_TRANSFER_BYTES) {
10442
+ throw new Error(`\u6587\u4EF6\u8FC7\u5927\uFF08>${Math.floor(MAX_TRANSFER_BYTES / 1024 / 1024)}MB\uFF09`);
10443
+ }
10444
+ this.emit({
10445
+ t: "download-start",
10446
+ sessionId,
10447
+ downloadId,
10448
+ filename: (0, import_node_path3.basename)(abs),
10449
+ size: st.size
10450
+ });
10451
+ this.log(`download start ${downloadId} \u2190 ${abs} (${st.size}B)`);
10452
+ await this.streamFile(sessionId, downloadId, abs, isBusy);
10453
+ this.emit({ t: "download-end", sessionId, downloadId });
10454
+ this.log(`download done ${downloadId}: ${abs}`);
10455
+ } catch (e) {
10456
+ this.emit({
10457
+ t: "download-err",
10458
+ sessionId,
10459
+ downloadId,
10460
+ reason: e.message
10461
+ });
10462
+ }
10463
+ }
10464
+ async streamFile(sessionId, downloadId, abs, isBusy) {
10465
+ const handle = await (0, import_promises.open)(abs, "r");
10466
+ try {
10467
+ let seq = 0;
10468
+ let pos = 0;
10469
+ for (; ; ) {
10470
+ const buf = Buffer.alloc(FILE_CHUNK_SIZE);
10471
+ const { bytesRead } = await handle.read(buf, 0, FILE_CHUNK_SIZE, pos);
10472
+ if (bytesRead === 0) break;
10473
+ pos += bytesRead;
10474
+ seq++;
10475
+ this.emit({
10476
+ t: "download-chunk",
10477
+ sessionId,
10478
+ downloadId,
10479
+ data: new Uint8Array(buf.buffer, buf.byteOffset, bytesRead),
10480
+ seq
10481
+ });
10482
+ if (isBusy()) {
10483
+ await new Promise((res) => setImmediate(res));
10484
+ let spins = 0;
10485
+ while (isBusy() && spins++ < 2e3) {
10486
+ await new Promise((res) => setTimeout(res, 20));
10487
+ }
10488
+ }
10489
+ }
10490
+ } finally {
10491
+ await handle.close();
10492
+ }
10493
+ }
10494
+ abortAll() {
10495
+ for (const id of [...this.uploads.keys()]) this.abortUpload(id);
10496
+ }
10497
+ emit(frame) {
10498
+ this.send(encodeFrame(frame));
10499
+ }
10500
+ };
10501
+ async function exists(p) {
10502
+ try {
10503
+ await (0, import_promises.stat)(p);
10504
+ return true;
10505
+ } catch {
10506
+ return false;
10507
+ }
10508
+ }
10509
+
10510
+ // src/auth.ts
10511
+ var import_node_child_process = require("node:child_process");
10512
+ var import_node_crypto = require("node:crypto");
10513
+ var import_promises2 = require("node:fs/promises");
10514
+ var import_node_os4 = require("node:os");
10515
+ var import_node_path4 = require("node:path");
10329
10516
  var import_qrcode = __toESM(require_lib(), 1);
10330
10517
  function credentialsPath() {
10331
- return (0, import_node_path3.join)((0, import_node_os3.homedir)(), DEFAULT_CONFIG_DIR, CREDENTIALS_FILE);
10518
+ return (0, import_node_path4.join)((0, import_node_os4.homedir)(), DEFAULT_CONFIG_DIR, CREDENTIALS_FILE);
10332
10519
  }
10333
10520
  async function loadCredentials() {
10334
10521
  try {
10335
- const raw = JSON.parse(await (0, import_promises.readFile)(credentialsPath(), "utf-8"));
10522
+ const raw = JSON.parse(await (0, import_promises2.readFile)(credentialsPath(), "utf-8"));
10336
10523
  if (!raw.deviceId || !raw.accessToken || !raw.refreshToken) return null;
10337
10524
  return raw;
10338
10525
  } catch {
@@ -10341,16 +10528,16 @@ async function loadCredentials() {
10341
10528
  }
10342
10529
  async function saveCredentials(creds) {
10343
10530
  const path = credentialsPath();
10344
- await (0, import_promises.mkdir)((0, import_node_path3.join)(path, ".."), { recursive: true });
10345
- await (0, import_promises.writeFile)(path, JSON.stringify(creds, null, 2) + "\n", { mode: 384 });
10531
+ await (0, import_promises2.mkdir)((0, import_node_path4.join)(path, ".."), { recursive: true });
10532
+ await (0, import_promises2.writeFile)(path, JSON.stringify(creds, null, 2) + "\n", { mode: 384 });
10346
10533
  try {
10347
- await (0, import_promises.chmod)(path, 384);
10534
+ await (0, import_promises2.chmod)(path, 384);
10348
10535
  } catch {
10349
10536
  }
10350
10537
  }
10351
10538
  async function clearCredentials() {
10352
10539
  try {
10353
- await (0, import_promises.writeFile)(credentialsPath(), "");
10540
+ await (0, import_promises2.writeFile)(credentialsPath(), "");
10354
10541
  } catch {
10355
10542
  }
10356
10543
  }
@@ -10382,17 +10569,17 @@ function openBrowser(url) {
10382
10569
  }
10383
10570
  }
10384
10571
  async function mergeLocalConfig(patch) {
10385
- const dir = (0, import_node_path3.join)((0, import_node_os3.homedir)(), DEFAULT_CONFIG_DIR);
10386
- const path = (0, import_node_path3.join)(dir, CONFIG_FILE);
10572
+ const dir = (0, import_node_path4.join)((0, import_node_os4.homedir)(), DEFAULT_CONFIG_DIR);
10573
+ const path = (0, import_node_path4.join)(dir, CONFIG_FILE);
10387
10574
  let existing = {};
10388
10575
  try {
10389
- existing = JSON.parse(await (0, import_promises.readFile)(path, "utf-8"));
10576
+ existing = JSON.parse(await (0, import_promises2.readFile)(path, "utf-8"));
10390
10577
  } catch {
10391
10578
  }
10392
- await (0, import_promises.mkdir)(dir, { recursive: true });
10393
- await (0, import_promises.writeFile)(path, JSON.stringify({ ...existing, ...patch }, null, 2) + "\n");
10579
+ await (0, import_promises2.mkdir)(dir, { recursive: true });
10580
+ await (0, import_promises2.writeFile)(path, JSON.stringify({ ...existing, ...patch }, null, 2) + "\n");
10394
10581
  }
10395
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
10582
+ var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
10396
10583
  var AuthError = class extends Error {
10397
10584
  };
10398
10585
  async function deviceFlowAuthorize(baseUrl, registrationKey, deviceId) {
@@ -10404,7 +10591,7 @@ async function deviceFlowAuthorize(baseUrl, registrationKey, deviceId) {
10404
10591
  const res = await postJson(`${baseUrl}/oauth/device_authorization`, {
10405
10592
  registrationKey,
10406
10593
  device_id: deviceId,
10407
- device_name: (0, import_node_os3.hostname)(),
10594
+ device_name: (0, import_node_os4.hostname)(),
10408
10595
  platform: process.platform
10409
10596
  });
10410
10597
  if (res.status !== 200) {
@@ -10507,9 +10694,9 @@ async function ensureCredentials(baseUrl, registrationKey) {
10507
10694
 
10508
10695
  // src/client.ts
10509
10696
  async function loadConfig() {
10510
- const path = (0, import_node_path4.join)((0, import_node_os4.homedir)(), DEFAULT_CONFIG_DIR, CONFIG_FILE);
10697
+ const path = (0, import_node_path5.join)((0, import_node_os5.homedir)(), DEFAULT_CONFIG_DIR, CONFIG_FILE);
10511
10698
  try {
10512
- return JSON.parse(await (0, import_promises2.readFile)(path, "utf-8"));
10699
+ return JSON.parse(await (0, import_promises3.readFile)(path, "utf-8"));
10513
10700
  } catch {
10514
10701
  return {};
10515
10702
  }
@@ -10526,7 +10713,7 @@ function isTTY() {
10526
10713
  async function readInput(prompt) {
10527
10714
  const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
10528
10715
  try {
10529
- return await new Promise((resolve) => rl.question(prompt, (answer) => resolve(answer)));
10716
+ return await new Promise((resolve2) => rl.question(prompt, (answer) => resolve2(answer)));
10530
10717
  } finally {
10531
10718
  rl.close();
10532
10719
  }
@@ -10596,27 +10783,25 @@ async function startLocal(opts = {}) {
10596
10783
  const pty = new PtyManager();
10597
10784
  const loggers = /* @__PURE__ */ new Map();
10598
10785
  const lineBuffers = /* @__PURE__ */ new Map();
10786
+ const transfers = new FileTransferManager(
10787
+ (data) => safeSend(data),
10788
+ (msg) => console.log(`[local] ${msg}`)
10789
+ );
10599
10790
  let credentials = await ensureCredentials(baseUrl, registrationKey);
10600
10791
  console.log(`[local] connecting to ${url}`);
10601
10792
  console.log(`[local] deviceId=${credentials.deviceId}`);
10602
10793
  console.log(`[local] whitelist: ${whitelist.size} commands`);
10603
- const ws = new wrapper_default(url);
10794
+ let ws = null;
10604
10795
  let heartbeatTimer = null;
10605
- function cleanup(code = 0) {
10606
- pty.killAll();
10607
- for (const [, logger] of loggers) logger.logExit(-1);
10608
- loggers.clear();
10609
- lineBuffers.clear();
10796
+ let reconnectTimer = null;
10797
+ let reconnectAttempts = 0;
10798
+ let userExitCode = null;
10799
+ function stopHeartbeat() {
10610
10800
  if (heartbeatTimer) clearInterval(heartbeatTimer);
10611
- try {
10612
- ws.close();
10613
- } catch {
10614
- }
10615
- process.exit(code);
10801
+ heartbeatTimer = null;
10616
10802
  }
10617
- ws.on("open", () => {
10618
- console.log("[local] connected");
10619
- ws.send(
10803
+ function sendAuth() {
10804
+ ws?.send(
10620
10805
  encodeFrame({
10621
10806
  t: "auth",
10622
10807
  role: "bridge",
@@ -10624,13 +10809,74 @@ async function startLocal(opts = {}) {
10624
10809
  token: credentials.accessToken
10625
10810
  })
10626
10811
  );
10627
- heartbeatTimer = setInterval(() => {
10628
- if (ws.readyState === wrapper_default.OPEN) {
10629
- ws.send(encodeFrame({ t: "ping" }));
10630
- }
10631
- }, HEARTBEAT_INTERVAL_MS);
10632
- });
10633
- ws.on("message", (raw) => {
10812
+ }
10813
+ function connect() {
10814
+ if (userExitCode !== null) return;
10815
+ const sock = new wrapper_default(url);
10816
+ ws = sock;
10817
+ sock.on("open", () => {
10818
+ console.log("[local] connected");
10819
+ sendAuth();
10820
+ stopHeartbeat();
10821
+ heartbeatTimer = setInterval(() => {
10822
+ if (sock.readyState === wrapper_default.OPEN) {
10823
+ sock.send(encodeFrame({ t: "ping" }));
10824
+ }
10825
+ }, HEARTBEAT_INTERVAL_MS);
10826
+ });
10827
+ sock.on("message", (raw) => handleFrame(sock, raw));
10828
+ sock.on("close", () => {
10829
+ stopHeartbeat();
10830
+ if (ws === sock) ws = null;
10831
+ if (userExitCode !== null) {
10832
+ process.exit(userExitCode);
10833
+ }
10834
+ console.log("[local] disconnected; PTY sessions kept alive, reconnecting\u2026");
10835
+ scheduleReconnect();
10836
+ });
10837
+ sock.on("error", (err) => {
10838
+ console.error("[local] ws error:", err.message);
10839
+ });
10840
+ }
10841
+ async function scheduleReconnect() {
10842
+ if (reconnectTimer || userExitCode !== null) return;
10843
+ const idx = Math.min(reconnectAttempts, RECONNECT_DELAYS_MS.length - 1);
10844
+ const delay = RECONNECT_DELAYS_MS[idx];
10845
+ reconnectAttempts++;
10846
+ console.log(`[local] reconnecting in ${Math.round(delay / 1e3)}s (attempt ${reconnectAttempts})`);
10847
+ reconnectTimer = setTimeout(() => {
10848
+ reconnectTimer = null;
10849
+ ensureFreshAccessToken(baseUrl, credentials).then((creds) => {
10850
+ credentials = creds;
10851
+ connect();
10852
+ }).catch(async (e) => {
10853
+ await clearCredentials();
10854
+ console.error(`[local] ${e.message}`);
10855
+ console.error("[local] \u51ED\u8BC1\u5DF2\u5931\u6548\u5E76\u6E05\u9664\uFF1B\u8BF7\u91CD\u65B0\u8FD0\u884C cli-local \u5B8C\u6210\u6388\u6743");
10856
+ exitProcess(1);
10857
+ });
10858
+ }, delay);
10859
+ }
10860
+ function exitProcess(code) {
10861
+ userExitCode = code;
10862
+ if (reconnectTimer) clearTimeout(reconnectTimer);
10863
+ reconnectTimer = null;
10864
+ transfers.abortAll();
10865
+ pty.killAll();
10866
+ for (const [, logger] of loggers) logger.logExit(-1);
10867
+ loggers.clear();
10868
+ lineBuffers.clear();
10869
+ stopHeartbeat();
10870
+ try {
10871
+ ws?.close();
10872
+ } catch {
10873
+ }
10874
+ process.exit(code);
10875
+ }
10876
+ function cleanup(code = 0) {
10877
+ exitProcess(code);
10878
+ }
10879
+ function handleFrame(sock, raw) {
10634
10880
  let frame;
10635
10881
  try {
10636
10882
  frame = decodeFrame(toUint8(raw));
@@ -10639,6 +10885,10 @@ async function startLocal(opts = {}) {
10639
10885
  }
10640
10886
  switch (frame.t) {
10641
10887
  case "auth-ok":
10888
+ if (reconnectAttempts > 0) {
10889
+ console.log(`[local] reconnected after ${reconnectAttempts} attempt(s); ${pty.list().length} PTY session(s) alive`);
10890
+ }
10891
+ reconnectAttempts = 0;
10642
10892
  console.log(`[local] auth ok, deviceId=${frame.channelId}`);
10643
10893
  break;
10644
10894
  case "auth-err":
@@ -10661,23 +10911,31 @@ async function startLocal(opts = {}) {
10661
10911
  case "replay-req":
10662
10912
  handleReplay(frame);
10663
10913
  break;
10914
+ case "upload-start":
10915
+ void transfers.handleUploadStart(frame);
10916
+ break;
10917
+ case "upload-chunk":
10918
+ transfers.handleUploadChunk(frame);
10919
+ break;
10920
+ case "upload-end":
10921
+ void transfers.handleUploadEnd(frame);
10922
+ break;
10923
+ case "download-req":
10924
+ void transfers.handleDownloadReq(
10925
+ frame,
10926
+ () => ws ? ws.bufferedAmount > 4 * 1024 * 1024 : false
10927
+ );
10928
+ break;
10664
10929
  default:
10665
10930
  console.log("[local] unhandled frame:", frame.t);
10666
10931
  }
10667
- });
10932
+ }
10668
10933
  async function handleAuthErr(reason) {
10669
10934
  if (reason === "access token expired") {
10670
10935
  try {
10671
10936
  credentials = await ensureFreshAccessToken(baseUrl, credentials);
10672
10937
  console.log("[local] token refreshed, retrying auth");
10673
- ws.send(
10674
- encodeFrame({
10675
- t: "auth",
10676
- role: "bridge",
10677
- deviceId: credentials.deviceId,
10678
- token: credentials.accessToken
10679
- })
10680
- );
10938
+ sendAuth();
10681
10939
  return;
10682
10940
  } catch (e) {
10683
10941
  console.error(`[local] ${e.message}`);
@@ -10686,7 +10944,7 @@ async function startLocal(opts = {}) {
10686
10944
  await clearCredentials();
10687
10945
  console.error("[local] auth failed:", reason);
10688
10946
  console.error("[local] \u51ED\u8BC1\u5DF2\u6E05\u9664\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C cli-local \u5B8C\u6210\u6388\u6743");
10689
- cleanup(1);
10947
+ exitProcess(1);
10690
10948
  }
10691
10949
  function handleSpawn(frame) {
10692
10950
  const sessionId = frame.sessionId;
@@ -10742,6 +11000,15 @@ async function startLocal(opts = {}) {
10742
11000
  const logger = loggers.get(sessionId);
10743
11001
  const session = pty.get(sessionId);
10744
11002
  if (!session || !logger) return;
11003
+ if (frame.force) {
11004
+ const line = new TextDecoder().decode(frame.data);
11005
+ ptyWriteChar(sessionId, line + "\r");
11006
+ logger.logInput(new TextEncoder().encode(line + "\r"));
11007
+ logger.logForced(line);
11008
+ lineBuffers.set(sessionId, "");
11009
+ console.log(`[local] forced execution in ${sessionId}: ${line}`);
11010
+ return;
11011
+ }
10745
11012
  if (session.cmd === "shell" && !session.inAltScreen) {
10746
11013
  handleShellInput(sessionId, frame.data, logger);
10747
11014
  } else {
@@ -10816,23 +11083,13 @@ async function startLocal(opts = {}) {
10816
11083
  );
10817
11084
  }
10818
11085
  function safeSend(data) {
10819
- if (ws.readyState === wrapper_default.OPEN) {
11086
+ if (ws && ws.readyState === wrapper_default.OPEN) {
10820
11087
  ws.send(data);
10821
11088
  }
10822
11089
  }
10823
- ws.on("close", () => {
10824
- if (heartbeatTimer) clearInterval(heartbeatTimer);
10825
- pty.killAll();
10826
- for (const [, logger] of loggers) logger.logExit(-1);
10827
- loggers.clear();
10828
- lineBuffers.clear();
10829
- console.log("[local] disconnected");
10830
- });
10831
- ws.on("error", (err) => {
10832
- console.error("[local] error:", err.message);
10833
- });
10834
11090
  process.on("SIGINT", () => cleanup(0));
10835
11091
  process.on("SIGTERM", () => cleanup(0));
11092
+ connect();
10836
11093
  }
10837
11094
  function toUint8(raw) {
10838
11095
  if (raw instanceof Buffer) return new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
@@ -10860,15 +11117,15 @@ function resolveHere() {
10860
11117
  if (DEBUG) console.error("[debug] cannot resolve script path");
10861
11118
  return;
10862
11119
  }
10863
- const hereDir = (0, import_node_path5.dirname)(here);
11120
+ const hereDir = (0, import_node_path6.dirname)(here);
10864
11121
  const candidates = [
10865
- (0, import_node_path5.join)(hereDir, "..", "node_modules", "node-pty", "prebuilds"),
10866
- (0, import_node_path5.join)(hereDir, "..", "..", "node-pty", "prebuilds"),
10867
- (0, import_node_path5.join)(hereDir, "..", "..", "..", "node_modules", "node-pty", "prebuilds")
11122
+ (0, import_node_path6.join)(hereDir, "..", "node_modules", "node-pty", "prebuilds"),
11123
+ (0, import_node_path6.join)(hereDir, "..", "..", "node-pty", "prebuilds"),
11124
+ (0, import_node_path6.join)(hereDir, "..", "..", "..", "node_modules", "node-pty", "prebuilds")
10868
11125
  ];
10869
11126
  let prebuildsDir = null;
10870
11127
  for (const c of candidates) {
10871
- if ((0, import_node_fs2.existsSync)(c)) {
11128
+ if ((0, import_node_fs3.existsSync)(c)) {
10872
11129
  prebuildsDir = c;
10873
11130
  break;
10874
11131
  }
@@ -10878,11 +11135,11 @@ function resolveHere() {
10878
11135
  return;
10879
11136
  }
10880
11137
  let n = 0;
10881
- for (const sub of (0, import_node_fs2.readdirSync)(prebuildsDir)) {
10882
- const helper = (0, import_node_path5.join)(prebuildsDir, sub, "spawn-helper");
10883
- if ((0, import_node_fs2.existsSync)(helper)) {
11138
+ for (const sub of (0, import_node_fs3.readdirSync)(prebuildsDir)) {
11139
+ const helper = (0, import_node_path6.join)(prebuildsDir, sub, "spawn-helper");
11140
+ if ((0, import_node_fs3.existsSync)(helper)) {
10884
11141
  try {
10885
- (0, import_node_fs2.chmodSync)(helper, 493);
11142
+ (0, import_node_fs3.chmodSync)(helper, 493);
10886
11143
  n++;
10887
11144
  } catch {
10888
11145
  }
@@ -10897,6 +11154,8 @@ if (argUrl && !argMatch && (argUrl === "-h" || argUrl === "--help" || argUrl ===
10897
11154
 
10898
11155
  usage:
10899
11156
  cli-local [HUB_URL]
11157
+ cli-local install [HUB_URL] \u5B89\u88C5 macOS launchd \u5E38\u9A7B\u670D\u52A1\uFF08\u5F00\u673A\u81EA\u542F + \u65AD\u7EBF\u81EA\u52A8\u91CD\u8FDE\uFF09
11158
+ cli-local uninstall \u5378\u8F7D launchd \u5E38\u9A7B\u670D\u52A1
10900
11159
 
10901
11160
  \u9996\u6B21\u8FD0\u884C\uFF08\u65E0\u914D\u7F6E\uFF09\u4F1A\u8FDB\u5165\u5F15\u5BFC\uFF1A
10902
11161
  1. \u63D0\u793A hub \u5730\u5740\uFF08\u56DE\u8F66\u7528\u9ED8\u8BA4\uFF09
@@ -10905,6 +11164,8 @@ usage:
10905
11164
  4. \u968F\u540E\u8D70 OAuth Device Flow\uFF08RFC 8628\uFF09\uFF1A\u81EA\u52A8\u6253\u5F00\u6388\u6743\u9875 \u2192 \u6279\u51C6\u672C\u8BBE\u5907
10906
11165
  5. \u51ED\u8BC1\u5B58\u5165 ~/.cli-remote/credentials.json\uFF080600\uFF09\uFF0C\u4E4B\u540E\u81EA\u52A8\u7EED\u671F
10907
11166
 
11167
+ \u65AD\u7EBF\u81EA\u52A8\u91CD\u8FDE\uFF1Ahub \u91CD\u542F/\u7F51\u7EDC\u95EA\u65AD\u540E\u8FDB\u7A0B\u4E0D\u9000\u51FA\uFF0CPTY \u4F1A\u8BDD\u4FDD\u6D3B\u5E76\u81EA\u52A8\u91CD\u8FDE\u3002
11168
+
10908
11169
  environment:
10909
11170
  RELAY_URL hub ws url (overrides argv and config)
10910
11171
  REGISTRATION_KEY registration key (fallback of config.json)
@@ -10919,7 +11180,119 @@ config (~/.cli-remote/config.json):
10919
11180
  }
10920
11181
 
10921
11182
  example:
10922
- cli-local wss://cli-remote.mkjs.net/ws`);
11183
+ cli-local wss://cli-remote.mkjs.net/ws
11184
+ cli-local install # \u914D\u7F6E\u5E76\u6388\u6743\u540E\uFF0C\u5B89\u88C5\u5E38\u9A7B\u670D\u52A1`);
11185
+ process.exit(0);
11186
+ }
11187
+ var LAUNCHD_LABEL = "net.cli-remote.local";
11188
+ function launchdPlistPath() {
11189
+ return (0, import_node_path6.join)((0, import_node_os6.homedir)(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
11190
+ }
11191
+ function entryScript() {
11192
+ const a1 = process.argv[1];
11193
+ if (!a1 || a1.endsWith(".ts")) return null;
11194
+ try {
11195
+ return (0, import_node_fs3.realpathSync)(a1);
11196
+ } catch {
11197
+ return a1;
11198
+ }
11199
+ }
11200
+ function run(cmd, args, okToFail = false) {
11201
+ try {
11202
+ return (0, import_node_child_process2.execFileSync)(cmd, args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
11203
+ } catch (e) {
11204
+ if (okToFail) return null;
11205
+ throw e;
11206
+ }
11207
+ }
11208
+ function launchdInstall(urlArg) {
11209
+ if (process.platform !== "darwin") {
11210
+ console.error(`[local] launchd \u5E38\u9A7B\u4EC5\u652F\u6301 macOS\uFF08\u5F53\u524D ${process.platform}\uFF09\uFF1BLinux \u8BF7\u7528 systemd --user \u81EA\u884C\u6258\u7BA1`);
11211
+ process.exit(1);
11212
+ }
11213
+ const home = (0, import_node_os6.homedir)();
11214
+ const entry = entryScript();
11215
+ if (!entry) {
11216
+ console.error("[local] \u5F53\u524D\u4EE5\u6E90\u7801\uFF08tsx\uFF09\u6A21\u5F0F\u8FD0\u884C\uFF0C\u65E0\u6CD5\u5B89\u88C5\u5E38\u9A7B\u670D\u52A1\u3002");
11217
+ console.error("[local] \u8BF7\u5148\u901A\u8FC7 npm \u5B89\u88C5\uFF1Anpm i -g @cli-remote/local\uFF0C\u518D\u7528\u5B89\u88C5\u7248\u6267\u884C install\u3002");
11218
+ process.exit(1);
11219
+ }
11220
+ if (urlArg) {
11221
+ const cfgDir = (0, import_node_path6.join)(home, DEFAULT_CONFIG_DIR);
11222
+ (0, import_node_fs3.mkdirSync)(cfgDir, { recursive: true });
11223
+ const cfgPath = (0, import_node_path6.join)(cfgDir, "config.json");
11224
+ let cfg = {};
11225
+ try {
11226
+ cfg = JSON.parse((0, import_node_fs3.readFileSync)(cfgPath, "utf-8"));
11227
+ } catch {
11228
+ }
11229
+ cfg.relayUrl = urlArg;
11230
+ (0, import_node_fs3.writeFileSync)(cfgPath, JSON.stringify(cfg, null, 2) + "\n");
11231
+ console.log(`[local] relayUrl \u5DF2\u5199\u5165 ${cfgPath}`);
11232
+ }
11233
+ const credPath = (0, import_node_path6.join)(home, DEFAULT_CONFIG_DIR, CREDENTIALS_FILE);
11234
+ if (!(0, import_node_fs3.existsSync)(credPath)) {
11235
+ console.error("[local] \u5C1A\u672A\u6388\u6743\uFF08\u627E\u4E0D\u5230 credentials.json\uFF09\u3002");
11236
+ console.error("[local] \u8BF7\u5148\u4EA4\u4E92\u5F0F\u8FD0\u884C\u4E00\u6B21 cli-local \u5B8C\u6210\u914D\u7F6E\u4E0E\u6388\u6743\uFF0C\u518D\u5B89\u88C5\u5E38\u9A7B\u670D\u52A1\u3002");
11237
+ process.exit(1);
11238
+ }
11239
+ const plist = launchdPlistPath();
11240
+ const logDir = (0, import_node_path6.join)(home, DEFAULT_CONFIG_DIR, "logs");
11241
+ (0, import_node_fs3.mkdirSync)((0, import_node_path6.dirname)(plist), { recursive: true });
11242
+ (0, import_node_fs3.mkdirSync)(logDir, { recursive: true });
11243
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
11244
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
11245
+ <plist version="1.0">
11246
+ <dict>
11247
+ <key>Label</key><string>${LAUNCHD_LABEL}</string>
11248
+ <key>ProgramArguments</key>
11249
+ <array>
11250
+ <string>${process.execPath}</string>
11251
+ <string>${entry}</string>
11252
+ </array>
11253
+ <key>RunAtLoad</key><true/>
11254
+ <key>KeepAlive</key><true/>
11255
+ <key>ThrottleInterval</key><integer>30</integer>
11256
+ <key>StandardOutPath</key><string>${(0, import_node_path6.join)(logDir, "daemon.out.log")}</string>
11257
+ <key>StandardErrorPath</key><string>${(0, import_node_path6.join)(logDir, "daemon.err.log")}</string>
11258
+ </dict>
11259
+ </plist>
11260
+ `;
11261
+ (0, import_node_fs3.writeFileSync)(plist, xml);
11262
+ const uid = process.getuid?.();
11263
+ if (uid === void 0) {
11264
+ console.error("[local] \u65E0\u6CD5\u786E\u5B9A uid\uFF0Claunchd \u5B89\u88C5\u5931\u8D25");
11265
+ process.exit(1);
11266
+ }
11267
+ const guiDomain = `gui/${uid}`;
11268
+ run("launchctl", ["bootout", guiDomain, plist], true);
11269
+ run("launchctl", ["unload", "-w", plist], true);
11270
+ run("launchctl", ["bootstrap", guiDomain, plist]);
11271
+ run("launchctl", ["enable", `${guiDomain}/${LAUNCHD_LABEL}`], true);
11272
+ console.log(`[local] launchd \u670D\u52A1\u5DF2\u5B89\u88C5\u5E76\u542F\u52A8\uFF1A${LAUNCHD_LABEL}`);
11273
+ console.log(`[local] plist: ${plist}`);
11274
+ console.log("[local] \u65E5\u5FD7: ~/.cli-remote/logs/daemon.out.log / daemon.err.log");
11275
+ console.log("[local] \u7BA1\u7406\u670D\u52A1: launchctl kickstart -k, bootout, \u6216 cli-local uninstall");
11276
+ }
11277
+ function launchdUninstall() {
11278
+ const plist = launchdPlistPath();
11279
+ if (!(0, import_node_fs3.existsSync)(plist)) {
11280
+ console.log("[local] \u672A\u627E\u5230\u5DF2\u5B89\u88C5\u7684\u5E38\u9A7B\u670D\u52A1\uFF08nothing to uninstall\uFF09");
11281
+ process.exit(0);
11282
+ }
11283
+ const uid = process.getuid?.();
11284
+ const guiDomain = `gui/${uid ?? 0}`;
11285
+ run("launchctl", ["bootout", guiDomain, plist], true);
11286
+ run("launchctl", ["unload", "-w", plist], true);
11287
+ (0, import_node_fs3.rmSync)(plist);
11288
+ console.log(`[local] launchd \u670D\u52A1\u5DF2\u505C\u6B62\u5E76\u5378\u8F7D\uFF1A${LAUNCHD_LABEL}`);
11289
+ }
11290
+ if (argUrl && !argMatch && argUrl === "install") {
11291
+ launchdInstall(process.argv[3]);
11292
+ process.exit(0);
11293
+ }
11294
+ if (argUrl && !argMatch && argUrl === "uninstall") {
11295
+ launchdUninstall();
10923
11296
  process.exit(0);
10924
11297
  }
10925
11298
  startLocal(argMatch ? { url: argUrl } : {}).catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cli-remote/local",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Local PTY agent for cli-remote — exposes your local CLI tools (claude/codex/kimi-cli/shell) to the mobile web client via a self-hosted hub.",
5
5
  "license": "MIT",
6
6
  "repository": {