@cli-remote/local 0.1.1 → 0.3.1

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 +229 -172
  2. package/package.json +2 -2
package/dist/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ '#!/usr/bin/env node'
2
2
  "use strict";
3
3
  var __create = Object.create;
4
4
  var __defProp = Object.defineProperty;
@@ -2265,7 +2265,7 @@ var require_websocket = __commonJS({
2265
2265
  var http = require("http");
2266
2266
  var net = require("net");
2267
2267
  var tls = require("tls");
2268
- var { randomBytes: randomBytes2, createHash } = require("crypto");
2268
+ var { randomBytes: randomBytes3, createHash } = require("crypto");
2269
2269
  var { Duplex, Readable } = require("stream");
2270
2270
  var { URL: URL2 } = require("url");
2271
2271
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -2803,7 +2803,7 @@ var require_websocket = __commonJS({
2803
2803
  }
2804
2804
  }
2805
2805
  const defaultPort = isSecure ? 443 : 80;
2806
- const key = randomBytes2(16).toString("base64");
2806
+ const key = randomBytes3(16).toString("base64");
2807
2807
  const request = isSecure ? https.request : http.request;
2808
2808
  const protocolSet = /* @__PURE__ */ new Set();
2809
2809
  let perMessageDeflate;
@@ -9981,6 +9981,7 @@ var ACCESS_TOKEN_TTL_MS = 60 * 6e4;
9981
9981
  var REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 6e4;
9982
9982
  var REFRESH_MARGIN_MS = 5 * 6e4;
9983
9983
  var OUTPUT_BUFFER_SIZE = 500;
9984
+ var AUDIT_RETENTION_DAYS = 14;
9984
9985
  var FILE_CHUNK_SIZE = 64 * 1024;
9985
9986
  var MAX_TRANSFER_BYTES = 500 * 1024 * 1024;
9986
9987
  var UPLOAD_DIR = "uploads";
@@ -9991,59 +9992,6 @@ var CONFIG_FILE = "config.json";
9991
9992
  var LOG_DIR = "logs";
9992
9993
 
9993
9994
  // ../shared/src/whitelist.ts
9994
- var DEFAULT_WHITELIST = [
9995
- "ls",
9996
- "cd",
9997
- "pwd",
9998
- "cat",
9999
- "less",
10000
- "head",
10001
- "tail",
10002
- "mkdir",
10003
- "rmdir",
10004
- "rm",
10005
- "cp",
10006
- "mv",
10007
- "touch",
10008
- "find",
10009
- "stat",
10010
- "tree",
10011
- "grep",
10012
- "awk",
10013
- "sed",
10014
- "sort",
10015
- "uniq",
10016
- "wc",
10017
- "cut",
10018
- "tr",
10019
- "xargs",
10020
- "git",
10021
- "ps",
10022
- "top",
10023
- "htop",
10024
- "kill",
10025
- "df",
10026
- "du",
10027
- "free",
10028
- "uname",
10029
- "whoami",
10030
- "env",
10031
- "echo",
10032
- "node",
10033
- "npm",
10034
- "pnpm",
10035
- "yarn",
10036
- "python",
10037
- "python3",
10038
- "pip",
10039
- "go",
10040
- "cargo",
10041
- "make",
10042
- "docker",
10043
- "claude",
10044
- "codex",
10045
- "kimi-cli"
10046
- ];
10047
9995
  var DANGEROUS_PATTERNS = [
10048
9996
  /\brm\s+-[rfRF]+\s+\/(\s|$)/,
10049
9997
  /\bsudo\b/,
@@ -10054,6 +10002,9 @@ var DANGEROUS_PATTERNS = [
10054
10002
  /\bshutdown\b/,
10055
10003
  /\breboot\b/
10056
10004
  ];
10005
+ function findDangerousPattern(line) {
10006
+ return DANGEROUS_PATTERNS.find((p) => p.test(line));
10007
+ }
10057
10008
 
10058
10009
  // src/pty-manager.ts
10059
10010
  var import_node_pty = require("node-pty");
@@ -10186,20 +10137,55 @@ var PtyManager = class {
10186
10137
  var import_node_fs = require("node:fs");
10187
10138
  var import_node_os2 = require("node:os");
10188
10139
  var import_node_path2 = require("node:path");
10140
+ function logRoot() {
10141
+ return (0, import_node_path2.join)((0, import_node_os2.homedir)(), DEFAULT_CONFIG_DIR, LOG_DIR);
10142
+ }
10189
10143
  function todayDir() {
10190
10144
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
10191
10145
  }
10146
+ function pruneAuditLogs(retentionDays) {
10147
+ if (retentionDays <= 0) return 0;
10148
+ const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 6e4).toISOString().slice(0, 10);
10149
+ let removed = 0;
10150
+ let entries;
10151
+ try {
10152
+ entries = (0, import_node_fs.readdirSync)(logRoot());
10153
+ } catch {
10154
+ return 0;
10155
+ }
10156
+ for (const name of entries) {
10157
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(name)) continue;
10158
+ if (name >= cutoff) continue;
10159
+ try {
10160
+ (0, import_node_fs.rmSync)((0, import_node_path2.join)(logRoot(), name), { recursive: true, force: true });
10161
+ removed++;
10162
+ } catch {
10163
+ }
10164
+ }
10165
+ return removed;
10166
+ }
10192
10167
  var SessionLogger = class {
10193
- path;
10194
- closed = false;
10195
- constructor(sessionId, cmd) {
10196
- const dir = (0, import_node_path2.join)((0, import_node_os2.homedir)(), DEFAULT_CONFIG_DIR, LOG_DIR, todayDir());
10197
- (0, import_node_fs.mkdirSync)(dir, { recursive: true });
10168
+ constructor(sessionId, cmd, mode = "full") {
10169
+ this.mode = mode;
10170
+ if (mode === "off") {
10171
+ this.path = "";
10172
+ return;
10173
+ }
10174
+ const dir = (0, import_node_path2.join)(logRoot(), todayDir());
10175
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: 448 });
10176
+ try {
10177
+ (0, import_node_fs.chmodSync)(dir, 448);
10178
+ } catch {
10179
+ }
10198
10180
  this.path = (0, import_node_path2.join)(dir, `${sessionId}.log`);
10199
- this.write({ ts: Date.now(), event: "spawn", cmd });
10181
+ (0, import_node_fs.appendFileSync)(this.path, "", { mode: 384 });
10182
+ (0, import_node_fs.chmodSync)(this.path, 384);
10183
+ this.write({ ts: Date.now(), event: "spawn", cmd, mode });
10200
10184
  }
10185
+ path;
10186
+ closed = false;
10201
10187
  logInput(data) {
10202
- if (this.closed) return;
10188
+ if (this.closed || this.mode === "off") return;
10203
10189
  this.write({
10204
10190
  ts: Date.now(),
10205
10191
  dir: "in",
@@ -10208,7 +10194,7 @@ var SessionLogger = class {
10208
10194
  });
10209
10195
  }
10210
10196
  logOutput(data) {
10211
- if (this.closed) return;
10197
+ if (this.closed || this.mode !== "full") return;
10212
10198
  this.write({
10213
10199
  ts: Date.now(),
10214
10200
  dir: "out",
@@ -10217,119 +10203,40 @@ var SessionLogger = class {
10217
10203
  });
10218
10204
  }
10219
10205
  logRejected(cmd, reason, dangerous) {
10220
- if (this.closed) return;
10206
+ if (this.closed || this.mode === "off") return;
10221
10207
  this.write({ ts: Date.now(), event: "rejected", cmd, reason, dangerous });
10222
10208
  }
10223
10209
  logForced(cmd) {
10224
- if (this.closed) return;
10210
+ if (this.closed || this.mode === "off") return;
10225
10211
  this.write({ ts: Date.now(), event: "forced", cmd });
10226
10212
  }
10227
10213
  logExit(code) {
10228
- if (this.closed) return;
10214
+ if (this.closed || this.mode === "off") return;
10229
10215
  this.write({ ts: Date.now(), event: "exit", code });
10230
10216
  this.closed = true;
10231
10217
  }
10232
10218
  write(record) {
10219
+ if (!this.path) return;
10233
10220
  try {
10234
- (0, import_node_fs.appendFileSync)(this.path, JSON.stringify(record) + "\n", { encoding: "utf-8" });
10221
+ (0, import_node_fs.appendFileSync)(this.path, JSON.stringify(record) + "\n", { encoding: "utf-8", mode: 384 });
10235
10222
  } catch {
10236
10223
  }
10237
10224
  }
10238
10225
  };
10239
10226
 
10240
10227
  // src/whitelist-checker.ts
10241
- function splitCommands(line) {
10242
- const parts = [];
10243
- let current = "";
10244
- let inSingle = false;
10245
- let inDouble = false;
10246
- for (let i = 0; i < line.length; i++) {
10247
- const c = line[i];
10248
- if (c === "'" && !inDouble) {
10249
- inSingle = !inSingle;
10250
- current += c;
10251
- continue;
10252
- }
10253
- if (c === '"' && !inSingle) {
10254
- inDouble = !inDouble;
10255
- current += c;
10256
- continue;
10257
- }
10258
- if (!inSingle && !inDouble) {
10259
- const next = line[i + 1];
10260
- if (c === "&" && next === "&") {
10261
- parts.push(current);
10262
- current = "";
10263
- i++;
10264
- continue;
10265
- }
10266
- if (c === "|" && next === "|") {
10267
- parts.push(current);
10268
- current = "";
10269
- i++;
10270
- continue;
10271
- }
10272
- if (c === ";" || c === "|") {
10273
- parts.push(current);
10274
- current = "";
10275
- continue;
10276
- }
10277
- }
10278
- current += c;
10279
- }
10280
- if (current.trim()) parts.push(current);
10281
- return parts;
10282
- }
10283
- function firstToken(cmd) {
10284
- const trimmed = cmd.trim();
10285
- if (!trimmed) return null;
10286
- const tokens = trimmed.split(/\s+/);
10287
- for (const t of tokens) {
10288
- if (!t) continue;
10289
- if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(t)) continue;
10290
- return t;
10291
- }
10292
- return null;
10293
- }
10294
- function checkCommand(line, whitelist, dangerousPatterns = DANGEROUS_PATTERNS) {
10228
+ function checkCommand(line, dangerConfirm) {
10295
10229
  const trimmed = line.trim();
10296
- if (!trimmed) return { allowed: true };
10297
- if (line.includes("$(") || line.includes("`")) {
10230
+ if (!trimmed || !dangerConfirm) return { allowed: true };
10231
+ const pattern = findDangerousPattern(trimmed);
10232
+ if (pattern) {
10298
10233
  return {
10299
10234
  allowed: false,
10300
10235
  rejectedCmd: trimmed,
10301
- reason: "\u5305\u542B\u547D\u4EE4\u66FF\u6362 ($() \u6216 `)\uFF0C\u5DF2\u88AB\u7981\u6B62"
10236
+ reason: "\u5339\u914D\u5371\u9669\u547D\u4EE4\u6A21\u5F0F\uFF0C\u8BF7\u4E8C\u6B21\u786E\u8BA4",
10237
+ dangerous: true
10302
10238
  };
10303
10239
  }
10304
- if (trimmed.startsWith("(")) {
10305
- return {
10306
- allowed: false,
10307
- rejectedCmd: trimmed,
10308
- reason: "\u5305\u542B\u5B50 shell ( )\uFF0C\u5DF2\u88AB\u7981\u6B62"
10309
- };
10310
- }
10311
- for (const p of dangerousPatterns) {
10312
- if (p.test(line)) {
10313
- return {
10314
- allowed: false,
10315
- rejectedCmd: trimmed,
10316
- reason: "\u5339\u914D\u5371\u9669\u547D\u4EE4\u6A21\u5F0F",
10317
- dangerous: true
10318
- };
10319
- }
10320
- }
10321
- const segments = splitCommands(line);
10322
- for (const seg of segments) {
10323
- const cmd = firstToken(seg);
10324
- if (!cmd) continue;
10325
- if (!whitelist.has(cmd)) {
10326
- return {
10327
- allowed: false,
10328
- rejectedCmd: cmd,
10329
- reason: `"${cmd}" \u4E0D\u5728\u767D\u540D\u5355`
10330
- };
10331
- }
10332
- }
10333
10240
  return { allowed: true };
10334
10241
  }
10335
10242
 
@@ -10595,9 +10502,108 @@ async function apiPost(port, path, timeoutMs = 800) {
10595
10502
  }
10596
10503
  }
10597
10504
 
10505
+ // src/setup-callback.ts
10506
+ var import_node_http2 = require("node:http");
10507
+ var import_node_crypto = require("node:crypto");
10508
+ var BODY_LIMIT = 64 * 1024;
10509
+ function startSetupCallback(timeoutMs = 3 * 6e4) {
10510
+ return new Promise((resolveStart, rejectStart) => {
10511
+ const chan = (0, import_node_crypto.randomBytes)(8).toString("hex");
10512
+ let timer = null;
10513
+ let settled = false;
10514
+ let server = null;
10515
+ const cors = {
10516
+ "access-control-allow-origin": "*",
10517
+ "access-control-allow-methods": "POST, OPTIONS",
10518
+ "access-control-allow-headers": "content-type",
10519
+ "access-control-allow-private-network": "true",
10520
+ "access-control-max-age": "600"
10521
+ };
10522
+ const done = new Promise((resolve2, reject) => {
10523
+ timer = setTimeout(() => {
10524
+ if (settled) return;
10525
+ settled = true;
10526
+ stop();
10527
+ reject(new Error("\u7B49\u5F85\u7F51\u9875\u56DE\u8C03\u8D85\u65F6"));
10528
+ }, timeoutMs);
10529
+ function stop() {
10530
+ if (timer) clearTimeout(timer);
10531
+ server?.close();
10532
+ }
10533
+ function finish(fn) {
10534
+ if (settled) return;
10535
+ settled = true;
10536
+ if (timer) clearTimeout(timer);
10537
+ server?.close();
10538
+ fn();
10539
+ }
10540
+ const tryPort = (port) => {
10541
+ if (port > 18802) {
10542
+ rejectStart(new Error("no free port for the setup callback"));
10543
+ return;
10544
+ }
10545
+ const s = (0, import_node_http2.createServer)((req, res) => {
10546
+ const reply = (code, body) => {
10547
+ res.writeHead(code, { "content-type": "application/json", ...cors });
10548
+ res.end(JSON.stringify(body));
10549
+ };
10550
+ if (req.method === "OPTIONS") {
10551
+ reply(204, {});
10552
+ return;
10553
+ }
10554
+ if (req.method !== "POST" || !req.url?.startsWith("/setup")) {
10555
+ reply(404, { ok: false, error: "not found" });
10556
+ return;
10557
+ }
10558
+ let size = 0;
10559
+ const chunks = [];
10560
+ req.on("data", (c) => {
10561
+ size += c.length;
10562
+ if (size > BODY_LIMIT) {
10563
+ req.destroy();
10564
+ return;
10565
+ }
10566
+ chunks.push(c);
10567
+ });
10568
+ req.on("end", () => {
10569
+ try {
10570
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
10571
+ if (body.chan !== chan || !body.registrationKey) {
10572
+ reply(400, { ok: false, error: "bad request" });
10573
+ return;
10574
+ }
10575
+ reply(200, { ok: true });
10576
+ const key = body.registrationKey;
10577
+ finish(() => resolve2(key));
10578
+ } catch {
10579
+ reply(400, { ok: false, error: "bad request" });
10580
+ }
10581
+ });
10582
+ });
10583
+ s.once("error", () => {
10584
+ s.close();
10585
+ tryPort(port + 1);
10586
+ });
10587
+ s.listen(port, "127.0.0.1", () => {
10588
+ server = s;
10589
+ resolveStart({
10590
+ port,
10591
+ chan,
10592
+ done,
10593
+ stop: () => {
10594
+ finish(() => reject(new Error("callback stopped")));
10595
+ }
10596
+ });
10597
+ });
10598
+ };
10599
+ tryPort(18793);
10600
+ });
10601
+ });
10602
+ }
10603
+
10598
10604
  // src/auth.ts
10599
10605
  var import_node_child_process = require("node:child_process");
10600
- var import_node_crypto = require("node:crypto");
10606
+ var import_node_crypto2 = require("node:crypto");
10601
10607
  var import_promises2 = require("node:fs/promises");
10602
10608
  var import_node_os5 = require("node:os");
10603
10609
  var import_node_path5 = require("node:path");
@@ -10785,7 +10791,7 @@ async function ensureMachineId() {
10785
10791
  if (typeof cfg.machineId === "string" && cfg.machineId) return cfg.machineId;
10786
10792
  } catch {
10787
10793
  }
10788
- const machineId = "m_" + (0, import_node_crypto.randomBytes)(16).toString("hex");
10794
+ const machineId = "m_" + (0, import_node_crypto2.randomBytes)(16).toString("hex");
10789
10795
  await mergeLocalConfig({ machineId });
10790
10796
  return machineId;
10791
10797
  }
@@ -10816,11 +10822,16 @@ async function loadConfig() {
10816
10822
  return {};
10817
10823
  }
10818
10824
  }
10819
- function buildWhitelist(cfg) {
10820
- const set = new Set(DEFAULT_WHITELIST);
10821
- cfg.whitelist?.add?.forEach((c) => set.add(c));
10822
- cfg.whitelist?.remove?.forEach((c) => set.delete(c));
10823
- return set;
10825
+ function applyConfigDeprecations(cfg) {
10826
+ if (cfg.whitelist) {
10827
+ console.log(
10828
+ '[local] config "whitelist" \u5DF2\u5E9F\u5F03\uFF08v0.4\uFF09\uFF1Abash \u5168\u900F\u4F20\uFF0C\u4E0D\u518D\u6709\u767D\u540D\u5355\u3002\u5982\u9700\u5371\u9669\u547D\u4EE4\u4E8C\u6B21\u786E\u8BA4\uFF0C\u8BBE\u7F6E dangerConfirm: true'
10829
+ );
10830
+ }
10831
+ if (cfg.auditMode && !["full", "commands", "off"].includes(cfg.auditMode)) {
10832
+ console.log(`[local] \u65E0\u6548\u7684 auditMode "${cfg.auditMode}"\uFF0C\u56DE\u9000 full`);
10833
+ cfg.auditMode = "full";
10834
+ }
10824
10835
  }
10825
10836
  function isTTY() {
10826
10837
  return process.stdin.isTTY === true;
@@ -10890,19 +10901,18 @@ async function setupWizard(config, opts) {
10890
10901
  console.log("[local] \u5F53\u524D\u975E\u4EA4\u4E92\u7EC8\u7AEF\uFF1A\u62FF\u5230 key \u540E\u5199\u5165 ~/.cli-remote/config.json \u518D\u8FD0\u884C\u3002");
10891
10902
  return null;
10892
10903
  }
10893
- console.log(" 1) \u767B\u5F55\u8D26\u53F7\u81EA\u52A8\u83B7\u53D6\uFF08\u63A8\u8350\uFF09");
10894
- console.log(" 2) \u4ECE\u7F51\u9875\u300C\u63A7\u5236\u9762\u677F\u300D\u590D\u5236\u7C98\u8D34");
10904
+ console.log(" 1) \u901A\u8FC7\u7F51\u9875\u767B\u5F55\u83B7\u53D6\uFF08\u63A8\u8350\uFF0C\u81EA\u52A8\u5B8C\u6210\uFF0C\u65E0\u9700\u590D\u5236\u7C98\u8D34\uFF09");
10905
+ console.log(" 2) \u76F4\u63A5\u8F93\u5165 registrationKey");
10906
+ console.log(" 3) \u5728\u6B64\u8F93\u5165\u8D26\u53F7\u5BC6\u7801\u83B7\u53D6");
10895
10907
  const mode = (await readInput("\u9009\u62E9 [1]: ")).trim();
10896
10908
  if (mode === "2") {
10897
- openBrowser(base);
10898
- console.log(`[local] \u5DF2\u6253\u5F00\u6D4F\u89C8\u5668\uFF0C\u8BF7\u767B\u5F55 ${base} \u5E76\u5728\u300C\u63A7\u5236\u9762\u677F\u300D\u9875\u590D\u5236 registrationKey`);
10899
10909
  const ans = (await readInput("\u7C98\u8D34 registrationKey: ")).trim();
10900
10910
  if (!ans) {
10901
10911
  console.log("[local] \u672A\u63D0\u4F9B registrationKey\uFF0C\u9000\u51FA\u3002");
10902
10912
  return null;
10903
10913
  }
10904
10914
  registrationKey = ans;
10905
- } else {
10915
+ } else if (mode === "3") {
10906
10916
  for (; ; ) {
10907
10917
  const email = (await readInput("\u90AE\u7BB1: ")).trim();
10908
10918
  const password = (await readInput("\u5BC6\u7801: ")).trim();
@@ -10920,6 +10930,26 @@ async function setupWizard(config, opts) {
10920
10930
  console.log("[local] \u91CD\u8BD5\uFF08\u6216 Ctrl+C \u9000\u51FA\uFF09\u3002");
10921
10931
  }
10922
10932
  }
10933
+ } else {
10934
+ const cb = await startSetupCallback();
10935
+ const setupUrl = `${base}/local-setup?port=${cb.port}&chan=${cb.chan}`;
10936
+ console.log(`[local] \u5DF2\u6253\u5F00\u6D4F\u89C8\u5668\uFF1A${setupUrl}`);
10937
+ console.log("[local] \u672A\u767B\u5F55\u65F6\u4F1A\u5148\u8FDB\u5165\u767B\u5F55\u9875\uFF0C\u767B\u5F55\u540E\u81EA\u52A8\u7EE7\u7EED\uFF0C\u65E0\u9700\u56DE\u5230\u7EC8\u7AEF");
10938
+ openBrowser(setupUrl);
10939
+ try {
10940
+ registrationKey = await cb.done;
10941
+ console.log("[local] \u5DF2\u4ECE\u7F51\u9875\u83B7\u53D6 registrationKey");
10942
+ } catch {
10943
+ console.log("[local] \u672A\u6536\u5230\u7F51\u9875\u56DE\u8C03\uFF0C\u53EF\u624B\u52A8\u7C98\u8D34 registrationKey\uFF08\u56DE\u8F66\u8DF3\u8FC7\u9000\u51FA\uFF09\uFF1A");
10944
+ const ans = (await readInput("registrationKey: ")).trim();
10945
+ if (!ans) {
10946
+ cb.stop();
10947
+ console.log("[local] \u672A\u63D0\u4F9B registrationKey\uFF0C\u9000\u51FA\u3002");
10948
+ return null;
10949
+ }
10950
+ registrationKey = ans;
10951
+ }
10952
+ cb.stop();
10923
10953
  }
10924
10954
  changed = true;
10925
10955
  }
@@ -10940,7 +10970,10 @@ async function startLocal(opts = {}) {
10940
10970
  registrationKey = setup.registrationKey;
10941
10971
  }
10942
10972
  const baseUrl = baseUrlFromWsUrl(url);
10943
- const whitelist = buildWhitelist(config);
10973
+ applyConfigDeprecations(config);
10974
+ const dangerConfirm = config.dangerConfirm === true;
10975
+ const auditMode = config.auditMode ?? "full";
10976
+ const retentionDays = config.auditRetentionDays ?? AUDIT_RETENTION_DAYS;
10944
10977
  const pty = new PtyManager();
10945
10978
  const loggers = /* @__PURE__ */ new Map();
10946
10979
  const lineBuffers = /* @__PURE__ */ new Map();
@@ -10984,8 +11017,15 @@ async function startLocal(opts = {}) {
10984
11017
  const commands = detectCommands();
10985
11018
  console.log(`[local] connecting to ${url}`);
10986
11019
  console.log(`[local] deviceId=${credentials.deviceId || "(\u672A\u6388\u6743)"}`);
10987
- console.log(`[local] whitelist: ${whitelist.size} commands`);
11020
+ console.log(`[local] bash: passthrough (audit: ${auditMode}, retention: ${retentionDays === 0 ? "forever" : retentionDays + "d"}${dangerConfirm ? ", dangerConfirm: on" : ""})`);
10988
11021
  console.log(`[local] available CLIs: ${commands.join(", ")}`);
11022
+ const pruned = pruneAuditLogs(retentionDays);
11023
+ if (pruned > 0) console.log(`[local] pruned ${pruned} expired audit log dir(s)`);
11024
+ const pruneTimer = setInterval(() => {
11025
+ const n = pruneAuditLogs(retentionDays);
11026
+ if (n > 0) console.log(`[local] pruned ${n} expired audit log dir(s)`);
11027
+ }, 24 * 60 * 6e4);
11028
+ pruneTimer.unref?.();
10989
11029
  let phase = "starting";
10990
11030
  let phaseReason = "";
10991
11031
  let reconnectAttempts = 0;
@@ -11236,7 +11276,7 @@ async function startLocal(opts = {}) {
11236
11276
  function handleSpawn(frame) {
11237
11277
  const sessionId = frame.sessionId;
11238
11278
  try {
11239
- const logger = new SessionLogger(sessionId, frame.cmd);
11279
+ const logger = new SessionLogger(sessionId, frame.cmd, auditMode);
11240
11280
  pty.spawn(
11241
11281
  sessionId,
11242
11282
  { cmd: frame.cmd, cols: frame.cols, rows: frame.rows, cwd: frame.cwd },
@@ -11314,7 +11354,7 @@ async function startLocal(opts = {}) {
11314
11354
  if (char === "\r" || char === "\n") {
11315
11355
  const line = buf;
11316
11356
  buf = "";
11317
- const result = checkCommand(line, whitelist);
11357
+ const result = checkCommand(line, dangerConfirm);
11318
11358
  if (result.allowed) {
11319
11359
  ptyWriteChar(sessionId, "\r");
11320
11360
  logger.logInput(new TextEncoder().encode(line + "\r"));
@@ -11518,11 +11558,28 @@ function resolveHere() {
11518
11558
  })();
11519
11559
  var argUrl = process.argv[2];
11520
11560
  var argMatch = argUrl && (argUrl.startsWith("ws://") || argUrl.startsWith("wss://"));
11561
+ function readVersion() {
11562
+ if (true) return "0.3.1";
11563
+ try {
11564
+ const pkg = JSON.parse(
11565
+ (0, import_node_fs5.readFileSync)(new URL("../package.json", import_meta.url), "utf-8")
11566
+ );
11567
+ return pkg.version ?? "unknown";
11568
+ } catch {
11569
+ return "unknown";
11570
+ }
11571
+ }
11572
+ if (argUrl === "-v" || argUrl === "--version" || argUrl === "version") {
11573
+ console.log(`cli-local ${readVersion()}`);
11574
+ console.log(`node ${process.version} (${process.platform} ${process.arch})`);
11575
+ process.exit(0);
11576
+ }
11521
11577
  if (argUrl && !argMatch && (argUrl === "-h" || argUrl === "--help" || argUrl === "help")) {
11522
11578
  console.log(`cli-local \u2014 local PTY agent for cli-remote (gateway \u5E38\u9A7B\u670D\u52A1)
11523
11579
 
11524
11580
  usage:
11525
11581
  cli-local \u65E0 gateway \u4E14\u5DF2\u914D\u7F6E\u65F6\u524D\u53F0\u8FD0\u884C\uFF1B\u5426\u5219\u663E\u793A\u72B6\u6001
11582
+ cli-local -v | --version \u67E5\u770B\u7248\u672C
11526
11583
  cli-local pair \u91CD\u65B0\u6388\u6743\uFF08\u540A\u9500/\u8FC7\u671F\u540E\u91CD\u914D\u5BF9\uFF1Bgateway \u70ED\u52A0\u8F7D\uFF09
11527
11584
  cli-local gateway start \u540E\u53F0\u542F\u52A8\u5E38\u9A7B gateway\uFF08\u63A8\u8350\uFF09
11528
11585
  cli-local gateway stop|restart|status|logs [-f] [-n N]
@@ -11841,7 +11898,7 @@ if (argUrl === "gateway") {
11841
11898
  const up = Math.floor(st.uptimeMs / 1e3);
11842
11899
  const upStr = up > 3600 ? `${Math.floor(up / 3600)}h${Math.floor(up % 3600 / 60)}m` : up > 60 ? `${Math.floor(up / 60)}m${up % 60}s` : `${up}s`;
11843
11900
  const exp = st.accessTokenExpiresAt ? new Date(st.accessTokenExpiresAt).toLocaleTimeString() : "\u2014";
11844
- console.log(`[gateway] running (pid ${gatewayPid() ?? "?"}, api :${port})`);
11901
+ console.log(`[gateway] running (pid ${gatewayPid() ?? "?"}, api :${port}) \u2014 cli-local ${readVersion()}`);
11845
11902
  console.log(`[gateway] \u72B6\u6001: ${phaseLabel[st.phase] ?? st.phase}${st.reason ? ` (${st.reason})` : ""}`);
11846
11903
  console.log(`[gateway] hub: ${st.hubUrl}`);
11847
11904
  console.log(`[gateway] deviceId: ${st.deviceId ?? "\u2014"}`);
@@ -11863,7 +11920,7 @@ if (argUrl === "gateway") {
11863
11920
  hub = cfg.relayUrl ?? hub;
11864
11921
  } catch {
11865
11922
  }
11866
- console.log(`[gateway] running (pid ${pid}) \u2014 \u63A7\u5236API\u672A\u54CD\u5E94\uFF08\u65E7\u7248\u672C\uFF1F\uFF09`);
11923
+ console.log(`[gateway] running (pid ${pid}) \u2014 cli-local ${readVersion()}\uFF08\u63A7\u5236API\u672A\u54CD\u5E94\uFF0C\u65E7\u7248\u672C\uFF1F\uFF09`);
11867
11924
  console.log(`[gateway] hub: ${hub}`);
11868
11925
  console.log(`[gateway] \u65E5\u5FD7: ${gatewayLogFile()}`);
11869
11926
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cli-remote/local",
3
- "version": "0.1.1",
3
+ "version": "0.3.1",
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": {
@@ -26,7 +26,7 @@
26
26
  ],
27
27
  "scripts": {
28
28
  "dev": "tsx watch src/index.ts",
29
- "build": "esbuild src/index.ts --bundle --platform=node --format=cjs --outfile=dist/index.cjs --external:node-pty --banner:js='#!/usr/bin/env node'",
29
+ "build": "node scripts/build.mjs",
30
30
  "typecheck": "tsc --noEmit",
31
31
  "prepublishOnly": "pnpm build"
32
32
  },