@skylandnpm/octopus-proxy 0.1.64 → 0.1.66

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
@@ -60,7 +60,7 @@ octopus-proxy restart
60
60
 
61
61
  Claw 完成主机信任和账号认证后,通过端到端会话向 Proxy 发送只读 `runtime.snapshot` 命令。响应包含当前运行状态、Hub 和工具设置、授权项目路径,以及工具可提供的模型信息;机器人 Secret、工具密码和提供商凭据不会返回。工作区的“Proxy 信息”按钮可查看这些内容。
62
62
 
63
- Codex 和 ZCode 由 Proxy 按授权项目启动各自的 app-server,默认随 Proxy 自动启动;服务未就绪时也可在 Claw 点击“启动服务”。若命令不可执行或某个项目启动失败,工作区会显示失败项目及原因。修改命令路径或授权项目后,运行 `octopus-proxy restart` 使配置生效。
63
+ Codex 和 ZCode 由 Proxy 按授权项目启动各自的 app-server,默认随 Proxy 自动启动;服务未就绪时也可在 Claw 点击“启动服务”。若命令不可执行或某个项目启动失败,工作区会显示失败项目及原因。通过 `onboard` 或配置文件修改授权项目后,Claw 打开、切换或刷新工作区项目时会读取最新项目配置;目录变化会释放该项目的旧运行资源和终端,并刷新文件面板。Proxy 不轮询配置文件,也不因主机配置变化主动推送刷新通知。Claw 也可在授权项目菜单中点击“修改项目”,修改“Proxy 主机上的目录”并保存;Proxy 校验目录存在且未重复授权后写入主机配置,Claw 随即刷新工作区,无需重启 Proxy。修改工具命令、机器人凭据等其他设置后,仍需运行 `octopus-proxy restart`。
64
64
 
65
65
  ## Claw 设备配对与撤销
66
66
 
@@ -123,6 +123,30 @@ octopus-proxy update
123
123
 
124
124
  安装或版本校验失败时不会触发服务重启;包安装失败不保证旧版本文件完整保留。已安装新版本但重启失败时会单独提示,可运行 `octopus-proxy doctor` 和 `octopus-proxy logs` 排查。此命令用于 npm 全局安装,覆盖安装前会检查 npm 安装目录是否与当前命令一致;源码、本地安装或 Node.js 环境不一致时会中止并给出提示。
125
125
 
126
+ ## Linux 服务在线,但终端显示“未运行、未启用”
127
+
128
+ 后台服务创建的终端可能没有登录会话中的 `XDG_RUNTIME_DIR`。此时 `systemctl --user` 会报 `Failed to connect to bus: No medium found`,不能据此判断服务已停止。CLI 会在未显式配置用户会话环境时,检查 `/run/user/<当前 UID>` 是否存在且属于当前用户,并用于服务查询和控制;查询失败会报告错误。此检查也支持只有 `systemd/private`、没有会话 `bus` socket 的环境。
129
+
130
+ 旧版可先验证:
131
+
132
+ ```sh
133
+ XDG_RUNTIME_DIR=/run/user/$(id -u) octopus-proxy status
134
+ ```
135
+
136
+ 如果显示正常,可在该服务账号的 `~/.bashrc` 中补上以下设置,使 Claw 新建的 Bash 终端也能查询用户服务:
137
+
138
+ ```sh
139
+ if [ -z "${XDG_RUNTIME_DIR:-}" ] && [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then
140
+ octopus_runtime_dir="/run/user/$(id -u)"
141
+ if [ -d "$octopus_runtime_dir" ] && [ -O "$octopus_runtime_dir" ]; then
142
+ export XDG_RUNTIME_DIR="$octopus_runtime_dir"
143
+ fi
144
+ unset octopus_runtime_dir
145
+ fi
146
+ ```
147
+
148
+ 重新打开终端或运行 `source ~/.bashrc` 后验证 `octopus-proxy status`。如果对应用户运行目录或用户服务管理器不存在,仍需检查主机的登录会话和 linger 配置。
149
+
126
150
  ## 服务启动时报 `Unexpected token '??='`
127
151
 
128
152
  这通常表示服务实际使用了旧版 Node.js。错误栈中 `.nvm/versions/node/v22.../lib/node_modules` 是包的安装位置,不能说明运行它的 Node.js 版本。安装器只会选择 Node.js 22 或更高版本;旧版安装器可能优先选中能执行 `--version` 的旧系统 Node。
@@ -12,7 +12,7 @@ import {
12
12
  probeRemote,
13
13
  remoteSessionBudget,
14
14
  z
15
- } from "./chunk-GS3Q6EU5.js";
15
+ } from "./chunk-UFLXQ4HQ.js";
16
16
 
17
17
  // ../../node_modules/.pnpm/ajv@8.17.1/node_modules/ajv/dist/compile/codegen/code.js
18
18
  var require_code = __commonJS({
@@ -11616,7 +11616,13 @@ function standaloneZcodeSource(source) {
11616
11616
  throw new Error("Unsupported ZCode account bootstrap; native compatibility check failed");
11617
11617
  }
11618
11618
  body = body.replace(registryCall, `${registry}($1,{standalone:{}})`).replace(nativePorts, "$&providerRuntimeHeadersPort:$1.providerRuntimeHeadersPort,");
11619
- return source.slice(0, start) + body + source.slice(end);
11619
+ source = source.slice(0, start) + body + source.slice(end);
11620
+ const close = /\br\(([$\w]+),"closeSession"\)/.exec(source)?.[1];
11621
+ if (!close) throw new Error("Unsupported ZCode bundle: session close is unavailable");
11622
+ const escapedClose = close.replace(/[$]/g, "\\$&");
11623
+ const closeBody = new RegExp(`async function ${escapedClose}\\(([$\\w]+),([$\\w]+)\\)\\{[^}]*?await ([$\\w]+)\\.eventStore\\.deleteSession\\(([$\\w]+)\\.sessionId\\),\\{closed:!0\\}`, "g");
11624
+ if ([...source.matchAll(closeBody)].length !== 1) throw new Error("Unsupported ZCode session close; native compatibility check failed");
11625
+ return source.replace(closeBody, (match, context, _params, record2, parsed) => match.replace(",{closed:!0}", `,${record2}.persistence!=="deferred"&&await ${context}.deps.sessionStore.updateSession({id:${parsed}.sessionId,timeArchived:Date.now()}),{closed:!0}`));
11620
11626
  }
11621
11627
  function zcodeServerLaunch(command, args) {
11622
11628
  if (basename4(command) !== "zcode.cjs") return { command, args: [...args] };
@@ -13613,6 +13619,7 @@ function createAdapter(runtime, workspace, uploads, emit) {
13613
13619
  // src/project-registry.ts
13614
13620
  import { realpath as realpath6, readFile as readFile9, stat as stat7 } from "node:fs/promises";
13615
13621
  import { isAbsolute as isAbsolute4 } from "node:path";
13622
+ import { isDeepStrictEqual } from "node:util";
13616
13623
  var ProjectMutationSchema = ProjectSchema.extend({
13617
13624
  name: ProjectSchema.shape.name.trim().max(120),
13618
13625
  specDirectories: ProjectSchema.shape.specDirectories.removeDefault().element.min(1).array().max(20).default(["specs", "docs"])
@@ -13628,13 +13635,21 @@ function rawProjectId(value) {
13628
13635
  return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.id === "string" ? value.id : void 0;
13629
13636
  }
13630
13637
  var ProjectRegistry = class {
13631
- constructor(configPath, config, runtime, adapter) {
13638
+ constructor(configPath, config, runtime, adapter, onChanged = () => {
13639
+ }) {
13632
13640
  this.configPath = configPath;
13633
13641
  this.config = config;
13634
13642
  this.runtime = runtime;
13635
13643
  this.adapter = adapter;
13644
+ this.onChanged = onChanged;
13636
13645
  }
13637
13646
  pending = Promise.resolve();
13647
+ // The daemon runs this under the same lock as tool selection and project mutations.
13648
+ reload() {
13649
+ return this.serial(async () => {
13650
+ await this.readCurrent();
13651
+ });
13652
+ }
13638
13653
  add(payload) {
13639
13654
  return this.serial(() => this.addUnlocked(payload));
13640
13655
  }
@@ -13658,17 +13673,36 @@ var ProjectRegistry = class {
13658
13673
  }
13659
13674
  async readCurrent() {
13660
13675
  const current = rawConfig(JSON.parse(await readFile9(this.configPath, "utf8")));
13661
- const diskProjects = current.projects.map((value) => {
13662
- const project = ProjectSchema.parse(value);
13663
- return { ...project, directory: expandHome(project.directory) };
13664
- });
13665
- if (JSON.stringify(diskProjects) !== JSON.stringify(this.config.projects)) {
13666
- throw new Error("Proxy \u914D\u7F6E\u5DF2\u5728\u4E3B\u673A\u4E0A\u88AB\u4FEE\u6539\uFF1B\u8BF7\u91CD\u542F Proxy \u540E\u518D\u7BA1\u7406\u9879\u76EE");
13667
- }
13676
+ const diskProjects = ConfigSchema.shape.projects.parse(current.projects).map((project) => ({ ...project, directory: expandHome(project.directory) }));
13677
+ if (new Set(diskProjects.map((project) => project.id)).size !== diskProjects.length) throw new Error("\u9879\u76EE ID \u91CD\u590D");
13678
+ if (diskProjects.some((project) => !isAbsolute4(project.directory))) throw new Error("\u9879\u76EE\u76EE\u5F55\u5FC5\u987B\u662F Proxy \u4E3B\u673A\u4E0A\u7684\u7EDD\u5BF9\u8DEF\u5F84");
13679
+ await this.applyProjects(diskProjects);
13668
13680
  return current;
13669
13681
  }
13682
+ async persist(expected, projects) {
13683
+ const latest = rawConfig(JSON.parse(await readFile9(this.configPath, "utf8")));
13684
+ if (!isDeepStrictEqual(latest.projects, expected.projects)) {
13685
+ throw new Error("\u4E3B\u673A\u4E0A\u7684\u9879\u76EE\u914D\u7F6E\u5728\u4FDD\u5B58\u671F\u95F4\u53D1\u751F\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5");
13686
+ }
13687
+ await atomicJson(this.configPath, { ...latest, projects });
13688
+ }
13689
+ async applyProjects(projects) {
13690
+ if (isDeepStrictEqual(projects, this.config.projects)) return;
13691
+ const reset = this.config.projects.filter((previous) => !projects.some((project) => project.id === previous.id && project.directory === previous.directory));
13692
+ const added = projects.filter((project) => !this.config.projects.some((previous) => previous.id === project.id && previous.directory === project.directory));
13693
+ for (const project of reset) {
13694
+ await Promise.all([
13695
+ this.bestEffort(() => this.adapter.removeProject?.(project.id)),
13696
+ this.bestEffort(() => this.runtime.removeProject?.(project.id))
13697
+ ]);
13698
+ }
13699
+ this.config.projects.splice(0, this.config.projects.length, ...projects);
13700
+ for (const project of added) await this.bestEffort(() => this.runtime.addProject?.(project.id));
13701
+ this.onChanged(reset.map((project) => project.id));
13702
+ }
13670
13703
  async addUnlocked(payload) {
13671
13704
  const parsed = ProjectMutationSchema.parse(payload);
13705
+ const raw = await this.readCurrent();
13672
13706
  const directory = expandHome(parsed.directory);
13673
13707
  if (!isAbsolute4(directory)) throw new Error("\u9879\u76EE\u76EE\u5F55\u5FC5\u987B\u662F Proxy \u4E3B\u673A\u4E0A\u7684\u7EDD\u5BF9\u8DEF\u5F84");
13674
13708
  const info = await stat7(directory).catch(() => void 0);
@@ -13679,14 +13713,13 @@ var ProjectRegistry = class {
13679
13713
  if (await realpath6(project2.directory).catch(() => project2.directory) === canonical) throw new Error("\u8BE5\u76EE\u5F55\u5DF2\u7ECF\u662F\u6388\u6743\u9879\u76EE");
13680
13714
  }
13681
13715
  const project = { ...parsed, directory: canonical };
13682
- const raw = await this.readCurrent();
13683
- await atomicJson(this.configPath, { ...raw, projects: [...raw.projects, project] });
13684
- this.config.projects.push(project);
13685
- await this.bestEffort(() => this.runtime.addProject?.(project.id));
13716
+ await this.persist(raw, [...raw.projects, project]);
13717
+ await this.applyProjects([...this.config.projects, project]);
13686
13718
  return publicProjects(this.config);
13687
13719
  }
13688
13720
  async updateUnlocked(payload) {
13689
13721
  const parsed = ProjectUpdateSchema.parse(payload);
13722
+ const raw = await this.readCurrent();
13690
13723
  const index = this.config.projects.findIndex((project) => project.id === parsed.id);
13691
13724
  if (index < 0) throw new Error(`\u672A\u77E5\u6388\u6743\u9879\u76EE\uFF1A${parsed.id}`);
13692
13725
  const directory = expandHome(parsed.directory);
@@ -13700,21 +13733,8 @@ var ProjectRegistry = class {
13700
13733
  }
13701
13734
  }
13702
13735
  const current = this.config.projects[index];
13703
- const oldDirectory = await realpath6(current.directory).catch(() => current.directory);
13704
- const directoryChanged = oldDirectory !== canonical;
13705
- const raw = await this.readCurrent();
13706
- await atomicJson(this.configPath, {
13707
- ...raw,
13708
- projects: raw.projects.map((project) => rawProjectId(project) === parsed.id ? { ...project, name: parsed.name, directory: canonical } : project)
13709
- });
13710
- if (directoryChanged) {
13711
- await Promise.all([
13712
- this.bestEffort(() => this.adapter.removeProject?.(parsed.id)),
13713
- this.bestEffort(() => this.runtime.removeProject?.(parsed.id))
13714
- ]);
13715
- }
13716
- this.config.projects[index] = { ...current, name: parsed.name, directory: canonical };
13717
- if (directoryChanged) await this.bestEffort(() => this.runtime.addProject?.(parsed.id));
13736
+ await this.persist(raw, raw.projects.map((project) => rawProjectId(project) === parsed.id ? { ...project, name: parsed.name, directory: canonical } : project));
13737
+ await this.applyProjects(this.config.projects.map((project) => project.id === parsed.id ? { ...current, name: parsed.name, directory: canonical } : project));
13718
13738
  return publicProjects(this.config);
13719
13739
  }
13720
13740
  async removeUnlocked(payload) {
@@ -13723,16 +13743,12 @@ var ProjectRegistry = class {
13723
13743
  throw new Error("\u5220\u9664\u9879\u76EE\u9700\u8981\u5408\u6CD5\u7684\u9879\u76EE ID");
13724
13744
  }
13725
13745
  const id = payload.id;
13746
+ const raw = await this.readCurrent();
13726
13747
  if (this.config.projects.length === 1) throw new Error("\u81F3\u5C11\u9700\u8981\u4FDD\u7559\u4E00\u4E2A\u6388\u6743\u9879\u76EE");
13727
13748
  const index = this.config.projects.findIndex((project) => project.id === id);
13728
13749
  if (index < 0) throw new Error(`\u672A\u77E5\u6388\u6743\u9879\u76EE\uFF1A${id}`);
13729
- const raw = await this.readCurrent();
13730
- await atomicJson(this.configPath, { ...raw, projects: raw.projects.filter((project) => rawProjectId(project) !== id) });
13731
- await Promise.all([
13732
- this.bestEffort(() => this.adapter.removeProject?.(id)),
13733
- this.bestEffort(() => this.runtime.removeProject?.(id))
13734
- ]);
13735
- this.config.projects.splice(index, 1);
13750
+ await this.persist(raw, raw.projects.filter((project) => rawProjectId(project) !== id));
13751
+ await this.applyProjects(this.config.projects.filter((project) => project.id !== id));
13736
13752
  return publicProjects(this.config);
13737
13753
  }
13738
13754
  };
@@ -13889,6 +13905,13 @@ var TerminalManager = class {
13889
13905
  terminal.process.kill();
13890
13906
  }
13891
13907
  }
13908
+ closeProject(project) {
13909
+ for (const terminal of this.terminals.values()) if (terminal.project === project) {
13910
+ this.terminals.delete(terminal.id);
13911
+ terminal.process.kill();
13912
+ this.emit(terminal.owner, project, "terminal.closed", { id: terminal.id });
13913
+ }
13914
+ }
13892
13915
  close() {
13893
13916
  for (const owner of new Set([...this.terminals.values()].map((t) => t.owner))) this.closeOwned(owner);
13894
13917
  }
@@ -13898,17 +13921,30 @@ var TerminalManager = class {
13898
13921
  import { execFile as execFile6 } from "node:child_process";
13899
13922
  import { promisify as promisify6 } from "node:util";
13900
13923
  import { userInfo as userInfo4 } from "node:os";
13924
+ import { lstat as lstat3 } from "node:fs/promises";
13901
13925
  var exec = promisify6(execFile6);
13902
13926
  var PlatformDesktopSession = class {
13903
- constructor(system = process.platform, environment = process.env, execute3 = async (args) => (await exec("loginctl", ["--no-ask-password", ...args], { timeout: 5e3, maxBuffer: 16384 })).stdout, account = userInfo4()) {
13927
+ constructor(x11Session = "logind", system = process.platform, environment = process.env, execute3 = async (args) => (await exec("loginctl", ["--no-ask-password", ...args], { timeout: 5e3, maxBuffer: 16384 })).stdout, account = userInfo4(), inspectSocket = lstat3) {
13928
+ this.x11Session = x11Session;
13904
13929
  this.system = system;
13905
13930
  this.environment = environment;
13906
13931
  this.execute = execute3;
13907
13932
  this.account = account;
13933
+ this.inspectSocket = inspectSocket;
13908
13934
  }
13909
13935
  async sessionIdentity() {
13910
13936
  if (this.system === "darwin") return String(this.account.uid);
13911
13937
  if (this.system !== "linux") throw new Error("\u6B64\u5E73\u53F0\u4E0D\u652F\u6301\u8FDC\u7A0B\u684C\u9762\u4F1A\u8BDD");
13938
+ if (this.x11Session === "standalone") {
13939
+ const display = /^(?:unix)?:([0-9]+)(?:\.([0-9]+))?$/.exec(this.environment.DISPLAY ?? "");
13940
+ if (!display || this.account.uid === 0 || this.environment.WAYLAND_DISPLAY || this.environment.XDG_SESSION_TYPE === "wayland") throw new Error("\u72EC\u7ACB X11 \u8FDC\u63A7\u9700\u8981\u975E root \u7528\u6237\u548C\u672C\u5730 DISPLAY\uFF08\u5982 :99\uFF09\uFF0C\u4E0D\u652F\u6301 Wayland \u6216\u8FDC\u7A0B X11");
13941
+ const path = `/tmp/.X11-unix/X${Number(display[1])}`;
13942
+ const socket = await this.inspectSocket(path).catch(() => {
13943
+ throw new Error(`\u72EC\u7ACB X11 socket \u4E0D\u53EF\u7528\uFF1A${path}\uFF1B\u8BF7\u5148\u542F\u52A8 Xvfb/\u684C\u9762\u670D\u52A1`);
13944
+ });
13945
+ if (!socket.isSocket()) throw new Error(`\u72EC\u7ACB X11 \u8DEF\u5F84\u4E0D\u662F socket\uFF1A${path}`);
13946
+ return `x11:${this.account.uid}:${Number(display[1])}.${Number(display[2] ?? 0)}:${socket.dev}:${socket.ino}:${socket.ctimeMs}`;
13947
+ }
13912
13948
  const ids = this.environment.XDG_SESSION_ID ? [this.environment.XDG_SESSION_ID] : (await this.execute(["list-sessions", "--no-legend"])).trim().split("\n").map((line) => line.trim().split(/\s+/)[0]).filter(Boolean);
13913
13949
  const sessions = [];
13914
13950
  for (const id of ids) {
@@ -14021,6 +14057,7 @@ var RemoteManager = class {
14021
14057
  const permissions = await this.probe(this.host.stateDirectory, this.config);
14022
14058
  const lockUnsupported = permissions.locked === true && permissions.lockCapture === false;
14023
14059
  const available = permissions.session && permissions.capture && permissions.input && !lockUnsupported;
14060
+ if (available) await this.desktopSession.sessionIdentity();
14024
14061
  return { ...base, available, permissions, ...!available ? { message: lockUnsupported ? "\u9501\u5C4F\u8FDC\u63A7\u9700\u8981 macOS 14 \u53CA\u4EE5\u4E0A\u53CA\u65B0\u7248 Xcode Command Line Tools\uFF1B\u5F53\u524D\u4E3B\u673A\u8BF7\u5148\u672C\u5730\u89E3\u9501" : "\u8BF7\u68C0\u67E5\u4E3B\u673A\u56FE\u5F62\u4F1A\u8BDD\uFF0C\u5E76\u4E3A\u5B9E\u9645\u8FD0\u884C Proxy \u7684\u7A0B\u5E8F\u6388\u6743\u5C4F\u5E55\u5F55\u5236\u548C\u8F85\u52A9\u529F\u80FD" } : {} };
14025
14062
  } catch (error) {
14026
14063
  return { ...base, available: false, message: error instanceof Error ? error.message : "\u8FDC\u63A7\u4F9D\u8D56\u4E0D\u53EF\u7528" };
@@ -14139,7 +14176,7 @@ var RemoteManager = class {
14139
14176
  import { randomBytes as randomBytes3, randomUUID as randomUUID19 } from "node:crypto";
14140
14177
  import { access as access3, constants as constants6, readFile as readFile10 } from "node:fs/promises";
14141
14178
  import { platform as platform5 } from "node:os";
14142
- import { isDeepStrictEqual } from "node:util";
14179
+ import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
14143
14180
 
14144
14181
  // src/node-resolution.ts
14145
14182
  import { realpath as realpath7, stat as stat10 } from "node:fs/promises";
@@ -14319,7 +14356,7 @@ var ToolSelection = class {
14319
14356
  }
14320
14357
  async readCurrent() {
14321
14358
  const disk = await loadConfig(this.configPath);
14322
- 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) {
14359
+ if (!isDeepStrictEqual2(JSON.parse(JSON.stringify(disk.tool)), JSON.parse(JSON.stringify(this.config.tool))) || !isDeepStrictEqual2(disk.projects, this.config.projects) || disk.botId !== this.config.botId || disk.secretKey !== this.config.secretKey) {
14323
14360
  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");
14324
14361
  }
14325
14362
  return JSON.parse(await readFile10(this.configPath, "utf8"));
@@ -14346,7 +14383,7 @@ async function runDaemon(configPath, authenticate = verifyHostPassword) {
14346
14383
  secureChat?.publish(projectId, event, data);
14347
14384
  }
14348
14385
  const terminals = new TerminalManager(workspace, (owner, project, event, data) => secureChat?.sendTo(owner, project, event, data));
14349
- const desktopBackend = new PlatformDesktop(config.stateDirectory), desktopSession = new PlatformDesktopSession();
14386
+ const desktopBackend = new PlatformDesktop(config.stateDirectory), desktopSession = new PlatformDesktopSession(config.desktopRemote?.x11Session);
14350
14387
  const desktop = new DesktopManager(desktopBackend);
14351
14388
  const remote = new RemoteManager(
14352
14389
  config,
@@ -14363,7 +14400,12 @@ async function runDaemon(configPath, authenticate = verifyHostPassword) {
14363
14400
  return { runtime, adapter };
14364
14401
  });
14365
14402
  const artifacts = new Artifacts(workspace, join15(config.stateDirectory, "artifacts"), emit);
14403
+ const projectRegistry = () => new ProjectRegistry(configPath, config, tools.current.runtime, tools.current.adapter, (resetProjects) => {
14404
+ for (const id of resetProjects) terminals.closeProject(id);
14405
+ });
14406
+ const reloadProjects = () => tools.exclusive(() => projectRegistry().reload());
14366
14407
  async function dispatch(c, secureSessionId) {
14408
+ if (["runtime.snapshot", "workspace.projects", "runtime.models", "runtime.switchTool"].includes(c.operation)) await reloadProjects();
14367
14409
  if (c.operation.startsWith("desktop.remote.")) return remote.dispatch(c, secureSessionId);
14368
14410
  if (c.operation.startsWith("desktop.")) return desktop.dispatch(c, secureSessionId);
14369
14411
  if (c.operation.startsWith("terminal.")) return terminals.dispatch(c, secureSessionId);
@@ -14383,17 +14425,17 @@ async function runDaemon(configPath, authenticate = verifyHostPassword) {
14383
14425
  if (c.operation === "runtime.restart") return runtime.restart();
14384
14426
  if (c.operation === "workspace.projects") return publicProjects(config);
14385
14427
  if (c.operation === "workspace.projects.add") {
14386
- const result2 = await tools.exclusive(() => new ProjectRegistry(configPath, config, tools.current.runtime, tools.current.adapter).add(p));
14428
+ const result2 = await tools.exclusive(() => projectRegistry().add(p));
14387
14429
  emit(String(p.id), "octopus.resync", {});
14388
14430
  return result2;
14389
14431
  }
14390
14432
  if (c.operation === "workspace.projects.update") {
14391
- const result2 = await tools.exclusive(() => new ProjectRegistry(configPath, config, tools.current.runtime, tools.current.adapter).update(p));
14433
+ const result2 = await tools.exclusive(() => projectRegistry().update(p));
14392
14434
  emit(String(p.id), "octopus.resync", {});
14393
14435
  return result2;
14394
14436
  }
14395
14437
  if (c.operation === "workspace.projects.remove") {
14396
- const result2 = await tools.exclusive(() => new ProjectRegistry(configPath, config, tools.current.runtime, tools.current.adapter).remove(p));
14438
+ const result2 = await tools.exclusive(() => projectRegistry().remove(p));
14397
14439
  emit(String(p.id), "octopus.resync", {});
14398
14440
  return result2;
14399
14441
  }
@@ -4104,7 +4104,8 @@ var RemoteConfigSchema = z.object({
4104
4104
  stunUrls: z.array(stunUrl).min(1).max(4).optional(),
4105
4105
  totalBitrate: z.number().int().min(1e5).max(REMOTE_DEFAULTS.totalBitrate).default(REMOTE_DEFAULTS.totalBitrate),
4106
4106
  videoMaxBitrate: z.number().int().min(5e4).max(REMOTE_DEFAULTS.videoMaxBitrate).default(REMOTE_DEFAULTS.videoMaxBitrate),
4107
- ffmpeg: z.string().min(1).default("ffmpeg")
4107
+ ffmpeg: z.string().min(1).default("ffmpeg"),
4108
+ x11Session: z.enum(["logind", "standalone"]).default("logind")
4108
4109
  }).strict().refine((c) => c.videoMaxBitrate <= c.totalBitrate * 0.8, "\u89C6\u9891\u989D\u5EA6\u4E0D\u5F97\u8D85\u8FC7\u603B\u9884\u7B97\u7684 80%");
4109
4110
  function remoteSessionBudget(config, quality, legacy = false) {
4110
4111
  const profile = legacy ? REMOTE_LEGACY_PROFILE : REMOTE_QUALITY_PROFILES[quality];
@@ -4456,8 +4457,12 @@ import { join as join2 } from "node:path";
4456
4457
  import { fileURLToPath as fileURLToPath2 } from "node:url";
4457
4458
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
4458
4459
  var exec = promisify2(execFile2);
4459
- function supportedRemotePlatform(system = process.platform, arch = process.arch, env = process.env) {
4460
- return system === "darwin" && ["arm64", "x64"].includes(arch) || system === "linux" && arch === "x64" && !!env.DISPLAY && !env.WAYLAND_DISPLAY && env.XDG_SESSION_TYPE !== "wayland";
4460
+ function remotePlatformError(system = process.platform, arch = process.arch, env = process.env) {
4461
+ if (!["darwin", "linux"].includes(system) || !["arm64", "x64"].includes(arch)) return `\u8FDC\u63A7\u4E0D\u652F\u6301\u5F53\u524D\u5E73\u53F0 ${system}/${arch}\uFF1B\u9700\u8981 macOS 12.3+ \u6216 Linux X11\uFF08x64/ARM64\uFF09`;
4462
+ if (system === "linux") {
4463
+ if (env.WAYLAND_DISPLAY || env.XDG_SESSION_TYPE === "wayland") return "\u8FDC\u63A7\u6682\u4E0D\u652F\u6301 Wayland\uFF0C\u8BF7\u4F7F\u7528 X11 \u684C\u9762\u4F1A\u8BDD";
4464
+ if (!env.DISPLAY) return "Proxy \u672A\u8FDE\u63A5 X11 \u56FE\u5F62\u4F1A\u8BDD\uFF1B\u8BF7\u4E3A\u5B9E\u9645\u8FD0\u884C Proxy \u7684\u670D\u52A1\u8BBE\u7F6E\u684C\u9762\u7528\u6237\u7684 DISPLAY \u548C XAUTHORITY\uFF0C\u5BB9\u5668\u4E2D\u8BF7\u5148\u542F\u52A8 Xvfb/\u684C\u9762\u670D\u52A1";
4465
+ }
4461
4466
  }
4462
4467
  async function ensureRemoteHelper(stateDirectory) {
4463
4468
  await mkdir2(stateDirectory, { recursive: true, mode: 448 });
@@ -4485,7 +4490,8 @@ async function ensureRemoteHelper(stateDirectory) {
4485
4490
  }
4486
4491
  }
4487
4492
  async function probeRemote(stateDirectory, config) {
4488
- if (!supportedRemotePlatform()) throw new Error("\u8FDC\u63A7\u9996\u7248\u652F\u6301 macOS 12.3+ \u4E0E Linux X11 x64\uFF1BWayland\u3001\u65E0\u56FE\u5F62\u4F1A\u8BDD\u6682\u4E0D\u652F\u6301");
4493
+ const platformError = remotePlatformError();
4494
+ if (platformError) throw new Error(platformError);
4489
4495
  await import("@roamhq/wrtc").catch(() => {
4490
4496
  throw new Error("WebRTC \u539F\u751F\u4F9D\u8D56\u4E0D\u53EF\u7528\uFF0C\u8BF7\u91CD\u65B0\u5B89\u88C5\u65B0\u7248 Proxy");
4491
4497
  });
package/dist/cli.js CHANGED
@@ -35,20 +35,20 @@ import {
35
35
  supportedPlatform,
36
36
  systemdUnitPath,
37
37
  toolSearchDirs
38
- } from "./chunk-J3ZMMSW2.js";
38
+ } from "./chunk-IV5ZGRAM.js";
39
39
  import {
40
40
  DESKTOP_PERMISSION_GUIDANCE,
41
41
  PlatformDesktop,
42
42
  checkDesktop,
43
43
  requestDesktopPermission
44
- } from "./chunk-GS3Q6EU5.js";
44
+ } from "./chunk-UFLXQ4HQ.js";
45
45
 
46
46
  // src/cli.ts
47
47
  import { spawn as spawn2 } from "node:child_process";
48
48
  import { access as access4 } from "node:fs/promises";
49
49
 
50
50
  // src/doctor.ts
51
- import { access as access2, chmod, constants, mkdir as mkdir3, stat } from "node:fs/promises";
51
+ import { access as access2, chmod, constants, mkdir as mkdir3, stat as stat2 } from "node:fs/promises";
52
52
  import { isAbsolute as isAbsolute3, join } from "node:path";
53
53
  import { execFile as execFile3 } from "node:child_process";
54
54
  import { promisify as promisify3 } from "node:util";
@@ -130,8 +130,9 @@ async function launchdStatus() {
130
130
  }
131
131
 
132
132
  // src/systemd.ts
133
- import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
133
+ import { mkdir as mkdir2, rm as rm2, stat, writeFile as writeFile2 } from "node:fs/promises";
134
134
  import { dirname as dirname2 } from "node:path";
135
+ import { userInfo as userInfo2 } from "node:os";
135
136
  import { execFile as execFile2 } from "node:child_process";
136
137
  import { promisify as promisify2 } from "node:util";
137
138
  var run2 = promisify2(execFile2);
@@ -170,8 +171,15 @@ async function writeUnitFile(text) {
170
171
  return path;
171
172
  }
172
173
  async function systemctl(args) {
174
+ const env = { ...process.env };
175
+ if (!env.XDG_RUNTIME_DIR && !env.DBUS_SESSION_BUS_ADDRESS) {
176
+ const uid = userInfo2().uid;
177
+ const runtimeDirectory = `/run/user/${uid}`;
178
+ const info = await stat(runtimeDirectory).catch(() => void 0);
179
+ if (info?.isDirectory() && info.uid === uid) env.XDG_RUNTIME_DIR = runtimeDirectory;
180
+ }
173
181
  try {
174
- const { stdout: stdout3 } = await run2("systemctl", ["--user", ...args]);
182
+ const { stdout: stdout3 } = await run2("systemctl", ["--user", ...args], { env });
175
183
  return stdout3;
176
184
  } catch (error) {
177
185
  const stderr = error instanceof Error ? error.stderr ?? error.message : String(error);
@@ -214,9 +222,9 @@ async function systemdUninstall() {
214
222
  await systemdReload();
215
223
  }
216
224
  async function systemdStatus() {
217
- const active = await ignoreFailure(() => systemctl(["is-active", SYSTEMD_UNIT]));
218
- const enabled = await ignoreFailure(() => systemctl(["is-enabled", SYSTEMD_UNIT]));
219
- return { active: active?.trim() === "active", enabled: enabled?.trim() === "enabled" };
225
+ const output = await systemctl(["show", SYSTEMD_UNIT, "--property=ActiveState", "--property=UnitFileState"]);
226
+ const properties = Object.fromEntries(output.trim().split("\n").map((line) => line.split("=")));
227
+ return { active: properties.ActiveState === "active", enabled: properties.UnitFileState === "enabled" };
220
228
  }
221
229
 
222
230
  // src/service.ts
@@ -339,7 +347,7 @@ async function runDoctor(opts) {
339
347
  results.push(await checkTerminalSupport(opts.fix));
340
348
  let config;
341
349
  try {
342
- const info = await stat(opts.configPath).catch((error) => {
350
+ const info = await stat2(opts.configPath).catch((error) => {
343
351
  if (error.code === "ENOENT") throw new Error(`\u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${opts.configPath}\uFF1B\u5148\u8FD0\u884C octopus-proxy onboard`);
344
352
  throw error;
345
353
  });
@@ -356,7 +364,7 @@ async function runDoctor(opts) {
356
364
  }
357
365
  if (config) {
358
366
  results.push(...await checkDesktop(config.stateDirectory));
359
- const remote = new RemoteManager(config, new PlatformDesktop(config.stateDirectory), new PlatformDesktopSession(), () => {
367
+ const remote = new RemoteManager(config, new PlatformDesktop(config.stateDirectory), new PlatformDesktopSession(config.desktopRemote?.x11Session), () => {
360
368
  }, () => false);
361
369
  try {
362
370
  const status = await remote.status();
@@ -457,7 +465,7 @@ async function runDoctor(opts) {
457
465
  import { createInterface } from "node:readline/promises";
458
466
  import { randomBytes } from "node:crypto";
459
467
  import { stdin, stdout } from "node:process";
460
- import { access as access3, constants as constants2, mkdir as mkdir4, stat as stat2 } from "node:fs/promises";
468
+ import { access as access3, constants as constants2, mkdir as mkdir4, stat as stat3 } from "node:fs/promises";
461
469
  import { execFile as execFile4 } from "node:child_process";
462
470
  import { platform } from "node:os";
463
471
  import { isAbsolute as isAbsolute4, join as join2 } from "node:path";
@@ -501,7 +509,7 @@ async function validateToolCommand(value, kind) {
501
509
  const command2 = expandHome(value);
502
510
  if (!isAbsolute4(command2)) return "\u670D\u52A1\u73AF\u5883\u9700\u8981\u7EDD\u5BF9\u8DEF\u5F84";
503
511
  try {
504
- if (!(await stat2(command2)).isFile()) return "\u547D\u4EE4\u8DEF\u5F84\u5FC5\u987B\u6307\u5411\u53EF\u6267\u884C\u6587\u4EF6";
512
+ if (!(await stat3(command2)).isFile()) return "\u547D\u4EE4\u8DEF\u5F84\u5FC5\u987B\u6307\u5411\u53EF\u6267\u884C\u6587\u4EF6";
505
513
  await access3(command2, constants2.X_OK);
506
514
  } catch {
507
515
  return `\u547D\u4EE4\u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u6267\u884C\uFF1A${command2}`;
@@ -514,7 +522,7 @@ async function validateToolCommand(value, kind) {
514
522
  return void 0;
515
523
  }
516
524
  async function runOnboard(opts) {
517
- const configExists = await stat2(opts.configPath).then(() => true, (error) => {
525
+ const configExists = await stat3(opts.configPath).then(() => true, (error) => {
518
526
  if (error.code === "ENOENT") return false;
519
527
  throw error;
520
528
  });
@@ -564,7 +572,7 @@ async function runOnboard(opts) {
564
572
  console.log(" \u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
565
573
  continue;
566
574
  }
567
- if (!await stat2(project.directory).then(() => true, () => false)) {
575
+ if (!await stat3(project.directory).then(() => true, () => false)) {
568
576
  console.log(" \u76EE\u5F55\u4E0D\u5B58\u5728");
569
577
  continue;
570
578
  }
@@ -593,7 +601,7 @@ async function runOnboard(opts) {
593
601
  console.log(" \u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
594
602
  continue;
595
603
  }
596
- if (!await stat2(directory).then(() => true, () => false)) {
604
+ if (!await stat3(directory).then(() => true, () => false)) {
597
605
  console.log(" \u76EE\u5F55\u4E0D\u5B58\u5728");
598
606
  continue;
599
607
  }
package/dist/main.js CHANGED
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  ConfigError,
4
4
  runDaemon
5
- } from "./chunk-J3ZMMSW2.js";
6
- import "./chunk-GS3Q6EU5.js";
5
+ } from "./chunk-IV5ZGRAM.js";
6
+ import "./chunk-UFLXQ4HQ.js";
7
7
 
8
8
  // src/main.ts
9
9
  runDaemon(process.env.OCTOPUS_PROXY_CONFIG ?? "proxy.json").catch((error) => {
@@ -14,7 +14,7 @@ import {
14
14
  remoteResolutionTiers,
15
15
  remoteSessionBudget,
16
16
  terminateNative
17
- } from "./chunk-GS3Q6EU5.js";
17
+ } from "./chunk-UFLXQ4HQ.js";
18
18
 
19
19
  // src/remote-worker.ts
20
20
  import rtc from "@roamhq/wrtc";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skylandnpm/octopus-proxy",
3
- "version": "0.1.64",
3
+ "version": "0.1.66",
4
4
  "type": "module",
5
5
  "description": "Octopus host proxy: OpenCode, Codex, ZCode and Pi adapters with service installer CLI",
6
6
  "bin": {