@skylandnpm/octopus-proxy 0.1.40 → 0.1.42

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/README.md CHANGED
@@ -116,6 +116,10 @@ npm run publish
116
116
 
117
117
  ## 编码工具账号与 OpenCode 服务
118
118
 
119
+ Claw 机器人设置可通过已解锁的端到端会话查询和切换当前机器人连接的 Proxy 智能体。`runtime.models` 接收 `{ "kind": "codex" }`(也支持 `opencode`、`zcode`、`pi`),临时查询所选工具原生模型目录,不修改当前 `tool`。`runtime.switchTool` 使用相同参数,先启动新工具并验证原生连接,成功后以 0600 权限原子保存配置,再关闭旧工具的 Proxy 管理进程;外部 OpenCode 服务继续由主机管理。
120
+
121
+ 各工具的命令、OpenCode 地址和服务密码保存在配置文件的 `toolProfiles` 中,切回时恢复。首次选择工具会在此 Proxy 主机探测命令;自定义安装位置可预先设置 `toolProfiles.<kind>`,格式与 `tool` 相同,且其中 `kind` 必须匹配。智能体未安装、启动或连接失败时保留当前工具和配置。已打开工作区会通过 `octopus.resync` 更新能力、会话和模型。
122
+
119
123
  OpenCode 2.0.19 使用 external HTTP 模式。`tool.command` 应为 `/Users/luhuili/.opencode/bin/opencode`,`tool.baseUrl` 明确指向回环服务,`tool.password` 必填并由 onboard 自动生成。Proxy 可拉起未监听的服务,也可连接以同一密码预先启动的服务;若配置端口已被密码不同的服务占用,Proxy 会在随机回环端口启动自己的实例并在后续重启时复用,避免接管或停止未知进程。启动日志保存在 `stateDirectory/opencode-serve.log`。如需只连接自行管理的服务,可设置 `autoStart: false`。旧顶层配置、managed 模式和 v1 API 不再兼容;Proxy 不停止或重启已有 OpenCode 服务。
120
124
 
121
125
  Claw 的“编码工具账号”窗口支持 Codex API Key/设备码、ZCode 原生 Z.AI OAuth、OpenCode 原生 API Key/OAuth。凭据由编码工具保存,模型列表从原生接口读取。详细行为与验证边界见 [编码工具账号](../../docs/tool-authentication.md)。
@@ -402,11 +402,11 @@ var require_codegen = __commonJS({
402
402
  const rhs = this.rhs === void 0 ? "" : ` = ${this.rhs}`;
403
403
  return `${varKind} ${this.name}${rhs};` + _n;
404
404
  }
405
- optimizeNames(names, constants6) {
405
+ optimizeNames(names, constants7) {
406
406
  if (!names[this.name.str])
407
407
  return;
408
408
  if (this.rhs)
409
- this.rhs = optimizeExpr(this.rhs, names, constants6);
409
+ this.rhs = optimizeExpr(this.rhs, names, constants7);
410
410
  return this;
411
411
  }
412
412
  get names() {
@@ -423,10 +423,10 @@ var require_codegen = __commonJS({
423
423
  render({ _n }) {
424
424
  return `${this.lhs} = ${this.rhs};` + _n;
425
425
  }
426
- optimizeNames(names, constants6) {
426
+ optimizeNames(names, constants7) {
427
427
  if (this.lhs instanceof code_1.Name && !names[this.lhs.str] && !this.sideEffects)
428
428
  return;
429
- this.rhs = optimizeExpr(this.rhs, names, constants6);
429
+ this.rhs = optimizeExpr(this.rhs, names, constants7);
430
430
  return this;
431
431
  }
432
432
  get names() {
@@ -487,8 +487,8 @@ var require_codegen = __commonJS({
487
487
  optimizeNodes() {
488
488
  return `${this.code}` ? this : void 0;
489
489
  }
490
- optimizeNames(names, constants6) {
491
- this.code = optimizeExpr(this.code, names, constants6);
490
+ optimizeNames(names, constants7) {
491
+ this.code = optimizeExpr(this.code, names, constants7);
492
492
  return this;
493
493
  }
494
494
  get names() {
@@ -517,12 +517,12 @@ var require_codegen = __commonJS({
517
517
  }
518
518
  return nodes.length > 0 ? this : void 0;
519
519
  }
520
- optimizeNames(names, constants6) {
520
+ optimizeNames(names, constants7) {
521
521
  const { nodes } = this;
522
522
  let i = nodes.length;
523
523
  while (i--) {
524
524
  const n = nodes[i];
525
- if (n.optimizeNames(names, constants6))
525
+ if (n.optimizeNames(names, constants7))
526
526
  continue;
527
527
  subtractNames(names, n.names);
528
528
  nodes.splice(i, 1);
@@ -575,12 +575,12 @@ var require_codegen = __commonJS({
575
575
  return void 0;
576
576
  return this;
577
577
  }
578
- optimizeNames(names, constants6) {
578
+ optimizeNames(names, constants7) {
579
579
  var _a;
580
- this.else = (_a = this.else) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants6);
581
- if (!(super.optimizeNames(names, constants6) || this.else))
580
+ this.else = (_a = this.else) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants7);
581
+ if (!(super.optimizeNames(names, constants7) || this.else))
582
582
  return;
583
- this.condition = optimizeExpr(this.condition, names, constants6);
583
+ this.condition = optimizeExpr(this.condition, names, constants7);
584
584
  return this;
585
585
  }
586
586
  get names() {
@@ -603,10 +603,10 @@ var require_codegen = __commonJS({
603
603
  render(opts) {
604
604
  return `for(${this.iteration})` + super.render(opts);
605
605
  }
606
- optimizeNames(names, constants6) {
607
- if (!super.optimizeNames(names, constants6))
606
+ optimizeNames(names, constants7) {
607
+ if (!super.optimizeNames(names, constants7))
608
608
  return;
609
- this.iteration = optimizeExpr(this.iteration, names, constants6);
609
+ this.iteration = optimizeExpr(this.iteration, names, constants7);
610
610
  return this;
611
611
  }
612
612
  get names() {
@@ -642,10 +642,10 @@ var require_codegen = __commonJS({
642
642
  render(opts) {
643
643
  return `for(${this.varKind} ${this.name} ${this.loop} ${this.iterable})` + super.render(opts);
644
644
  }
645
- optimizeNames(names, constants6) {
646
- if (!super.optimizeNames(names, constants6))
645
+ optimizeNames(names, constants7) {
646
+ if (!super.optimizeNames(names, constants7))
647
647
  return;
648
- this.iterable = optimizeExpr(this.iterable, names, constants6);
648
+ this.iterable = optimizeExpr(this.iterable, names, constants7);
649
649
  return this;
650
650
  }
651
651
  get names() {
@@ -687,11 +687,11 @@ var require_codegen = __commonJS({
687
687
  (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNodes();
688
688
  return this;
689
689
  }
690
- optimizeNames(names, constants6) {
690
+ optimizeNames(names, constants7) {
691
691
  var _a, _b;
692
- super.optimizeNames(names, constants6);
693
- (_a = this.catch) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants6);
694
- (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants6);
692
+ super.optimizeNames(names, constants7);
693
+ (_a = this.catch) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants7);
694
+ (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants7);
695
695
  return this;
696
696
  }
697
697
  get names() {
@@ -992,7 +992,7 @@ var require_codegen = __commonJS({
992
992
  function addExprNames(names, from) {
993
993
  return from instanceof code_1._CodeOrName ? addNames(names, from.names) : names;
994
994
  }
995
- function optimizeExpr(expr, names, constants6) {
995
+ function optimizeExpr(expr, names, constants7) {
996
996
  if (expr instanceof code_1.Name)
997
997
  return replaceName(expr);
998
998
  if (!canOptimize(expr))
@@ -1007,14 +1007,14 @@ var require_codegen = __commonJS({
1007
1007
  return items;
1008
1008
  }, []));
1009
1009
  function replaceName(n) {
1010
- const c = constants6[n.str];
1010
+ const c = constants7[n.str];
1011
1011
  if (c === void 0 || names[n.str] !== 1)
1012
1012
  return n;
1013
1013
  delete names[n.str];
1014
1014
  return c;
1015
1015
  }
1016
1016
  function canOptimize(e) {
1017
- return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants6[c.str] !== void 0);
1017
+ return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants7[c.str] !== void 0);
1018
1018
  }
1019
1019
  }
1020
1020
  function subtractNames(names, from) {
@@ -11715,7 +11715,7 @@ async function ensureHostIdentity(stateDirectory) {
11715
11715
 
11716
11716
  // src/daemon.ts
11717
11717
  import { mkdir as mkdir11 } from "node:fs/promises";
11718
- import { join as join14 } from "node:path";
11718
+ import { join as join15 } from "node:path";
11719
11719
 
11720
11720
  // src/workspace-diff.ts
11721
11721
  import { execFile as execFile2 } from "node:child_process";
@@ -11987,9 +11987,9 @@ var Uploads = class {
11987
11987
  this.directory = directory;
11988
11988
  }
11989
11989
  pending = /* @__PURE__ */ new Map();
11990
- serial(id, run4) {
11990
+ serial(id, run5) {
11991
11991
  const work = (this.pending.get(id) ?? Promise.resolve()).catch(() => {
11992
- }).then(run4);
11992
+ }).then(run5);
11993
11993
  this.pending.set(id, work);
11994
11994
  const clear = () => {
11995
11995
  if (this.pending.get(id) === work) this.pending.delete(id);
@@ -12197,6 +12197,8 @@ var Operations = [
12197
12197
  "auth.cancel",
12198
12198
  "runtime.status",
12199
12199
  "runtime.snapshot",
12200
+ "runtime.models",
12201
+ "runtime.switchTool",
12200
12202
  "runtime.start",
12201
12203
  "runtime.stop",
12202
12204
  "runtime.restart",
@@ -12467,12 +12469,12 @@ var PlatformScreenLock = class {
12467
12469
  if (signal.aborted) throw new ScreenLockError("\u8FDE\u63A5\u5DF2\u5173\u95ED\uFF0C\u5C4F\u5E55\u89E3\u9501\u5DF2\u53D6\u6D88\u3002");
12468
12470
  };
12469
12471
  check();
12470
- if (!password || Buffer.byteLength(password) > 1024 || /[\x00-\x1f\x7f]/.test(password)) throw new ScreenLockError("\u4E3B\u673A\u5BC6\u7801\u683C\u5F0F\u65E0\u6548\u3002");
12471
- if (Date.now() < this.blockedUntil) throw new ScreenLockError("\u5BC6\u7801\u9A8C\u8BC1\u5931\u8D25\u6B21\u6570\u8FC7\u591A\uFF0C\u8BF7\u7B49\u5F85 30 \u79D2\u540E\u91CD\u8BD5\u3002");
12472
12472
  const status = await this.status();
12473
12473
  check();
12474
12474
  if (!status.locked) return status;
12475
12475
  if (!status.canUnlock) throw new ScreenLockError(status.message ?? "\u4E3B\u673A\u4E0D\u652F\u6301\u5C4F\u5E55\u89E3\u9501\u3002");
12476
+ if (!password || Buffer.byteLength(password) > 1024 || /[\x00-\x1f\x7f]/.test(password)) throw new ScreenLockError("\u4E3B\u673A\u5BC6\u7801\u683C\u5F0F\u65E0\u6548\u3002");
12477
+ if (Date.now() < this.blockedUntil) throw new ScreenLockError("\u5BC6\u7801\u9A8C\u8BC1\u5931\u8D25\u6B21\u6570\u8FC7\u591A\uFF0C\u8BF7\u7B49\u5F85 30 \u79D2\u540E\u91CD\u8BD5\u3002");
12476
12478
  let valid;
12477
12479
  try {
12478
12480
  valid = await this.authenticate(this.stateDirectory, this.account.username, password);
@@ -12489,6 +12491,10 @@ var PlatformScreenLock = class {
12489
12491
  }
12490
12492
  this.failures = 0;
12491
12493
  if (this.system === "darwin") {
12494
+ const current = await this.status();
12495
+ check();
12496
+ if (!current.locked) return current;
12497
+ if (!current.canUnlock) throw new ScreenLockError(current.message ?? "\u4E3B\u673A\u4E0D\u652F\u6301\u5C4F\u5E55\u89E3\u9501\u3002");
12492
12498
  const helper = await this.macHelper();
12493
12499
  check();
12494
12500
  const bytes = Buffer.from(password, "utf8"), input = Buffer.alloc(bytes.length + 2);
@@ -12507,7 +12513,8 @@ var PlatformScreenLock = class {
12507
12513
  }
12508
12514
  const session = await this.linuxSession(signal);
12509
12515
  check();
12510
- if (session.locked) await this.execute("loginctl", ["--no-ask-password", "unlock-session", session.id], void 0, signal);
12516
+ if (!session.locked) return { ...status, locked: false };
12517
+ await this.execute("loginctl", ["--no-ask-password", "unlock-session", session.id], void 0, signal);
12511
12518
  for (let i = 0; i < 20; i++) {
12512
12519
  check();
12513
12520
  const current = await this.linuxSession(signal);
@@ -12533,7 +12540,7 @@ var Journal = class {
12533
12540
  if (active.hash !== hash) return Promise.resolve(this.failure(command, "Request id reused"));
12534
12541
  return active.result;
12535
12542
  }
12536
- const run4 = async () => {
12543
+ const run5 = async () => {
12537
12544
  const path = join6(this.directory, `${command.id}.json`);
12538
12545
  const readOnly = command.operation.startsWith("desktop.") || isReadOperation(command.operation) || isAuthentication(command.operation);
12539
12546
  if (!readOnly) {
@@ -12555,8 +12562,8 @@ var Journal = class {
12555
12562
  return response;
12556
12563
  };
12557
12564
  const key = this.queueKey(command);
12558
- const result = key === void 0 ? run4() : (this.queues.get(key) ?? Promise.resolve()).catch(() => {
12559
- }).then(run4);
12565
+ const result = key === void 0 ? run5() : (this.queues.get(key) ?? Promise.resolve()).catch(() => {
12566
+ }).then(run5);
12560
12567
  if (key !== void 0) {
12561
12568
  this.queues.set(key, result);
12562
12569
  result.finally(() => {
@@ -12664,9 +12671,9 @@ var Artifacts = class {
12664
12671
  this.emit = emit;
12665
12672
  }
12666
12673
  pending = /* @__PURE__ */ new Map();
12667
- serial(project, run4) {
12674
+ serial(project, run5) {
12668
12675
  const result = (this.pending.get(project) ?? Promise.resolve()).catch(() => {
12669
- }).then(run4);
12676
+ }).then(run5);
12670
12677
  this.pending.set(project, result);
12671
12678
  return result;
12672
12679
  }
@@ -15068,7 +15075,7 @@ var OpenCodeAdapter = class {
15068
15075
  });
15069
15076
  }
15070
15077
  };
15071
- const run4 = async () => {
15078
+ const run5 = async () => {
15072
15079
  if (method.type === "key") {
15073
15080
  if (typeof input.key !== "string" || !input.key.trim()) throw new Error("API Key required");
15074
15081
  await this.request(project, "POST", `/api/integration/${this.identifier(integration.id)}/connect/key`, { key: input.key, ...method.label ? { label: method.label } : {}, ...Object.keys(input).length > 1 ? { answer: input } : {} }, controller.signal);
@@ -15086,7 +15093,7 @@ var OpenCodeAdapter = class {
15086
15093
  };
15087
15094
  } else await wait();
15088
15095
  };
15089
- void run4().catch(() => this.auth.update(project, flow.flowId, { status: "failed", message: "OpenCode \u8BA4\u8BC1\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u8BA4\u8BC1\u4FE1\u606F\u4E0E\u4F9B\u5E94\u5546\u914D\u7F6E\u540E\u91CD\u8BD5" }));
15096
+ void run5().catch(() => this.auth.update(project, flow.flowId, { status: "failed", message: "OpenCode \u8BA4\u8BC1\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u8BA4\u8BC1\u4FE1\u606F\u4E0E\u4F9B\u5E94\u5546\u914D\u7F6E\u540E\u91CD\u8BD5" }));
15090
15097
  return { ...flow };
15091
15098
  }
15092
15099
  subscribe(project) {
@@ -15901,6 +15908,8 @@ import { join as join11 } from "node:path";
15901
15908
  var DAEMON_OPERATIONS = [
15902
15909
  "runtime.status",
15903
15910
  "runtime.snapshot",
15911
+ "runtime.models",
15912
+ "runtime.switchTool",
15904
15913
  "runtime.start",
15905
15914
  "runtime.stop",
15906
15915
  "runtime.restart",
@@ -17765,23 +17774,228 @@ var DesktopManager = class {
17765
17774
  }
17766
17775
  };
17767
17776
 
17777
+ // src/tool-selection.ts
17778
+ import { randomBytes as randomBytes2, randomUUID as randomUUID18 } from "node:crypto";
17779
+ import { access as access3, constants as constants6, readFile as readFile10 } from "node:fs/promises";
17780
+ import { platform as platform5 } from "node:os";
17781
+ import { isDeepStrictEqual } from "node:util";
17782
+
17783
+ // src/node-resolution.ts
17784
+ import { realpath as realpath7, stat as stat12 } from "node:fs/promises";
17785
+ import { execFile as execFile7 } from "node:child_process";
17786
+ import { promisify as promisify7 } from "node:util";
17787
+ import { join as join14 } from "node:path";
17788
+ import { homedir as homedir2, platform as platform4 } from "node:os";
17789
+ var run4 = promisify7(execFile7);
17790
+ var VERSION_MANAGED_SEGMENTS = /* @__PURE__ */ new Set([".nvm", "nvm", ".volta", "volta", ".fnm", "fnm", ".asdf", "asdf", ".mise", "mise"]);
17791
+ var execProbe = async (path) => {
17792
+ try {
17793
+ await run4(path, ["--version"], { timeout: 5e3 });
17794
+ return true;
17795
+ } catch {
17796
+ return false;
17797
+ }
17798
+ };
17799
+ function isVersionManagedPath(path) {
17800
+ return path.split("/").some((segment) => VERSION_MANAGED_SEGMENTS.has(segment));
17801
+ }
17802
+ function nodeCandidates() {
17803
+ return platform4() === "darwin" ? ["/opt/homebrew/bin/node", "/opt/homebrew/opt/node/bin/node", "/usr/local/bin/node", "/usr/bin/node"] : ["/usr/local/bin/node", "/usr/bin/node", "/snap/bin/node"];
17804
+ }
17805
+ async function resolveNodePath(options = {}) {
17806
+ const probe = options.probe ?? execProbe;
17807
+ if (options.pinned) {
17808
+ if (!await probe(options.pinned)) throw new Error(`--node-path ${options.pinned} is not a working node executable`);
17809
+ return { nodePath: options.pinned, source: "pinned" };
17810
+ }
17811
+ let exec = options.execPath ?? process.execPath;
17812
+ try {
17813
+ exec = await realpath7(exec);
17814
+ } catch {
17815
+ }
17816
+ if (!isVersionManagedPath(exec) && await probe(exec)) return { nodePath: exec, source: "exec" };
17817
+ for (const candidate of nodeCandidates()) if (!isVersionManagedPath(candidate) && await probe(candidate)) return { nodePath: candidate, source: "candidate" };
17818
+ if (await probe(exec)) return { nodePath: exec, source: "exec", warning: `only a version-manager node was found (${exec}); install node with the system package manager or pass --node-path, otherwise the service breaks when that node is upgraded` };
17819
+ return { nodePath: null, source: "exec" };
17820
+ }
17821
+ function toolSearchDirs(kind) {
17822
+ const dirs = [...describeTool(kind).searchDirs, join14(homedir2(), ".local", "bin"), join14(homedir2(), ".opencode", "bin"), join14(homedir2(), ".codex", "bin")];
17823
+ return [...new Set(dirs)];
17824
+ }
17825
+ async function resolveCommandOnPath(name, extraDirs = []) {
17826
+ const dirs = [...(process.env.PATH ?? "").split(":"), ...extraDirs].filter(Boolean);
17827
+ for (const dir of new Set(dirs)) {
17828
+ const full = join14(dir, name);
17829
+ try {
17830
+ const info = await stat12(full);
17831
+ if (info.isFile() && info.mode & 73) return full;
17832
+ } catch {
17833
+ }
17834
+ }
17835
+ return null;
17836
+ }
17837
+
17838
+ // src/tool-selection.ts
17839
+ var selectionSchema = z.object({ kind: z.enum(TOOL_KINDS) }).strict();
17840
+ var ToolSelection = class {
17841
+ constructor(configPath, config, create) {
17842
+ this.configPath = configPath;
17843
+ this.config = config;
17844
+ this.create = create;
17845
+ this.current = create({ ...config, tool: { ...config.tool } });
17846
+ }
17847
+ current;
17848
+ pending = Promise.resolve();
17849
+ closed = false;
17850
+ discovered = /* @__PURE__ */ new Map();
17851
+ exclusive(work) {
17852
+ const result = this.pending.catch(() => {
17853
+ }).then(() => {
17854
+ if (this.closed) throw new Error("Proxy \u5DF2\u505C\u6B62");
17855
+ return work();
17856
+ });
17857
+ this.pending = result;
17858
+ return result;
17859
+ }
17860
+ models(payload, projectId) {
17861
+ const { kind } = selectionSchema.parse(payload);
17862
+ return this.exclusive(async () => {
17863
+ if (kind === this.current.runtime.kind) {
17864
+ return { ...await buildProxySnapshot(this.config, this.current.runtime, this.current.adapter, projectId), activeTool: kind };
17865
+ }
17866
+ const raw = await this.readCurrent();
17867
+ const tool = await this.toolConfig(kind, raw);
17868
+ const candidateConfig = { ...this.config, tool };
17869
+ candidateConfig.projects = [this.config.projects.find((project) => project.id === projectId) ?? this.config.projects[0]];
17870
+ const candidate = this.create(candidateConfig);
17871
+ try {
17872
+ await candidate.runtime.init();
17873
+ return { ...await buildProxySnapshot(candidateConfig, candidate.runtime, candidate.adapter, projectId), activeTool: this.current.runtime.kind };
17874
+ } finally {
17875
+ await this.release(candidate);
17876
+ }
17877
+ });
17878
+ }
17879
+ switchTool(payload) {
17880
+ const { kind } = selectionSchema.parse(payload);
17881
+ return this.exclusive(async () => {
17882
+ if (kind === this.current.runtime.kind) return this.current.runtime.status();
17883
+ const raw = await this.readCurrent();
17884
+ const tool = await this.toolConfig(kind, raw);
17885
+ const candidate = this.create({ ...this.config, tool });
17886
+ let committed = false;
17887
+ try {
17888
+ await candidate.runtime.init();
17889
+ const status = await candidate.runtime.status();
17890
+ if (!status.healthy) throw new Error(status.error ?? `${describeTool(kind).label} \u670D\u52A1\u672A\u5C31\u7EEA`);
17891
+ if (candidate.adapter.supportedOperations().includes("configuration.models")) {
17892
+ await candidate.adapter.dispatch(this.modelCommand(candidate.runtime.config.projects[0].id));
17893
+ }
17894
+ const latest = await this.readCurrent();
17895
+ if (this.closed) throw new Error("Proxy \u5DF2\u505C\u6B62");
17896
+ await atomicJson(this.configPath, {
17897
+ ...latest,
17898
+ tool,
17899
+ toolProfiles: { ...this.profiles(latest), [this.config.tool.kind]: this.config.tool, [kind]: tool }
17900
+ });
17901
+ const previous = this.current;
17902
+ this.current = candidate;
17903
+ this.config.tool = tool;
17904
+ committed = true;
17905
+ await this.release(previous);
17906
+ return candidate.runtime.status();
17907
+ } finally {
17908
+ if (!committed) await this.release(candidate);
17909
+ }
17910
+ });
17911
+ }
17912
+ modelCommand(projectId) {
17913
+ return { type: "command", id: randomUUID18(), botId: this.config.botId, operation: "configuration.models", projectId, payload: {} };
17914
+ }
17915
+ profiles(raw) {
17916
+ return raw.toolProfiles && typeof raw.toolProfiles === "object" && !Array.isArray(raw.toolProfiles) ? raw.toolProfiles : {};
17917
+ }
17918
+ async toolConfig(kind, raw) {
17919
+ const saved = this.profiles(raw)[kind];
17920
+ if (saved) {
17921
+ const tool2 = ToolSchema.parse(saved);
17922
+ if (tool2.kind !== kind) throw new Error("Proxy \u667A\u80FD\u4F53\u914D\u7F6E\u4E0E\u7C7B\u578B\u4E0D\u4E00\u81F4");
17923
+ return { ...tool2, command: expandHome(tool2.command || describeTool(kind).defaultCommand), autoStart: true };
17924
+ }
17925
+ const discovered = this.discovered.get(kind);
17926
+ if (discovered) return discovered;
17927
+ let command = await resolveCommandOnPath(kind, toolSearchDirs(kind));
17928
+ if (!command && platform5() === "darwin") {
17929
+ const candidates = kind === "codex" ? [
17930
+ "/Applications/ChatGPT.app/Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex",
17931
+ "/Applications/ChatGPT.app/Contents/Resources/codex"
17932
+ ] : kind === "zcode" ? ["/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs"] : [];
17933
+ for (const path of candidates) {
17934
+ if (await access3(path, constants6.X_OK).then(() => true, () => false)) {
17935
+ command = path;
17936
+ break;
17937
+ }
17938
+ }
17939
+ }
17940
+ if (!command) throw new Error(`${describeTool(kind).label} \u672A\u5B89\u88C5\u6216\u547D\u4EE4\u4E0D\u53EF\u6267\u884C\uFF1B\u8BF7\u5728\u6B64 Proxy \u4E3B\u673A\u914D\u7F6E toolProfiles.${kind}.command \u540E\u91CD\u8BD5`);
17941
+ const tool = ToolSchema.parse({
17942
+ kind,
17943
+ command,
17944
+ autoStart: true,
17945
+ ...kind === "opencode" ? { baseUrl: "http://127.0.0.1:4096", password: randomBytes2(32).toString("base64url") } : {}
17946
+ });
17947
+ this.discovered.set(kind, tool);
17948
+ return tool;
17949
+ }
17950
+ async readCurrent() {
17951
+ const disk = await loadConfig(this.configPath);
17952
+ if (!isDeepStrictEqual(JSON.parse(JSON.stringify(disk.tool)), JSON.parse(JSON.stringify(this.config.tool))) || !isDeepStrictEqual(disk.projects, this.config.projects) || disk.botId !== this.config.botId || disk.secretKey !== this.config.secretKey) {
17953
+ throw new Error("Proxy \u914D\u7F6E\u5DF2\u5728\u4E3B\u673A\u4E0A\u88AB\u4FEE\u6539\uFF1B\u8BF7\u91CD\u542F Proxy \u540E\u518D\u5207\u6362\u667A\u80FD\u4F53");
17954
+ }
17955
+ return JSON.parse(await readFile10(this.configPath, "utf8"));
17956
+ }
17957
+ async release(instance) {
17958
+ instance.adapter.close();
17959
+ instance.runtime.detach();
17960
+ if (instance.runtime.kind !== "opencode") await instance.runtime.stop().catch(() => {
17961
+ });
17962
+ }
17963
+ close() {
17964
+ this.closed = true;
17965
+ void this.release(this.current);
17966
+ }
17967
+ };
17968
+
17768
17969
  // src/daemon.ts
17769
17970
  async function runDaemon(configPath, authenticate = verifyHostPassword) {
17770
17971
  const config = await loadConfig(configPath);
17771
17972
  await mkdir11(config.stateDirectory, { recursive: true, mode: 448 });
17772
- const workspace = new Workspace(config.projects), runtime = createRuntime(config), uploads = new Uploads(workspace, join14(config.stateDirectory, "uploads")), journal = new Journal(join14(config.stateDirectory, "journal"));
17973
+ const workspace = new Workspace(config.projects), uploads = new Uploads(workspace, join15(config.stateDirectory, "uploads")), journal = new Journal(join15(config.stateDirectory, "journal"));
17773
17974
  let secureChat;
17774
17975
  function emit(projectId, event, data) {
17775
17976
  secureChat?.publish(projectId, event, data);
17776
17977
  }
17777
17978
  const terminals = new TerminalManager(workspace, (owner, project, event, data) => secureChat?.sendTo(owner, project, event, data));
17778
17979
  const desktop = new DesktopManager(new PlatformDesktop(config.stateDirectory), new PlatformScreenLock(config.stateDirectory));
17779
- const adapter = createAdapter(runtime, workspace, uploads, emit), artifacts = new Artifacts(workspace, join14(config.stateDirectory, "artifacts"), emit);
17780
- const projectRegistry = new ProjectRegistry(configPath, config, runtime, adapter);
17980
+ const tools = new ToolSelection(configPath, config, (toolConfig) => {
17981
+ const runtime = createRuntime(toolConfig);
17982
+ const adapter = createAdapter(runtime, workspace, uploads, (project, event, data) => {
17983
+ if (tools.current.runtime === runtime) emit(project, event, data);
17984
+ });
17985
+ return { runtime, adapter };
17986
+ });
17987
+ const artifacts = new Artifacts(workspace, join15(config.stateDirectory, "artifacts"), emit);
17781
17988
  async function dispatch(c, secureSessionId) {
17782
17989
  if (c.operation.startsWith("desktop.")) return desktop.dispatch(c, secureSessionId);
17783
17990
  if (c.operation.startsWith("terminal.")) return terminals.dispatch(c, secureSessionId);
17784
17991
  const p = c.payload;
17992
+ if (c.operation === "runtime.models") return tools.models(p, c.projectId);
17993
+ if (c.operation === "runtime.switchTool") {
17994
+ const result2 = await tools.switchTool(p);
17995
+ emit("", "octopus.resync", { toolChanged: true });
17996
+ return result2;
17997
+ }
17998
+ const { runtime, adapter } = tools.current;
17785
17999
  if (c.operation === "runtime.status") return { ...await runtime.status(), capabilities: adapter.supportedOperations() };
17786
18000
  if (c.operation === "runtime.snapshot") return buildProxySnapshot(config, runtime, adapter, c.projectId);
17787
18001
  if (c.operation === "runtime.logs") return runtime.logs();
@@ -17790,17 +18004,17 @@ async function runDaemon(configPath, authenticate = verifyHostPassword) {
17790
18004
  if (c.operation === "runtime.restart") return runtime.restart();
17791
18005
  if (c.operation === "workspace.projects") return publicProjects(config);
17792
18006
  if (c.operation === "workspace.projects.add") {
17793
- const result2 = await projectRegistry.add(p);
18007
+ const result2 = await tools.exclusive(() => new ProjectRegistry(configPath, config, tools.current.runtime, tools.current.adapter).add(p));
17794
18008
  emit(String(p.id), "octopus.resync", {});
17795
18009
  return result2;
17796
18010
  }
17797
18011
  if (c.operation === "workspace.projects.update") {
17798
- const result2 = await projectRegistry.update(p);
18012
+ const result2 = await tools.exclusive(() => new ProjectRegistry(configPath, config, tools.current.runtime, tools.current.adapter).update(p));
17799
18013
  emit(String(p.id), "octopus.resync", {});
17800
18014
  return result2;
17801
18015
  }
17802
18016
  if (c.operation === "workspace.projects.remove") {
17803
- const result2 = await projectRegistry.remove(p);
18017
+ const result2 = await tools.exclusive(() => new ProjectRegistry(configPath, config, tools.current.runtime, tools.current.adapter).remove(p));
17804
18018
  emit(String(p.id), "octopus.resync", {});
17805
18019
  return result2;
17806
18020
  }
@@ -17842,7 +18056,7 @@ async function runDaemon(configPath, authenticate = verifyHostPassword) {
17842
18056
  terminals.closeOwned(owner);
17843
18057
  desktop.closeOwned(owner);
17844
18058
  }, authenticate);
17845
- await runtime.init().catch((error) => console.error(`${runtime.kind} startup:`, error.message));
18059
+ await tools.current.runtime.init().catch((error) => console.error(`${tools.current.runtime.kind} startup:`, error.message));
17846
18060
  secureChat.start();
17847
18061
  const scan = setInterval(async () => {
17848
18062
  for (const p of config.projects) await artifacts.scan(p.id).catch(() => {
@@ -17859,8 +18073,7 @@ async function runDaemon(configPath, authenticate = verifyHostPassword) {
17859
18073
  secureChat?.stop();
17860
18074
  terminals.close();
17861
18075
  desktop.close();
17862
- runtime.detach();
17863
- adapter.close();
18076
+ tools.close();
17864
18077
  }
17865
18078
  function onSignal() {
17866
18079
  stop();
@@ -17900,5 +18113,9 @@ export {
17900
18113
  DESKTOP_PERMISSION_GUIDANCE,
17901
18114
  requestDesktopPermission,
17902
18115
  checkDesktop,
18116
+ isVersionManagedPath,
18117
+ resolveNodePath,
18118
+ toolSearchDirs,
18119
+ resolveCommandOnPath,
17903
18120
  runDaemon
17904
18121
  };
package/dist/cli.js CHANGED
@@ -19,93 +19,42 @@ import {
19
19
  ensureHostIdentity,
20
20
  expandHome,
21
21
  isMacOS,
22
+ isVersionManagedPath,
22
23
  launchdLogPath,
23
24
  launchdPlistPath,
24
25
  loadConfig,
25
26
  proxyVersion,
26
27
  requestDesktopPermission,
28
+ resolveCommandOnPath,
27
29
  resolveConfigPath,
30
+ resolveNodePath,
28
31
  runDaemon,
29
32
  supportedPlatform,
30
- systemdUnitPath
31
- } from "./chunk-OXA2S7UY.js";
33
+ systemdUnitPath,
34
+ toolSearchDirs
35
+ } from "./chunk-HB3ETNMW.js";
32
36
 
33
37
  // src/cli.ts
34
38
  import { spawn as spawn2 } from "node:child_process";
35
39
  import { access as access4 } from "node:fs/promises";
36
40
 
37
41
  // src/doctor.ts
38
- import { access as access2, chmod, constants, mkdir as mkdir3, stat as stat2 } from "node:fs/promises";
39
- import { isAbsolute as isAbsolute2, join as join2 } from "node:path";
40
- import { execFile as execFile4 } from "node:child_process";
41
- import { promisify as promisify4 } from "node:util";
42
+ import { access as access2, chmod, constants, mkdir as mkdir3, stat } from "node:fs/promises";
43
+ import { isAbsolute as isAbsolute2, join } from "node:path";
44
+ import { execFile as execFile3 } from "node:child_process";
45
+ import { promisify as promisify3 } from "node:util";
42
46
 
43
47
  // src/service.ts
44
48
  import { readFile, access } from "node:fs/promises";
45
49
  import { dirname as dirname3, isAbsolute, resolve } from "node:path";
46
50
 
47
- // src/node-resolution.ts
48
- import { realpath, stat } from "node:fs/promises";
49
- import { execFile } from "node:child_process";
50
- import { promisify } from "node:util";
51
- import { join } from "node:path";
52
- import { homedir, platform } from "node:os";
53
- var run = promisify(execFile);
54
- var VERSION_MANAGED_SEGMENTS = /* @__PURE__ */ new Set([".nvm", "nvm", ".volta", "volta", ".fnm", "fnm", ".asdf", "asdf", ".mise", "mise"]);
55
- var execProbe = async (path) => {
56
- try {
57
- await run(path, ["--version"], { timeout: 5e3 });
58
- return true;
59
- } catch {
60
- return false;
61
- }
62
- };
63
- function isVersionManagedPath(path) {
64
- return path.split("/").some((segment) => VERSION_MANAGED_SEGMENTS.has(segment));
65
- }
66
- function nodeCandidates() {
67
- return platform() === "darwin" ? ["/opt/homebrew/bin/node", "/opt/homebrew/opt/node/bin/node", "/usr/local/bin/node", "/usr/bin/node"] : ["/usr/local/bin/node", "/usr/bin/node", "/snap/bin/node"];
68
- }
69
- async function resolveNodePath(options = {}) {
70
- const probe = options.probe ?? execProbe;
71
- if (options.pinned) {
72
- if (!await probe(options.pinned)) throw new Error(`--node-path ${options.pinned} is not a working node executable`);
73
- return { nodePath: options.pinned, source: "pinned" };
74
- }
75
- let exec = options.execPath ?? process.execPath;
76
- try {
77
- exec = await realpath(exec);
78
- } catch {
79
- }
80
- if (!isVersionManagedPath(exec) && await probe(exec)) return { nodePath: exec, source: "exec" };
81
- for (const candidate of nodeCandidates()) if (!isVersionManagedPath(candidate) && await probe(candidate)) return { nodePath: candidate, source: "candidate" };
82
- if (await probe(exec)) return { nodePath: exec, source: "exec", warning: `only a version-manager node was found (${exec}); install node with the system package manager or pass --node-path, otherwise the service breaks when that node is upgraded` };
83
- return { nodePath: null, source: "exec" };
84
- }
85
- function toolSearchDirs(kind) {
86
- const dirs = [...describeTool(kind).searchDirs, join(homedir(), ".local", "bin"), join(homedir(), ".opencode", "bin"), join(homedir(), ".codex", "bin")];
87
- return [...new Set(dirs)];
88
- }
89
- async function resolveCommandOnPath(name, extraDirs = []) {
90
- const dirs = [...(process.env.PATH ?? "").split(":"), ...extraDirs].filter(Boolean);
91
- for (const dir of new Set(dirs)) {
92
- const full = join(dir, name);
93
- try {
94
- const info = await stat(full);
95
- if (info.isFile() && info.mode & 73) return full;
96
- } catch {
97
- }
98
- }
99
- return null;
100
- }
101
-
102
51
  // src/launchd.ts
103
52
  import { mkdir, rm, writeFile } from "node:fs/promises";
104
53
  import { dirname } from "node:path";
105
- import { execFile as execFile2 } from "node:child_process";
106
- import { promisify as promisify2 } from "node:util";
54
+ import { execFile } from "node:child_process";
55
+ import { promisify } from "node:util";
107
56
  import { userInfo } from "node:os";
108
- var run2 = promisify2(execFile2);
57
+ var run = promisify(execFile);
109
58
  var guiDomain = () => `gui/${userInfo().uid}`;
110
59
  function xml(value) {
111
60
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
@@ -138,22 +87,22 @@ async function ignoreMissing(action) {
138
87
  }
139
88
  }
140
89
  async function launchdLoad() {
141
- await ignoreMissing(() => run2("launchctl", ["bootout", `${guiDomain()}/${SERVICE_LABEL}`]));
142
- await run2("launchctl", ["bootstrap", guiDomain(), launchdPlistPath()]);
90
+ await ignoreMissing(() => run("launchctl", ["bootout", `${guiDomain()}/${SERVICE_LABEL}`]));
91
+ await run("launchctl", ["bootstrap", guiDomain(), launchdPlistPath()]);
143
92
  }
144
93
  async function launchdStart() {
145
- await ignoreMissing(() => run2("launchctl", ["bootstrap", guiDomain(), launchdPlistPath()]));
146
- await run2("launchctl", ["kickstart", `${guiDomain()}/${SERVICE_LABEL}`]);
94
+ await ignoreMissing(() => run("launchctl", ["bootstrap", guiDomain(), launchdPlistPath()]));
95
+ await run("launchctl", ["kickstart", `${guiDomain()}/${SERVICE_LABEL}`]);
147
96
  }
148
97
  async function launchdStop() {
149
- await ignoreMissing(() => run2("launchctl", ["bootout", `${guiDomain()}/${SERVICE_LABEL}`]));
98
+ await ignoreMissing(() => run("launchctl", ["bootout", `${guiDomain()}/${SERVICE_LABEL}`]));
150
99
  }
151
100
  async function launchdRestart() {
152
101
  const target = `${guiDomain()}/${SERVICE_LABEL}`;
153
102
  if ((await launchdStatus()).loaded) {
154
- await run2("launchctl", ["kickstart", "-k", target]);
103
+ await run("launchctl", ["kickstart", "-k", target]);
155
104
  } else {
156
- await run2("launchctl", ["bootstrap", guiDomain(), launchdPlistPath()]);
105
+ await run("launchctl", ["bootstrap", guiDomain(), launchdPlistPath()]);
157
106
  }
158
107
  }
159
108
  async function launchdUninstall() {
@@ -162,7 +111,7 @@ async function launchdUninstall() {
162
111
  }
163
112
  async function launchdStatus() {
164
113
  try {
165
- const { stdout: stdout3 } = await run2("launchctl", ["print", `${guiDomain()}/${SERVICE_LABEL}`]);
114
+ const { stdout: stdout3 } = await run("launchctl", ["print", `${guiDomain()}/${SERVICE_LABEL}`]);
166
115
  const running = /state = running/.test(stdout3);
167
116
  const pid = Number(/pid = (\d+)/.exec(stdout3)?.[1]);
168
117
  return { loaded: true, running, pid: Number.isFinite(pid) && pid > 0 ? pid : void 0 };
@@ -174,9 +123,9 @@ async function launchdStatus() {
174
123
  // src/systemd.ts
175
124
  import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
176
125
  import { dirname as dirname2 } from "node:path";
177
- import { execFile as execFile3 } from "node:child_process";
178
- import { promisify as promisify3 } from "node:util";
179
- var run3 = promisify3(execFile3);
126
+ import { execFile as execFile2 } from "node:child_process";
127
+ import { promisify as promisify2 } from "node:util";
128
+ var run2 = promisify2(execFile2);
180
129
  function quoteSystemdArg(value) {
181
130
  const escaped = value.replaceAll("%", "%%").replaceAll('"', '\\"');
182
131
  return /[\s"\\]/.test(value) ? `"${escaped}"` : escaped;
@@ -213,7 +162,7 @@ async function writeUnitFile(text) {
213
162
  }
214
163
  async function systemctl(args) {
215
164
  try {
216
- const { stdout: stdout3 } = await run3("systemctl", ["--user", ...args]);
165
+ const { stdout: stdout3 } = await run2("systemctl", ["--user", ...args]);
217
166
  return stdout3;
218
167
  } catch (error) {
219
168
  const stderr = error instanceof Error ? error.stderr ?? error.message : String(error);
@@ -339,14 +288,14 @@ function logLocationHint() {
339
288
 
340
289
  // src/doctor.ts
341
290
  import { randomUUID } from "node:crypto";
342
- var run4 = promisify4(execFile4);
291
+ var run3 = promisify3(execFile3);
343
292
  async function runDoctor(opts) {
344
293
  const results = [];
345
294
  const record = (name, status, detail) => results.push({ name, status, detail });
346
295
  results.push(await checkTerminalSupport(opts.fix));
347
296
  let config;
348
297
  try {
349
- const info = await stat2(opts.configPath).catch((error) => {
298
+ const info = await stat(opts.configPath).catch((error) => {
350
299
  if (error.code === "ENOENT") throw new Error(`\u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${opts.configPath}\uFF1B\u5148\u8FD0\u884C octopus-proxy onboard`);
351
300
  throw error;
352
301
  });
@@ -379,7 +328,7 @@ async function runDoctor(opts) {
379
328
  record(checkName, "fail", `${command2} \u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u6267\u884C`);
380
329
  } else {
381
330
  try {
382
- const { stdout: stdout3 } = await run4(command2, [...descriptor.versionArgs], { timeout: 15e3 });
331
+ const { stdout: stdout3 } = await run3(command2, [...descriptor.versionArgs], { timeout: 15e3 });
383
332
  const version = stdout3.trim().split("\n")[0].trim();
384
333
  if (version === descriptor.baseline) record(checkName, "ok", `${command2}\uFF08${version}\uFF09`);
385
334
  else if (descriptor.versionPolicy === "exact") record(checkName, "warn", `${command2} \u7248\u672C\u4E3A ${version}\uFF0C\u5DF2\u9A8C\u8BC1\u57FA\u7EBF\u662F ${descriptor.baseline}\uFF08\u8BF7\u786E\u8BA4\u5916\u90E8\u670D\u52A1 API \u517C\u5BB9\uFF09`);
@@ -396,7 +345,7 @@ async function runDoctor(opts) {
396
345
  if (config) {
397
346
  const probeConfig = { ...config, tool: { ...config.tool, autoStart: false } };
398
347
  const runtime = createRuntime(probeConfig), workspace = new Workspace(config.projects);
399
- const adapter = createAdapter(runtime, workspace, new Uploads(workspace, join2(config.stateDirectory, "uploads")), () => {
348
+ const adapter = createAdapter(runtime, workspace, new Uploads(workspace, join(config.stateDirectory, "uploads")), () => {
400
349
  });
401
350
  try {
402
351
  await runtime.init();
@@ -455,11 +404,11 @@ async function runDoctor(opts) {
455
404
  import { createInterface } from "node:readline/promises";
456
405
  import { randomBytes } from "node:crypto";
457
406
  import { stdin, stdout } from "node:process";
458
- import { access as access3, constants as constants2, mkdir as mkdir4, stat as stat3 } from "node:fs/promises";
459
- import { execFile as execFile5 } from "node:child_process";
460
- import { platform as platform2 } from "node:os";
461
- import { isAbsolute as isAbsolute3, join as join3 } from "node:path";
462
- import { promisify as promisify5 } from "node:util";
407
+ import { access as access3, constants as constants2, mkdir as mkdir4, stat as stat2 } from "node:fs/promises";
408
+ import { execFile as execFile4 } from "node:child_process";
409
+ import { platform } from "node:os";
410
+ import { isAbsolute as isAbsolute3, join as join2 } from "node:path";
411
+ import { promisify as promisify4 } from "node:util";
463
412
  var DEFAULT_HUB_URL = "https://octopus.wuyehao.com";
464
413
  async function ask(rl, question, options = {}) {
465
414
  for (; ; ) {
@@ -488,7 +437,7 @@ var validUrl = (value) => {
488
437
  var validId = (value) => !value ? "\u9879\u76EE ID \u4E0D\u80FD\u4E3A\u7A7A" : /^[a-zA-Z0-9_-]+$/.test(value) ? void 0 : "\u4EC5\u9650\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u548C\u8FDE\u5B57\u7B26";
489
438
  var validateProjectId = (value, previousProjects) => validId(value) ?? (previousProjects.some((project) => project.id === value) ? `\u9879\u76EE ID\u300C${value}\u300D\u5DF2\u5B58\u5728\uFF0C\u8BF7\u8F93\u5165\u5176\u4ED6 ID` : void 0);
490
439
  var validBotId = (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value) ? void 0 : "\u8BF7\u8F93\u5165\u673A\u5668\u4EBA UUID";
491
- var run5 = promisify5(execFile5);
440
+ var run4 = promisify4(execFile4);
492
441
  var MACOS_TOOL_EXAMPLES = {
493
442
  opencode: "/Users/luhuili/.opencode/bin/opencode",
494
443
  codex: "/Applications/ChatGPT.app/Contents/Resources/codex",
@@ -499,20 +448,20 @@ async function validateToolCommand(value, kind) {
499
448
  const command2 = expandHome(value);
500
449
  if (!isAbsolute3(command2)) return "\u670D\u52A1\u73AF\u5883\u9700\u8981\u7EDD\u5BF9\u8DEF\u5F84";
501
450
  try {
502
- if (!(await stat3(command2)).isFile()) return "\u547D\u4EE4\u8DEF\u5F84\u5FC5\u987B\u6307\u5411\u53EF\u6267\u884C\u6587\u4EF6";
451
+ if (!(await stat2(command2)).isFile()) return "\u547D\u4EE4\u8DEF\u5F84\u5FC5\u987B\u6307\u5411\u53EF\u6267\u884C\u6587\u4EF6";
503
452
  await access3(command2, constants2.X_OK);
504
453
  } catch {
505
454
  return `\u547D\u4EE4\u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u6267\u884C\uFF1A${command2}`;
506
455
  }
507
456
  try {
508
- await run5(command2, [...describeTool(kind).versionArgs], { timeout: 1e4 });
457
+ await run4(command2, [...describeTool(kind).versionArgs], { timeout: 1e4 });
509
458
  } catch {
510
459
  return `${describeTool(kind).label} \u7248\u672C\u68C0\u67E5\u5931\u8D25\uFF0C\u8BF7\u786E\u8BA4\u547D\u4EE4\u8DEF\u5F84\uFF1A${command2}`;
511
460
  }
512
461
  return void 0;
513
462
  }
514
463
  async function runOnboard(opts) {
515
- const configExists = await stat3(opts.configPath).then(() => true, (error) => {
464
+ const configExists = await stat2(opts.configPath).then(() => true, (error) => {
516
465
  if (error.code === "ENOENT") return false;
517
466
  throw error;
518
467
  });
@@ -536,7 +485,7 @@ async function runOnboard(opts) {
536
485
  if (toolKind !== previousToolKind) toolCommand = toolKind === existing?.tool.kind ? existing.tool.command : "";
537
486
  const descriptor = describeTool(toolKind);
538
487
  const found = await resolveCommandOnPath(toolKind, toolSearchDirs(toolKind));
539
- const example = platform2() === "darwin" ? MACOS_TOOL_EXAMPLES[toolKind] : void 0;
488
+ const example = platform() === "darwin" ? MACOS_TOOL_EXAMPLES[toolKind] : void 0;
540
489
  if (toolKind === "opencode") {
541
490
  toolCommand = expandHome(await ask(rl, "OpenCode \u547D\u4EE4\uFF08Claw \u8FDC\u7A0B\u542F\u52A8\u65F6\u4F7F\u7528\uFF09", { default: toolCommand || found || example, validate: (value) => validateToolCommand(value, toolKind) }));
542
491
  console.log(" Proxy \u53EF\u4ECE Claw \u8FDC\u7A0B\u542F\u52A8 OpenCode\uFF1B\u5DF2\u6709\u670D\u52A1\u4F1A\u76F4\u63A5\u8FDE\u63A5\u3002");
@@ -561,7 +510,7 @@ async function runOnboard(opts) {
561
510
  console.log(" \u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
562
511
  continue;
563
512
  }
564
- if (!await stat3(project.directory).then(() => true, () => false)) {
513
+ if (!await stat2(project.directory).then(() => true, () => false)) {
565
514
  console.log(" \u76EE\u5F55\u4E0D\u5B58\u5728");
566
515
  continue;
567
516
  }
@@ -580,7 +529,7 @@ async function runOnboard(opts) {
580
529
  console.log(" \u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
581
530
  continue;
582
531
  }
583
- if (!await stat3(directory).then(() => true, () => false)) {
532
+ if (!await stat2(directory).then(() => true, () => false)) {
584
533
  console.log(" \u76EE\u5F55\u4E0D\u5B58\u5728");
585
534
  continue;
586
535
  }
@@ -647,8 +596,8 @@ async function runOnboard(opts) {
647
596
  stateDirectory = expandHome(stateDirectory);
648
597
  await mkdir4(stateDirectory, { recursive: true, mode: 448 });
649
598
  try {
650
- if (platform2() === "darwin") {
651
- console.log(`\u622A\u5C4F\u8F85\u52A9\u7A0B\u5E8F\u8DEF\u5F84\uFF1A${join3(stateDirectory, "octopus-desktop-capture")}\uFF08\u6587\u4EF6\u540D\u4E0D\u4E00\u5B9A\u662F\u7CFB\u7EDF\u6388\u6743\u540D\u79F0\uFF09`);
599
+ if (platform() === "darwin") {
600
+ console.log(`\u622A\u5C4F\u8F85\u52A9\u7A0B\u5E8F\u8DEF\u5F84\uFF1A${join2(stateDirectory, "octopus-desktop-capture")}\uFF08\u6587\u4EF6\u540D\u4E0D\u4E00\u5B9A\u662F\u7CFB\u7EDF\u6388\u6743\u540D\u79F0\uFF09`);
652
601
  console.log("\u6B63\u5728\u68C0\u67E5\u5C4F\u5E55\u5F55\u5236\u6743\u9650\uFF1B\u5982\u51FA\u73B0\u7CFB\u7EDF\u6388\u6743\u63D0\u793A\uFF0C\u8BF7\u6309\u5F39\u7A97\u4E2D\u7684\u5B9E\u9645\u7A0B\u5E8F\u540D\u79F0\u6388\u6743\uFF0C\u53EF\u80FD\u663E\u793A Terminal\u3001iTerm\u3001\u7F16\u8F91\u5668\u6216 node\u3002");
653
602
  }
654
603
  const permission = await requestDesktopPermission(stateDirectory);
@@ -722,18 +671,18 @@ async function manageProjects(configPath2, remove) {
722
671
  }
723
672
 
724
673
  // src/update.ts
725
- import { execFile as execFile6, spawn } from "node:child_process";
726
- import { readFile as readFile3, realpath as realpath2 } from "node:fs/promises";
727
- import { join as join4 } from "node:path";
674
+ import { execFile as execFile5, spawn } from "node:child_process";
675
+ import { readFile as readFile3, realpath } from "node:fs/promises";
676
+ import { join as join3 } from "node:path";
728
677
  import { stdin as stdin2, stdout as stdout2 } from "node:process";
729
678
  import { createInterface as createInterface2 } from "node:readline/promises";
730
679
  import { setTimeout as delay } from "node:timers/promises";
731
680
  import { fileURLToPath } from "node:url";
732
- import { promisify as promisify6 } from "node:util";
681
+ import { promisify as promisify5 } from "node:util";
733
682
  var PACKAGE = "@skylandnpm/octopus-proxy";
734
683
  var REGISTRY = "https://registry.npmjs.org";
735
684
  var packageDirectory = fileURLToPath(new URL("../", import.meta.url));
736
- var run6 = promisify6(execFile6);
685
+ var run5 = promisify5(execFile5);
737
686
  function parseVersion(version) {
738
687
  const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*))?(?:\+[\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*)?$/.exec(version);
739
688
  if (!match) throw new Error(`\u65E0\u6548\u7684\u7248\u672C\u53F7\uFF1A${version}`);
@@ -762,7 +711,7 @@ function message(error) {
762
711
  }
763
712
  async function npmOutput(args) {
764
713
  try {
765
- const { stdout: stdout3 } = await run6("npm", args, { timeout: 3e4, maxBuffer: 1024 * 1024 });
714
+ const { stdout: stdout3 } = await run5("npm", args, { timeout: 3e4, maxBuffer: 1024 * 1024 });
766
715
  return stdout3.trim();
767
716
  } catch (error) {
768
717
  throw new Error(`npm ${args[0]} \u5931\u8D25\uFF1A${message(error)}`);
@@ -812,9 +761,9 @@ async function runUpdate() {
812
761
  return;
813
762
  }
814
763
  const prefix = await npmOutput(["prefix", "--global"]);
815
- const target = join4(prefix, "lib", "node_modules", PACKAGE);
816
- const installedDirectory = await realpath2(target).catch(() => null);
817
- if (installedDirectory !== await realpath2(packageDirectory)) {
764
+ const target = join3(prefix, "lib", "node_modules", PACKAGE);
765
+ const installedDirectory = await realpath(target).catch(() => null);
766
+ if (installedDirectory !== await realpath(packageDirectory)) {
818
767
  throw new Error("\u5F53\u524D\u547D\u4EE4\u4E0D\u5C5E\u4E8E npm \u7684\u5168\u5C40\u5B89\u88C5\u76EE\u5F55\uFF1B\u8BF7\u786E\u8BA4 npm \u4E0E octopus-proxy \u4F7F\u7528\u540C\u4E00\u5957 Node.js \u73AF\u5883\uFF0C\u5E76\u4ECE\u5168\u5C40\u5B89\u88C5\u8FD0\u884C update");
819
768
  }
820
769
  const state = await serviceStatus();
@@ -826,7 +775,7 @@ async function runUpdate() {
826
775
  }
827
776
  console.log(`\u6B63\u5728\u5B89\u88C5 ${PACKAGE}@${latest}\u2026`);
828
777
  await installVersion(latest, prefix);
829
- const installed = JSON.parse(await readFile3(join4(target, "package.json"), "utf8"));
778
+ const installed = JSON.parse(await readFile3(join3(target, "package.json"), "utf8"));
830
779
  if (installed.version !== latest) throw new Error(`\u5B89\u88C5\u7248\u672C\u6821\u9A8C\u5931\u8D25\uFF1A\u671F\u671B ${latest}\uFF0C\u5B9E\u9645 ${installed.version ?? "\u672A\u77E5"}`);
831
780
  } catch (error) {
832
781
  let recovery = "";
package/dist/main.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  ConfigError,
4
4
  runDaemon
5
- } from "./chunk-OXA2S7UY.js";
5
+ } from "./chunk-HB3ETNMW.js";
6
6
 
7
7
  // src/main.ts
8
8
  runDaemon(process.env.OCTOPUS_PROXY_CONFIG ?? "proxy.json").catch((error) => {
@@ -29,6 +29,39 @@ static NSDictionary *displayInfo(CGDirectDisplayID displayId) {
29
29
  @"width": @(CGDisplayPixelsWide(displayId)), @"height": @(CGDisplayPixelsHigh(displayId))};
30
30
  }
31
31
 
32
+ static BOOL saveImage(CGImageRef image, NSString *path) {
33
+ if (!image) return NO;
34
+ CGImageDestinationRef destination = CGImageDestinationCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:path], CFSTR("public.png"), 1, NULL);
35
+ if (!destination) return NO;
36
+ CGImageDestinationAddImage(destination, image, NULL);
37
+ BOOL saved = CGImageDestinationFinalize(destination);
38
+ CFRelease(destination);
39
+ return saved;
40
+ }
41
+
42
+ static void finishImage(BOOL saved) {
43
+ dispatch_async(dispatch_get_main_queue(), ^{
44
+ if (finished) return;
45
+ if (saved) { resultCode = 0; finished = YES; }
46
+ else fail(@"无法保存桌面 PNG 截图,请检查 Proxy 状态目录。" );
47
+ });
48
+ }
49
+
50
+ #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 140000
51
+ static void captureImage(SCContentFilter *filter, SCStreamConfiguration *configuration, NSString *path) API_AVAILABLE(macos(14.0));
52
+ static void captureImage(SCContentFilter *filter, SCStreamConfiguration *configuration, NSString *path) {
53
+ // A stream may start successfully without delivering frames while the screen
54
+ // is locked. The screenshot API completes a single capture without that wait.
55
+ [SCScreenshotManager captureImageWithFilter:filter configuration:configuration completionHandler:^(CGImageRef image, NSError *error) {
56
+ if (error || !image) {
57
+ dispatch_async(dispatch_get_main_queue(), ^{ if (!finished) fail(permissionMessage()); });
58
+ return;
59
+ }
60
+ finishImage(saveImage(image, path));
61
+ }];
62
+ }
63
+ #endif
64
+
32
65
  @interface DesktopFrame : NSObject <SCStreamOutput, SCStreamDelegate>
33
66
  @property(nonatomic, strong) SCStream *stream;
34
67
  @property(nonatomic, copy) NSString *path;
@@ -49,23 +82,11 @@ static NSDictionary *displayInfo(CGDirectDisplayID displayId) {
49
82
  self.received = YES;
50
83
  CGImageRef image = NULL;
51
84
  OSStatus conversion = VTCreateCGImageFromCVPixelBuffer(pixel, NULL, &image);
52
- BOOL saved = NO;
53
- if (conversion == noErr && image) {
54
- CGImageDestinationRef destination = CGImageDestinationCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:self.path], CFSTR("public.png"), 1, NULL);
55
- if (destination) {
56
- CGImageDestinationAddImage(destination, image, NULL);
57
- saved = CGImageDestinationFinalize(destination);
58
- CFRelease(destination);
59
- }
60
- CGImageRelease(image);
61
- }
62
- [stream stopCaptureWithCompletionHandler:^(NSError *error) {
63
- dispatch_async(dispatch_get_main_queue(), ^{
64
- if (finished) return;
65
- if (saved) { resultCode = 0; finished = YES; }
66
- else fail(@"无法保存桌面 PNG 截图,请检查 Proxy 状态目录。" );
67
- });
68
- }];
85
+ BOOL saved = conversion == noErr && saveImage(image, self.path);
86
+ if (image) CGImageRelease(image);
87
+ // Stopping a stream is cleanup; a saved PNG must not depend on its callback.
88
+ [stream stopCaptureWithCompletionHandler:nil];
89
+ finishImage(saved);
69
90
  }
70
91
  @end
71
92
 
@@ -121,6 +142,12 @@ int main(int argc, const char *argv[]) {
121
142
  configuration.showsCursor = YES;
122
143
  configuration.queueDepth = 3;
123
144
  configuration.minimumFrameInterval = CMTimeMake(1, 30);
145
+ #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 140000
146
+ if (@available(macOS 14.0, *)) {
147
+ captureImage(filter, configuration, frame.path);
148
+ return;
149
+ }
150
+ #endif
124
151
  frame.stream = [[SCStream alloc] initWithFilter:filter configuration:configuration delegate:frame];
125
152
  NSError *outputError = nil;
126
153
  if (![frame.stream addStreamOutput:frame type:SCStreamOutputTypeScreen sampleHandlerQueue:dispatch_queue_create("octopus.desktop.frame", DISPATCH_QUEUE_SERIAL) error:&outputError]) {
@@ -63,25 +63,22 @@ static BOOL key(CGKeyCode code, CGEventFlags flags, const UniChar *text, size_t
63
63
  if (!down || !up) { if (down) CFRelease(down); if (up) CFRelease(up); return NO; }
64
64
  CGEventSetFlags(down, flags); CGEventSetFlags(up, flags);
65
65
  if (text) { CGEventKeyboardSetUnicodeString(down, length, text); CGEventKeyboardSetUnicodeString(up, length, text); }
66
- CGEventPost(kCGHIDEventTap, down); CGEventPost(kCGHIDEventTap, up);
66
+ CGEventPost(kCGHIDEventTap, down);
67
+ usleep(50000);
68
+ CGEventPost(kCGHIDEventTap, up);
67
69
  CFRelease(down); CFRelease(up);
68
- usleep(20000);
70
+ usleep(50000);
69
71
  return YES;
70
72
  }
71
73
 
72
- // The locked console owns keyboard input. Space reveals the password prompt,
73
- // and Backspace removes that Space if the field was already visible. Do not
74
- // depend on loginwindow exposing an Accessibility tree (some versions do not).
74
+ // Space reveals and focuses the locked console's password prompt. Allow the
75
+ // transition to finish before typing; no Accessibility discovery is required.
75
76
  static BOOL wakePasswordPrompt(void) {
76
- const CGKeyCode keys[] = {56, 49, 51};
77
- const useconds_t settle[] = {1000000, 500000, 200000};
78
- for (int i = 0; i < 3; i++) {
79
- BOOL locked = NO;
80
- if (!lockState(&locked)) return NO;
81
- if (!locked) return YES;
82
- if (!key(keys[i], 0, NULL, 0)) return NO;
83
- usleep(settle[i]);
84
- }
77
+ BOOL locked = NO;
78
+ if (!lockState(&locked)) return NO;
79
+ if (!locked) return YES;
80
+ if (!key(49, 0, NULL, 0)) return NO;
81
+ usleep(1000000);
85
82
  return YES;
86
83
  }
87
84
 
@@ -92,7 +89,7 @@ static BOOL lockedKey(CGKeyCode code, CGEventFlags flags, const UniChar *text, s
92
89
 
93
90
  static void printDiagnostics(BOOL locked) {
94
91
  // Read-only; no wake, focus, field contents, screenshots, or input events.
95
- printf("{\"locked\":%s,\"canUnlock\":%s,\"loginwindowFound\":%s,\"inputMethod\":\"space-backspace\"}",
92
+ printf("{\"locked\":%s,\"canUnlock\":%s,\"loginwindowFound\":%s,\"inputMethod\":\"space-password\"}",
96
93
  locked ? "true" : "false", AXIsProcessTrusted() && CGPreflightPostEventAccess() ? "true" : "false",
97
94
  loginWindowPID() ? "true" : "false");
98
95
  }
@@ -118,8 +115,6 @@ int main(int argc, const char *argv[]) {
118
115
  if (!lockState(&locked)) return fail("session");
119
116
  // Saver dismissal, Touch ID or a manual unlock may finish during wake.
120
117
  if (!locked) { printStatus(NO); return 0; }
121
- if (!lockedKey(0, kCGEventFlagMaskCommand, NULL, 0) ||
122
- !lockedKey(51, 0, NULL, 0)) return fail("changed");
123
118
  UniChar characters[1024] = {0};
124
119
  NSUInteger length = password.length;
125
120
  if (length > 1024) return fail("password");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skylandnpm/octopus-proxy",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
4
4
  "type": "module",
5
5
  "description": "Octopus host proxy: OpenCode, Codex, ZCode and Pi adapters with service installer CLI",
6
6
  "bin": {