@cli-remote/local 0.0.2 → 0.0.3

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 +217 -44
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -9940,6 +9940,8 @@ var require_lib = __commonJS({
9940
9940
 
9941
9941
  // src/index.ts
9942
9942
  var import_node_fs2 = require("node:fs");
9943
+ var import_node_child_process2 = require("node:child_process");
9944
+ var import_node_os5 = require("node:os");
9943
9945
  var import_node_path5 = require("node:path");
9944
9946
  var import_node_url = require("node:url");
9945
9947
 
@@ -9978,6 +9980,7 @@ 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 RECONNECT_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
9981
9984
  var DEFAULT_CONFIG_DIR = ".cli-remote";
9982
9985
  var CREDENTIALS_FILE = "credentials.json";
9983
9986
  var CONFIG_FILE = "config.json";
@@ -10211,6 +10214,10 @@ var SessionLogger = class {
10211
10214
  if (this.closed) return;
10212
10215
  this.write({ ts: Date.now(), event: "rejected", cmd, reason, dangerous });
10213
10216
  }
10217
+ logForced(cmd) {
10218
+ if (this.closed) return;
10219
+ this.write({ ts: Date.now(), event: "forced", cmd });
10220
+ }
10214
10221
  logExit(code) {
10215
10222
  if (this.closed) return;
10216
10223
  this.write({ ts: Date.now(), event: "exit", code });
@@ -10600,23 +10607,17 @@ async function startLocal(opts = {}) {
10600
10607
  console.log(`[local] connecting to ${url}`);
10601
10608
  console.log(`[local] deviceId=${credentials.deviceId}`);
10602
10609
  console.log(`[local] whitelist: ${whitelist.size} commands`);
10603
- const ws = new wrapper_default(url);
10610
+ let ws = null;
10604
10611
  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();
10612
+ let reconnectTimer = null;
10613
+ let reconnectAttempts = 0;
10614
+ let userExitCode = null;
10615
+ function stopHeartbeat() {
10610
10616
  if (heartbeatTimer) clearInterval(heartbeatTimer);
10611
- try {
10612
- ws.close();
10613
- } catch {
10614
- }
10615
- process.exit(code);
10617
+ heartbeatTimer = null;
10616
10618
  }
10617
- ws.on("open", () => {
10618
- console.log("[local] connected");
10619
- ws.send(
10619
+ function sendAuth() {
10620
+ ws?.send(
10620
10621
  encodeFrame({
10621
10622
  t: "auth",
10622
10623
  role: "bridge",
@@ -10624,13 +10625,73 @@ async function startLocal(opts = {}) {
10624
10625
  token: credentials.accessToken
10625
10626
  })
10626
10627
  );
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) => {
10628
+ }
10629
+ function connect() {
10630
+ if (userExitCode !== null) return;
10631
+ const sock = new wrapper_default(url);
10632
+ ws = sock;
10633
+ sock.on("open", () => {
10634
+ console.log("[local] connected");
10635
+ sendAuth();
10636
+ stopHeartbeat();
10637
+ heartbeatTimer = setInterval(() => {
10638
+ if (sock.readyState === wrapper_default.OPEN) {
10639
+ sock.send(encodeFrame({ t: "ping" }));
10640
+ }
10641
+ }, HEARTBEAT_INTERVAL_MS);
10642
+ });
10643
+ sock.on("message", (raw) => handleFrame(sock, raw));
10644
+ sock.on("close", () => {
10645
+ stopHeartbeat();
10646
+ if (ws === sock) ws = null;
10647
+ if (userExitCode !== null) {
10648
+ process.exit(userExitCode);
10649
+ }
10650
+ console.log("[local] disconnected; PTY sessions kept alive, reconnecting\u2026");
10651
+ scheduleReconnect();
10652
+ });
10653
+ sock.on("error", (err) => {
10654
+ console.error("[local] ws error:", err.message);
10655
+ });
10656
+ }
10657
+ async function scheduleReconnect() {
10658
+ if (reconnectTimer || userExitCode !== null) return;
10659
+ const idx = Math.min(reconnectAttempts, RECONNECT_DELAYS_MS.length - 1);
10660
+ const delay = RECONNECT_DELAYS_MS[idx];
10661
+ reconnectAttempts++;
10662
+ console.log(`[local] reconnecting in ${Math.round(delay / 1e3)}s (attempt ${reconnectAttempts})`);
10663
+ reconnectTimer = setTimeout(() => {
10664
+ reconnectTimer = null;
10665
+ ensureFreshAccessToken(baseUrl, credentials).then((creds) => {
10666
+ credentials = creds;
10667
+ connect();
10668
+ }).catch(async (e) => {
10669
+ await clearCredentials();
10670
+ console.error(`[local] ${e.message}`);
10671
+ console.error("[local] \u51ED\u8BC1\u5DF2\u5931\u6548\u5E76\u6E05\u9664\uFF1B\u8BF7\u91CD\u65B0\u8FD0\u884C cli-local \u5B8C\u6210\u6388\u6743");
10672
+ exitProcess(1);
10673
+ });
10674
+ }, delay);
10675
+ }
10676
+ function exitProcess(code) {
10677
+ userExitCode = code;
10678
+ if (reconnectTimer) clearTimeout(reconnectTimer);
10679
+ reconnectTimer = null;
10680
+ pty.killAll();
10681
+ for (const [, logger] of loggers) logger.logExit(-1);
10682
+ loggers.clear();
10683
+ lineBuffers.clear();
10684
+ stopHeartbeat();
10685
+ try {
10686
+ ws?.close();
10687
+ } catch {
10688
+ }
10689
+ process.exit(code);
10690
+ }
10691
+ function cleanup(code = 0) {
10692
+ exitProcess(code);
10693
+ }
10694
+ function handleFrame(sock, raw) {
10634
10695
  let frame;
10635
10696
  try {
10636
10697
  frame = decodeFrame(toUint8(raw));
@@ -10639,6 +10700,10 @@ async function startLocal(opts = {}) {
10639
10700
  }
10640
10701
  switch (frame.t) {
10641
10702
  case "auth-ok":
10703
+ if (reconnectAttempts > 0) {
10704
+ console.log(`[local] reconnected after ${reconnectAttempts} attempt(s); ${pty.list().length} PTY session(s) alive`);
10705
+ }
10706
+ reconnectAttempts = 0;
10642
10707
  console.log(`[local] auth ok, deviceId=${frame.channelId}`);
10643
10708
  break;
10644
10709
  case "auth-err":
@@ -10664,20 +10729,13 @@ async function startLocal(opts = {}) {
10664
10729
  default:
10665
10730
  console.log("[local] unhandled frame:", frame.t);
10666
10731
  }
10667
- });
10732
+ }
10668
10733
  async function handleAuthErr(reason) {
10669
10734
  if (reason === "access token expired") {
10670
10735
  try {
10671
10736
  credentials = await ensureFreshAccessToken(baseUrl, credentials);
10672
10737
  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
- );
10738
+ sendAuth();
10681
10739
  return;
10682
10740
  } catch (e) {
10683
10741
  console.error(`[local] ${e.message}`);
@@ -10686,7 +10744,7 @@ async function startLocal(opts = {}) {
10686
10744
  await clearCredentials();
10687
10745
  console.error("[local] auth failed:", reason);
10688
10746
  console.error("[local] \u51ED\u8BC1\u5DF2\u6E05\u9664\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C cli-local \u5B8C\u6210\u6388\u6743");
10689
- cleanup(1);
10747
+ exitProcess(1);
10690
10748
  }
10691
10749
  function handleSpawn(frame) {
10692
10750
  const sessionId = frame.sessionId;
@@ -10742,6 +10800,15 @@ async function startLocal(opts = {}) {
10742
10800
  const logger = loggers.get(sessionId);
10743
10801
  const session = pty.get(sessionId);
10744
10802
  if (!session || !logger) return;
10803
+ if (frame.force) {
10804
+ const line = new TextDecoder().decode(frame.data);
10805
+ ptyWriteChar(sessionId, line + "\r");
10806
+ logger.logInput(new TextEncoder().encode(line + "\r"));
10807
+ logger.logForced(line);
10808
+ lineBuffers.set(sessionId, "");
10809
+ console.log(`[local] forced execution in ${sessionId}: ${line}`);
10810
+ return;
10811
+ }
10745
10812
  if (session.cmd === "shell" && !session.inAltScreen) {
10746
10813
  handleShellInput(sessionId, frame.data, logger);
10747
10814
  } else {
@@ -10816,23 +10883,13 @@ async function startLocal(opts = {}) {
10816
10883
  );
10817
10884
  }
10818
10885
  function safeSend(data) {
10819
- if (ws.readyState === wrapper_default.OPEN) {
10886
+ if (ws && ws.readyState === wrapper_default.OPEN) {
10820
10887
  ws.send(data);
10821
10888
  }
10822
10889
  }
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
10890
  process.on("SIGINT", () => cleanup(0));
10835
10891
  process.on("SIGTERM", () => cleanup(0));
10892
+ connect();
10836
10893
  }
10837
10894
  function toUint8(raw) {
10838
10895
  if (raw instanceof Buffer) return new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
@@ -10897,6 +10954,8 @@ if (argUrl && !argMatch && (argUrl === "-h" || argUrl === "--help" || argUrl ===
10897
10954
 
10898
10955
  usage:
10899
10956
  cli-local [HUB_URL]
10957
+ 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
10958
+ cli-local uninstall \u5378\u8F7D launchd \u5E38\u9A7B\u670D\u52A1
10900
10959
 
10901
10960
  \u9996\u6B21\u8FD0\u884C\uFF08\u65E0\u914D\u7F6E\uFF09\u4F1A\u8FDB\u5165\u5F15\u5BFC\uFF1A
10902
10961
  1. \u63D0\u793A hub \u5730\u5740\uFF08\u56DE\u8F66\u7528\u9ED8\u8BA4\uFF09
@@ -10905,6 +10964,8 @@ usage:
10905
10964
  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
10965
  5. \u51ED\u8BC1\u5B58\u5165 ~/.cli-remote/credentials.json\uFF080600\uFF09\uFF0C\u4E4B\u540E\u81EA\u52A8\u7EED\u671F
10907
10966
 
10967
+ \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
10968
+
10908
10969
  environment:
10909
10970
  RELAY_URL hub ws url (overrides argv and config)
10910
10971
  REGISTRATION_KEY registration key (fallback of config.json)
@@ -10919,7 +10980,119 @@ config (~/.cli-remote/config.json):
10919
10980
  }
10920
10981
 
10921
10982
  example:
10922
- cli-local wss://cli-remote.mkjs.net/ws`);
10983
+ cli-local wss://cli-remote.mkjs.net/ws
10984
+ cli-local install # \u914D\u7F6E\u5E76\u6388\u6743\u540E\uFF0C\u5B89\u88C5\u5E38\u9A7B\u670D\u52A1`);
10985
+ process.exit(0);
10986
+ }
10987
+ var LAUNCHD_LABEL = "net.cli-remote.local";
10988
+ function launchdPlistPath() {
10989
+ return (0, import_node_path5.join)((0, import_node_os5.homedir)(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
10990
+ }
10991
+ function entryScript() {
10992
+ const a1 = process.argv[1];
10993
+ if (!a1 || a1.endsWith(".ts")) return null;
10994
+ try {
10995
+ return (0, import_node_fs2.realpathSync)(a1);
10996
+ } catch {
10997
+ return a1;
10998
+ }
10999
+ }
11000
+ function run(cmd, args, okToFail = false) {
11001
+ try {
11002
+ return (0, import_node_child_process2.execFileSync)(cmd, args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
11003
+ } catch (e) {
11004
+ if (okToFail) return null;
11005
+ throw e;
11006
+ }
11007
+ }
11008
+ function launchdInstall(urlArg) {
11009
+ if (process.platform !== "darwin") {
11010
+ 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`);
11011
+ process.exit(1);
11012
+ }
11013
+ const home = (0, import_node_os5.homedir)();
11014
+ const entry = entryScript();
11015
+ if (!entry) {
11016
+ console.error("[local] \u5F53\u524D\u4EE5\u6E90\u7801\uFF08tsx\uFF09\u6A21\u5F0F\u8FD0\u884C\uFF0C\u65E0\u6CD5\u5B89\u88C5\u5E38\u9A7B\u670D\u52A1\u3002");
11017
+ 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");
11018
+ process.exit(1);
11019
+ }
11020
+ if (urlArg) {
11021
+ const cfgDir = (0, import_node_path5.join)(home, DEFAULT_CONFIG_DIR);
11022
+ (0, import_node_fs2.mkdirSync)(cfgDir, { recursive: true });
11023
+ const cfgPath = (0, import_node_path5.join)(cfgDir, "config.json");
11024
+ let cfg = {};
11025
+ try {
11026
+ cfg = JSON.parse((0, import_node_fs2.readFileSync)(cfgPath, "utf-8"));
11027
+ } catch {
11028
+ }
11029
+ cfg.relayUrl = urlArg;
11030
+ (0, import_node_fs2.writeFileSync)(cfgPath, JSON.stringify(cfg, null, 2) + "\n");
11031
+ console.log(`[local] relayUrl \u5DF2\u5199\u5165 ${cfgPath}`);
11032
+ }
11033
+ const credPath = (0, import_node_path5.join)(home, DEFAULT_CONFIG_DIR, CREDENTIALS_FILE);
11034
+ if (!(0, import_node_fs2.existsSync)(credPath)) {
11035
+ console.error("[local] \u5C1A\u672A\u6388\u6743\uFF08\u627E\u4E0D\u5230 credentials.json\uFF09\u3002");
11036
+ 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");
11037
+ process.exit(1);
11038
+ }
11039
+ const plist = launchdPlistPath();
11040
+ const logDir = (0, import_node_path5.join)(home, DEFAULT_CONFIG_DIR, "logs");
11041
+ (0, import_node_fs2.mkdirSync)((0, import_node_path5.dirname)(plist), { recursive: true });
11042
+ (0, import_node_fs2.mkdirSync)(logDir, { recursive: true });
11043
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
11044
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
11045
+ <plist version="1.0">
11046
+ <dict>
11047
+ <key>Label</key><string>${LAUNCHD_LABEL}</string>
11048
+ <key>ProgramArguments</key>
11049
+ <array>
11050
+ <string>${process.execPath}</string>
11051
+ <string>${entry}</string>
11052
+ </array>
11053
+ <key>RunAtLoad</key><true/>
11054
+ <key>KeepAlive</key><true/>
11055
+ <key>ThrottleInterval</key><integer>30</integer>
11056
+ <key>StandardOutPath</key><string>${(0, import_node_path5.join)(logDir, "daemon.out.log")}</string>
11057
+ <key>StandardErrorPath</key><string>${(0, import_node_path5.join)(logDir, "daemon.err.log")}</string>
11058
+ </dict>
11059
+ </plist>
11060
+ `;
11061
+ (0, import_node_fs2.writeFileSync)(plist, xml);
11062
+ const uid = process.getuid?.();
11063
+ if (uid === void 0) {
11064
+ console.error("[local] \u65E0\u6CD5\u786E\u5B9A uid\uFF0Claunchd \u5B89\u88C5\u5931\u8D25");
11065
+ process.exit(1);
11066
+ }
11067
+ const guiDomain = `gui/${uid}`;
11068
+ run("launchctl", ["bootout", guiDomain, plist], true);
11069
+ run("launchctl", ["unload", "-w", plist], true);
11070
+ run("launchctl", ["bootstrap", guiDomain, plist]);
11071
+ run("launchctl", ["enable", `${guiDomain}/${LAUNCHD_LABEL}`], true);
11072
+ console.log(`[local] launchd \u670D\u52A1\u5DF2\u5B89\u88C5\u5E76\u542F\u52A8\uFF1A${LAUNCHD_LABEL}`);
11073
+ console.log(`[local] plist: ${plist}`);
11074
+ console.log("[local] \u65E5\u5FD7: ~/.cli-remote/logs/daemon.out.log / daemon.err.log");
11075
+ console.log("[local] \u7BA1\u7406\u670D\u52A1: launchctl kickstart -k, bootout, \u6216 cli-local uninstall");
11076
+ }
11077
+ function launchdUninstall() {
11078
+ const plist = launchdPlistPath();
11079
+ if (!(0, import_node_fs2.existsSync)(plist)) {
11080
+ console.log("[local] \u672A\u627E\u5230\u5DF2\u5B89\u88C5\u7684\u5E38\u9A7B\u670D\u52A1\uFF08nothing to uninstall\uFF09");
11081
+ process.exit(0);
11082
+ }
11083
+ const uid = process.getuid?.();
11084
+ const guiDomain = `gui/${uid ?? 0}`;
11085
+ run("launchctl", ["bootout", guiDomain, plist], true);
11086
+ run("launchctl", ["unload", "-w", plist], true);
11087
+ (0, import_node_fs2.rmSync)(plist);
11088
+ console.log(`[local] launchd \u670D\u52A1\u5DF2\u505C\u6B62\u5E76\u5378\u8F7D\uFF1A${LAUNCHD_LABEL}`);
11089
+ }
11090
+ if (argUrl && !argMatch && argUrl === "install") {
11091
+ launchdInstall(process.argv[3]);
11092
+ process.exit(0);
11093
+ }
11094
+ if (argUrl && !argMatch && argUrl === "uninstall") {
11095
+ launchdUninstall();
10923
11096
  process.exit(0);
10924
11097
  }
10925
11098
  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.3",
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": {