@landh93/web-codex-client 0.1.0-rc.1 → 0.1.0-rc.2

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.
package/NPM_RELEASE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # npm 客户端发布与安装
2
2
 
3
- 包名:`@landh93/web-codex-client`。首版为 `0.1.0-rc.1`,使用 `next` 标签。仅支持 Linux / Node.js 24;这是独立安装候选版,不代表公网、多人或真实模型执行已完成生产验收。`remote-wss-v1` 明确允许远端 WSS,继续使用固定 Noise 加密、TLS 校验及本机指纹授权,不新增任何加密实现。项目尚未选定开源许可证,包暂保留 `UNLICENSED`;第三方代码许可见包内 `sdk/THIRD_PARTY_NOTICES.md`。
3
+ 包名:`@landh93/web-codex-client`。当前候选版为 `0.1.0-rc.2`,使用 `next` 标签。仅支持 Linux / Node.js 24;这是独立安装候选版,不代表公网、多人或真实模型执行已完成生产验收。`remote-wss-v1` 明确允许远端 WSS,继续使用固定 Noise 加密、TLS 校验及本机指纹授权,不新增任何加密实现。项目尚未选定开源许可证,包暂保留 `UNLICENSED`;第三方代码许可见包内 `sdk/THIRD_PARTY_NOTICES.md`。
4
4
 
5
5
  ## 用户安装
6
6
 
@@ -25,7 +25,17 @@ web-codex-client configure --relay wss://relay.example.com --token-stdin < /path
25
25
 
26
26
  使用公开 CA 证书时通常无需额外设置;私有 CA 可在启动 Node 前设置 `NODE_EXTRA_CA_CERTS=/path/to/ca.pem`。禁止 `NODE_TLS_REJECT_UNAUTHORIZED=0`。Nginx 要正确代理 `/ws/v1/client`,token 经 Authorization 头发送。
27
27
 
28
- Client 运行后,在另一终端执行 `web-codex-client pair-open`、`pair-list`,核对浏览器的完整指纹,再用 `pair-approve --fingerprint FULL_HASH --root /absolute/project` 授权。默认只读,需要执行任务时才加 `--write`。这些命令自动使用配置中的 stateDir;旧的 `--state` 用法仍有效。
28
+ Client 运行后,在网页选择设备,然后在另一终端执行 **`web-codex-client pair`**。对照终端与网页的完整指纹,在网页点击“与本机显示一致”,再在终端输入 `yes`。无需提前执行 `pair-open`,无需复制 64 位指纹或逐个配置已有项目目录。
29
+
30
+ 核对并信任浏览器后,从本机 Codex 项目目录及活动/归档历史的 cwd 识别已有项目,自动授予这些目录的读写权限;扫描有分页、去重和数量上限,不会把浏览器任意指定的路径当作已有项目。系统根目录、用户家目录以及包含 Client 状态的父目录不自动授予。没有识别到项目时明确报错,可以使用 `pair --root /absolute/project` 在终端明确确认目录读写授权。
31
+
32
+ 新增目录执行 **`web-codex-client authorize --root /absolute/directory`**:已有 Codex 项目直接授权读写,其他目录要求终端输入 `yes`。若有多个活动浏览器,使用 `--peer` 指定 `status` 中的浏览器公钥。权限按目录检查,新增写权限不会提升其他只读目录。Codex 执行任务仍使用工作区沙箱、操作审批及写租约;网页中的“申请控制权”管理同一对话的写入归属。
33
+
34
+ 自动授权是配对时的目录快照。配对后新建的 Codex 项目可使用上述 `authorize` 命令加入,无需重新配对。升级不会擅自提升已有只读授权。
35
+
36
+ 旧的 `pair-approve --fingerprint FULL_HASH`、`pair-list`、`--state` 用法仍有效。`pair-approve` 不传 `--root` 时采用已有项目;指定已有项目且不传权限参数时自动读写;`--read-only` 明确保持只读;未知目录可通过 `pair --root` 交互确认,或通过显式 `pair-approve --root PATH --write` 授权。重复批准同一已授权指纹返回成功,不会因为窗口已关闭而失败。
37
+
38
+ 配对等待最多 5 分钟,期间只能核对身份,不能读取内容。`pair-list` 返回网页是否确认、剩余时间和可否批准;本机错误响应区分指纹不存在、网页未确认、路由过期、目录不可访问、授权保存失败和授权数量上限,且不包含底层异常正文。网页刷新或重新连接后必须使用新的指纹。旧 Web 的 2 分钟等待仍可能先到期;更新 Server 前端后会显示新指引并使用 5 分钟等待。协议及固定 SDK 版本保持兼容。
29
39
 
30
40
  后台常驻可使用 systemd 用户服务,将 `ExecStart` 设为实际安装的 `web-codex-client run --config /absolute/config.json`。npm 安装不会自动启用服务或打开配对窗口。
31
41
 
@@ -65,7 +75,7 @@ npm whoami --registry=https://registry.npmjs.org/ --userconfig="$HOME/.config/we
65
75
  随后发布已通过验证的**具体 tgz**,不重新构建另一个包:
66
76
 
67
77
  ```bash
68
- npm publish .test-output/landh93-web-codex-client-0.1.0-rc.1.tgz \
78
+ npm publish .test-output/landh93-web-codex-client-0.1.0-rc.2.tgz \
69
79
  --access public --tag next --ignore-scripts \
70
80
  --registry=https://registry.npmjs.org/ \
71
81
  --userconfig="$HOME/.config/web-codex-npm/npmrc"
package/README.md CHANGED
@@ -19,7 +19,7 @@ web-codex-client run
19
19
 
20
20
  `configure` 交互输入 WSS 地址、Codex socket 和隐藏的设备 token,保存在用户配置目录。`doctor` 检查私有配置、WASM、WSS 认证和 Codex 初始化,失败退出非零;既有 Client 在线时不会抢占它的连接。支持 `--quiet`。安装和升级不会重启 Codex,也不会自动批准浏览器。
21
21
 
22
- 另开终端运行 `web-codex-client pair-open`,浏览器连接后用 `pair-list` 核对完整指纹,再 `pair-approve --fingerprint FULL_HASH --root /absolute/project`。配置、身份和授权独立于 npm 安装目录,升级时保留。完整安装、token 标准输入、证书、后台运行与发布说明见 [NPM_RELEASE.md](./NPM_RELEASE.md)。
22
+ 网页选择设备后,另开终端运行 `web-codex-client pair`,核对完整指纹并在网页和终端确认。无需提前开启窗口;Codex 已有项目自动授予读写,新目录用 `web-codex-client authorize --root /absolute/path` 在本机确认。配置、身份和授权独立于 npm 安装目录,升级时保留。完整安装、token 标准输入、证书、后台运行与发布说明见 [NPM_RELEASE.md](./NPM_RELEASE.md)。
23
23
 
24
24
  ## 安装、构建、自测
25
25
 
@@ -64,7 +64,7 @@ node dist/cli.js run --config ~/.config/web-codex-client/config.json
64
64
 
65
65
  ```bash
66
66
  node dist/cli.js status --state ~/.config/web-codex-client/state
67
- node dist/cli.js pair-open --state ~/.config/web-codex-client/state
67
+ node dist/cli.js pair --config ~/.config/web-codex-client/config.json
68
68
  node dist/cli.js pair-list --state ~/.config/web-codex-client/state
69
69
  # 浏览器完成握手后,与本地 pair-list 核对完整的 64 位指纹。
70
70
  # 在可信浏览器点击确认,再在本机明确授予目录权限:
@@ -75,7 +75,7 @@ node dist/cli.js revoke --state ~/.config/web-codex-client/state \
75
75
  --peer BROWSER_PUBLIC_KEY_FROM_PAIR_LIST
76
76
  ```
77
77
 
78
- 配对窗口 120 秒,本机确认还要求浏览器已确认相同指纹。默认只读;`--write` 才允许执行和租约。撤销立即关闭该身份的会话并拒绝后续业务。浏览器身份默认仅存内存,刷新后重新配对;同一页面断线重连可以继续使用内存身份和固定公钥。
78
+ rc.2 配对等待 5 分钟,本机确认还要求浏览器已确认相同指纹;不再强制先执行 `pair-open`。默认对识别到的 Codex 项目授予读写,其他目录需要显式本机授权;`--read-only` 可指定只读。`pair-list` 显示剩余时间,失败返回白名单错误码和处理提示。撤销立即关闭该身份的会话并拒绝后续业务。浏览器身份默认仅存内存,刷新后重新配对;同一页面断线重连可以继续使用内存身份和固定公钥。
79
79
 
80
80
  `status` 分别返回 relayOnline、handshakes、authorizedSessions、codexAvailable。Codex 连接按第一次已授权请求惰性建立,因此仅中继上线时 codexAvailable 仍为 false。上游断线后下一次请求重连,旧 epoch/订阅失效。后台服务无有限总工作量,不显示伪造百分比。
81
81
 
package/dist/cli.js CHANGED
@@ -1356,15 +1356,18 @@ async function loadState(dir) {
1356
1356
  list = z4.array(
1357
1357
  z4.strictObject({
1358
1358
  peer: z4.string(),
1359
- roots: z4.array(z4.string()).min(1).max(32),
1360
- write: z4.boolean()
1359
+ roots: z4.array(z4.string()).min(1).max(256),
1360
+ write: z4.boolean(),
1361
+ writeRoots: z4.array(z4.string()).max(256).optional()
1361
1362
  })
1362
1363
  ).max(256).parse(JSON.parse(await privateRead(dir + "/grants.json")));
1363
1364
  } catch (e) {
1364
1365
  if (e.code !== "ENOENT") throw e;
1365
1366
  }
1366
- for (const g of list)
1367
+ for (const g of list) {
1367
1368
  g.roots = await Promise.all(g.roots.map((r) => realpath(r)));
1369
+ if (g.writeRoots) g.writeRoots = await Promise.all(g.writeRoots.map((r) => realpath(r)));
1370
+ }
1368
1371
  return { identity, grants: new Map(list.map((g) => [g.peer, g])) };
1369
1372
  }
1370
1373
 
@@ -1374,7 +1377,7 @@ import {
1374
1377
  createServer,
1375
1378
  createConnection
1376
1379
  } from "node:net";
1377
- import { chmod, realpath as realpath3, unlink, lstat as lstat2 } from "node:fs/promises";
1380
+ import { chmod, realpath as realpath4, unlink, lstat as lstat2, stat as stat3 } from "node:fs/promises";
1378
1381
  import { z as z5 } from "zod";
1379
1382
 
1380
1383
  // src/bridge.ts
@@ -1772,6 +1775,11 @@ var Bridge = class {
1772
1775
  if (!grant) throw new BridgeError("NOT_PAIRED");
1773
1776
  return grant;
1774
1777
  }
1778
+ async writable(owner, path) {
1779
+ const grant = this.grant(owner);
1780
+ if (!grant.write) throw new BridgeError("FORBIDDEN", "\u76EE\u5F55\u5C1A\u672A\u5141\u8BB8\u5199\u5165\uFF0C\u8BF7\u5728\u672C\u673A\u8FD0\u884C web-codex-client authorize --root \u76EE\u5F55\u3002");
1781
+ await authorizedPath(grant.writeRoots ?? grant.roots, path);
1782
+ }
1775
1783
  async project(owner, id2) {
1776
1784
  const g = this.grant(owner);
1777
1785
  if (id2.startsWith("cwd:")) {
@@ -1979,7 +1987,10 @@ var Bridge = class {
1979
1987
  historyFallback: "bounded",
1980
1988
  activeTakeover: "unverified"
1981
1989
  };
1982
- if ("threadId" in p) await this.thread(owner, p.threadId);
1990
+ if ("threadId" in p) {
1991
+ const thread2 = await this.thread(owner, p.threadId);
1992
+ if (writes.has(method) || method === "leases.acquire") await this.writable(owner, thread2.cwd);
1993
+ }
1983
1994
  if (writes.has(method) && "threadId" in p) {
1984
1995
  this.leases.assert(p.threadId, owner);
1985
1996
  if ((method === "turns.start" || method === "turns.interrupt") && p.connectionEpoch !== this.subscriptions.epoch)
@@ -2124,6 +2135,7 @@ var Bridge = class {
2124
2135
  }
2125
2136
  case "threads.start": {
2126
2137
  const project = await this.project(owner, p.projectId);
2138
+ await this.writable(owner, project.cwd);
2127
2139
  const r = await this.upstream.call("thread/start", {
2128
2140
  cwd: project.cwd,
2129
2141
  sandbox: "workspace-write",
@@ -2237,7 +2249,8 @@ var Bridge = class {
2237
2249
  throw new BridgeError("APPROVAL_EXPIRED");
2238
2250
  if (!g.write) throw new BridgeError("FORBIDDEN");
2239
2251
  this.leases.assert(a.thread, owner);
2240
- await this.thread(owner, a.thread);
2252
+ const thread2 = await this.thread(owner, a.thread);
2253
+ await this.writable(owner, thread2.cwd);
2241
2254
  if (this.approvals.get(id2) !== a) throw new BridgeError("APPROVAL_EXPIRED");
2242
2255
  this.grant(owner);
2243
2256
  this.leases.assert(a.thread, owner);
@@ -2296,7 +2309,83 @@ var Bridge = class {
2296
2309
  }
2297
2310
  };
2298
2311
 
2312
+ // src/admin-errors.ts
2313
+ var adminMessages = {
2314
+ FINGERPRINT_NOT_FOUND: "\u5F53\u524D\u6307\u7EB9\u4E0D\u5B58\u5728\u3002\u8BF7\u5728\u7F51\u9875\u91CD\u65B0\u8FDE\u63A5\uFF0C\u518D\u8FD0\u884C web-codex-client pair\u3002",
2315
+ BROWSER_NOT_CONFIRMED: "\u8BF7\u5148\u5728\u7F51\u9875\u70B9\u51FB\u201C\u4E0E\u672C\u673A\u663E\u793A\u4E00\u81F4\u201D\uFF0C\u7136\u540E\u91CD\u8BD5\u3002",
2316
+ PAIRING_ROUTE_EXPIRED: "\u6B64\u914D\u5BF9\u5DF2\u8FC7\u671F\u3002\u8BF7\u5728\u7F51\u9875\u91CD\u65B0\u8FDE\u63A5\u5E76\u6838\u5BF9\u65B0\u6307\u7EB9\u3002",
2317
+ PAIRING_WINDOW_EXPIRED: "\u914D\u5BF9\u7A97\u53E3\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u8FDE\u63A5\u7F51\u9875\u3002",
2318
+ DIRECTORY_UNAVAILABLE: "\u76EE\u5F55\u4E0D\u5B58\u5728\u3001\u4E0D\u662F\u76EE\u5F55\u6216\u5F53\u524D\u7528\u6237\u65E0\u6CD5\u8BBF\u95EE\u3002",
2319
+ GRANT_SAVE_FAILED: "\u6388\u6743\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u72B6\u6001\u76EE\u5F55\u6743\u9650\u548C\u78C1\u76D8\u7A7A\u95F4\u3002",
2320
+ GRANT_LIMIT: "\u6388\u6743\u6570\u91CF\u5DF2\u8FBE\u4E0A\u9650\uFF0C\u8BF7\u5148\u64A4\u9500\u4E0D\u518D\u4F7F\u7528\u7684\u6D4F\u89C8\u5668\u3002",
2321
+ CODEX_UNAVAILABLE: "\u65E0\u6CD5\u8BFB\u53D6\u672C\u673A Codex \u9879\u76EE\uFF0C\u8BF7\u8FD0\u884C web-codex-client doctor\u3002",
2322
+ PROJECT_DISCOVERY_LIMIT: "\u9879\u76EE\u76EE\u5F55\u626B\u63CF\u8FBE\u5230\u4E0A\u9650\uFF0C\u8BF7\u7528 --root \u660E\u786E\u6307\u5B9A\u9879\u76EE\u3002",
2323
+ NO_KNOWN_PROJECTS: "\u6CA1\u6709\u627E\u5230\u53EF\u81EA\u52A8\u6388\u6743\u7684 Codex \u9879\u76EE\u3002\u8BF7\u7528 --root \u6307\u5B9A\u76EE\u5F55\u5E76\u786E\u8BA4\u6388\u6743\u3002",
2324
+ DIRECTORY_APPROVAL_REQUIRED: "\u8BE5\u76EE\u5F55\u4E0D\u662F\u5DF2\u8BC6\u522B\u7684 Codex \u9879\u76EE\uFF1B\u9996\u6B21\u914D\u5BF9\u7528 pair --root \u8DEF\u5F84\uFF0C\u5DF2\u914D\u5BF9\u7528 authorize --root \u8DEF\u5F84\uFF0C\u5728\u672C\u673A\u786E\u8BA4\u3002",
2325
+ PEER_REQUIRED: "\u5B58\u5728\u591A\u4E2A\u6D4F\u89C8\u5668\uFF0C\u8BF7\u7528 --peer \u6307\u5B9A pair-list \u6216 status \u4E2D\u7684\u6D4F\u89C8\u5668\u3002",
2326
+ NOT_PAIRED: "\u5C1A\u672A\u4FE1\u4EFB\u6D4F\u89C8\u5668\uFF0C\u8BF7\u5148\u5728\u7F51\u9875\u8FDE\u63A5\u5E76\u8FD0\u884C web-codex-client pair\u3002",
2327
+ INVALID_ADMIN_REQUEST: "\u547D\u4EE4\u53C2\u6570\u65E0\u6548\uFF0C\u8BF7\u67E5\u770B web-codex-client --help\u3002",
2328
+ ADMIN_REQUEST_REJECTED: "\u672C\u673A\u63A7\u5236\u8BF7\u6C42\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5 Client \u72B6\u6001\u3002"
2329
+ };
2330
+ var AdminError = class extends Error {
2331
+ constructor(code) {
2332
+ super(adminMessages[code]);
2333
+ this.code = code;
2334
+ }
2335
+ };
2336
+ function adminFailure(error) {
2337
+ const code = error instanceof AdminError ? error.code : error instanceof Error && error.name === "ZodError" || error instanceof SyntaxError ? "INVALID_ADMIN_REQUEST" : "ADMIN_REQUEST_REJECTED";
2338
+ return { error: code, message: adminMessages[code] };
2339
+ }
2340
+
2341
+ // src/known-projects.ts
2342
+ import { realpath as realpath3, stat as stat2 } from "node:fs/promises";
2343
+ import { homedir as homedir2 } from "node:os";
2344
+ import { isAbsolute as isAbsolute2 } from "node:path";
2345
+ async function knownProjects(upstream, stateDir) {
2346
+ const roots = /* @__PURE__ */ new Set();
2347
+ async function add(value) {
2348
+ if (typeof value !== "string" || !isAbsolute2(value)) return;
2349
+ try {
2350
+ const root = await realpath3(value);
2351
+ if (!(await stat2(root)).isDirectory() || root === "/" || root === homedir2() || within(root, stateDir)) return;
2352
+ roots.add(root);
2353
+ if (roots.size > 256) throw new AdminError("PROJECT_DISCOVERY_LIMIT");
2354
+ } catch (error) {
2355
+ if (error instanceof AdminError) throw error;
2356
+ }
2357
+ }
2358
+ async function scan(method, extra, consume) {
2359
+ let cursor;
2360
+ const seen = /* @__PURE__ */ new Set();
2361
+ for (let page2 = 0; page2 < 100; page2++) {
2362
+ const result = await upstream.call(method, { ...extra, limit: 100, cursor });
2363
+ if (!Array.isArray(result.data) || result.data.length > 100) throw new AdminError("CODEX_UNAVAILABLE");
2364
+ for (const row of result.data) await consume(row);
2365
+ if (!result.nextCursor) return;
2366
+ if (typeof result.nextCursor !== "string" || seen.has(result.nextCursor)) throw new AdminError("PROJECT_DISCOVERY_LIMIT");
2367
+ cursor = result.nextCursor;
2368
+ seen.add(result.nextCursor);
2369
+ }
2370
+ throw new AdminError("PROJECT_DISCOVERY_LIMIT");
2371
+ }
2372
+ try {
2373
+ await scan("project/list", {}, async (row) => {
2374
+ for (const root of row.roots ?? []) await add(root.path);
2375
+ });
2376
+ } catch (error) {
2377
+ if (!(error instanceof RpcError && error.code === -32601)) throw error;
2378
+ }
2379
+ for (const archived of [false, true]) {
2380
+ await scan("thread/list", { useStateDbOnly: true, archived, sourceKinds: ["cli", "vscode", "exec", "appServer", "subAgent", "subAgentReview", "subAgentCompact", "subAgentThreadSpawn", "subAgentOther", "unknown"] }, async (row) => {
2381
+ await add(row.cwd);
2382
+ });
2383
+ }
2384
+ return [...roots].sort();
2385
+ }
2386
+
2299
2387
  // src/daemon.ts
2388
+ var PAIRING_MS = 5 * 60 * 1e3;
2300
2389
  var ClientDaemon = class {
2301
2390
  constructor(config, identity, grants, connectUpstream = () => CodexRpc.connect(config.upstream)) {
2302
2391
  this.config = config;
@@ -2310,7 +2399,10 @@ var ClientDaemon = class {
2310
2399
  stopped = false;
2311
2400
  routes = /* @__PURE__ */ new Map();
2312
2401
  pairUntil = 0;
2402
+ lastRouteClosure;
2403
+ expiredFingerprints = /* @__PURE__ */ new Set();
2313
2404
  admin;
2405
+ adminQueue = Promise.resolve();
2314
2406
  timer;
2315
2407
  retry;
2316
2408
  attempts = 0;
@@ -2333,7 +2425,7 @@ var ClientDaemon = class {
2333
2425
  }
2334
2426
  }
2335
2427
  for (const [id2, r] of this.routes)
2336
- if (Date.now() > r.expires && !r.owner) this.closeRoute(id2);
2428
+ if (Date.now() > r.expires && !r.owner) this.closeRoute(id2, true, "PAIRING_ROUTE_EXPIRED");
2337
2429
  }, 1e4);
2338
2430
  this.timer.unref();
2339
2431
  }
@@ -2406,7 +2498,7 @@ var ClientDaemon = class {
2406
2498
  }
2407
2499
  this.routes.set(m.routeId, {
2408
2500
  confirmed: false,
2409
- expires: Date.now() + 12e4,
2501
+ expires: Date.now() + PAIRING_MS,
2410
2502
  queue: Promise.resolve(),
2411
2503
  pending: 0,
2412
2504
  out: [],
@@ -2416,7 +2508,7 @@ var ClientDaemon = class {
2416
2508
  return;
2417
2509
  }
2418
2510
  if (m.type === "route.closed") {
2419
- this.closeRoute(m.routeId, false);
2511
+ this.closeRoute(m.routeId, false, "RELAY_CLOSED");
2420
2512
  return;
2421
2513
  }
2422
2514
  if (m.type !== "secure") return;
@@ -2457,8 +2549,6 @@ var ClientDaemon = class {
2457
2549
  fingerprint: r.session.fingerprint
2458
2550
  });
2459
2551
  } else {
2460
- if (Date.now() > this.pairUntil)
2461
- throw new BridgeError("NOT_PAIRED");
2462
2552
  this.send(frame.routeId, {
2463
2553
  kind: "pairing",
2464
2554
  status: "pending",
@@ -2478,10 +2568,18 @@ var ClientDaemon = class {
2478
2568
  }
2479
2569
  if (!r.owner || !this.grants.has(r.owner))
2480
2570
  throw new BridgeError("NOT_PAIRED");
2481
- await this.ensureUpstream();
2571
+ try {
2572
+ await this.ensureUpstream();
2573
+ } catch {
2574
+ if (message.kind === "request") {
2575
+ this.send(frame.routeId, { kind: "response", requestId: message.requestId, error: { code: "UPSTREAM_UNAVAILABLE", message: "\u672C\u673A Codex \u6682\u4E0D\u53EF\u7528\uFF0C\u8BF7\u8FD0\u884C web-codex-client doctor \u540E\u91CD\u8BD5\u3002", retryable: true } });
2576
+ return;
2577
+ }
2578
+ throw new BridgeError("UPSTREAM_UNAVAILABLE");
2579
+ }
2482
2580
  const response = await this.bridge.handle(r.owner, message);
2483
2581
  if (response) this.send(frame.routeId, response);
2484
- }).catch(() => this.closeRoute(frame.routeId)).finally(() => r.pending--);
2582
+ }).catch((error) => this.closeRoute(frame.routeId, true, error instanceof BridgeError ? error.code : "PROTOCOL_ERROR")).finally(() => r.pending--);
2485
2583
  }
2486
2584
  send(id2, message) {
2487
2585
  const r = this.routes.get(id2);
@@ -2518,9 +2616,14 @@ var ClientDaemon = class {
2518
2616
  r.sending = false;
2519
2617
  }
2520
2618
  }
2521
- closeRoute(id2, notify = true) {
2619
+ closeRoute(id2, notify = true, reason = "CLOSED") {
2522
2620
  const r = this.routes.get(id2);
2523
2621
  if (!r) return;
2622
+ this.lastRouteClosure = { at: Date.now(), reason };
2623
+ if (reason === "PAIRING_ROUTE_EXPIRED" && r.session?.fingerprint) {
2624
+ if (this.expiredFingerprints.size >= 64) this.expiredFingerprints.delete(this.expiredFingerprints.values().next().value);
2625
+ this.expiredFingerprints.add(r.session.fingerprint);
2626
+ }
2524
2627
  this.routes.delete(id2);
2525
2628
  r.session?.close();
2526
2629
  r.out = [];
@@ -2541,50 +2644,65 @@ var ClientDaemon = class {
2541
2644
  authorizedSessions: Array.from(this.routes.values()).filter(
2542
2645
  (r) => r.owner
2543
2646
  ).length,
2544
- pairingUntil: this.pairUntil
2647
+ pairingUntil: this.pairUntil,
2648
+ pairingMode: "local-confirmation",
2649
+ pairingRemainingMs: Math.max(0, this.pairUntil - Date.now()),
2650
+ lastRouteClosure: this.lastRouteClosure,
2651
+ grants: Array.from(this.grants.values()).map((g) => ({ peer: g.peer, rootCount: g.roots.length, write: g.write }))
2545
2652
  };
2546
2653
  case "pair-open":
2547
- this.pairUntil = Date.now() + 12e4;
2654
+ this.pairUntil = Date.now() + PAIRING_MS;
2655
+ for (const r of this.routes.values()) if (!r.owner) r.expires = this.pairUntil;
2548
2656
  return { expiresAt: this.pairUntil };
2549
2657
  case "pair-list":
2550
2658
  return {
2659
+ pairingMode: "local-confirmation",
2660
+ windowRequired: false,
2661
+ pairingUntil: this.pairUntil,
2662
+ pairingRemainingMs: Math.max(0, this.pairUntil - Date.now()),
2551
2663
  pending: Array.from(this.routes.entries()).filter(([, r]) => r.session?.ready && !r.owner).map(([routeId, r]) => ({
2552
2664
  routeId,
2553
2665
  fingerprint: r.session.fingerprint,
2554
2666
  peer: r.session.peerPublicKey,
2555
2667
  browserConfirmed: r.confirmed,
2556
- expiresAt: r.expires
2668
+ expiresAt: r.expires,
2669
+ remainingMs: Math.max(0, r.expires - Date.now()),
2670
+ canApprove: r.confirmed && Date.now() <= r.expires
2557
2671
  }))
2558
2672
  };
2559
2673
  case "pair-approve": {
2560
2674
  const p = z5.object({
2561
2675
  fingerprint: z5.string().regex(/^[a-f0-9]{64}$/),
2562
- roots: z5.array(z5.string()).min(1).max(32),
2563
- write: z5.boolean().default(false)
2676
+ roots: z5.array(z5.string()).min(1).max(32).optional(),
2677
+ write: z5.boolean().optional(),
2678
+ allowUnknown: z5.boolean().default(false)
2564
2679
  }).parse(m);
2565
2680
  const found = Array.from(this.routes.entries()).find(
2566
2681
  ([, r2]) => r2.session?.fingerprint === p.fingerprint
2567
2682
  );
2568
- if (!found || !found[1].confirmed || Date.now() > this.pairUntil || Date.now() > found[1].expires)
2569
- throw Error("PAIRING_EXPIRED_OR_UNCONFIRMED");
2570
- if (this.grants.size >= 256) throw Error("GRANT_LIMIT");
2683
+ if (!found) throw new AdminError(this.expiredFingerprints.has(p.fingerprint) ? "PAIRING_ROUTE_EXPIRED" : "FINGERPRINT_NOT_FOUND");
2571
2684
  const [id2, r] = found;
2685
+ if (r.owner) return { peer: r.owner, approved: true, alreadyApproved: true };
2686
+ if (Date.now() > r.expires) throw new AdminError("PAIRING_ROUTE_EXPIRED");
2687
+ if (!r.confirmed) throw new AdminError("BROWSER_NOT_CONFIRMED");
2688
+ if (this.grants.size >= 256) throw new AdminError("GRANT_LIMIT");
2572
2689
  const peer = r.session.peerPublicKey;
2690
+ const selectedRoots = p.roots ? await this.canonicalRoots(p.roots) : void 0;
2691
+ let known = [];
2692
+ if (p.write === void 0 || !p.roots) known = await this.discoverProjects();
2693
+ const roots = selectedRoots ?? known;
2694
+ if (!roots.length) throw new AdminError("NO_KNOWN_PROJECTS");
2695
+ const unknown = roots.filter((root) => !known.includes(root));
2696
+ if (unknown.length && p.write === void 0 && !p.allowUnknown) throw new AdminError("DIRECTORY_APPROVAL_REQUIRED");
2697
+ const writeRoots = p.write === false ? [] : p.write || p.allowUnknown ? roots : roots.filter((root) => known.includes(root));
2698
+ if (this.routes.get(id2) !== r || Date.now() > r.expires) throw new AdminError("PAIRING_ROUTE_EXPIRED");
2573
2699
  const grant = {
2574
2700
  peer,
2575
- roots: await Promise.all(p.roots.map((x) => realpath3(x))),
2576
- write: p.write
2701
+ roots,
2702
+ write: writeRoots.length > 0,
2703
+ writeRoots
2577
2704
  };
2578
- this.grants.set(peer, grant);
2579
- try {
2580
- await atomicPrivate(
2581
- this.config.stateDir + "/grants.json",
2582
- Array.from(this.grants.values())
2583
- );
2584
- } catch (e) {
2585
- this.grants.delete(peer);
2586
- throw e;
2587
- }
2705
+ await this.saveGrant(grant, () => this.routes.get(id2) === r && Date.now() <= r.expires);
2588
2706
  r.session.trust(p.fingerprint);
2589
2707
  r.owner = peer;
2590
2708
  this.pairUntil = 0;
@@ -2593,7 +2711,24 @@ var ClientDaemon = class {
2593
2711
  status: "ready",
2594
2712
  fingerprint: p.fingerprint
2595
2713
  });
2596
- return { peer, approved: true };
2714
+ return { peer, approved: true, roots, write: grant.write, writeRoots };
2715
+ }
2716
+ case "projects-known":
2717
+ return { roots: await this.discoverProjects() };
2718
+ case "authorize": {
2719
+ const p = z5.object({ peer: z5.string().optional(), roots: z5.array(z5.string()).min(1).max(32), allowUnknown: z5.boolean().default(false) }).parse(m);
2720
+ const activePeers = [...new Set([...this.routes.values()].map((r) => r.owner).filter((peer2) => !!peer2))];
2721
+ const peer = p.peer ?? (activePeers.length === 1 ? activePeers[0] : this.grants.size === 1 ? this.grants.keys().next().value : void 0);
2722
+ if (!peer) throw new AdminError(this.grants.size ? "PEER_REQUIRED" : "NOT_PAIRED");
2723
+ const old = this.grants.get(peer);
2724
+ if (!old) throw new AdminError("NOT_PAIRED");
2725
+ const roots = await this.canonicalRoots(p.roots), known = await this.discoverProjects();
2726
+ if (!p.allowUnknown && roots.some((root) => !known.includes(root))) throw new AdminError("DIRECTORY_APPROVAL_REQUIRED");
2727
+ const nextRoots = [.../* @__PURE__ */ new Set([...old.roots, ...roots])];
2728
+ if (nextRoots.length > 256) throw new AdminError("GRANT_LIMIT");
2729
+ const writeRoots = [.../* @__PURE__ */ new Set([...old.writeRoots ?? (old.write ? old.roots : []), ...roots])];
2730
+ await this.saveGrant({ ...old, roots: nextRoots, write: true, writeRoots });
2731
+ return { peer, approved: true, roots: nextRoots, writeRoots };
2597
2732
  }
2598
2733
  case "revoke": {
2599
2734
  const peer = z5.string().parse(m.peer);
@@ -2609,8 +2744,46 @@ var ClientDaemon = class {
2609
2744
  return { revoked: true };
2610
2745
  }
2611
2746
  default:
2612
- throw Error("ADMIN_COMMAND");
2747
+ throw new AdminError("INVALID_ADMIN_REQUEST");
2748
+ }
2749
+ }
2750
+ async canonicalRoots(roots) {
2751
+ try {
2752
+ return await Promise.all(roots.map(async (path) => {
2753
+ const root = await realpath4(path);
2754
+ if (!(await stat3(root)).isDirectory()) throw Error();
2755
+ return root;
2756
+ }));
2757
+ } catch {
2758
+ throw new AdminError("DIRECTORY_UNAVAILABLE");
2759
+ }
2760
+ }
2761
+ async discoverProjects() {
2762
+ try {
2763
+ await this.ensureUpstream();
2764
+ return await knownProjects(this.bridge.upstream, this.config.stateDir);
2765
+ } catch (error) {
2766
+ if (error instanceof AdminError) throw error;
2767
+ throw new AdminError("CODEX_UNAVAILABLE");
2768
+ }
2769
+ }
2770
+ async saveGrant(grant, stillValid) {
2771
+ const values = new Map(this.grants);
2772
+ values.set(grant.peer, grant);
2773
+ try {
2774
+ await atomicPrivate(this.config.stateDir + "/grants.json", [...values.values()]);
2775
+ } catch {
2776
+ throw new AdminError("GRANT_SAVE_FAILED");
2777
+ }
2778
+ if (stillValid && !stillValid()) {
2779
+ try {
2780
+ await atomicPrivate(this.config.stateDir + "/grants.json", [...this.grants.values()]);
2781
+ } catch {
2782
+ throw new AdminError("GRANT_SAVE_FAILED");
2783
+ }
2784
+ throw new AdminError("PAIRING_ROUTE_EXPIRED");
2613
2785
  }
2786
+ this.grants.set(grant.peer, grant);
2614
2787
  }
2615
2788
  async startAdmin() {
2616
2789
  const path = this.config.stateDir + "/control.sock";
@@ -2641,7 +2814,7 @@ var ClientDaemon = class {
2641
2814
  if (e.code !== "ENOENT") throw e;
2642
2815
  }
2643
2816
  this.admin = createServer((socket) => {
2644
- socket.setTimeout(5e3, () => socket.destroy());
2817
+ socket.setTimeout(6e4, () => socket.destroy());
2645
2818
  let data = "";
2646
2819
  socket.on("data", (chunk) => {
2647
2820
  data += chunk.toString();
@@ -2651,10 +2824,17 @@ var ClientDaemon = class {
2651
2824
  }
2652
2825
  if (!data.includes("\n")) return;
2653
2826
  socket.pause();
2654
- void Promise.resolve().then(() => this.adminCommand(JSON.parse(data))).then(
2827
+ void Promise.resolve().then(() => {
2828
+ const request = JSON.parse(data);
2829
+ const result = this.adminQueue.then(() => this.adminCommand(request));
2830
+ this.adminQueue = result.then(() => {
2831
+ }, () => {
2832
+ });
2833
+ return result;
2834
+ }).then(
2655
2835
  (result) => socket.end(JSON.stringify({ result }) + "\n"),
2656
- () => socket.end(
2657
- JSON.stringify({ error: "ADMIN_REQUEST_REJECTED" }) + "\n"
2836
+ (error) => socket.end(
2837
+ JSON.stringify(adminFailure(error)) + "\n"
2658
2838
  )
2659
2839
  );
2660
2840
  });
@@ -2690,7 +2870,7 @@ function control(stateDir, request) {
2690
2870
  return new Promise((resolve5, reject) => {
2691
2871
  const socket = createConnection2(join2(stateDir, "control.sock"));
2692
2872
  let data = "";
2693
- socket.setTimeout(5e3, () => socket.destroy(Error("ADMIN_TIMEOUT")));
2873
+ socket.setTimeout(6e4, () => socket.destroy(Error("ADMIN_TIMEOUT: \u672C\u673A\u5904\u7406\u8D85\u65F6\uFF0C\u8BF7\u8FD0\u884C doctor \u540E\u91CD\u8BD5")));
2694
2874
  socket.on("connect", () => socket.write(JSON.stringify(request) + "\n"));
2695
2875
  socket.on("data", (chunk) => {
2696
2876
  data += chunk.toString();
@@ -2708,14 +2888,14 @@ function control(stateDir, request) {
2708
2888
  }
2709
2889
 
2710
2890
  // src/setup.ts
2711
- import { homedir as homedir2 } from "node:os";
2891
+ import { homedir as homedir3 } from "node:os";
2712
2892
  import { dirname as dirname3, join as join3, resolve as resolve3, basename as basename3 } from "node:path";
2713
2893
  import { mkdir as mkdir2, lstat as lstat3, writeFile as writeFile2, unlink as unlink2 } from "node:fs/promises";
2714
2894
  import { randomUUID as randomUUID5 } from "node:crypto";
2715
2895
  import { createInterface } from "node:readline/promises";
2716
2896
  import { Writable } from "node:stream";
2717
2897
  function defaultConfigPath() {
2718
- return resolve3(process.env.WEB_CODEX_CLIENT_CONFIG ?? join3(process.env.XDG_CONFIG_HOME ?? join3(homedir2(), ".config"), "web-codex-client", "config.json"));
2898
+ return resolve3(process.env.WEB_CODEX_CLIENT_CONFIG ?? join3(process.env.XDG_CONFIG_HOME ?? join3(homedir3(), ".config"), "web-codex-client", "config.json"));
2719
2899
  }
2720
2900
  function validateToken(value) {
2721
2901
  const token = value.trim();
@@ -2891,37 +3071,39 @@ async function doctor(configPath, quiet = false, reportPath = join4(dirname4(con
2891
3071
  }
2892
3072
 
2893
3073
  // src/cli.ts
2894
- var { values, positionals } = parseArgs({
2895
- allowPositionals: true,
2896
- options: {
2897
- config: { type: "string" },
2898
- state: { type: "string" },
2899
- socket: { type: "string" },
2900
- binary: { type: "string" },
2901
- standalone: { type: "boolean" },
2902
- transport: { type: "string" },
2903
- "codex-home": { type: "string" },
2904
- fingerprint: { type: "string" },
2905
- peer: { type: "string" },
2906
- root: { type: "string", multiple: true },
2907
- write: { type: "boolean" },
2908
- quiet: { type: "boolean" },
2909
- report: { type: "string" },
2910
- relay: { type: "string" },
2911
- "token-stdin": { type: "boolean" },
2912
- help: { type: "boolean", short: "h" },
2913
- version: { type: "boolean", short: "v" }
2914
- }
2915
- });
2916
- var command = positionals[0] ?? "help";
2917
3074
  async function main() {
3075
+ const { values, positionals } = parseArgs({
3076
+ allowPositionals: true,
3077
+ options: {
3078
+ config: { type: "string" },
3079
+ state: { type: "string" },
3080
+ socket: { type: "string" },
3081
+ binary: { type: "string" },
3082
+ standalone: { type: "boolean" },
3083
+ transport: { type: "string" },
3084
+ "codex-home": { type: "string" },
3085
+ fingerprint: { type: "string" },
3086
+ peer: { type: "string" },
3087
+ root: { type: "string", multiple: true },
3088
+ write: { type: "boolean" },
3089
+ "read-only": { type: "boolean" },
3090
+ quiet: { type: "boolean" },
3091
+ report: { type: "string" },
3092
+ relay: { type: "string" },
3093
+ "token-stdin": { type: "boolean" },
3094
+ help: { type: "boolean", short: "h" },
3095
+ version: { type: "boolean", short: "v" }
3096
+ }
3097
+ });
3098
+ const command = positionals[0] ?? "help";
3099
+ if (values.write && values["read-only"]) throw Error("\u4E0D\u8981\u540C\u65F6\u4F7F\u7528 --write \u548C --read-only\uFF0C\u8BF7\u67E5\u770B --help\u3002");
2918
3100
  if (values.version) {
2919
3101
  console.log(JSON.parse(await readFile3(new URL("../package.json", import.meta.url), "utf8")).version);
2920
3102
  return;
2921
3103
  }
2922
3104
  if (command === "help" || values.help) {
2923
3105
  console.log(
2924
- "web-codex-client configure [--config FILE] [--relay wss://HOST --token-stdin] [--socket PATH]\nweb-codex-client doctor [--config FILE] [--quiet] [--report FILE]\nweb-codex-client run [--config FILE]\nweb-codex-client init [--state DIR]\nweb-codex-client status|pair-open|pair-list [--state DIR] [--config FILE]\nweb-codex-client pair-approve --fingerprint FULL_HASH --root PATH [--write] [--state DIR]\nweb-codex-client revoke --peer PUBLIC_KEY [--state DIR]\nweb-codex-client probe [--transport unix|proxy] [--socket PATH] [--binary PATH] [--quiet] [--report FILE] (default: unix; --standalone starts an isolated stdio server)\nDefault config: " + defaultConfigPath()
3106
+ "web-codex-client configure [--config FILE] [--relay wss://HOST --token-stdin] [--socket PATH]\nweb-codex-client doctor [--config FILE] [--quiet] [--report FILE]\nweb-codex-client run [--config FILE]\nweb-codex-client pair [--root PATH] [--read-only] [--config FILE] (\u4EA4\u4E92\u6838\u5BF9\u6307\u7EB9\uFF1BCodex \u5DF2\u6709\u9879\u76EE\u9ED8\u8BA4\u8BFB\u5199)\nweb-codex-client authorize --root PATH [--peer PUBLIC_KEY] (\u65B0\u589E\u76EE\u5F55\uFF0C\u672C\u673A\u786E\u8BA4\u540E\u8BFB\u5199)\nweb-codex-client projects-known [--config FILE]\nweb-codex-client init [--state DIR]\nweb-codex-client status|pair-open|pair-list [--state DIR] [--config FILE]\nweb-codex-client pair-approve --fingerprint FULL_HASH [--root PATH] [--write|--read-only] [--state DIR]\nweb-codex-client revoke --peer PUBLIC_KEY [--state DIR]\nweb-codex-client probe [--transport unix|proxy] [--socket PATH] [--binary PATH] [--quiet] [--report FILE] (default: unix; --standalone starts an isolated stdio server)\nDefault config: " + defaultConfigPath()
2925
3107
  );
2926
3108
  return;
2927
3109
  }
@@ -2967,7 +3149,7 @@ async function main() {
2967
3149
  const daemon = new ClientDaemon(config, state.identity, state.grants);
2968
3150
  await daemon.start();
2969
3151
  console.log(
2970
- "Client running; use local status/pair-open/pair-list commands."
3152
+ "Client running. \u5728\u7F51\u9875\u9009\u62E9\u8BBE\u5907\u540E\uFF0C\u53E6\u5F00\u7EC8\u7AEF\u6267\u884C web-codex-client pair \u6838\u5BF9\u5E76\u4FE1\u4EFB\u6D4F\u89C8\u5668\u3002"
2971
3153
  );
2972
3154
  let stopping = false;
2973
3155
  const stop = () => {
@@ -2979,21 +3161,55 @@ async function main() {
2979
3161
  process.on("SIGTERM", stop);
2980
3162
  return;
2981
3163
  }
2982
- if (!["status", "pair-open", "pair-list", "pair-approve", "revoke"].includes(command)) throw Error("UNKNOWN_COMMAND: use --help");
3164
+ if (!["status", "pair", "pair-open", "pair-list", "pair-approve", "revoke", "authorize", "projects-known"].includes(command)) throw Error("UNKNOWN_COMMAND: use --help");
2983
3165
  const stateDir = values.state ? resolve4(values.state) : (await readConfig(configPath)).stateDir;
3166
+ const send = async (request2) => {
3167
+ const reply = await control(stateDir, request2);
3168
+ if (reply.error) throw Error(reply.error + ": " + (reply.message ?? "\u672C\u673A Client \u62D2\u7EDD\u8BF7\u6C42\uFF0C\u8BF7\u68C0\u67E5 pair-list \u5E76\u786E\u8BA4 run \u4F7F\u7528\u540C\u4E00\u914D\u7F6E\u3002"));
3169
+ return reply.result;
3170
+ };
3171
+ if (command === "pair") {
3172
+ const list = await send({ command: "pair-list" });
3173
+ const pending2 = list.pending.filter((p) => (p.remainingMs ?? p.expiresAt - Date.now()) > 0);
3174
+ if (!pending2.length) throw Error("NO_PENDING_BROWSER: \u8BF7\u5148\u5728\u7F51\u9875\u9009\u62E9\u8BBE\u5907\u6216\u91CD\u65B0\u8FDE\u63A5\uFF0C\u7136\u540E\u518D\u6B21\u8FD0\u884C pair\u3002\u65E0\u9700\u5148\u6267\u884C pair-open\u3002");
3175
+ for (let index = 0; index < pending2.length; index++) console.log(`${index + 1}. ${pending2[index].fingerprint} \u7F51\u9875\u786E\u8BA4\uFF1A${pending2[index].browserConfirmed ? "\u662F" : "\u5426"}`);
3176
+ const selected = pending2.length === 1 ? pending2[0] : pending2[Number(await question("\u9009\u62E9\u6D4F\u89C8\u5668\u7F16\u53F7\uFF1A")) - 1];
3177
+ if (!selected) throw Error("INVALID_SELECTION");
3178
+ console.log("\u8BF7\u6838\u5BF9\u7F51\u9875\u4E2D\u7684\u5B8C\u6574\u6307\u7EB9\uFF0C\u5E76\u5148\u70B9\u51FB\u7F51\u9875\u7684\u201C\u4E0E\u672C\u673A\u663E\u793A\u4E00\u81F4\u201D\u3002");
3179
+ const access = values["read-only"] ? "\u53EA\u8BFB" : "\u8BFB\u5199";
3180
+ console.log(values.root ? `\u5C06\u6388\u6743\u6307\u5B9A\u76EE\u5F55${access}\uFF1A` + values.root.map((root) => resolve4(root)).join(", ") : `\u786E\u8BA4\u540E\uFF0C\u4EC5\u5BF9\u672C\u673A Codex \u5DF2\u6709\u9879\u76EE\u6388\u4E88${access}\u6743\u9650\u3002`);
3181
+ if ((await question("\u6307\u7EB9\u5B8C\u5168\u4E00\u81F4\u5E76\u4FE1\u4EFB\u6B64\u6D4F\u89C8\u5668\uFF1F\u8F93\u5165 yes\uFF1A")).trim().toLowerCase() !== "yes") throw Error("PAIRING_CANCELLED");
3182
+ console.log("\u6B63\u5728\u8BFB\u53D6\u672C\u673A\u9879\u76EE\u5E76\u4FDD\u5B58\u6388\u6743\u2026");
3183
+ const result2 = await send({ command: "pair-approve", fingerprint: selected.fingerprint, roots: values.root?.map((root) => resolve4(root)), ...values.root ? { write: !values["read-only"], allowUnknown: true } : values["read-only"] ? { write: false } : {} });
3184
+ console.log(`\u5DF2\u4FE1\u4EFB\u6D4F\u89C8\u5668\uFF0C${result2.roots?.length ?? 0} \u4E2A\u9879\u76EE\u5DF2\u6388\u6743\u3002\u8FD4\u56DE\u7F51\u9875\u5373\u53EF\u4F7F\u7528\u3002`);
3185
+ return;
3186
+ }
3187
+ if (command === "authorize") {
3188
+ if (!values.root?.length) throw Error("ROOT_REQUIRED: \u8BF7\u7528 --root \u6307\u5B9A\u76EE\u5F55");
3189
+ const request2 = { command, roots: values.root.map((root) => resolve4(root)), peer: values.peer };
3190
+ let reply = await control(stateDir, request2);
3191
+ if (reply.error === "DIRECTORY_APPROVAL_REQUIRED") {
3192
+ console.log("Codex \u672A\u8BC6\u522B\u4E3A\u5DF2\u6709\u9879\u76EE\uFF0C\u9700\u8981\u4F60\u5728\u672C\u673A\u786E\u8BA4\u8BFB\u5199\u6388\u6743\uFF1A" + request2.roots.join(", "));
3193
+ if ((await question("\u5141\u8BB8\u8BE5\u6D4F\u89C8\u5668\u8BBF\u95EE\u8FD9\u4E9B\u76EE\u5F55\uFF1F\u8F93\u5165 yes\uFF1A")).trim().toLowerCase() !== "yes") throw Error("AUTHORIZATION_CANCELLED");
3194
+ reply = await control(stateDir, { ...request2, allowUnknown: true });
3195
+ }
3196
+ console.log(JSON.stringify(reply));
3197
+ if (reply.error) process.exitCode = 1;
3198
+ return;
3199
+ }
2984
3200
  const request = {
2985
3201
  command,
2986
3202
  fingerprint: values.fingerprint,
2987
3203
  peer: values.peer,
2988
- roots: values.root,
2989
- write: values.write ?? false
3204
+ roots: values.root?.map((root) => resolve4(root)),
3205
+ write: values["read-only"] ? false : values.write
2990
3206
  };
2991
3207
  const result = await control(stateDir, request);
2992
3208
  console.log(JSON.stringify(result));
2993
3209
  if (result.error) process.exitCode = 1;
2994
3210
  }
2995
3211
  main().catch((e) => {
2996
- console.error(e?.code === "ENOENT" ? "CONFIG_OR_FILE_MISSING: run configure; check the configured paths" : e?.name === "ZodError" ? "CONFIG_INVALID: check the configuration fields" : e instanceof Error ? e.message : "CLIENT_FAILED");
3212
+ console.error(e?.code?.startsWith("ERR_PARSE_ARGS") ? "\u53C2\u6570\u4E0D\u53D7\u652F\u6301\u6216\u683C\u5F0F\u65E0\u6548\uFF08\u6CA1\u6709 --admin \u53C2\u6570\uFF09\uFF1B\u8BF7\u67E5\u770B web-codex-client --help\u3002" : e?.code === "ENOENT" ? "CONFIG_OR_FILE_MISSING: run configure; check the configured paths" : e?.name === "ZodError" ? "CONFIG_INVALID: check the configuration fields" : e instanceof Error ? e.message : "CLIENT_FAILED");
2997
3213
  process.exitCode = 1;
2998
3214
  });
2999
3215
  //# sourceMappingURL=cli.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@landh93/web-codex-client",
3
- "version": "0.1.0-rc.1",
3
+ "version": "0.1.0-rc.2",
4
4
  "description": "Encrypted local Codex bridge with explicit browser pairing and workspace authorization",
5
5
  "license": "UNLICENSED",
6
6
  "bin": { "web-codex-client": "dist/cli.js" },