@skylandnpm/octopus-proxy 0.1.21 → 0.1.23

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
@@ -15,6 +15,8 @@ octopus-proxy onboard
15
15
 
16
16
  向导默认使用 `https://octopus.wuyehao.com` 作为 Hub 地址;按需可输入其他地址,或用 `--hub-url` 指定。再次运行向导时会读取现有配置,直接回车即可保留各项设置;机器人 Secret 只提示已配置,不在终端显示原值。macOS 上选择编码工具时,向导优先显示已配置或探测到的命令路径,否则给出该工具的 macOS 路径示例;回车后会检查文件可执行性并运行版本命令,失败时要求重新输入。无交互环境可使用 `octopus-proxy onboard --non-interactive`,未提供 `--hub-url` 时也使用该默认地址;完整选项见 `octopus-proxy help`。安装服务后可运行 `octopus-proxy status` 和 `octopus-proxy doctor` 检查状态。
17
17
 
18
+ Proxy 正常使用独立的控制与文件 WebSocket。若旧版 Hub 或反向代理把 `/ws/proxy-files` 错误路由到控制通道,Proxy 会在连续握手冲突后自动切换为单控制通道兼容模式,避免两个连接互相挤掉而导致 Claw 永久显示离线;日志会明确记录该降级。同一套 bot ID 和 Secret 被另一台 Proxy 主机连接后,Hub 会让新实例接管,旧实例会同时关闭两个通道并停止自动重连;若要让旧实例重新接管,必须显式重启其服务。
19
+
18
20
  添加授权项目时,填写项目 ID 后会询问是否继续添加;直接回车即可结束添加并继续保存配置。
19
21
 
20
22
  如果旧配置中的项目 ID 重复或有错误项,先列出项目,再按序号删除指定项。此命令直接读取原始配置,因此配置校验失败时也能使用;删除前会生成仅当前用户可读的备份。清理完成后检查配置并重启服务:
@@ -72,6 +74,6 @@ npm run publish
72
74
 
73
75
  ## 编码工具账号与 OpenCode 服务
74
76
 
75
- OpenCode 2.0.19 使用 external HTTP 模式。`tool.command` 应为 `/Users/luhuili/.opencode/bin/opencode`,`tool.baseUrl` 明确指向回环服务,`tool.password` 必填并由 onboard 自动生成。Proxy 可拉起未监听的服务,也可连接以同一密码预先启动的服务;启动日志保存在 `stateDirectory/opencode-serve.log`。如需只连接自行管理的服务,可设置 `autoStart: false`。旧顶层配置、managed 模式和 v1 API 不再兼容;Proxy 不停止或重启已有 OpenCode 服务。
77
+ 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 服务。
76
78
 
77
79
  Claw 的“编码工具账号”窗口支持 Codex API Key/设备码、ZCode 原生 Z.AI OAuth、OpenCode 原生 API Key/OAuth。凭据由编码工具保存,模型列表从原生接口读取。详细行为与验证边界见 [编码工具账号](../../docs/tool-authentication.md)。
@@ -12505,7 +12505,7 @@ async function buildProxySnapshot(config, runtime, adapter, requestedProjectId)
12505
12505
  command: config.tool.command,
12506
12506
  mode: config.tool.mode,
12507
12507
  autoStart: config.tool.autoStart,
12508
- ...config.tool.kind === "opencode" ? { baseUrl: config.tool.baseUrl } : {}
12508
+ ...config.tool.kind === "opencode" ? { baseUrl: typeof status.baseUrl === "string" ? status.baseUrl : config.tool.baseUrl } : {}
12509
12509
  }
12510
12510
  },
12511
12511
  projects,
@@ -14244,7 +14244,7 @@ var OpenCodeAdapter = class {
14244
14244
  media;
14245
14245
  async request(project, method, path, body, signal) {
14246
14246
  const root = await this.workspace.root(project);
14247
- const url = new URL(path, this.runtime.config.tool.baseUrl);
14247
+ const url = new URL(path, this.runtime.baseUrl);
14248
14248
  if (path === "/api/session") url.searchParams.set("directory", root);
14249
14249
  else if (/^\/api\/(?:agent|model|provider|mcp|command|config|pty|permission\/request|form|integration|location|experimental\/mcp)(?:\/|$)/.test(path)) url.searchParams.set("location[directory]", root);
14250
14250
  const response = await fetch(url, {
@@ -14490,7 +14490,7 @@ var OpenCodeAdapter = class {
14490
14490
  const root = await this.workspace.root(project);
14491
14491
  while (!controller.signal.aborted) {
14492
14492
  try {
14493
- const response = await fetch(new URL("/api/event", this.runtime.config.tool.baseUrl), { headers: this.runtime.headers(), signal: controller.signal });
14493
+ const response = await fetch(new URL("/api/event", this.runtime.baseUrl), { headers: this.runtime.headers(), signal: controller.signal });
14494
14494
  if (!response.ok || !response.body) throw new Error("Event connection unavailable");
14495
14495
  this.emit(project, "octopus.resync", {});
14496
14496
  let buffer = "";
@@ -14536,7 +14536,7 @@ var OpenCodeAdapter = class {
14536
14536
  const previous = this.terminals.get(key);
14537
14537
  this.terminals.delete(key);
14538
14538
  previous?.close();
14539
- const url = new URL(`/api/pty/${id}/connect`, this.runtime.config.tool.baseUrl);
14539
+ const url = new URL(`/api/pty/${id}/connect`, this.runtime.baseUrl);
14540
14540
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
14541
14541
  url.searchParams.set("location[directory]", await this.workspace.root(project));
14542
14542
  const socket2 = new WebSocket(url, { headers: this.runtime.headers() });
@@ -14646,17 +14646,32 @@ function redact(value) {
14646
14646
 
14647
14647
  // src/runtime.ts
14648
14648
  import { spawn } from "node:child_process";
14649
- import { mkdir as mkdir4, open as open4 } from "node:fs/promises";
14649
+ import { createHash as createHash7 } from "node:crypto";
14650
+ import { mkdir as mkdir4, open as open4, readFile as readFile5, rm, writeFile as writeFile4 } from "node:fs/promises";
14651
+ import { createServer } from "node:net";
14650
14652
  import { join as join5 } from "node:path";
14651
14653
  var Runtime = class {
14652
14654
  constructor(config) {
14653
14655
  this.config = config;
14656
+ this.endpoint = config.tool.baseUrl;
14654
14657
  }
14655
14658
  kind = "opencode";
14656
14659
  version = "";
14657
14660
  state = "external";
14658
14661
  starting;
14662
+ endpoint;
14663
+ endpointRestored = false;
14664
+ get baseUrl() {
14665
+ return this.endpoint;
14666
+ }
14667
+ get endpointStatePath() {
14668
+ return join5(this.config.stateDirectory, "opencode-endpoint.json");
14669
+ }
14670
+ get passwordHash() {
14671
+ return createHash7("sha256").update(this.config.tool.password).digest("hex");
14672
+ }
14659
14673
  async init() {
14674
+ await this.restoreEndpoint();
14660
14675
  if (this.config.tool.autoStart) await this.start();
14661
14676
  else await this.health();
14662
14677
  }
@@ -14666,7 +14681,7 @@ var Runtime = class {
14666
14681
  }
14667
14682
  async health() {
14668
14683
  try {
14669
- const response = await fetch(new URL("/api/info", this.config.tool.baseUrl), { headers: this.headers(), signal: AbortSignal.timeout(2e3) });
14684
+ const response = await fetch(new URL("/api/info", this.baseUrl), { headers: this.headers(), signal: AbortSignal.timeout(2e3) });
14670
14685
  if (!response.ok) return false;
14671
14686
  const data = await response.json();
14672
14687
  this.version = data.version ?? this.version;
@@ -14683,7 +14698,9 @@ var Runtime = class {
14683
14698
  state: this.state,
14684
14699
  healthy,
14685
14700
  version: this.version,
14686
- ...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` }
14701
+ baseUrl: this.baseUrl,
14702
+ ...this.baseUrl === this.config.tool.baseUrl ? {} : { configuredBaseUrl: this.config.tool.baseUrl },
14703
+ ...healthy ? {} : { error: `\u65E0\u6CD5\u8FDE\u63A5 OpenCode\uFF1A${this.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` }
14687
14704
  };
14688
14705
  }
14689
14706
  async start() {
@@ -14692,7 +14709,8 @@ var Runtime = class {
14692
14709
  });
14693
14710
  }
14694
14711
  async launch() {
14695
- const url = new URL(this.config.tool.baseUrl);
14712
+ await this.restoreEndpoint();
14713
+ let url = new URL(this.baseUrl);
14696
14714
  const endpoint = new URL("/api/info", url);
14697
14715
  let response;
14698
14716
  try {
@@ -14702,7 +14720,12 @@ var Runtime = class {
14702
14720
  }
14703
14721
  if (response) {
14704
14722
  if (response.ok) return this.status();
14705
- 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`);
14723
+ if ((response.status === 401 || response.status === 403) && url.origin === new URL(this.config.tool.baseUrl).origin) {
14724
+ url = await this.reserveFallbackUrl(url);
14725
+ this.endpoint = url.origin;
14726
+ } else {
14727
+ 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`);
14728
+ }
14706
14729
  }
14707
14730
  await mkdir4(this.config.stateDirectory, { recursive: true, mode: 448 });
14708
14731
  const logPath = join5(this.config.stateDirectory, "opencode-serve.log");
@@ -14724,7 +14747,20 @@ var Runtime = class {
14724
14747
  });
14725
14748
  child.unref();
14726
14749
  } catch (error) {
14750
+ this.endpoint = this.config.tool.baseUrl;
14727
14751
  throw new Error(`\u65E0\u6CD5\u6267\u884C OpenCode \u547D\u4EE4 ${this.config.tool.command}\uFF1A${error instanceof Error ? error.message : String(error)}`);
14752
+ }
14753
+ try {
14754
+ await writeFile4(this.endpointStatePath, JSON.stringify({
14755
+ configuredBaseUrl: this.config.tool.baseUrl,
14756
+ baseUrl: url.origin,
14757
+ passwordHash: this.passwordHash,
14758
+ pid: child.pid
14759
+ }, null, 2), { mode: 384 });
14760
+ } catch (error) {
14761
+ if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
14762
+ this.endpoint = this.config.tool.baseUrl;
14763
+ throw new Error(`\u65E0\u6CD5\u4FDD\u5B58 OpenCode \u8FD0\u884C\u7AEF\u70B9\uFF1A${error instanceof Error ? error.message : String(error)}`);
14728
14764
  } finally {
14729
14765
  await log.close();
14730
14766
  }
@@ -14735,8 +14771,40 @@ var Runtime = class {
14735
14771
  await new Promise((resolve4) => setTimeout(resolve4, 250));
14736
14772
  }
14737
14773
  if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
14774
+ await rm(this.endpointStatePath, { force: true }).catch(() => {
14775
+ });
14776
+ this.endpoint = this.config.tool.baseUrl;
14738
14777
  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`);
14739
14778
  }
14779
+ async restoreEndpoint() {
14780
+ if (this.endpointRestored) return;
14781
+ this.endpointRestored = true;
14782
+ this.endpoint = this.config.tool.baseUrl;
14783
+ try {
14784
+ const saved = JSON.parse(await readFile5(this.endpointStatePath, "utf8"));
14785
+ if (saved.configuredBaseUrl !== this.config.tool.baseUrl || saved.passwordHash !== this.passwordHash || typeof saved.baseUrl !== "string") return;
14786
+ const url = new URL(saved.baseUrl);
14787
+ if (url.protocol !== "http:" || !["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)) return;
14788
+ this.endpoint = url.origin;
14789
+ if (!await this.health()) this.endpoint = this.config.tool.baseUrl;
14790
+ } catch {
14791
+ }
14792
+ }
14793
+ async reserveFallbackUrl(configured) {
14794
+ const host = configured.hostname === "[::1]" ? "::1" : configured.hostname;
14795
+ const server = createServer();
14796
+ await new Promise((resolve4, reject) => {
14797
+ server.once("error", reject);
14798
+ server.listen(0, host, resolve4);
14799
+ });
14800
+ const address = server.address();
14801
+ const port = typeof address === "object" && address ? address.port : 0;
14802
+ await new Promise((resolve4, reject) => server.close((error) => error ? reject(error) : resolve4()));
14803
+ if (!port) throw new Error("\u65E0\u6CD5\u4E3A OpenCode \u5206\u914D\u5907\u7528\u56DE\u73AF\u7AEF\u53E3");
14804
+ const fallback = new URL(configured.origin);
14805
+ fallback.port = String(port);
14806
+ return fallback;
14807
+ }
14740
14808
  async stop() {
14741
14809
  throw new Error("OpenCode \u670D\u52A1\u7531\u5916\u90E8\u7BA1\u7406\uFF0CProxy \u4E0D\u652F\u6301\u505C\u6B62");
14742
14810
  }
@@ -16019,7 +16087,7 @@ var ZcodeAdapter = class {
16019
16087
 
16020
16088
  // src/tools/pi.ts
16021
16089
  import { randomUUID as randomUUID11 } from "node:crypto";
16022
- import { readFile as readFile5, readdir as readdir3, rm } from "node:fs/promises";
16090
+ import { readFile as readFile6, readdir as readdir3, rm as rm2 } from "node:fs/promises";
16023
16091
  import { dirname as dirname5 } from "node:path";
16024
16092
  var OPERATIONS3 = [
16025
16093
  ...DAEMON_OPERATIONS,
@@ -16265,8 +16333,8 @@ var PiAdapter = class {
16265
16333
  this.deleting.add(this.key(project, id));
16266
16334
  try {
16267
16335
  await this.runtime.closeSession(project, id);
16268
- await rm(this.runtime.sessionFile(project, id), { force: true });
16269
- await rm(this.metadataPath(project, id));
16336
+ await rm2(this.runtime.sessionFile(project, id), { force: true });
16337
+ await rm2(this.metadataPath(project, id));
16270
16338
  this.clearQuestions(project, id);
16271
16339
  } finally {
16272
16340
  this.deleting.delete(this.key(project, id));
@@ -16299,7 +16367,7 @@ var PiAdapter = class {
16299
16367
  for (const uploadId of Array.isArray(p.attachments) ? p.attachments : []) {
16300
16368
  const upload = await this.uploads.attachment(project, String(uploadId), id);
16301
16369
  if (["image/png", "image/jpeg", "image/webp", "image/gif"].includes(upload.mime)) {
16302
- images.push({ type: "image", data: (await readFile5(await this.workspace.path(project, upload.destination))).toString("base64"), mimeType: upload.mime });
16370
+ images.push({ type: "image", data: (await readFile6(await this.workspace.path(project, upload.destination))).toString("base64"), mimeType: upload.mime });
16303
16371
  } else lines.push(`Attached file: ${upload.destination} (${upload.name}, ${upload.mime}). Read this file if relevant to the task.`);
16304
16372
  }
16305
16373
  if (!lines.length && !images.length) throw new Error("Message or attachments required");
@@ -16489,14 +16557,55 @@ function createAdapter(runtime, workspace, uploads, emit) {
16489
16557
  return new OpenCodeAdapter(runtime, workspace, uploads, forward);
16490
16558
  }
16491
16559
 
16560
+ // src/hub-channel-compatibility.ts
16561
+ var HubChannelCompatibility = class {
16562
+ enabled = false;
16563
+ filesAttemptedAt = 0;
16564
+ filesFailures = 0;
16565
+ filesAttempt(now = Date.now()) {
16566
+ this.filesAttemptedAt = now;
16567
+ }
16568
+ filesWelcome() {
16569
+ this.filesFailures = 0;
16570
+ }
16571
+ controlClosed(filesPending, now = Date.now()) {
16572
+ if (this.enabled || !filesPending || now - this.filesAttemptedAt >= 5e3) return false;
16573
+ this.enabled = true;
16574
+ return true;
16575
+ }
16576
+ filesClosedBeforeWelcome(stopping) {
16577
+ if (this.enabled || stopping || ++this.filesFailures < 3) return false;
16578
+ this.enabled = true;
16579
+ return true;
16580
+ }
16581
+ };
16582
+
16583
+ // src/proxy-binding.ts
16584
+ var PROXY_REPLACED_CLOSE_CODE = 4009;
16585
+ var ProxyBinding = class {
16586
+ replaced = false;
16587
+ get active() {
16588
+ return !this.replaced;
16589
+ }
16590
+ observeClose(code) {
16591
+ if (code !== PROXY_REPLACED_CLOSE_CODE) return false;
16592
+ const first = !this.replaced;
16593
+ this.replaced = true;
16594
+ return first;
16595
+ }
16596
+ };
16597
+
16492
16598
  // src/daemon.ts
16493
16599
  async function runDaemon(configPath) {
16494
16600
  const config = await loadConfig(configPath);
16495
16601
  await mkdir7(config.stateDirectory, { recursive: true, mode: 448 });
16496
16602
  const workspace = new Workspace(config.projects), runtime = createRuntime(config), uploads = new Uploads(workspace, join10(config.stateDirectory, "uploads")), journal = new Journal(join10(config.stateDirectory, "journal"));
16497
16603
  const epoch = randomUUID12();
16498
- let stopping = false, botId = "";
16604
+ let stopping = false, botId = "", filesStarted = false;
16605
+ const hubChannels = new HubChannelCompatibility();
16606
+ const binding = new ProxyBinding();
16499
16607
  const sockets = /* @__PURE__ */ new Set();
16608
+ let filesSocket;
16500
16609
  const events = new EventStream(epoch, () => {
16501
16610
  for (const p of config.projects) emit(p.id, "octopus.resync", {});
16502
16611
  });
@@ -16544,13 +16653,17 @@ async function runDaemon(configPath) {
16544
16653
  return adapter.dispatch(c);
16545
16654
  }
16546
16655
  function connect(files, attempt = 0) {
16547
- if (stopping) return;
16656
+ if (stopping || !binding.active || files && hubChannels.enabled) return;
16548
16657
  const url = new URL(files ? "/ws/proxy-files" : "/ws/proxy", config.hubUrl);
16549
16658
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
16550
- const headers = { Authorization: `SecretKey ${config.secretKey}`, "X-Octopus-Bot-Id": config.botId };
16659
+ const headers = { Authorization: `SecretKey ${config.secretKey}`, "X-Octopus-Bot-Id": config.botId, "X-Octopus-Proxy-Instance-Id": epoch };
16551
16660
  const socket = new WebSocket2(url, { headers, maxPayload: 3 * 1024 * 1024 });
16552
16661
  sockets.add(socket);
16553
- let lastPong = Date.now();
16662
+ if (files) {
16663
+ filesSocket = socket;
16664
+ hubChannels.filesAttempt();
16665
+ }
16666
+ let lastPong = Date.now(), welcomed = false;
16554
16667
  const heartbeat = setInterval(() => {
16555
16668
  if (Date.now() - lastPong > 45e3) socket.terminate();
16556
16669
  else if (socket.readyState === WebSocket2.OPEN) socket.send(JSON.stringify({ type: "ping", proxyVersion }));
@@ -16571,11 +16684,17 @@ async function runDaemon(configPath) {
16571
16684
  return;
16572
16685
  }
16573
16686
  if (value.type === "welcome") {
16687
+ welcomed = true;
16688
+ if (files) hubChannels.filesWelcome();
16574
16689
  botId = value.botId;
16575
16690
  uploads.limits = value.limits;
16576
16691
  if (!files) {
16577
16692
  events.bind(socket, value.resume);
16578
16693
  for (const p of config.projects) emit(p.id, "octopus.resync", {});
16694
+ if (!filesStarted && !hubChannels.enabled) {
16695
+ filesStarted = true;
16696
+ connect(true);
16697
+ }
16579
16698
  }
16580
16699
  return;
16581
16700
  }
@@ -16588,18 +16707,32 @@ async function runDaemon(configPath) {
16588
16707
  console.error("Rejected message:", error instanceof Error ? error.message : "invalid message");
16589
16708
  }
16590
16709
  });
16591
- socket.on("error", () => {
16592
- });
16593
- socket.on("close", () => {
16710
+ socket.on("unexpected-response", (_request, response) => console.error(`Hub ${files ? "files" : "control"} handshake failed: HTTP ${response.statusCode}`));
16711
+ socket.on("error", (error) => console.error(`Hub ${files ? "files" : "control"} connection: ${error.message}`));
16712
+ socket.on("close", (code, reason) => {
16594
16713
  sockets.delete(socket);
16595
16714
  clearInterval(heartbeat);
16596
16715
  events.unbind(socket);
16597
- if (!stopping) setTimeout(() => connect(files, attempt + 1), Math.min(3e4, 1e3 * 2 ** Math.min(attempt, 5)) + Math.random() * 500);
16716
+ if (files && filesSocket === socket) filesSocket = void 0;
16717
+ const replaced = binding.observeClose(code);
16718
+ if (replaced) {
16719
+ console.error("This bot was claimed by another Proxy instance; automatic reconnection is disabled until this service is restarted");
16720
+ for (const peer of sockets) peer.close();
16721
+ }
16722
+ const collision = binding.active && !files && hubChannels.controlClosed(filesSocket !== void 0 && filesSocket.readyState !== WebSocket2.CLOSED);
16723
+ const rejectedFilesChannel = binding.active && files && !welcomed && hubChannels.filesClosedBeforeWelcome(stopping);
16724
+ if (collision || rejectedFilesChannel) {
16725
+ console.warn("Hub routes proxy-files as the control channel; using compatibility mode with one control connection");
16726
+ filesSocket?.close();
16727
+ filesSocket = void 0;
16728
+ } else if (!stopping && binding.active && code !== 1e3) {
16729
+ console.warn(`Hub ${files ? "files" : "control"} connection closed: ${code}${reason.length ? ` ${reason.toString()}` : ""}`);
16730
+ }
16731
+ if (!stopping && binding.active && (!files || !hubChannels.enabled)) setTimeout(() => connect(files, attempt + 1), collision ? 100 : Math.min(3e4, 1e3 * 2 ** Math.min(attempt, 5)) + Math.random() * 500);
16598
16732
  });
16599
16733
  }
16600
16734
  await runtime.init().catch((error) => console.error(`${runtime.kind} startup:`, error.message));
16601
16735
  connect(false);
16602
- connect(true);
16603
16736
  const scan = setInterval(async () => {
16604
16737
  for (const p of config.projects) await artifacts.scan(p.id).catch(() => {
16605
16738
  });
package/dist/cli.js CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  runDaemon,
23
23
  supportedPlatform,
24
24
  systemdUnitPath
25
- } from "./chunk-TXFPGQ4Z.js";
25
+ } from "./chunk-KYGWORYZ.js";
26
26
 
27
27
  // src/cli.ts
28
28
  import { spawn } from "node:child_process";
package/dist/main.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  ConfigError,
4
4
  runDaemon
5
- } from "./chunk-TXFPGQ4Z.js";
5
+ } from "./chunk-KYGWORYZ.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.21",
3
+ "version": "0.1.23",
4
4
  "type": "module",
5
5
  "description": "Octopus host proxy: OpenCode, Codex, ZCode and Pi adapters with service installer CLI",
6
6
  "bin": {