@skylandnpm/octopus-proxy 0.1.11 → 0.1.13

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
@@ -28,6 +28,8 @@ octopus-proxy restart
28
28
 
29
29
  Claw 打开机器人工作区时会通过 Hub 向 Proxy 发送只读 `runtime.snapshot` 命令。响应包含当前运行状态、Hub 和工具设置、授权项目路径,以及工具可提供的模型信息;机器人 Secret、工具密码和提供商凭据不会返回。工作区的“Proxy 信息”按钮可查看这些内容。
30
30
 
31
+ Codex 和 ZCode 由 Proxy 按授权项目启动各自的 app-server,默认随 Proxy 自动启动;服务未就绪时也可在 Claw 点击“启动服务”。若命令不可执行或某个项目启动失败,工作区会显示失败项目及原因。修改命令路径或授权项目后,运行 `octopus-proxy restart` 使配置生效。
32
+
31
33
  ## Pi agent
32
34
 
33
35
  支持 Pi coding agent 的 RPC 模式,验证基线为 `@earendil-works/pi-coding-agent` **0.87.1**(旧 npm 包名为 `@mariozechner/pi-coding-agent`)。先安装 Pi,在代理运行用户下启动 `pi`,用 `/login` 配置提供商;也可按 Pi 文档配置 API key 环境变量或 `~/.pi/agent/models.json`。系统服务需要能读取相同的凭据和环境。
@@ -70,6 +72,6 @@ npm run publish
70
72
 
71
73
  ## 编码工具账号与 OpenCode 服务
72
74
 
73
- OpenCode 只支持 external:先自行运行 `opencode serve --hostname 127.0.0.1 --port 4096`,配置 `tool.mode: "external"` 和 `tool.baseUrl`。旧 managed 配置必须迁移,Proxy 不再管理 OpenCode 生命周期。
75
+ OpenCode 使用 external HTTP 模式。可自行运行 `opencode serve --hostname 127.0.0.1 --port 4096`,也可在 Claw 工作区点击“启动服务”,由 Proxy 在目标主机使用 `tool.command` 拉起服务;配置 `tool.baseUrl` 与端口一致。启动日志保存在 `stateDirectory/opencode-serve.log`,`autoStart: true` 可让 Proxy 启动时自动尝试拉起。旧 managed 配置必须迁移;Proxy 不停止或重启已有 OpenCode 服务。
74
76
 
75
77
  Claw 的“编码工具账号”窗口支持 Codex API Key/设备码、ZCode 原生 Z.AI OAuth、OpenCode 原生 API Key/OAuth。凭据由编码工具保存,模型列表从原生接口读取。详细行为与验证边界见 [编码工具账号](../../docs/tool-authentication.md)。
@@ -11606,8 +11606,8 @@ var ToolSchema = z.object({
11606
11606
  autoStart: z.boolean().optional(),
11607
11607
  password: z.string().optional()
11608
11608
  }).superRefine((tool, context) => {
11609
- if (tool.kind === "opencode" && tool.mode === "managed") context.addIssue({ code: z.ZodIssueCode.custom, path: ["mode"], message: "OpenCode \u4EC5\u652F\u6301 external \u6A21\u5F0F\uFF1B\u8BF7\u5148\u81EA\u884C\u542F\u52A8 opencode serve\uFF0C\u518D\u5C06 tool.mode \u6539\u4E3A external" });
11610
- }).transform((tool) => ({ ...tool, mode: tool.kind === "opencode" ? "external" : tool.mode ?? "managed", autoStart: tool.kind === "opencode" ? false : tool.autoStart ?? true }));
11609
+ if (tool.kind === "opencode" && tool.mode === "managed") context.addIssue({ code: z.ZodIssueCode.custom, path: ["mode"], message: "OpenCode \u4EC5\u652F\u6301 external \u6A21\u5F0F\uFF1B\u8BF7\u5C06 tool.mode \u6539\u4E3A external\uFF0C\u7136\u540E\u53EF\u4ECE Claw \u8FDC\u7A0B\u542F\u52A8\u670D\u52A1" });
11610
+ }).transform((tool) => ({ ...tool, mode: tool.kind === "opencode" ? "external" : tool.mode ?? "managed", autoStart: tool.autoStart ?? tool.kind !== "opencode" }));
11611
11611
  var ConfigSchema = z.object({
11612
11612
  hubUrl: z.string().url(),
11613
11613
  botId: z.string().uuid(),
@@ -11656,10 +11656,14 @@ async function loadConfig(path) {
11656
11656
  }
11657
11657
  }
11658
11658
 
11659
+ // src/version.ts
11660
+ import { readFileSync } from "node:fs";
11661
+ var proxyVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
11662
+
11659
11663
  // src/daemon.ts
11660
11664
  import WebSocket2 from "ws";
11661
- import { mkdir as mkdir5 } from "node:fs/promises";
11662
- import { join as join9 } from "node:path";
11665
+ import { mkdir as mkdir7 } from "node:fs/promises";
11666
+ import { join as join10 } from "node:path";
11663
11667
  import { randomUUID as randomUUID12 } from "node:crypto";
11664
11668
 
11665
11669
  // ../../packages/protocol/src/index.ts
@@ -14226,7 +14230,7 @@ var OpenCodeAdapter = class {
14226
14230
  this.media = new RemoteMedia(workspace, () => uploads.limits.imageBytes);
14227
14231
  }
14228
14232
  supportedOperations() {
14229
- return Operations.filter((op) => !["runtime.start", "runtime.stop", "runtime.restart", "runtime.logs"].includes(op));
14233
+ return Operations.filter((op) => !["runtime.stop", "runtime.restart", "runtime.logs"].includes(op));
14230
14234
  }
14231
14235
  auth = new AuthFlows();
14232
14236
  authDirty = /* @__PURE__ */ new Set();
@@ -14541,6 +14545,9 @@ function redact(value) {
14541
14545
  }
14542
14546
 
14543
14547
  // src/runtime.ts
14548
+ import { spawn } from "node:child_process";
14549
+ import { mkdir as mkdir4, open as open4 } from "node:fs/promises";
14550
+ import { join as join5 } from "node:path";
14544
14551
  var Runtime = class {
14545
14552
  constructor(config) {
14546
14553
  this.config = config;
@@ -14548,8 +14555,10 @@ var Runtime = class {
14548
14555
  kind = "opencode";
14549
14556
  version = "";
14550
14557
  state = "external";
14558
+ starting;
14551
14559
  async init() {
14552
- await this.health();
14560
+ if (this.config.tool.autoStart) await this.start();
14561
+ else await this.health();
14553
14562
  }
14554
14563
  headers() {
14555
14564
  const password = this.config.tool.password;
@@ -14557,7 +14566,7 @@ var Runtime = class {
14557
14566
  }
14558
14567
  async health() {
14559
14568
  try {
14560
- const response = await fetch(`${this.config.tool.baseUrl}/global/health`, { headers: this.headers(), signal: AbortSignal.timeout(2e3) });
14569
+ const response = await fetch(new URL("/global/health", this.config.tool.baseUrl), { headers: this.headers(), signal: AbortSignal.timeout(2e3) });
14561
14570
  if (!response.ok) return false;
14562
14571
  const data = await response.json();
14563
14572
  this.version = data.version ?? this.version;
@@ -14574,11 +14583,66 @@ var Runtime = class {
14574
14583
  state: this.state,
14575
14584
  healthy,
14576
14585
  version: this.version,
14577
- ...healthy ? {} : { error: `\u65E0\u6CD5\u8FDE\u63A5 OpenCode\uFF1A${this.config.tool.baseUrl}\u3002\u8BF7\u5728\u8BE5\u4E3B\u673A\u542F\u52A8 opencode serve\uFF0C\u5E76\u68C0\u67E5\u670D\u52A1\u5730\u5740\u548C\u5BC6\u7801\u3002` }
14586
+ ...healthy ? {} : { error: `\u65E0\u6CD5\u8FDE\u63A5 OpenCode\uFF1A${this.config.tool.baseUrl}\u3002\u53EF\u5728 Claw \u4E2D\u70B9\u51FB\u201C\u542F\u52A8\u670D\u52A1\u201D\uFF0C\u6216\u5728\u8BE5\u4E3B\u673A\u542F\u52A8 opencode serve\uFF1B\u8BF7\u68C0\u67E5\u670D\u52A1\u5730\u5740\u548C\u5BC6\u7801\u3002` }
14578
14587
  };
14579
14588
  }
14580
14589
  async start() {
14581
- throw new Error("OpenCode \u4EC5\u652F\u6301 external \u6A21\u5F0F\uFF0C\u8BF7\u5728\u4E3B\u673A\u4E0A\u8FD0\u884C opencode serve");
14590
+ return this.starting ??= this.launch().finally(() => {
14591
+ this.starting = void 0;
14592
+ });
14593
+ }
14594
+ async launch() {
14595
+ const url = new URL(this.config.tool.baseUrl);
14596
+ const endpoint = new URL("/global/health", url);
14597
+ let response;
14598
+ try {
14599
+ response = await fetch(endpoint, { headers: this.headers(), signal: AbortSignal.timeout(2e3) });
14600
+ } catch (error) {
14601
+ if (error instanceof Error && error.name === "TimeoutError") throw new Error(`OpenCode \u5730\u5740 ${url.origin} \u54CD\u5E94\u8D85\u65F6\uFF1B\u8BF7\u68C0\u67E5\u670D\u52A1\u540E\u91CD\u8BD5\uFF0CProxy \u4E0D\u4F1A\u91CD\u590D\u542F\u52A8`);
14602
+ }
14603
+ if (response) {
14604
+ const health = await response.json().catch(() => null);
14605
+ if (response.ok && health?.healthy === true) return this.status();
14606
+ throw new Error(`OpenCode \u5730\u5740 ${url.origin} \u5DF2\u6709 HTTP \u670D\u52A1\uFF0C\u4F46\u5065\u5EB7\u68C0\u67E5\u5931\u8D25\uFF08HTTP ${response.status}\uFF09\uFF1B\u8BF7\u68C0\u67E5\u670D\u52A1\u5BC6\u7801\u6216\u7AEF\u53E3`);
14607
+ }
14608
+ await mkdir4(this.config.stateDirectory, { recursive: true, mode: 448 });
14609
+ const logPath = join5(this.config.stateDirectory, "opencode-serve.log");
14610
+ const log = await open4(logPath, "a", 384);
14611
+ const env = { ...process.env };
14612
+ delete env.OPENCODE_SERVER_PASSWORD;
14613
+ delete env.OPENCODE_SERVER_USERNAME;
14614
+ if (this.config.tool.password) {
14615
+ env.OPENCODE_SERVER_PASSWORD = this.config.tool.password;
14616
+ env.OPENCODE_SERVER_USERNAME = "opencode";
14617
+ }
14618
+ let child;
14619
+ try {
14620
+ child = spawn(this.config.tool.command, ["serve", "--hostname", url.hostname === "[::1]" ? "::1" : url.hostname, "--port", url.port || "80"], {
14621
+ cwd: this.config.projects[0].directory,
14622
+ env,
14623
+ detached: true,
14624
+ stdio: ["ignore", log.fd, log.fd]
14625
+ });
14626
+ await new Promise((resolve4, reject) => {
14627
+ child.once("spawn", resolve4);
14628
+ child.once("error", reject);
14629
+ });
14630
+ child.on("error", () => {
14631
+ });
14632
+ child.unref();
14633
+ } catch (error) {
14634
+ throw new Error(`\u65E0\u6CD5\u6267\u884C OpenCode \u547D\u4EE4 ${this.config.tool.command}\uFF1A${error instanceof Error ? error.message : String(error)}`);
14635
+ } finally {
14636
+ await log.close();
14637
+ }
14638
+ const deadline = Date.now() + 2e4;
14639
+ while (Date.now() < deadline) {
14640
+ if (child.exitCode !== null || child.signalCode !== null) break;
14641
+ if (await this.health()) return this.status();
14642
+ await new Promise((resolve4) => setTimeout(resolve4, 250));
14643
+ }
14644
+ if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
14645
+ throw new Error(`OpenCode \u542F\u52A8\u5931\u8D25\u6216\u5065\u5EB7\u68C0\u67E5\u8D85\u65F6\uFF1B\u8BF7\u68C0\u67E5 ${logPath}\u3001\u670D\u52A1\u5730\u5740\u548C\u5BC6\u7801`);
14582
14646
  }
14583
14647
  async stop() {
14584
14648
  throw new Error("OpenCode \u670D\u52A1\u7531\u5916\u90E8\u7BA1\u7406\uFF0CProxy \u4E0D\u652F\u6301\u505C\u6B62");
@@ -14595,21 +14659,21 @@ var Runtime = class {
14595
14659
 
14596
14660
  // src/tools/zcode-env.ts
14597
14661
  import { existsSync } from "node:fs";
14598
- import { dirname as dirname4, join as join5 } from "node:path";
14662
+ import { dirname as dirname4, join as join6 } from "node:path";
14599
14663
  function zcodeEnvironment(command) {
14600
14664
  const env = { ...process.env };
14601
14665
  if (!env.ZCODE_BUILTIN_PROVIDER_CONFIG_FILE) {
14602
- const bundled = join5(dirname4(command), "..", "config", "provider", "zcode-builtin.json");
14666
+ const bundled = join6(dirname4(command), "..", "config", "provider", "zcode-builtin.json");
14603
14667
  if (existsSync(bundled)) env.ZCODE_BUILTIN_PROVIDER_CONFIG_FILE = bundled;
14604
14668
  }
14605
14669
  return env;
14606
14670
  }
14607
14671
 
14608
14672
  // src/tools/stdio-runtime.ts
14609
- import { spawn, execFile as execFile2 } from "node:child_process";
14673
+ import { spawn as spawn2, execFile as execFile2 } from "node:child_process";
14610
14674
  import { promisify as promisify2 } from "node:util";
14611
- import { open as open4 } from "node:fs/promises";
14612
- import { join as join6 } from "node:path";
14675
+ import { mkdir as mkdir5, open as open5 } from "node:fs/promises";
14676
+ import { join as join7 } from "node:path";
14613
14677
 
14614
14678
  // src/tools/line-rpc.ts
14615
14679
  import { randomUUID as randomUUID8 } from "node:crypto";
@@ -14820,6 +14884,7 @@ var StdioRuntime = class {
14820
14884
  attempts = /* @__PURE__ */ new Map();
14821
14885
  timers = /* @__PURE__ */ new Map();
14822
14886
  refreshing = /* @__PURE__ */ new Set();
14887
+ failures = /* @__PURE__ */ new Map();
14823
14888
  wanted = false;
14824
14889
  detached = false;
14825
14890
  async init() {
@@ -14827,7 +14892,10 @@ var StdioRuntime = class {
14827
14892
  const { stdout } = await run(this.config.tool.command, [...this.descriptor.versionArgs], { timeout: 15e3 });
14828
14893
  this.version = stdout.trim().split("\n")[0].trim();
14829
14894
  } catch (error) {
14830
- throw new Error(`Cannot execute ${this.config.tool.command}: ${error instanceof Error ? error.message : String(error)}`);
14895
+ const reason = `\u65E0\u6CD5\u6267\u884C ${this.config.tool.command}\uFF1A${error instanceof Error ? error.message : String(error)}`;
14896
+ this.wanted = this.config.tool.autoStart;
14897
+ for (const project of this.config.projects) this.failures.set(project.id, reason);
14898
+ throw new Error(reason);
14831
14899
  }
14832
14900
  if (this.descriptor.versionPolicy === "warn" && this.version !== this.descriptor.baseline) {
14833
14901
  console.warn(`Warning: ${this.descriptor.label} ${this.version} differs from the verified baseline ${this.descriptor.baseline}; the app-server protocol may have drifted`);
@@ -14837,19 +14905,27 @@ var StdioRuntime = class {
14837
14905
  }
14838
14906
  async start() {
14839
14907
  this.wanted = true;
14908
+ for (const timer of this.timers.values()) clearTimeout(timer);
14909
+ this.timers.clear();
14910
+ this.attempts.clear();
14840
14911
  await Promise.allSettled(this.config.projects.map((project) => this.track(project.id, project.directory)));
14841
- return this.status();
14912
+ const status = await this.status();
14913
+ if (!status.healthy) throw new Error(`${this.descriptor.label} \u542F\u52A8\u5931\u8D25\uFF1A${status.error ?? "\u8BF7\u68C0\u67E5 Proxy \u65E5\u5FD7"}`);
14914
+ return status;
14842
14915
  }
14843
14916
  async stop() {
14844
14917
  this.wanted = false;
14918
+ for (const timer of this.timers.values()) clearTimeout(timer);
14919
+ this.timers.clear();
14920
+ this.attempts.clear();
14921
+ await Promise.allSettled([...this.tracking.values()]);
14845
14922
  for (const projectId of [...this.children.keys()]) this.kill(projectId);
14923
+ this.failures.clear();
14846
14924
  return this.status();
14847
14925
  }
14848
14926
  async restart() {
14849
- this.wanted = true;
14850
- for (const projectId of [...this.children.keys()]) this.kill(projectId);
14851
- await Promise.allSettled(this.config.projects.map((project) => this.track(project.id, project.directory)));
14852
- return this.status();
14927
+ await this.stop();
14928
+ return this.start();
14853
14929
  }
14854
14930
  async reconnectProject(projectId) {
14855
14931
  const child = this.children.get(projectId);
@@ -14866,7 +14942,7 @@ var StdioRuntime = class {
14866
14942
  async logs() {
14867
14943
  const chunks = [];
14868
14944
  for (const [projectId, logPath] of this.logPaths) {
14869
- const file = await open4(logPath, "r").catch(() => void 0);
14945
+ const file = await open5(logPath, "r").catch(() => void 0);
14870
14946
  if (!file) continue;
14871
14947
  try {
14872
14948
  const size = (await file.stat()).size, bytes = Buffer.alloc(Math.min(size, 3e4));
@@ -14883,13 +14959,24 @@ ${bytes.toString("utf8")}`);
14883
14959
  const children = [...this.children].map(([projectId, child]) => ({ projectId, pid: child.pid, state: child.exitCode === null && child.signalCode === null ? "running" : "stopped" }));
14884
14960
  const alive = children.filter((child) => child.state === "running");
14885
14961
  this.state = alive.length === 0 ? this.wanted ? "failed" : "stopped" : "running";
14886
- return { tool: this.kind, mode: "managed", state: this.state, healthy: this.children.size > 0 && alive.length === this.children.size, version: this.version, children };
14962
+ const missing = this.config.projects.filter((project) => !alive.some((child) => child.projectId === project.id));
14963
+ return {
14964
+ tool: this.kind,
14965
+ mode: "managed",
14966
+ state: this.state,
14967
+ healthy: missing.length === 0,
14968
+ version: this.version,
14969
+ children,
14970
+ ...missing.length && (this.wanted || missing.some((project) => this.failures.has(project.id))) ? { error: missing.map((project) => `${project.name}\uFF1A${this.failures.get(project.id) ?? "\u8FDB\u7A0B\u672A\u8FD0\u884C"}`).join("\uFF1B") } : {}
14971
+ };
14887
14972
  }
14888
14973
  headers() {
14889
14974
  return {};
14890
14975
  }
14891
14976
  detach() {
14892
14977
  this.detached = true;
14978
+ for (const timer of this.timers.values()) clearTimeout(timer);
14979
+ this.timers.clear();
14893
14980
  }
14894
14981
  // Get (or lazily spawn) the RPC connection for a project. `onConnect` runs after every
14895
14982
  // (re)spawn, e.g. Codex sends its initialize handshake there. Concurrent callers share one launch.
@@ -14912,7 +14999,7 @@ ${bytes.toString("utf8")}`);
14912
14999
  const child = this.children.get(projectId);
14913
15000
  const rpc = new LineRpc(child, { ...handlers, onClose: (reason) => {
14914
15001
  this.connections.delete(projectId);
14915
- this.onExited(projectId, reason);
15002
+ if (this.children.get(projectId) === child) this.onExited(projectId, reason);
14916
15003
  handlers.onClose?.(reason);
14917
15004
  } });
14918
15005
  try {
@@ -14928,29 +15015,58 @@ ${bytes.toString("utf8")}`);
14928
15015
  }
14929
15016
  // Spawn the supervised child if absent (concurrent callers share one spawn); resolves once the process exists.
14930
15017
  async track(projectId, directory) {
14931
- if (this.children.has(projectId)) return;
14932
15018
  const inFlight = this.tracking.get(projectId);
14933
15019
  if (inFlight) return inFlight;
15020
+ if (this.children.has(projectId)) return;
14934
15021
  const spawnOnce = (async () => {
14935
- const logPath = join6(this.config.stateDirectory, `${this.kind}-${projectId}.log`);
14936
- const log = await open4(logPath, "a", 384).catch(() => void 0);
14937
- const child = spawn(this.config.tool.command, [...this.descriptor.serverArgs], { cwd: directory, env: this.kind === "zcode" ? zcodeEnvironment(this.config.tool.command) : { ...process.env }, stdio: ["pipe", "pipe", log ? log.fd : "ignore"] });
14938
- if (log) child.once("exit", () => {
15022
+ const logPath = join7(this.config.stateDirectory, `${this.kind}-${projectId}.log`);
15023
+ await mkdir5(this.config.stateDirectory, { recursive: true, mode: 448 });
15024
+ const log = await open5(logPath, "a", 384);
15025
+ let child;
15026
+ try {
15027
+ child = spawn2(this.config.tool.command, [...this.descriptor.serverArgs], { cwd: directory, env: this.kind === "zcode" ? zcodeEnvironment(this.config.tool.command) : { ...process.env }, stdio: ["pipe", "pipe", log.fd] });
15028
+ } catch (error) {
15029
+ await log.close();
15030
+ throw error;
15031
+ }
15032
+ child.once("close", () => {
14939
15033
  void log.close();
14940
15034
  });
14941
15035
  this.children.set(projectId, child);
14942
15036
  this.logPaths.set(projectId, logPath);
14943
- child.once("exit", () => {
14944
- if (this.children.get(projectId) === child) this.children.delete(projectId);
15037
+ child.on("error", (error) => {
15038
+ this.failures.set(projectId, error.message);
14945
15039
  });
14946
- })();
15040
+ child.once("exit", (code, signal) => {
15041
+ if (this.children.get(projectId) === child) this.onExited(projectId, `\u8FDB\u7A0B\u9000\u51FA\uFF08${code ?? signal}\uFF09`);
15042
+ });
15043
+ try {
15044
+ await new Promise((resolve4, reject) => {
15045
+ child.once("spawn", resolve4);
15046
+ child.once("error", reject);
15047
+ });
15048
+ this.failures.delete(projectId);
15049
+ } catch (error) {
15050
+ if (this.children.get(projectId) === child) this.children.delete(projectId);
15051
+ this.failures.set(projectId, error instanceof Error ? error.message : String(error));
15052
+ throw error;
15053
+ }
15054
+ })().catch((error) => {
15055
+ this.failures.set(projectId, error instanceof Error ? error.message : String(error));
15056
+ throw error;
15057
+ });
14947
15058
  this.tracking.set(projectId, spawnOnce);
14948
15059
  spawnOnce.finally(() => this.tracking.delete(projectId)).catch(() => {
14949
15060
  });
14950
15061
  return spawnOnce;
14951
15062
  }
14952
- onExited(projectId, _reason) {
15063
+ onExited(projectId, reason) {
15064
+ const child = this.children.get(projectId);
15065
+ this.children.delete(projectId);
15066
+ if (child && child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
15067
+ this.failures.set(projectId, reason);
14953
15068
  if (this.detached || !this.wanted || this.refreshing.has(projectId)) return;
15069
+ if (this.timers.has(projectId)) return;
14954
15070
  const attempts = (this.attempts.get(projectId) ?? 0) + 1;
14955
15071
  if (attempts > 5) {
14956
15072
  console.error(`${projectId}: app server exited; retry budget exhausted`);
@@ -14960,6 +15076,7 @@ ${bytes.toString("utf8")}`);
14960
15076
  const delay = Math.min(3e4, 1e3 * 2 ** attempts);
14961
15077
  this.timers.set(projectId, setTimeout(() => {
14962
15078
  this.timers.delete(projectId);
15079
+ if (!this.wanted || this.detached) return;
14963
15080
  const project = this.config.projects.find((entry) => entry.id === projectId);
14964
15081
  if (project) void this.track(projectId, project.directory).catch(() => {
14965
15082
  });
@@ -14982,7 +15099,7 @@ ${bytes.toString("utf8")}`);
14982
15099
  };
14983
15100
 
14984
15101
  // src/tools/codex.ts
14985
- import { join as join7 } from "node:path";
15102
+ import { join as join8 } from "node:path";
14986
15103
 
14987
15104
  // src/adapter.ts
14988
15105
  var DAEMON_OPERATIONS = [
@@ -15123,21 +15240,21 @@ var CodexAdapter = class {
15123
15240
  respond({ decision: "decline" });
15124
15241
  }
15125
15242
  async index(project) {
15126
- const stored = await readJson(join7(this.runtime.config.stateDirectory, `codex-${project}.json`));
15243
+ const stored = await readJson(join8(this.runtime.config.stateDirectory, `codex-${project}.json`));
15127
15244
  return { threadIds: stored?.threadIds ?? [] };
15128
15245
  }
15129
15246
  async remember(project, threadId) {
15130
15247
  const index = await this.index(project);
15131
15248
  if (!index.threadIds.includes(threadId)) {
15132
15249
  index.threadIds.push(threadId);
15133
- await atomicJson(join7(this.runtime.config.stateDirectory, `codex-${project}.json`), index);
15250
+ await atomicJson(join8(this.runtime.config.stateDirectory, `codex-${project}.json`), index);
15134
15251
  }
15135
15252
  }
15136
15253
  async forget(project, threadId) {
15137
15254
  const index = await this.index(project);
15138
15255
  if (index.threadIds.includes(threadId)) {
15139
15256
  index.threadIds = index.threadIds.filter((entry) => entry !== threadId);
15140
- await atomicJson(join7(this.runtime.config.stateDirectory, `codex-${project}.json`), index);
15257
+ await atomicJson(join8(this.runtime.config.stateDirectory, `codex-${project}.json`), index);
15141
15258
  }
15142
15259
  }
15143
15260
  async resume(project, threadId, rpc) {
@@ -15477,7 +15594,7 @@ var CodexAdapter = class {
15477
15594
  };
15478
15595
 
15479
15596
  // src/tools/zcode.ts
15480
- import { spawn as spawn2 } from "node:child_process";
15597
+ import { spawn as spawn3 } from "node:child_process";
15481
15598
  import { randomUUID as randomUUID10 } from "node:crypto";
15482
15599
  import { realpath as realpath5 } from "node:fs/promises";
15483
15600
  var OPERATIONS2 = [
@@ -15670,7 +15787,7 @@ var ZcodeAdapter = class {
15670
15787
  const flow = this.auth.create(project, () => {
15671
15788
  child?.kill("SIGTERM");
15672
15789
  });
15673
- child = spawn2(this.runtime.config.tool.command, ["login", "--no-browser", "--json"], { cwd: await this.workspace.root(project), env: zcodeEnvironment(this.runtime.config.tool.command), stdio: ["ignore", "pipe", "pipe"] });
15790
+ child = spawn3(this.runtime.config.tool.command, ["login", "--no-browser", "--json"], { cwd: await this.workspace.root(project), env: zcodeEnvironment(this.runtime.config.tool.command), stdio: ["ignore", "pipe", "pipe"] });
15674
15791
  let output = "", diagnostic = "";
15675
15792
  child.stdout?.on("data", (data) => {
15676
15793
  output = (output + data.toString()).slice(-65536);
@@ -16106,9 +16223,9 @@ var PiAdapter = class {
16106
16223
  };
16107
16224
 
16108
16225
  // src/tools/pi-runtime.ts
16109
- import { execFile as execFile3, spawn as spawn3 } from "node:child_process";
16110
- import { mkdir as mkdir4, open as open5 } from "node:fs/promises";
16111
- import { join as join8 } from "node:path";
16226
+ import { execFile as execFile3, spawn as spawn4 } from "node:child_process";
16227
+ import { mkdir as mkdir6, open as open6 } from "node:fs/promises";
16228
+ import { join as join9 } from "node:path";
16112
16229
  import { promisify as promisify3 } from "node:util";
16113
16230
  var run2 = promisify3(execFile3);
16114
16231
  var PiRuntime = class {
@@ -16170,7 +16287,7 @@ var PiRuntime = class {
16170
16287
  async logs() {
16171
16288
  const chunks = [];
16172
16289
  for (const path of this.logPaths) {
16173
- const file = await open5(path, "r").catch(() => void 0);
16290
+ const file = await open6(path, "r").catch(() => void 0);
16174
16291
  if (!file) continue;
16175
16292
  try {
16176
16293
  const size = (await file.stat()).size, bytes = Buffer.alloc(Math.min(size, 3e4));
@@ -16186,7 +16303,7 @@ ${bytes.toString("utf8")}`);
16186
16303
  sessionFile(projectId, sessionId) {
16187
16304
  this.project(projectId);
16188
16305
  if (!describeTool("pi").sessionPattern.test(sessionId)) throw new Error("Invalid session id");
16189
- return join8(this.config.stateDirectory, "pi-sessions", projectId, `${sessionId}.jsonl`);
16306
+ return join9(this.config.stateDirectory, "pi-sessions", projectId, `${sessionId}.jsonl`);
16190
16307
  }
16191
16308
  project(id) {
16192
16309
  const project = this.config.projects.find((entry) => entry.id === id);
@@ -16217,15 +16334,15 @@ ${bytes.toString("utf8")}`);
16217
16334
  async launch(key, projectId, sessionId, handlers, generation) {
16218
16335
  await this.kill(key);
16219
16336
  const directory = this.project(projectId).directory;
16220
- await mkdir4(join8(this.config.stateDirectory, "pi-sessions", projectId), { recursive: true, mode: 448 });
16221
- const logPath = join8(this.config.stateDirectory, `pi-${key}.log`);
16222
- const log = await open5(logPath, "a", 384);
16337
+ await mkdir6(join9(this.config.stateDirectory, "pi-sessions", projectId), { recursive: true, mode: 448 });
16338
+ const logPath = join9(this.config.stateDirectory, `pi-${key}.log`);
16339
+ const log = await open6(logPath, "a", 384);
16223
16340
  if (generation !== this.generation) {
16224
16341
  await log.close();
16225
16342
  throw new Error("Pi runtime stopped during startup");
16226
16343
  }
16227
16344
  const args = [...describeTool("pi").serverArgs, ...sessionId ? ["--session", this.sessionFile(projectId, sessionId)] : ["--no-session"]];
16228
- const child = spawn3(this.config.tool.command, args, { cwd: directory, env: { ...process.env }, stdio: ["pipe", "pipe", log.fd] });
16345
+ const child = spawn4(this.config.tool.command, args, { cwd: directory, env: { ...process.env }, stdio: ["pipe", "pipe", log.fd] });
16229
16346
  child.once("close", () => {
16230
16347
  void log.close();
16231
16348
  });
@@ -16282,8 +16399,8 @@ function createAdapter(runtime, workspace, uploads, emit) {
16282
16399
  // src/daemon.ts
16283
16400
  async function runDaemon(configPath) {
16284
16401
  const config = await loadConfig(configPath);
16285
- await mkdir5(config.stateDirectory, { recursive: true, mode: 448 });
16286
- const workspace = new Workspace(config.projects), runtime = createRuntime(config), uploads = new Uploads(workspace, join9(config.stateDirectory, "uploads")), journal = new Journal(join9(config.stateDirectory, "journal"));
16402
+ await mkdir7(config.stateDirectory, { recursive: true, mode: 448 });
16403
+ const workspace = new Workspace(config.projects), runtime = createRuntime(config), uploads = new Uploads(workspace, join10(config.stateDirectory, "uploads")), journal = new Journal(join10(config.stateDirectory, "journal"));
16287
16404
  const epoch = randomUUID12();
16288
16405
  let stopping = false, botId = "";
16289
16406
  const sockets = /* @__PURE__ */ new Set();
@@ -16293,7 +16410,7 @@ async function runDaemon(configPath) {
16293
16410
  function emit(projectId, event, data) {
16294
16411
  events.publish({ type: "event", botId, projectId, event, data });
16295
16412
  }
16296
- const adapter = createAdapter(runtime, workspace, uploads, emit), artifacts = new Artifacts(workspace, join9(config.stateDirectory, "artifacts"), emit);
16413
+ const adapter = createAdapter(runtime, workspace, uploads, emit), artifacts = new Artifacts(workspace, join10(config.stateDirectory, "artifacts"), emit);
16297
16414
  async function dispatch(c) {
16298
16415
  if (!adapter.supportedOperations().includes(c.operation)) throw new Error(`Unsupported ${runtime.kind} operation: ${c.operation}`);
16299
16416
  const p = c.payload;
@@ -16343,11 +16460,11 @@ async function runDaemon(configPath) {
16343
16460
  let lastPong = Date.now();
16344
16461
  const heartbeat = setInterval(() => {
16345
16462
  if (Date.now() - lastPong > 45e3) socket.terminate();
16346
- else if (socket.readyState === WebSocket2.OPEN) socket.send(JSON.stringify({ type: "ping" }));
16463
+ else if (socket.readyState === WebSocket2.OPEN) socket.send(JSON.stringify({ type: "ping", proxyVersion }));
16347
16464
  }, 15e3);
16348
16465
  socket.on("open", () => {
16349
16466
  attempt = 0;
16350
- socket.send(JSON.stringify({ type: "hello", protocol: PROTOCOL_VERSION, version: runtime.version, capabilities: adapter.supportedOperations(), epoch }));
16467
+ socket.send(JSON.stringify({ type: "hello", protocol: PROTOCOL_VERSION, version: runtime.version, proxyVersion, capabilities: adapter.supportedOperations(), epoch }));
16351
16468
  });
16352
16469
  socket.on("message", async (raw) => {
16353
16470
  try {
@@ -16430,5 +16547,6 @@ export {
16430
16547
  Uploads,
16431
16548
  createRuntime,
16432
16549
  createAdapter,
16550
+ proxyVersion,
16433
16551
  runDaemon
16434
16552
  };
package/dist/cli.js CHANGED
@@ -17,15 +17,15 @@ import {
17
17
  launchdLogPath,
18
18
  launchdPlistPath,
19
19
  loadConfig,
20
+ proxyVersion,
20
21
  resolveConfigPath,
21
22
  runDaemon,
22
23
  supportedPlatform,
23
24
  systemdUnitPath
24
- } from "./chunk-AGWRSSBQ.js";
25
+ } from "./chunk-JHJQMGCD.js";
25
26
 
26
27
  // src/cli.ts
27
28
  import { spawn } from "node:child_process";
28
- import { readFileSync } from "node:fs";
29
29
  import { access as access4 } from "node:fs/promises";
30
30
 
31
31
  // src/doctor.ts
@@ -363,9 +363,7 @@ async function runDoctor(opts) {
363
363
  const descriptor = describeTool(config.tool.kind);
364
364
  const checkName = `${descriptor.label} \u547D\u4EE4`;
365
365
  const command2 = config.tool.command;
366
- if (config.tool.kind === "opencode") {
367
- record(checkName, "ok", "external \u6A21\u5F0F\u901A\u8FC7 HTTP \u68C0\u67E5\u670D\u52A1\uFF0C\u4E0D\u9700\u8981 Proxy \u6267\u884C OpenCode \u547D\u4EE4");
368
- } else if (!isAbsolute2(command2)) {
366
+ if (!isAbsolute2(command2)) {
369
367
  record(checkName, "warn", `\u5F53\u524D\u4E3A\u300C${command2}\u300D\uFF0C\u4F9D\u8D56 PATH \u67E5\u627E\uFF1B\u670D\u52A1\u73AF\u5883\u5EFA\u8BAE\u6539\u7528\u7EDD\u5BF9\u8DEF\u5F84\uFF08\u91CD\u65B0\u8FD0\u884C onboard \u6216\u624B\u5DE5\u4FEE\u6539\u914D\u7F6E\uFF09`);
370
368
  } else if (!await access2(command2, constants.X_OK).then(() => true, () => false)) {
371
369
  record(checkName, "fail", `${command2} \u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u6267\u884C`);
@@ -375,7 +373,7 @@ async function runDoctor(opts) {
375
373
  const version = stdout2.trim().split("\n")[0].trim();
376
374
  if (version === descriptor.baseline) record(checkName, "ok", `${command2}\uFF08${version}\uFF09`);
377
375
  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`);
378
- else record(checkName, "warn", `${command2} \u7248\u672C\u4E3A ${version}\uFF0C\u5DF2\u9A8C\u8BC1\u57FA\u7EBF\u662F ${descriptor.baseline}\uFF1Bapp-server \u534F\u8BAE\u53EF\u80FD\u6F02\u79FB`);
376
+ else record(checkName, "warn", `${command2} \u7248\u672C\u4E3A ${version}\uFF0C\u5DF2\u9A8C\u8BC1\u57FA\u7EBF\u662F ${descriptor.baseline}\uFF1B\u5DE5\u5177\u534F\u8BAE\u53EF\u80FD\u6F02\u79FB`);
379
377
  } catch (error) {
380
378
  record(checkName, "warn", `\u6267\u884C ${command2} ${descriptor.versionArgs.join(" ")} \u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
381
379
  }
@@ -525,8 +523,8 @@ async function runOnboard(opts) {
525
523
  const found = await resolveCommandOnPath(toolKind, toolSearchDirs(toolKind));
526
524
  const example = platform2() === "darwin" ? MACOS_TOOL_EXAMPLES[toolKind] : void 0;
527
525
  if (toolKind === "opencode") {
528
- toolCommand = toolCommand || found || "opencode";
529
- console.log(" OpenCode \u7531\u5916\u90E8\u7BA1\u7406\uFF1B\u8BF7\u5728\u8BE5\u4E3B\u673A\u9884\u5148\u542F\u52A8 opencode serve\u3002");
526
+ 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) }));
527
+ console.log(" Proxy \u53EF\u4ECE Claw \u8FDC\u7A0B\u542F\u52A8 OpenCode\uFF1B\u5DF2\u6709\u670D\u52A1\u4F1A\u76F4\u63A5\u8FDE\u63A5\u3002");
530
528
  baseUrl = await ask(rl, "OpenCode \u670D\u52A1\u5730\u5740", { default: baseUrl || "http://127.0.0.1:4096", validate: (value) => {
531
529
  try {
532
530
  const url = new URL(value);
@@ -627,7 +625,7 @@ async function runOnboard(opts) {
627
625
  stateDirectory,
628
626
  ...loopbackHttp ? { allowInsecureLocalhost: true } : {},
629
627
  projects: projects.map((project) => ({ id: project.id, name: project.name, directory: project.directory, specDirectories: project.specDirectories ?? ["specs", "docs"] })),
630
- tool: { kind: toolKind, command: toolCommand || describeTool(toolKind).defaultCommand, ...toolKind === "opencode" ? { baseUrl, mode: "external" } : {}, ...toolKind === existing?.tool.kind && existing.tool.password ? { password: existing.tool.password } : {}, ...toolKind === "opencode" ? {} : { autoStart: toolKind === existing?.tool.kind ? existing.tool.autoStart : true } }
628
+ tool: { kind: toolKind, command: toolCommand || describeTool(toolKind).defaultCommand, ...toolKind === "opencode" ? { baseUrl, mode: "external" } : {}, ...toolKind === existing?.tool.kind && existing.tool.password ? { password: existing.tool.password } : {}, autoStart: toolKind === existing?.tool.kind ? existing.tool.autoStart : toolKind !== "opencode" }
631
629
  });
632
630
  await loadConfig(opts.configPath);
633
631
  console.log(`\u5DF2\u5199\u5165\u914D\u7F6E\uFF1A${opts.configPath}\uFF080600\uFF09`);
@@ -796,8 +794,7 @@ async function showLogs(follow) {
796
794
  }
797
795
  var { command, flags, projectArgs } = parse(process.argv.slice(2));
798
796
  if (command === "version" || flags["--version"]) {
799
- const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
800
- console.log(packageJson.version);
797
+ console.log(proxyVersion);
801
798
  process.exit(0);
802
799
  }
803
800
  if (!command || command === "help" || flags["--help"]) {
package/dist/main.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  ConfigError,
4
4
  runDaemon
5
- } from "./chunk-AGWRSSBQ.js";
5
+ } from "./chunk-JHJQMGCD.js";
6
6
 
7
7
  // src/main.ts
8
8
  runDaemon(process.env.OCTOPUS_PROXY_CONFIG ?? "proxy.json").catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skylandnpm/octopus-proxy",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "description": "Octopus host proxy: OpenCode, Codex, ZCode and Pi adapters with service installer CLI",
6
6
  "bin": {