@skylandnpm/octopus-proxy 0.1.7 → 0.1.11

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
@@ -67,3 +67,9 @@ npm run publish
67
67
  ```
68
68
 
69
69
  预览包内容可运行 `pnpm build && npm pack --dry-run`。直接运行 `npm publish` 会提示使用 `npm run publish`,以防绕过版本递增和编译。
70
+
71
+ ## 编码工具账号与 OpenCode 服务
72
+
73
+ OpenCode 只支持 external:先自行运行 `opencode serve --hostname 127.0.0.1 --port 4096`,配置 `tool.mode: "external"` 和 `tool.baseUrl`。旧 managed 配置必须迁移,Proxy 不再管理 OpenCode 生命周期。
74
+
75
+ Claw 的“编码工具账号”窗口支持 Codex API Key/设备码、ZCode 原生 Z.AI OAuth、OpenCode 原生 API Key/OAuth。凭据由编码工具保存,模型列表从原生接口读取。详细行为与验证边界见 [编码工具账号](../../docs/tool-authentication.md)。
@@ -11602,10 +11602,12 @@ var ToolSchema = z.object({
11602
11602
  kind: z.enum(TOOL_KINDS).default("opencode"),
11603
11603
  command: z.string().default(""),
11604
11604
  baseUrl: z.string().url().optional(),
11605
- mode: z.enum(["managed", "external"]).default("managed"),
11606
- autoStart: z.boolean().default(true),
11605
+ mode: z.enum(["managed", "external"]).optional(),
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
11611
  var ConfigSchema = z.object({
11610
11612
  hubUrl: z.string().url(),
11611
11613
  botId: z.string().uuid(),
@@ -11658,11 +11660,15 @@ async function loadConfig(path) {
11658
11660
  import WebSocket2 from "ws";
11659
11661
  import { mkdir as mkdir5 } from "node:fs/promises";
11660
11662
  import { join as join9 } from "node:path";
11661
- import { randomUUID as randomUUID11 } from "node:crypto";
11663
+ import { randomUUID as randomUUID12 } from "node:crypto";
11662
11664
 
11663
11665
  // ../../packages/protocol/src/index.ts
11664
11666
  var PROTOCOL_VERSION = "1.0";
11665
11667
  var Operations = [
11668
+ "auth.status",
11669
+ "auth.login",
11670
+ "auth.poll",
11671
+ "auth.cancel",
11666
11672
  "runtime.status",
11667
11673
  "runtime.snapshot",
11668
11674
  "runtime.start",
@@ -12145,6 +12151,69 @@ var Uploads = class {
12145
12151
  }
12146
12152
  };
12147
12153
 
12154
+ // src/auth.ts
12155
+ import { randomUUID as randomUUID4 } from "node:crypto";
12156
+ var AUTH_OPERATIONS = ["auth.status", "auth.login", "auth.poll", "auth.cancel"];
12157
+ var AuthFlows = class {
12158
+ flows = /* @__PURE__ */ new Map();
12159
+ create(project, cancel) {
12160
+ if ([...this.flows.values()].some((f) => ["pending", "input"].includes(f.state.status))) throw new Error("\u5DF2\u6709\u767B\u5F55\u6B63\u5728\u8FDB\u884C\uFF0C\u8BF7\u5148\u5B8C\u6210\u6216\u53D6\u6D88");
12161
+ const state = { flowId: randomUUID4(), status: "pending" };
12162
+ const timer = setTimeout(() => {
12163
+ void this.cancel(project, state.flowId).catch(() => {
12164
+ });
12165
+ this.flows.delete(state.flowId);
12166
+ }, 10 * 6e4);
12167
+ timer.unref();
12168
+ this.flows.set(state.flowId, { project, state, cancel, timer });
12169
+ return state;
12170
+ }
12171
+ get(project, id) {
12172
+ const flow = this.flows.get(String(id));
12173
+ if (!flow || flow.project !== project) throw new Error("\u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u5F00\u59CB");
12174
+ return flow;
12175
+ }
12176
+ update(project, id, patch) {
12177
+ const flow = this.flows.get(id);
12178
+ if (!flow || flow.project !== project || flow.state.status === "cancelled") return;
12179
+ Object.assign(flow.state, patch);
12180
+ if (["completed", "failed"].includes(flow.state.status)) {
12181
+ delete flow.state.url;
12182
+ delete flow.state.code;
12183
+ delete flow.state.fields;
12184
+ flow.submit = void 0;
12185
+ }
12186
+ }
12187
+ async poll(project, payload) {
12188
+ const flow = this.get(project, payload.flowId);
12189
+ if (payload.input && flow.submit && flow.state.status === "input") {
12190
+ flow.state.status = "pending";
12191
+ const submit = flow.submit;
12192
+ flow.submit = void 0;
12193
+ void submit(payload.input).catch(() => this.update(project, flow.state.flowId, { status: "failed", message: "\u8BA4\u8BC1\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u8BA4\u8BC1\u4FE1\u606F\u540E\u91CD\u8BD5" }));
12194
+ }
12195
+ return { ...flow.state };
12196
+ }
12197
+ async cancel(project, id) {
12198
+ const flow = this.get(project, id);
12199
+ if (["pending", "input"].includes(flow.state.status)) await flow.cancel?.();
12200
+ flow.state = { flowId: flow.state.flowId, status: "cancelled" };
12201
+ flow.submit = void 0;
12202
+ return flow.state;
12203
+ }
12204
+ close() {
12205
+ for (const flow of this.flows.values()) {
12206
+ clearTimeout(flow.timer);
12207
+ void Promise.resolve(flow.cancel?.()).catch(() => {
12208
+ });
12209
+ }
12210
+ this.flows.clear();
12211
+ }
12212
+ };
12213
+ function isAuthentication(operation) {
12214
+ return operation.startsWith("auth.") || ["configuration.auth", "configuration.oauthAuthorize", "configuration.oauthCallback"].includes(operation);
12215
+ }
12216
+
12148
12217
  // src/journal.ts
12149
12218
  import { join as join3 } from "node:path";
12150
12219
  import { createHash as createHash3 } from "node:crypto";
@@ -12162,7 +12231,7 @@ var Journal = class {
12162
12231
  }
12163
12232
  const run3 = async () => {
12164
12233
  const path = join3(this.directory, `${command.id}.json`);
12165
- const readOnly = isReadOperation(command.operation);
12234
+ const readOnly = isReadOperation(command.operation) || isAuthentication(command.operation);
12166
12235
  if (!readOnly) {
12167
12236
  const old = await readJson(path);
12168
12237
  if (old) {
@@ -12175,7 +12244,7 @@ var Journal = class {
12175
12244
  try {
12176
12245
  response = { type: "response", id: command.id, botId: command.botId, status: "completed", data: await action() };
12177
12246
  } catch (error) {
12178
- response = this.failure(command, error instanceof Error ? error.message : "Operation failed");
12247
+ response = this.failure(command, isAuthentication(command.operation) ? "\u8BA4\u8BC1\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F16\u7801\u5DE5\u5177\u7684\u767B\u5F55\u72B6\u6001\u540E\u91CD\u8BD5" : error instanceof Error ? error.message : "Operation failed");
12179
12248
  if (!readOnly && error instanceof Error && ["TimeoutError", "AbortError", "UnknownResultError"].includes(error.name)) response.status = "unknown";
12180
12249
  }
12181
12250
  if (!readOnly) await atomicJson(path, { hash, response, finishedAt: (/* @__PURE__ */ new Date()).toISOString() });
@@ -12197,7 +12266,7 @@ var Journal = class {
12197
12266
  return result;
12198
12267
  }
12199
12268
  queueKey(c) {
12200
- if (isReadOperation(c.operation)) return void 0;
12269
+ if (isReadOperation(c.operation) || isAuthentication(c.operation)) return void 0;
12201
12270
  const [group] = c.operation.split(".");
12202
12271
  if (c.operation === "sessions.abort") return JSON.stringify(["abort", c.projectId, c.sessionId]);
12203
12272
  if (group === "sessions") return JSON.stringify(["session", c.projectId, c.sessionId ?? c.id]);
@@ -12390,7 +12459,7 @@ var EventStream = class {
12390
12459
  };
12391
12460
 
12392
12461
  // src/proxy-snapshot.ts
12393
- import { randomUUID as randomUUID4 } from "node:crypto";
12462
+ import { randomUUID as randomUUID5 } from "node:crypto";
12394
12463
  function record(value) {
12395
12464
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
12396
12465
  }
@@ -12419,34 +12488,13 @@ async function buildProxySnapshot(config, runtime, adapter, requestedProjectId)
12419
12488
  let modelCatalogStatus = capabilities.includes("configuration.models") ? "unavailable" : "unsupported";
12420
12489
  if (status.healthy === true && projectId && capabilities.includes("configuration.models")) {
12421
12490
  try {
12422
- const command = { type: "command", id: randomUUID4(), botId: config.botId, operation: "configuration.models", projectId, payload: {} };
12491
+ const command = { type: "command", id: randomUUID5(), botId: config.botId, operation: "configuration.models", projectId, payload: {} };
12423
12492
  models.push(...publicModels(await adapter.dispatch(command)));
12424
- modelCatalogStatus = "available";
12493
+ modelCatalogStatus = models.length ? "available" : "unavailable";
12425
12494
  } catch {
12426
12495
  modelCatalogStatus = "unavailable";
12427
12496
  }
12428
12497
  }
12429
- let recentModel;
12430
- if (status.healthy === true && projectId && modelCatalogStatus === "unsupported" && capabilities.includes("sessions.list")) {
12431
- try {
12432
- const command = { type: "command", id: randomUUID4(), botId: config.botId, operation: "sessions.list", projectId, payload: {} };
12433
- const sessions = await adapter.dispatch(command);
12434
- if (Array.isArray(sessions)) {
12435
- const withModel = sessions.map(record).filter((session) => {
12436
- const model = record(session.model);
12437
- return typeof model.providerId === "string" && typeof model.modelId === "string";
12438
- }).sort((left, right) => {
12439
- const leftTime = record(left.time).updated, rightTime = record(right.time).updated;
12440
- return (typeof rightTime === "number" ? rightTime : 0) - (typeof leftTime === "number" ? leftTime : 0);
12441
- })[0];
12442
- if (withModel) {
12443
- const model = record(withModel.model);
12444
- recentModel = { providerId: model.providerId, modelId: model.modelId };
12445
- }
12446
- }
12447
- } catch {
12448
- }
12449
- }
12450
12498
  return {
12451
12499
  runtime: { ...status, capabilities },
12452
12500
  config: {
@@ -12462,12 +12510,12 @@ async function buildProxySnapshot(config, runtime, adapter, requestedProjectId)
12462
12510
  }
12463
12511
  },
12464
12512
  projects,
12465
- modelCatalog: { projectId, status: modelCatalogStatus, models, ...recentModel ? { recentModel } : {} }
12513
+ modelCatalog: { projectId, status: modelCatalogStatus, models }
12466
12514
  };
12467
12515
  }
12468
12516
 
12469
12517
  // src/media.ts
12470
- import { createHash as createHash5, randomUUID as randomUUID5 } from "node:crypto";
12518
+ import { createHash as createHash5, randomUUID as randomUUID6 } from "node:crypto";
12471
12519
  import { constants as constants3 } from "node:fs";
12472
12520
  import { open as open3, unlink as unlink3, realpath as realpath3 } from "node:fs/promises";
12473
12521
  import { basename as basename3, isAbsolute as isAbsolute3, relative as relative3, resolve as resolve3, sep as sep2 } from "node:path";
@@ -12684,7 +12732,7 @@ var RemoteMedia = class {
12684
12732
  if (bytes.length === 0 || bytes.length > this.imageLimit()) throw new Error("Image is empty or exceeds size limit");
12685
12733
  const extension = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp", "application/pdf": "pdf" }[mime] ?? "bin";
12686
12734
  const directory = await this.workspace.attachmentDirectory(project, "media", sha256), path = directory + "/asset." + extension;
12687
- const temp = await this.workspace.path(project, directory + "/.octopus-media-" + randomUUID5(), true);
12735
+ const temp = await this.workspace.path(project, directory + "/.octopus-media-" + randomUUID6(), true);
12688
12736
  const file = await open3(temp, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | constants3.O_NOFOLLOW, 384);
12689
12737
  try {
12690
12738
  await file.writeFile(bytes);
@@ -14102,7 +14150,7 @@ function applyEdits(text, edits) {
14102
14150
 
14103
14151
  // src/project-config.ts
14104
14152
  import { readFile as readFile4, writeFile as writeFile3, unlink as unlink4 } from "node:fs/promises";
14105
- import { createHash as createHash6, randomUUID as randomUUID6 } from "node:crypto";
14153
+ import { createHash as createHash6, randomUUID as randomUUID7 } from "node:crypto";
14106
14154
  var ProjectConfig = class {
14107
14155
  constructor(workspace, request) {
14108
14156
  this.workspace = workspace;
@@ -14153,7 +14201,7 @@ var ProjectConfig = class {
14153
14201
  if (!this.validator(parse2(updated))) throw new Error("Merged OpenCode configuration is invalid: " + JSON.stringify(this.validator.errors));
14154
14202
  if (exists) await this.workspace.write(project, name, updated, createHash6("sha256").update(original).digest("hex"));
14155
14203
  else {
14156
- const temporary = await this.workspace.path(project, `.octopus-config-${randomUUID6()}.tmp`, true);
14204
+ const temporary = await this.workspace.path(project, `.octopus-config-${randomUUID7()}.tmp`, true);
14157
14205
  try {
14158
14206
  await writeFile3(temporary, updated, { mode: 384, flag: "wx" });
14159
14207
  await this.workspace.publish(project, temporary, name);
@@ -14178,8 +14226,11 @@ var OpenCodeAdapter = class {
14178
14226
  this.media = new RemoteMedia(workspace, () => uploads.limits.imageBytes);
14179
14227
  }
14180
14228
  supportedOperations() {
14181
- return Operations;
14229
+ return Operations.filter((op) => !["runtime.start", "runtime.stop", "runtime.restart", "runtime.logs"].includes(op));
14182
14230
  }
14231
+ auth = new AuthFlows();
14232
+ authDirty = /* @__PURE__ */ new Set();
14233
+ authRefreshing = /* @__PURE__ */ new Map();
14183
14234
  streams = /* @__PURE__ */ new Map();
14184
14235
  terminals = /* @__PURE__ */ new Map();
14185
14236
  // OpenCode 1.1.32 only buffers output while no client is attached.
@@ -14188,10 +14239,10 @@ var OpenCodeAdapter = class {
14188
14239
  resolving = /* @__PURE__ */ new Set();
14189
14240
  config;
14190
14241
  media;
14191
- async request(project, method, path, body) {
14242
+ async request(project, method, path, body, signal) {
14192
14243
  const root = await this.workspace.root(project), url = new URL(path, this.runtime.config.tool.baseUrl);
14193
14244
  url.searchParams.set("directory", root);
14194
- const response = await fetch(url, { method, headers: { ...this.runtime.headers(), "Content-Type": "application/json" }, body: body === void 0 ? void 0 : JSON.stringify(body), signal: AbortSignal.timeout(35e3) }).catch((error) => {
14245
+ const response = await fetch(url, { method, headers: { ...this.runtime.headers(), "Content-Type": "application/json" }, body: body === void 0 ? void 0 : JSON.stringify(body), signal: signal ?? AbortSignal.timeout(35e3) }).catch((error) => {
14195
14246
  if (method !== "GET") {
14196
14247
  const unknown = new Error("Transport interrupted; inspect remote state before retrying");
14197
14248
  unknown.name = "UnknownResultError";
@@ -14220,6 +14271,7 @@ var OpenCodeAdapter = class {
14220
14271
  this.workspace.project(project);
14221
14272
  this.subscribe(project);
14222
14273
  const op = command.operation, p = command.payload, id = command.sessionId;
14274
+ if (op.startsWith("auth.")) return this.authentication(project, op, p);
14223
14275
  const needsSession = op.startsWith("sessions.") && !["sessions.list", "sessions.create", "sessions.status"].includes(op);
14224
14276
  if (needsSession) {
14225
14277
  if (!id) throw new Error("Session required");
@@ -14259,6 +14311,11 @@ var OpenCodeAdapter = class {
14259
14311
  "terminal.list": ["GET", "/pty"],
14260
14312
  "terminal.create": ["POST", "/pty"]
14261
14313
  };
14314
+ if (op === "configuration.models") {
14315
+ await this.refreshAuth(project);
14316
+ const result2 = await this.request(project, "GET", "/provider");
14317
+ return redact({ providers: result2.all.filter((provider) => result2.connected.includes(provider.id)) });
14318
+ }
14262
14319
  if (op === "sessions.prompt") {
14263
14320
  const parts = [];
14264
14321
  if (typeof p.text === "string" && p.text.trim()) parts.push({ type: "text", text: screenshotPrompt(p.text) });
@@ -14317,6 +14374,71 @@ var OpenCodeAdapter = class {
14317
14374
  if (op.startsWith("sessions.")) return this.media.normalize(project, result);
14318
14375
  return result;
14319
14376
  }
14377
+ async refreshAuth(project) {
14378
+ if (!this.authDirty.has(project)) return;
14379
+ const pending = this.authRefreshing.get(project);
14380
+ if (pending) return pending;
14381
+ const refresh = (async () => {
14382
+ const statuses = await this.request(project, "GET", "/session/status");
14383
+ if (Object.values(statuses).some((status) => status.type !== "idle")) return;
14384
+ await this.request(project, "POST", "/instance/dispose");
14385
+ this.authDirty.delete(project);
14386
+ })();
14387
+ this.authRefreshing.set(project, refresh);
14388
+ try {
14389
+ await refresh;
14390
+ } finally {
14391
+ this.authRefreshing.delete(project);
14392
+ }
14393
+ }
14394
+ async authentication(project, operation, payload) {
14395
+ if (operation === "auth.poll") return this.auth.poll(project, payload);
14396
+ if (operation === "auth.cancel") return this.auth.cancel(project, payload.flowId);
14397
+ await this.refreshAuth(project);
14398
+ const methods = await this.request(project, "GET", "/provider/auth");
14399
+ const providers = await this.request(project, "GET", "/provider");
14400
+ for (const provider2 of providers.all) if (!methods[provider2.id]?.length) methods[provider2.id] = [{ type: "api", label: "API Key" }];
14401
+ if (operation === "auth.status") {
14402
+ const choices = Object.entries(methods).flatMap(([provider2, entries]) => entries.map((method2, index2) => ({
14403
+ id: `${provider2}:${index2}`,
14404
+ label: `${providers.all.find((p) => p.id === provider2)?.name ?? provider2} \xB7 ${method2.label}`,
14405
+ fields: method2.type === "api" ? [{ id: "key", label: "API Key", secret: true }] : (method2.prompts ?? []).map((prompt) => ({ id: prompt.key, label: prompt.message, secret: prompt.type !== "select", ...prompt.options ? { options: prompt.options.map(({ label, value }) => ({ label, value })) } : {} }))
14406
+ })));
14407
+ return { status: providers.connected.length ? "authenticated" : "unauthenticated", message: providers.connected.length ? "OpenCode \u5DF2\u914D\u7F6E\u4F9B\u5E94\u5546" : "OpenCode \u672A\u914D\u7F6E\u53EF\u7528\u4F9B\u5E94\u5546", providers: providers.connected, methods: choices };
14408
+ }
14409
+ const methodId = String(payload.method ?? ""), split = methodId.lastIndexOf(":");
14410
+ const provider = methodId.slice(0, split), index = Number(methodId.slice(split + 1));
14411
+ const method = methods[provider]?.[index];
14412
+ if (split < 0 || !method || !["api", "oauth"].includes(method.type)) throw new Error("\u4E0D\u652F\u6301\u7684 OpenCode \u767B\u5F55\u65B9\u5F0F");
14413
+ const input = payload.input ?? {};
14414
+ const controller = new AbortController();
14415
+ const flow = this.auth.create(project, () => controller.abort());
14416
+ const run3 = async () => {
14417
+ if (method.type === "api") {
14418
+ if (typeof input.key !== "string" || !input.key.trim()) throw new Error("API Key required");
14419
+ await this.request(project, "PUT", `/auth/${this.identifier(provider)}`, { type: "api", key: input.key }, controller.signal);
14420
+ for (const entry of this.runtime.config.projects) this.authDirty.add(entry.id);
14421
+ await this.refreshAuth(project);
14422
+ this.auth.update(project, flow.flowId, { status: "completed", message: this.authDirty.has(project) ? "\u8BA4\u8BC1\u4FE1\u606F\u5DF2\u4FDD\u5B58\uFF1B\u5F53\u524D\u4EFB\u52A1\u7ED3\u675F\u540E\u5237\u65B0\u6A21\u578B\u5217\u8868" : "\u8BA4\u8BC1\u4FE1\u606F\u5DF2\u7531 OpenCode \u4FDD\u5B58" });
14423
+ return;
14424
+ }
14425
+ const result = await this.request(project, "POST", `/provider/${this.identifier(provider)}/oauth/authorize`, { method: index, inputs: input }, controller.signal);
14426
+ this.auth.update(project, flow.flowId, { url: result.url, message: result.instructions });
14427
+ const callback = async (code) => {
14428
+ await this.request(project, "POST", `/provider/${this.identifier(provider)}/oauth/callback`, { method: index, ...typeof code === "string" ? { code } : {} }, controller.signal);
14429
+ for (const entry of this.runtime.config.projects) this.authDirty.add(entry.id);
14430
+ await this.refreshAuth(project);
14431
+ this.auth.update(project, flow.flowId, { status: "completed", message: this.authDirty.has(project) ? "\u767B\u5F55\u6210\u529F\uFF1B\u5F53\u524D\u4EFB\u52A1\u7ED3\u675F\u540E\u5237\u65B0\u6A21\u578B\u5217\u8868" : "\u767B\u5F55\u6210\u529F" });
14432
+ };
14433
+ if (result.method === "auto") await callback();
14434
+ else {
14435
+ this.auth.update(project, flow.flowId, { status: "input", fields: [{ id: "code", label: "\u6388\u6743\u7801\u6216\u56DE\u8C03\u5730\u5740", secret: true }] });
14436
+ this.auth.get(project, flow.flowId).submit = (values) => callback(values.code);
14437
+ }
14438
+ };
14439
+ void run3().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" }));
14440
+ return { ...flow };
14441
+ }
14320
14442
  subscribe(project) {
14321
14443
  if (this.streams.has(project)) return;
14322
14444
  const controller = new AbortController();
@@ -14369,7 +14491,9 @@ var OpenCodeAdapter = class {
14369
14491
  const history = this.terminalOutput.get(key) ?? "";
14370
14492
  this.emit(project, "terminal.reset", { id: p.id });
14371
14493
  for (let offset = 0; offset < history.length; offset += 65536) this.emit(project, "terminal.output", { id: p.id, data: history.slice(offset, offset + 65536) });
14372
- this.terminals.get(key)?.close();
14494
+ const previous = this.terminals.get(key);
14495
+ this.terminals.delete(key);
14496
+ previous?.close();
14373
14497
  const url = new URL(`/pty/${id}/connect`, this.runtime.config.tool.baseUrl);
14374
14498
  url.protocol = "ws:";
14375
14499
  url.searchParams.set("directory", await this.workspace.root(project));
@@ -14405,6 +14529,7 @@ var OpenCodeAdapter = class {
14405
14529
  return true;
14406
14530
  }
14407
14531
  close() {
14532
+ this.auth.close();
14408
14533
  for (const c of this.streams.values()) c.abort();
14409
14534
  for (const t of this.terminals.values()) t.close();
14410
14535
  }
@@ -14416,207 +14541,78 @@ function redact(value) {
14416
14541
  }
14417
14542
 
14418
14543
  // src/runtime.ts
14419
- import { spawn, execFile as execFile2 } from "node:child_process";
14420
- import { promisify as promisify2 } from "node:util";
14421
- import { randomBytes } from "node:crypto";
14422
- import { open as open4 } from "node:fs/promises";
14423
- import { join as join5 } from "node:path";
14424
14544
  var Runtime = class {
14425
14545
  constructor(config) {
14426
14546
  this.config = config;
14427
14547
  }
14428
- detached = false;
14429
- child;
14430
- wanted = false;
14431
- attempts = 0;
14432
- timer;
14433
- lines = [];
14434
- starting;
14435
- owned;
14436
- monitor;
14437
14548
  kind = "opencode";
14438
- password = "";
14439
14549
  version = "";
14440
- state = "stopped";
14441
- async identity(pid) {
14442
- try {
14443
- return (await promisify2(execFile2)("ps", ["-p", String(pid), "-o", "lstart=", "-o", "command="])).stdout.trim();
14444
- } catch {
14445
- return "";
14446
- }
14447
- }
14448
- async save() {
14449
- await atomicJson(join5(this.config.stateDirectory, "runtime.json"), { password: this.password, wanted: this.wanted, ...this.owned, baseUrl: this.config.tool.baseUrl, command: this.config.tool.command });
14450
- }
14451
- async stillOwned() {
14452
- const owned = this.owned;
14453
- return !!owned && await this.identity(owned.pid) === owned.identity && this.owned === owned;
14454
- }
14550
+ state = "external";
14455
14551
  async init() {
14456
- const saved = await readJson(join5(this.config.stateDirectory, "runtime.json"));
14457
- this.password = this.config.tool.password ?? saved?.password ?? randomBytes(32).toString("hex");
14458
- if (this.config.tool.mode === "external") {
14459
- this.password = this.config.tool.password ?? "";
14460
- this.state = "external";
14461
- return;
14462
- }
14463
- this.wanted = saved?.wanted ?? this.config.tool.autoStart;
14464
- if (saved?.pid && saved.identity && saved.baseUrl === this.config.tool.baseUrl && saved.command === this.config.tool.command) {
14465
- this.owned = { pid: saved.pid, identity: saved.identity };
14466
- if (await this.stillOwned() && await this.health()) this.state = "running";
14467
- else this.owned = void 0;
14468
- }
14469
- await this.save();
14470
- this.monitor = setInterval(() => {
14471
- if (this.owned && !this.child) void this.stillOwned().then((alive) => {
14472
- if (!alive) {
14473
- this.owned = void 0;
14474
- this.exited();
14475
- }
14476
- });
14477
- }, 3e3);
14478
- this.monitor.unref();
14479
- if (this.wanted && !this.owned) await this.start();
14552
+ await this.health();
14480
14553
  }
14481
14554
  headers() {
14482
- return this.password ? { Authorization: `Basic ${Buffer.from(`opencode:${this.password}`).toString("base64")}` } : {};
14555
+ const password = this.config.tool.password;
14556
+ return password ? { Authorization: `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` } : {};
14483
14557
  }
14484
14558
  async health() {
14485
14559
  try {
14486
- const r = await fetch(`${this.config.tool.baseUrl}/global/health`, { headers: this.headers(), signal: AbortSignal.timeout(2e3) });
14487
- if (!r.ok) return false;
14488
- const data = await r.json();
14560
+ const response = await fetch(`${this.config.tool.baseUrl}/global/health`, { headers: this.headers(), signal: AbortSignal.timeout(2e3) });
14561
+ if (!response.ok) return false;
14562
+ const data = await response.json();
14489
14563
  this.version = data.version ?? this.version;
14490
- return true;
14564
+ return data.healthy === true;
14491
14565
  } catch {
14492
14566
  return false;
14493
14567
  }
14494
14568
  }
14495
14569
  async status() {
14496
- return { tool: this.kind, mode: this.config.tool.mode, state: this.state, healthy: await this.health(), version: this.version, pid: this.owned?.pid, attempts: this.attempts };
14497
- }
14498
- start() {
14499
- if (this.detached) throw new Error("Runtime supervisor is detached");
14500
- if (this.config.tool.mode !== "managed") throw new Error("External instance is not owned; configure managed mode after stopping it explicitly");
14501
- if (this.starting) return this.starting;
14502
- this.starting = this.startOwned().finally(() => {
14503
- this.starting = void 0;
14504
- });
14505
- return this.starting;
14506
- }
14507
- exited() {
14508
- if (this.detached) return;
14509
- this.state = this.wanted ? "failed" : "stopped";
14510
- void this.save();
14511
- if (this.wanted && this.attempts < 5) {
14512
- const delay = Math.min(3e4, 1e3 * 2 ** this.attempts++);
14513
- this.timer = setTimeout(() => this.start().catch((e) => this.lines.push(String(e))), delay);
14514
- }
14515
- }
14516
- // Detach supervision when Proxy exits; the independently running tool is kept alive.
14517
- detach() {
14518
- this.detached = true;
14519
- clearTimeout(this.timer);
14520
- clearInterval(this.monitor);
14570
+ const healthy = await this.health();
14571
+ return {
14572
+ tool: this.kind,
14573
+ mode: "external",
14574
+ state: this.state,
14575
+ healthy,
14576
+ 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` }
14578
+ };
14521
14579
  }
14522
- async startOwned() {
14523
- this.wanted = true;
14524
- await this.save();
14525
- if (await this.stillOwned()) return this.status();
14526
- if (await this.health()) {
14527
- this.state = "unowned";
14528
- throw new Error("A server already occupies the endpoint; refusing implicit takeover");
14529
- }
14530
- const { stdout } = await promisify2(execFile2)(this.config.tool.command, ["--version"], { timeout: 15e3 });
14531
- this.version = stdout.trim();
14532
- if (this.version !== "1.1.32") throw new Error(`Unsupported OpenCode ${this.version}; verified baseline is 1.1.32`);
14533
- const url = new URL(this.config.tool.baseUrl);
14534
- this.state = "starting";
14535
- const log = await open4(join5(this.config.stateDirectory, "opencode.log"), "a", 384);
14536
- const child = spawn(this.config.tool.command, ["serve", "--hostname", "127.0.0.1", "--port", url.port || "4096"], { detached: true, cwd: this.config.projects[0].directory, env: { ...process.env, OPENCODE_SERVER_PASSWORD: this.password }, stdio: ["ignore", log.fd, log.fd] });
14537
- await log.close();
14538
- this.child = child;
14539
- child.unref();
14540
- child.once("error", (e) => {
14541
- this.lines.push(e.message);
14542
- if (this.child === child) {
14543
- this.child = void 0;
14544
- this.owned = void 0;
14545
- this.exited();
14546
- }
14547
- });
14548
- child.once("exit", () => {
14549
- if (this.child === child) {
14550
- this.child = void 0;
14551
- this.owned = void 0;
14552
- this.exited();
14553
- }
14554
- });
14555
- if (child.pid) {
14556
- const identity = await this.identity(child.pid);
14557
- if (identity) {
14558
- this.owned = { pid: child.pid, identity };
14559
- await this.save();
14560
- }
14561
- }
14562
- for (let i = 0; i < 100; i++) {
14563
- if (await this.health()) {
14564
- this.state = "running";
14565
- return this.status();
14566
- }
14567
- if (!this.child) throw new Error("OpenCode exited during startup");
14568
- await new Promise((resolve4) => setTimeout(resolve4, 200));
14569
- }
14570
- await this.stop();
14571
- throw new Error("OpenCode startup timed out");
14580
+ async start() {
14581
+ throw new Error("OpenCode \u4EC5\u652F\u6301 external \u6A21\u5F0F\uFF0C\u8BF7\u5728\u4E3B\u673A\u4E0A\u8FD0\u884C opencode serve");
14572
14582
  }
14573
14583
  async stop() {
14574
- if (this.config.tool.mode !== "managed") throw new Error("Cannot stop an externally managed instance");
14575
- this.wanted = false;
14576
- clearTimeout(this.timer);
14577
- await this.save();
14578
- if (this.owned && await this.stillOwned()) {
14579
- const pid = this.owned.pid;
14580
- process.kill(pid, "SIGTERM");
14581
- for (let i = 0; i < 50 && await this.stillOwned(); i++) await new Promise((resolve4) => setTimeout(resolve4, 100));
14582
- if (await this.stillOwned()) process.kill(pid, "SIGKILL");
14583
- }
14584
- this.child = void 0;
14585
- this.owned = void 0;
14586
- this.state = "stopped";
14587
- await this.save();
14588
- return this.status();
14584
+ throw new Error("OpenCode \u670D\u52A1\u7531\u5916\u90E8\u7BA1\u7406\uFF0CProxy \u4E0D\u652F\u6301\u505C\u6B62");
14589
14585
  }
14590
14586
  async restart() {
14591
- await this.stop();
14592
- this.attempts = 0;
14593
- return this.start();
14587
+ throw new Error("OpenCode \u670D\u52A1\u7531\u5916\u90E8\u7BA1\u7406\uFF0CProxy \u4E0D\u652F\u6301\u91CD\u542F");
14594
14588
  }
14595
14589
  async logs() {
14596
- let log = "";
14597
- const file = await open4(join5(this.config.stateDirectory, "opencode.log"), "r").catch(() => void 0);
14598
- if (file) {
14599
- try {
14600
- const size = (await file.stat()).size, bytes = Buffer.alloc(Math.min(size, 1e5));
14601
- await file.read(bytes, 0, bytes.length, Math.max(0, size - bytes.length));
14602
- log = bytes.toString("utf8");
14603
- } finally {
14604
- await file.close();
14605
- }
14606
- }
14607
- const result = log + this.lines.slice(-100).join("\n");
14608
- return this.password ? result.replaceAll(this.password, "[redacted]") : result;
14590
+ throw new Error("OpenCode \u65E5\u5FD7\u7531\u5916\u90E8\u670D\u52A1\u7BA1\u7406");
14591
+ }
14592
+ detach() {
14609
14593
  }
14610
14594
  };
14611
14595
 
14596
+ // src/tools/zcode-env.ts
14597
+ import { existsSync } from "node:fs";
14598
+ import { dirname as dirname4, join as join5 } from "node:path";
14599
+ function zcodeEnvironment(command) {
14600
+ const env = { ...process.env };
14601
+ if (!env.ZCODE_BUILTIN_PROVIDER_CONFIG_FILE) {
14602
+ const bundled = join5(dirname4(command), "..", "config", "provider", "zcode-builtin.json");
14603
+ if (existsSync(bundled)) env.ZCODE_BUILTIN_PROVIDER_CONFIG_FILE = bundled;
14604
+ }
14605
+ return env;
14606
+ }
14607
+
14612
14608
  // src/tools/stdio-runtime.ts
14613
- import { spawn as spawn2, execFile as execFile3 } from "node:child_process";
14614
- import { promisify as promisify3 } from "node:util";
14615
- import { open as open5 } from "node:fs/promises";
14609
+ import { spawn, execFile as execFile2 } from "node:child_process";
14610
+ import { promisify as promisify2 } from "node:util";
14611
+ import { open as open4 } from "node:fs/promises";
14616
14612
  import { join as join6 } from "node:path";
14617
14613
 
14618
14614
  // src/tools/line-rpc.ts
14619
- import { randomUUID as randomUUID7 } from "node:crypto";
14615
+ import { randomUUID as randomUUID8 } from "node:crypto";
14620
14616
  import { crc32 } from "node:zlib";
14621
14617
  import { StringDecoder } from "node:string_decoder";
14622
14618
  function interrupted(message) {
@@ -14801,11 +14797,11 @@ var LineRpc = class {
14801
14797
  }
14802
14798
  };
14803
14799
  function wrapEnvelope(frame) {
14804
- return { wireVersion: 3, kind: "complete", deliveryKind: "desktop-continuous", logicalFrameId: randomUUID7(), logicalFrameOrdinal: 1, topic: "outbound", subscriptionId: "outbound", frame };
14800
+ return { wireVersion: 3, kind: "complete", deliveryKind: "desktop-continuous", logicalFrameId: randomUUID8(), logicalFrameOrdinal: 1, topic: "outbound", subscriptionId: "outbound", frame };
14805
14801
  }
14806
14802
 
14807
14803
  // src/tools/stdio-runtime.ts
14808
- var run = promisify3(execFile3);
14804
+ var run = promisify2(execFile2);
14809
14805
  var StdioRuntime = class {
14810
14806
  constructor(config) {
14811
14807
  this.config = config;
@@ -14823,6 +14819,7 @@ var StdioRuntime = class {
14823
14819
  launching = /* @__PURE__ */ new Map();
14824
14820
  attempts = /* @__PURE__ */ new Map();
14825
14821
  timers = /* @__PURE__ */ new Map();
14822
+ refreshing = /* @__PURE__ */ new Set();
14826
14823
  wanted = false;
14827
14824
  detached = false;
14828
14825
  async init() {
@@ -14854,10 +14851,22 @@ var StdioRuntime = class {
14854
14851
  await Promise.allSettled(this.config.projects.map((project) => this.track(project.id, project.directory)));
14855
14852
  return this.status();
14856
14853
  }
14854
+ async reconnectProject(projectId) {
14855
+ const child = this.children.get(projectId);
14856
+ if (!child) return;
14857
+ this.refreshing.add(projectId);
14858
+ try {
14859
+ const exited = child.exitCode !== null || child.signalCode !== null ? Promise.resolve() : new Promise((resolve4) => child.once("exit", () => resolve4()));
14860
+ this.kill(projectId);
14861
+ await exited;
14862
+ } finally {
14863
+ this.refreshing.delete(projectId);
14864
+ }
14865
+ }
14857
14866
  async logs() {
14858
14867
  const chunks = [];
14859
14868
  for (const [projectId, logPath] of this.logPaths) {
14860
- const file = await open5(logPath, "r").catch(() => void 0);
14869
+ const file = await open4(logPath, "r").catch(() => void 0);
14861
14870
  if (!file) continue;
14862
14871
  try {
14863
14872
  const size = (await file.stat()).size, bytes = Buffer.alloc(Math.min(size, 3e4));
@@ -14924,8 +14933,8 @@ ${bytes.toString("utf8")}`);
14924
14933
  if (inFlight) return inFlight;
14925
14934
  const spawnOnce = (async () => {
14926
14935
  const logPath = join6(this.config.stateDirectory, `${this.kind}-${projectId}.log`);
14927
- const log = await open5(logPath, "a", 384).catch(() => void 0);
14928
- const child = spawn2(this.config.tool.command, [...this.descriptor.serverArgs], { cwd: directory, env: { ...process.env }, stdio: ["pipe", "pipe", log ? log.fd : "ignore"] });
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"] });
14929
14938
  if (log) child.once("exit", () => {
14930
14939
  void log.close();
14931
14940
  });
@@ -14941,7 +14950,7 @@ ${bytes.toString("utf8")}`);
14941
14950
  return spawnOnce;
14942
14951
  }
14943
14952
  onExited(projectId, _reason) {
14944
- if (this.detached || !this.wanted) return;
14953
+ if (this.detached || !this.wanted || this.refreshing.has(projectId)) return;
14945
14954
  const attempts = (this.attempts.get(projectId) ?? 0) + 1;
14946
14955
  if (attempts > 5) {
14947
14956
  console.error(`${projectId}: app server exited; retry budget exhausted`);
@@ -15004,9 +15013,10 @@ function unsupportedOperation(kind, operation) {
15004
15013
  }
15005
15014
 
15006
15015
  // src/tools/codex.ts
15007
- import { randomUUID as randomUUID8 } from "node:crypto";
15016
+ import { randomUUID as randomUUID9 } from "node:crypto";
15008
15017
  var OPERATIONS = [
15009
15018
  ...DAEMON_OPERATIONS,
15019
+ ...AUTH_OPERATIONS,
15010
15020
  "sessions.list",
15011
15021
  "sessions.create",
15012
15022
  "sessions.get",
@@ -15030,6 +15040,10 @@ var CodexAdapter = class {
15030
15040
  this.emit = emit;
15031
15041
  this.media = new RemoteMedia(workspace, () => uploads.limits.imageBytes);
15032
15042
  }
15043
+ auth = new AuthFlows();
15044
+ authDirty = /* @__PURE__ */ new Set();
15045
+ authReloading = /* @__PURE__ */ new Map();
15046
+ logins = /* @__PURE__ */ new Map();
15033
15047
  media;
15034
15048
  liveTurns = /* @__PURE__ */ new Map();
15035
15049
  approvals = /* @__PURE__ */ new Map();
@@ -15039,10 +15053,36 @@ var CodexAdapter = class {
15039
15053
  supportedOperations() {
15040
15054
  return OPERATIONS;
15041
15055
  }
15056
+ accountChanged(project) {
15057
+ for (const entry of this.runtime.config.projects) if (entry.id !== project) this.authDirty.add(entry.id);
15058
+ }
15042
15059
  async rpc(project) {
15043
15060
  this.workspace.project(project);
15061
+ if (this.authDirty.has(project) && ![...this.activeTurns.keys()].some((id) => this.loadedThreads.get(project)?.has(id))) {
15062
+ let pending = this.authReloading.get(project);
15063
+ if (!pending) {
15064
+ pending = this.runtime.reconnectProject(project).then(() => {
15065
+ this.authDirty.delete(project);
15066
+ }).finally(() => {
15067
+ this.authReloading.delete(project);
15068
+ });
15069
+ this.authReloading.set(project, pending);
15070
+ }
15071
+ await pending;
15072
+ }
15044
15073
  const rpc = await this.runtime.connect(project, {
15045
15074
  onNotification: (method, params) => {
15075
+ if (method === "account/login/completed") {
15076
+ const result = params;
15077
+ if (result.success) this.accountChanged(project);
15078
+ const login = this.logins.get(project);
15079
+ if (login && result.loginId) {
15080
+ if (login.loginId === result.loginId) this.auth.update(project, login.flowId, { status: result.success ? "completed" : "failed", message: result.success ? "\u767B\u5F55\u6210\u529F" : "\u767B\u5F55\u672A\u5B8C\u6210\uFF0C\u8BF7\u91CD\u8BD5" });
15081
+ else if (!login.loginId && login.early.size < 8) login.early.set(result.loginId, result.success === true);
15082
+ }
15083
+ return;
15084
+ }
15085
+ if (method.startsWith("account/")) return;
15046
15086
  if (method === "deprecationNotice") return;
15047
15087
  this.trackNotification(project, method, params);
15048
15088
  if (method === "turn/completed") {
@@ -15068,7 +15108,7 @@ var CodexAdapter = class {
15068
15108
  }
15069
15109
  onServerRequest(project, id, method, params, respond) {
15070
15110
  const detail = params ?? {};
15071
- const requestId = randomUUID8();
15111
+ const requestId = randomUUID9();
15072
15112
  const threadId = String(detail.threadId ?? "");
15073
15113
  if (["item/permissions/requestApproval", "item/commandExecution/requestApproval", "item/fileChange/requestApproval"].includes(method)) {
15074
15114
  this.approvals.set(requestId, { project, method, requestId, threadId, detail, respond });
@@ -15245,6 +15285,7 @@ var CodexAdapter = class {
15245
15285
  await this.rpc(project);
15246
15286
  }
15247
15287
  close() {
15288
+ this.auth.close();
15248
15289
  for (const approval of this.approvals.values()) approval.respond(this.approvalResponse(approval, "reject"));
15249
15290
  for (const input of this.inputs.values()) input.respond({ answers: {} });
15250
15291
  this.approvals.clear();
@@ -15256,8 +15297,47 @@ var CodexAdapter = class {
15256
15297
  if (!project) throw new Error("Project required");
15257
15298
  this.workspace.project(project);
15258
15299
  const op = command.operation, p = command.payload, id = command.sessionId;
15300
+ if (op === "auth.poll") return this.auth.poll(project, p);
15301
+ if (op === "auth.cancel") return this.auth.cancel(project, p.flowId);
15259
15302
  const rpc = await this.rpc(project);
15303
+ if (op === "auth.status") {
15304
+ const result = await rpc.request("account/read", { refreshToken: false });
15305
+ return {
15306
+ status: result.account ? "authenticated" : result.requiresOpenaiAuth ? "unauthenticated" : "notRequired",
15307
+ account: result.account ? { type: result.account.type, email: result.account.email, planType: result.account.planType } : null,
15308
+ methods: [{ id: "chatgptDeviceCode", label: "ChatGPT \u8BBE\u5907\u7801\u767B\u5F55" }, { id: "apiKey", label: "OpenAI API Key", fields: [{ id: "apiKey", label: "API Key", secret: true }] }]
15309
+ };
15310
+ }
15311
+ if (op === "auth.login") {
15312
+ if (!["apiKey", "chatgptDeviceCode"].includes(String(p.method))) throw new Error("\u4E0D\u652F\u6301\u7684 Codex \u767B\u5F55\u65B9\u5F0F");
15313
+ const input = p.input ?? {};
15314
+ if (p.method === "apiKey" && (typeof input.apiKey !== "string" || !input.apiKey.trim())) throw new Error("\u8BF7\u8F93\u5165 API Key");
15315
+ let loginId;
15316
+ const flow = this.auth.create(project, async () => {
15317
+ if (loginId) await rpc.request("account/login/cancel", { loginId });
15318
+ });
15319
+ const login = { flowId: flow.flowId, loginId: void 0, early: /* @__PURE__ */ new Map() };
15320
+ this.logins.set(project, login);
15321
+ void rpc.request("account/login/start", { type: p.method, ...p.method === "apiKey" ? { apiKey: input.apiKey } : {} }).then(async (result) => {
15322
+ if (result.type === "apiKey") this.accountChanged(project);
15323
+ loginId = result.loginId;
15324
+ login.loginId = loginId;
15325
+ if (this.auth.get(project, flow.flowId).state.status === "cancelled") {
15326
+ if (loginId) await rpc.request("account/login/cancel", { loginId });
15327
+ return;
15328
+ }
15329
+ if (loginId && login.early.has(loginId)) {
15330
+ this.auth.update(project, flow.flowId, { status: login.early.get(loginId) ? "completed" : "failed", message: login.early.get(loginId) ? "\u767B\u5F55\u6210\u529F" : "\u767B\u5F55\u672A\u5B8C\u6210\uFF0C\u8BF7\u91CD\u8BD5" });
15331
+ login.early.clear();
15332
+ return;
15333
+ }
15334
+ this.auth.update(project, flow.flowId, result.type === "apiKey" ? { status: "completed", message: "\u8BA4\u8BC1\u4FE1\u606F\u5DF2\u7531 Codex \u4FDD\u5B58" } : { url: result.verificationUrl, code: result.userCode, message: "\u6253\u5F00\u94FE\u63A5\u5E76\u8F93\u5165\u8BBE\u5907\u7801\u5B8C\u6210\u767B\u5F55" });
15335
+ }).catch(() => this.auth.update(project, flow.flowId, { status: "failed", message: "Codex \u767B\u5F55\u5931\u8D25\uFF1B\u8BF7\u68C0\u67E5\u767B\u5F55\u65B9\u5F0F\u662F\u5426\u53D7\u5F53\u524D\u7248\u672C\u53CA\u8D26\u53F7\u652F\u6301" }));
15336
+ return { ...flow };
15337
+ }
15260
15338
  if (op === "configuration.models") {
15339
+ const account = await rpc.request("account/read", { refreshToken: false });
15340
+ if (!account.account && account.requiresOpenaiAuth) return { providers: [] };
15261
15341
  const models = [];
15262
15342
  let cursor = null;
15263
15343
  const seenCursors = /* @__PURE__ */ new Set();
@@ -15397,10 +15477,13 @@ var CodexAdapter = class {
15397
15477
  };
15398
15478
 
15399
15479
  // src/tools/zcode.ts
15400
- import { randomUUID as randomUUID9 } from "node:crypto";
15480
+ import { spawn as spawn2 } from "node:child_process";
15481
+ import { randomUUID as randomUUID10 } from "node:crypto";
15401
15482
  import { realpath as realpath5 } from "node:fs/promises";
15402
15483
  var OPERATIONS2 = [
15403
15484
  ...DAEMON_OPERATIONS,
15485
+ ...AUTH_OPERATIONS,
15486
+ "configuration.models",
15404
15487
  "sessions.list",
15405
15488
  "sessions.create",
15406
15489
  "sessions.get",
@@ -15423,6 +15506,7 @@ var ZcodeAdapter = class {
15423
15506
  this.emit = emit;
15424
15507
  this.media = new RemoteMedia(workspace, () => uploads.limits.imageBytes);
15425
15508
  }
15509
+ auth = new AuthFlows();
15426
15510
  media;
15427
15511
  permissions = /* @__PURE__ */ new Map();
15428
15512
  inputs = /* @__PURE__ */ new Map();
@@ -15449,7 +15533,7 @@ var ZcodeAdapter = class {
15449
15533
  }
15450
15534
  onServerRequest(project, id, method, params, respond, respondError) {
15451
15535
  const detail = params ?? {};
15452
- const requestId = randomUUID9();
15536
+ const requestId = randomUUID10();
15453
15537
  const sessionId = String(detail.sessionId ?? "");
15454
15538
  if (method === "session/requestRuntimePreferences") {
15455
15539
  respond({ nativeSearchEnhancementsEnabled: false });
@@ -15550,6 +15634,7 @@ var ZcodeAdapter = class {
15550
15634
  await this.rpc(project);
15551
15635
  }
15552
15636
  close() {
15637
+ this.auth.close();
15553
15638
  for (const entry of this.permissions.values()) entry.respond({ decision: "deny" });
15554
15639
  for (const entry of this.inputs.values()) entry.respond({ action: "decline" });
15555
15640
  this.permissions.clear();
@@ -15560,7 +15645,54 @@ var ZcodeAdapter = class {
15560
15645
  if (!project) throw new Error("Project required");
15561
15646
  this.workspace.project(project);
15562
15647
  const op = command.operation, p = command.payload, id = command.sessionId;
15648
+ if (op === "auth.poll") return this.auth.poll(project, p);
15649
+ if (op === "auth.cancel") return this.auth.cancel(project, p.flowId);
15563
15650
  const rpc = await this.rpc(project);
15651
+ if (op === "configuration.models" || op === "auth.status") {
15652
+ const snapshot = await rpc.request("session/create", { workspace: await this.workspaceRef(project), persistence: "deferred", titleGenerationEnabled: false, toolAllowlist: [] });
15653
+ try {
15654
+ const model = snapshot.settings.model;
15655
+ if (op === "auth.status") return { status: model.available.length ? "authenticated" : "unauthenticated", message: model.available.length ? "ZCode \u5DF2\u914D\u7F6E\u53EF\u7528\u6A21\u578B\uFF08\u51ED\u636E\u6709\u6548\u6027\u7531\u5DE5\u5177\u5728\u8C03\u7528\u65F6\u68C0\u67E5\uFF09" : "ZCode \u672A\u914D\u7F6E\u53EF\u7528\u6A21\u578B", methods: [{ id: "zai", label: "Z.AI \u6D4F\u89C8\u5668\u767B\u5F55" }] };
15656
+ const providers = /* @__PURE__ */ new Map();
15657
+ for (const entry of model.available) {
15658
+ const provider = providers.get(entry.ref.providerId) ?? { id: entry.ref.providerId, name: entry.providerLabel, models: /* @__PURE__ */ Object.create(null) };
15659
+ provider.models[entry.ref.modelId] = { id: entry.ref.modelId, name: entry.label, isDefault: entry.ref.providerId === model.current?.providerId && entry.ref.modelId === model.current?.modelId };
15660
+ providers.set(provider.id, provider);
15661
+ }
15662
+ return { providers: [...providers.values()] };
15663
+ } finally {
15664
+ await rpc.request("session/close", { sessionId: snapshot.session.sessionId, expectedPersistence: "deferred" });
15665
+ }
15666
+ }
15667
+ if (op === "auth.login") {
15668
+ if (p.method !== "zai") throw new Error("\u4E0D\u652F\u6301\u7684 ZCode \u767B\u5F55\u65B9\u5F0F");
15669
+ let child;
15670
+ const flow = this.auth.create(project, () => {
15671
+ child?.kill("SIGTERM");
15672
+ });
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"] });
15674
+ let output = "", diagnostic = "";
15675
+ child.stdout?.on("data", (data) => {
15676
+ output = (output + data.toString()).slice(-65536);
15677
+ });
15678
+ child.stderr?.on("data", (data) => {
15679
+ diagnostic = (diagnostic + data.toString()).slice(-8192);
15680
+ const match = /Open this URL to sign in:\s*(https?:\/\/[^\s]+)/.exec(diagnostic);
15681
+ if (match) this.auth.update(project, flow.flowId, { url: match[1], message: "\u5728\u6D4F\u89C8\u5668\u5B8C\u6210 Z.AI \u6388\u6743\uFF0CZCode \u5C06\u81EA\u884C\u4FDD\u5B58\u8BA4\u8BC1\u4FE1\u606F" });
15682
+ });
15683
+ child.once("error", () => this.auth.update(project, flow.flowId, { status: "failed", message: "\u65E0\u6CD5\u542F\u52A8 ZCode \u539F\u751F\u767B\u5F55" }));
15684
+ child.once("exit", (code) => {
15685
+ let ready = false;
15686
+ try {
15687
+ ready = JSON.parse(output).status === "ready";
15688
+ } catch {
15689
+ }
15690
+ output = "";
15691
+ diagnostic = "";
15692
+ this.auth.update(project, flow.flowId, { status: code === 0 && ready ? "completed" : "failed", message: code === 0 && ready ? "ZCode \u767B\u5F55\u6210\u529F" : "ZCode \u767B\u5F55\u672A\u5B8C\u6210\uFF0C\u8BF7\u91CD\u8BD5" });
15693
+ });
15694
+ return { ...flow };
15695
+ }
15564
15696
  if (op === "sessions.list") {
15565
15697
  const result = await rpc.request("session/list", { workspace: await this.workspaceRef(project) });
15566
15698
  return (result.sessions ?? []).map((session) => this.sessionProjection(session));
@@ -15599,6 +15731,8 @@ var ZcodeAdapter = class {
15599
15731
  lines.push(`Attached file: ${upload.destination} (${upload.name}, ${upload.mime}). Read this file if relevant to the task.`);
15600
15732
  }
15601
15733
  if (!lines.length) throw new Error("Message or attachments required");
15734
+ const selected = p.model;
15735
+ if (selected?.providerID && selected.modelID) await rpc.request("session/setModel", { sessionId: id, model: { providerId: selected.providerID, modelId: selected.modelID } });
15602
15736
  return await rpc.request("session/send", { sessionId: id, content: lines.join("\n") });
15603
15737
  }
15604
15738
  if (op === "sessions.abort") {
@@ -15674,9 +15808,9 @@ var ZcodeAdapter = class {
15674
15808
  };
15675
15809
 
15676
15810
  // src/tools/pi.ts
15677
- import { randomUUID as randomUUID10 } from "node:crypto";
15811
+ import { randomUUID as randomUUID11 } from "node:crypto";
15678
15812
  import { readFile as readFile5, readdir as readdir3, rm } from "node:fs/promises";
15679
- import { dirname as dirname4 } from "node:path";
15813
+ import { dirname as dirname5 } from "node:path";
15680
15814
  var OPERATIONS3 = [
15681
15815
  ...DAEMON_OPERATIONS,
15682
15816
  "sessions.list",
@@ -15687,6 +15821,7 @@ var OPERATIONS3 = [
15687
15821
  "sessions.prompt",
15688
15822
  "sessions.abort",
15689
15823
  "sessions.delete",
15824
+ "auth.status",
15690
15825
  "configuration.models",
15691
15826
  "permissions.list",
15692
15827
  "questions.list",
@@ -15768,7 +15903,7 @@ var PiAdapter = class {
15768
15903
  }
15769
15904
  onQuestion(project, sessionId, detail) {
15770
15905
  if (!["select", "confirm", "input", "editor"].includes(String(detail.method))) return;
15771
- const id = randomUUID10();
15906
+ const id = randomUUID11();
15772
15907
  void this.rpc(project, sessionId).then((rpc) => {
15773
15908
  if (rpc.exited) return;
15774
15909
  const question = { id, project, sessionId, detail, rpc };
@@ -15847,6 +15982,11 @@ var PiAdapter = class {
15847
15982
  async execute(command, project) {
15848
15983
  const op = command.operation, p = command.payload, id = command.sessionId;
15849
15984
  if (!OPERATIONS3.includes(op)) unsupportedOperation("pi", op);
15985
+ if (op === "auth.status") {
15986
+ const rpc2 = await this.runtime.connect(project);
15987
+ const result = await rpc2.request("get_available_models");
15988
+ return { status: result.models.length ? "authenticated" : "unknown", methods: [], message: result.models.length ? "Pi \u5DF2\u914D\u7F6E\u53EF\u7528\u6A21\u578B" : "Pi RPC \u672A\u63D0\u4F9B\u8FDC\u7A0B\u767B\u5F55\u63A5\u53E3\uFF1B\u8BF7\u5728\u4E3B\u673A\u4F7F\u7528 Pi \u539F\u751F\u767B\u5F55\u540E\u5237\u65B0" };
15989
+ }
15850
15990
  if (op === "configuration.models") {
15851
15991
  const rpc2 = await this.runtime.connect(project);
15852
15992
  const { models } = await rpc2.request("get_available_models");
@@ -15859,7 +15999,7 @@ var PiAdapter = class {
15859
15999
  return { providers: [...providers.values()] };
15860
16000
  }
15861
16001
  if (op === "sessions.list") {
15862
- const directory = dirname4(this.metadataPath(project, "00000000-0000-0000-0000-000000000000"));
16002
+ const directory = dirname5(this.metadataPath(project, "00000000-0000-0000-0000-000000000000"));
15863
16003
  const files = await readdir3(directory).catch((error) => {
15864
16004
  if (error.code === "ENOENT") return [];
15865
16005
  throw error;
@@ -15868,7 +16008,7 @@ var PiAdapter = class {
15868
16008
  return sessions.sort((a, b) => b.time.updated - a.time.updated);
15869
16009
  }
15870
16010
  if (op === "sessions.create") {
15871
- const session2 = { id: randomUUID10(), title: typeof p.title === "string" && p.title.trim() ? p.title : "\u65B0\u4EFB\u52A1", directory: await this.workspace.root(project), time: { created: Date.now(), updated: Date.now() } };
16011
+ const session2 = { id: randomUUID11(), title: typeof p.title === "string" && p.title.trim() ? p.title : "\u65B0\u4EFB\u52A1", directory: await this.workspace.root(project), time: { created: Date.now(), updated: Date.now() } };
15872
16012
  try {
15873
16013
  const rpc2 = await this.rpc(project, session2.id);
15874
16014
  await rpc2.request("set_session_name", { name: session2.title });
@@ -15966,11 +16106,11 @@ var PiAdapter = class {
15966
16106
  };
15967
16107
 
15968
16108
  // src/tools/pi-runtime.ts
15969
- import { execFile as execFile4, spawn as spawn3 } from "node:child_process";
15970
- import { mkdir as mkdir4, open as open6 } from "node:fs/promises";
16109
+ import { execFile as execFile3, spawn as spawn3 } from "node:child_process";
16110
+ import { mkdir as mkdir4, open as open5 } from "node:fs/promises";
15971
16111
  import { join as join8 } from "node:path";
15972
- import { promisify as promisify4 } from "node:util";
15973
- var run2 = promisify4(execFile4);
16112
+ import { promisify as promisify3 } from "node:util";
16113
+ var run2 = promisify3(execFile3);
15974
16114
  var PiRuntime = class {
15975
16115
  constructor(config) {
15976
16116
  this.config = config;
@@ -16030,7 +16170,7 @@ var PiRuntime = class {
16030
16170
  async logs() {
16031
16171
  const chunks = [];
16032
16172
  for (const path of this.logPaths) {
16033
- const file = await open6(path, "r").catch(() => void 0);
16173
+ const file = await open5(path, "r").catch(() => void 0);
16034
16174
  if (!file) continue;
16035
16175
  try {
16036
16176
  const size = (await file.stat()).size, bytes = Buffer.alloc(Math.min(size, 3e4));
@@ -16079,7 +16219,7 @@ ${bytes.toString("utf8")}`);
16079
16219
  const directory = this.project(projectId).directory;
16080
16220
  await mkdir4(join8(this.config.stateDirectory, "pi-sessions", projectId), { recursive: true, mode: 448 });
16081
16221
  const logPath = join8(this.config.stateDirectory, `pi-${key}.log`);
16082
- const log = await open6(logPath, "a", 384);
16222
+ const log = await open5(logPath, "a", 384);
16083
16223
  if (generation !== this.generation) {
16084
16224
  await log.close();
16085
16225
  throw new Error("Pi runtime stopped during startup");
@@ -16144,7 +16284,7 @@ async function runDaemon(configPath) {
16144
16284
  const config = await loadConfig(configPath);
16145
16285
  await mkdir5(config.stateDirectory, { recursive: true, mode: 448 });
16146
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"));
16147
- const epoch = randomUUID11();
16287
+ const epoch = randomUUID12();
16148
16288
  let stopping = false, botId = "";
16149
16289
  const sockets = /* @__PURE__ */ new Set();
16150
16290
  const events = new EventStream(epoch, () => {
@@ -16155,6 +16295,7 @@ async function runDaemon(configPath) {
16155
16295
  }
16156
16296
  const adapter = createAdapter(runtime, workspace, uploads, emit), artifacts = new Artifacts(workspace, join9(config.stateDirectory, "artifacts"), emit);
16157
16297
  async function dispatch(c) {
16298
+ if (!adapter.supportedOperations().includes(c.operation)) throw new Error(`Unsupported ${runtime.kind} operation: ${c.operation}`);
16158
16299
  const p = c.payload;
16159
16300
  if (c.operation === "runtime.status") return { ...await runtime.status(), capabilities: adapter.supportedOperations() };
16160
16301
  if (c.operation === "runtime.snapshot") return buildProxySnapshot(config, runtime, adapter, c.projectId);
@@ -16284,6 +16425,10 @@ export {
16284
16425
  describeTool,
16285
16426
  ConfigError,
16286
16427
  loadConfig,
16428
+ Workspace,
16287
16429
  atomicJson,
16430
+ Uploads,
16431
+ createRuntime,
16432
+ createAdapter,
16288
16433
  runDaemon
16289
16434
  };
package/dist/cli.js CHANGED
@@ -4,7 +4,11 @@ import {
4
4
  SERVICE_LABEL,
5
5
  SYSTEMD_UNIT,
6
6
  TOOL_KINDS,
7
+ Uploads,
8
+ Workspace,
7
9
  atomicJson,
10
+ createAdapter,
11
+ createRuntime,
8
12
  daemonEntryPath,
9
13
  defaultStateDirectory,
10
14
  describeTool,
@@ -17,7 +21,7 @@ import {
17
21
  runDaemon,
18
22
  supportedPlatform,
19
23
  systemdUnitPath
20
- } from "./chunk-JWIM27KY.js";
24
+ } from "./chunk-AGWRSSBQ.js";
21
25
 
22
26
  // src/cli.ts
23
27
  import { spawn } from "node:child_process";
@@ -27,7 +31,6 @@ import { access as access4 } from "node:fs/promises";
27
31
  // src/doctor.ts
28
32
  import { access as access2, chmod, constants, mkdir as mkdir3, stat as stat2 } from "node:fs/promises";
29
33
  import { isAbsolute as isAbsolute2, join as join2 } from "node:path";
30
- import { homedir as homedir2 } from "node:os";
31
34
  import { execFile as execFile4 } from "node:child_process";
32
35
  import { promisify as promisify4 } from "node:util";
33
36
 
@@ -140,7 +143,12 @@ async function launchdStop() {
140
143
  await ignoreMissing(() => run2("launchctl", ["bootout", `${guiDomain()}/${SERVICE_LABEL}`]));
141
144
  }
142
145
  async function launchdRestart() {
143
- await launchdLoad();
146
+ const target = `${guiDomain()}/${SERVICE_LABEL}`;
147
+ if ((await launchdStatus()).loaded) {
148
+ await run2("launchctl", ["kickstart", "-k", target]);
149
+ } else {
150
+ await run2("launchctl", ["bootstrap", guiDomain(), launchdPlistPath()]);
151
+ }
144
152
  }
145
153
  async function launchdUninstall() {
146
154
  await launchdStop();
@@ -322,6 +330,7 @@ function logLocationHint() {
322
330
  }
323
331
 
324
332
  // src/doctor.ts
333
+ import { randomUUID } from "node:crypto";
325
334
  var run4 = promisify4(execFile4);
326
335
  async function runDoctor(opts) {
327
336
  const results = [];
@@ -354,7 +363,9 @@ async function runDoctor(opts) {
354
363
  const descriptor = describeTool(config.tool.kind);
355
364
  const checkName = `${descriptor.label} \u547D\u4EE4`;
356
365
  const command2 = config.tool.command;
357
- if (!isAbsolute2(command2)) {
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)) {
358
369
  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`);
359
370
  } else if (!await access2(command2, constants.X_OK).then(() => true, () => false)) {
360
371
  record(checkName, "fail", `${command2} \u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u6267\u884C`);
@@ -363,22 +374,29 @@ async function runDoctor(opts) {
363
374
  const { stdout: stdout2 } = await run4(command2, [...descriptor.versionArgs], { timeout: 15e3 });
364
375
  const version = stdout2.trim().split("\n")[0].trim();
365
376
  if (version === descriptor.baseline) record(checkName, "ok", `${command2}\uFF08${version}\uFF09`);
366
- else if (descriptor.versionPolicy === "exact") record(checkName, "warn", `${command2} \u7248\u672C\u4E3A ${version}\uFF0C\u5DF2\u9A8C\u8BC1\u57FA\u7EBF\u662F ${descriptor.baseline}\uFF08\u4EE3\u7406\u4F1A\u62D2\u7EDD\u6258\u7BA1\u8BE5\u7248\u672C\uFF09`);
377
+ 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`);
367
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`);
368
379
  } catch (error) {
369
380
  record(checkName, "warn", `\u6267\u884C ${command2} ${descriptor.versionArgs.join(" ")} \u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
370
381
  }
371
- if (config.tool.kind === "codex") {
372
- try {
373
- await run4(command2, ["login", "status"], { timeout: 15e3 });
374
- } catch {
375
- record("Codex \u767B\u5F55", "warn", "codex login status \u672A\u901A\u8FC7\uFF1B\u65E0\u5934\u4E3B\u673A\u8BF7\u5148\u5B8C\u6210 codex \u767B\u5F55\uFF08--with-api-key \u6216\u590D\u5236 auth.json\uFF09");
376
- }
377
- }
378
- if (config.tool.kind === "zcode") {
379
- const credentials = join2(homedir2(), ".zcode", "v2", "credentials.json");
380
- if (!await access2(credentials).then(() => true, () => false)) record("ZCode \u767B\u5F55", "warn", `\u672A\u627E\u5230 ${credentials}\uFF1B\u8BF7\u5148\u5728\u8BE5\u4E3B\u673A\u5B8C\u6210 zcode login`);
381
- }
382
+ }
383
+ }
384
+ if (config) {
385
+ const probeConfig = { ...config, tool: { ...config.tool, autoStart: false } };
386
+ const runtime = createRuntime(probeConfig), workspace = new Workspace(config.projects);
387
+ const adapter = createAdapter(runtime, workspace, new Uploads(workspace, join2(config.stateDirectory, "uploads")), () => {
388
+ });
389
+ try {
390
+ await runtime.init();
391
+ const result = await adapter.dispatch({ type: "command", id: randomUUID(), botId: config.botId, projectId: config.projects[0].id, operation: "auth.status", payload: {} });
392
+ const ready = ["authenticated", "notRequired"].includes(result.status);
393
+ record("\u7F16\u7801\u5DE5\u5177\u8D26\u53F7", ready ? "ok" : "warn", result.message ?? (ready ? "\u539F\u751F\u5DE5\u5177\u5DF2\u914D\u7F6E\u8D26\u53F7\u6216\u65E0\u9700\u8BA4\u8BC1" : "\u539F\u751F\u5DE5\u5177\u672A\u767B\u5F55\uFF1B\u53EF\u5728 Claw \u7684\u201C\u7F16\u7801\u5DE5\u5177\u8D26\u53F7\u201D\u4E2D\u767B\u5F55"));
394
+ } catch {
395
+ record("\u7F16\u7801\u5DE5\u5177\u8D26\u53F7", "warn", "\u65E0\u6CD5\u83B7\u53D6\u539F\u751F\u767B\u5F55\u72B6\u6001\uFF1B\u8BF7\u68C0\u67E5\u5DE5\u5177\u670D\u52A1\u3001\u7248\u672C\u4E0E\u914D\u7F6E\uFF0C\u518D\u5728 Claw \u4E2D\u91CD\u8BD5");
396
+ } finally {
397
+ adapter.close();
398
+ if (config.tool.kind !== "opencode") await runtime.stop();
399
+ runtime.detach();
382
400
  }
383
401
  }
384
402
  try {
@@ -489,7 +507,7 @@ async function runOnboard(opts) {
489
507
  let hubUrl = opts.hubUrl || existing?.hubUrl || DEFAULT_HUB_URL, botId = opts.botId ?? existing?.botId ?? "", secretKey = opts.secretKey ?? existing?.secretKey ?? "", stateDirectory = opts.stateDirectory ?? existing?.stateDirectory ?? defaultStateDirectory();
490
508
  let toolKind = opts.toolKind ?? existing?.tool.kind ?? "opencode";
491
509
  let toolCommand = opts.toolCommand ?? (toolKind === existing?.tool.kind ? existing.tool.command : "");
492
- let mode = toolKind === existing?.tool.kind ? existing.tool.mode : "managed";
510
+ let baseUrl = existing?.tool.kind === "opencode" ? existing.tool.baseUrl : "http://127.0.0.1:4096";
493
511
  let projects = [...existing?.projects ?? [], ...opts.projects].map((project) => ({ ...project, directory: expandHome(project.directory) }));
494
512
  if (opts.interactive) {
495
513
  const rl = createInterface({ input: stdin, output: stdout });
@@ -506,9 +524,21 @@ async function runOnboard(opts) {
506
524
  const descriptor = describeTool(toolKind);
507
525
  const found = await resolveCommandOnPath(toolKind, toolSearchDirs(toolKind));
508
526
  const example = platform2() === "darwin" ? MACOS_TOOL_EXAMPLES[toolKind] : void 0;
509
- toolCommand = expandHome(await ask(rl, found ? `${descriptor.label} \u547D\u4EE4\uFF08\u63A2\u6D4B\u5230 ${found}\uFF1B\u56DE\u8F66\u4F1A\u9A8C\u8BC1\uFF09` : `${descriptor.label} \u547D\u4EE4\uFF08\u56DE\u8F66\u4F1A\u9A8C\u8BC1\u8DEF\u5F84\uFF09`, { default: toolCommand || found || example, validate: (value) => validateToolCommand(value, toolKind) }));
510
- console.log(` \u5DF2\u786E\u8BA4 ${descriptor.label} \u547D\u4EE4\u53EF\u6267\u884C\uFF1A${toolCommand}`);
511
- if (toolKind === "opencode") mode = await ask(rl, "OpenCode \u6A21\u5F0F\uFF08managed=\u7531\u4EE3\u7406\u6258\u7BA1\u542F\u52A8\uFF0Cexternal=\u4EC5\u8FDE\u63A5\u5DF2\u6709\u670D\u52A1\uFF09", { default: mode, validate: (value) => value === "managed" || value === "external" ? void 0 : "\u8F93\u5165 managed \u6216 external" });
527
+ 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");
530
+ baseUrl = await ask(rl, "OpenCode \u670D\u52A1\u5730\u5740", { default: baseUrl || "http://127.0.0.1:4096", validate: (value) => {
531
+ try {
532
+ const url = new URL(value);
533
+ return url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname) ? void 0 : "\u8BF7\u8F93\u5165 HTTP loopback \u5730\u5740\uFF0C\u4F8B\u5982 http://127.0.0.1:4096";
534
+ } catch {
535
+ return "\u8BF7\u8F93\u5165\u6709\u6548 URL";
536
+ }
537
+ } });
538
+ } else {
539
+ toolCommand = expandHome(await ask(rl, `${descriptor.label} \u547D\u4EE4\uFF08\u56DE\u8F66\u4F1A\u9A8C\u8BC1\u8DEF\u5F84\uFF09`, { default: toolCommand || found || example, validate: (value) => validateToolCommand(value, toolKind) }));
540
+ console.log(` \u5DF2\u786E\u8BA4 ${descriptor.label} \u547D\u4EE4\u53EF\u6267\u884C\uFF1A${toolCommand}`);
541
+ }
512
542
  if (projects.length) console.log("\n\u5DF2\u6709\u6388\u6743\u9879\u76EE\uFF08\u76F4\u63A5\u56DE\u8F66\u4FDD\u7559\u5404\u9879\uFF09\uFF1A");
513
543
  for (const [index, project] of projects.entries()) {
514
544
  project.name = await ask(rl, " \u9879\u76EE\u540D\u79F0", { default: project.name });
@@ -597,7 +627,7 @@ async function runOnboard(opts) {
597
627
  stateDirectory,
598
628
  ...loopbackHttp ? { allowInsecureLocalhost: true } : {},
599
629
  projects: projects.map((project) => ({ id: project.id, name: project.name, directory: project.directory, specDirectories: project.specDirectories ?? ["specs", "docs"] })),
600
- tool: { kind: toolKind, command: toolCommand || describeTool(toolKind).defaultCommand, ...toolKind === "opencode" ? { baseUrl: toolKind === existing?.tool.kind ? existing.tool.baseUrl : "http://127.0.0.1:4096", mode } : {}, ...toolKind === existing?.tool.kind && existing.tool.password ? { password: existing.tool.password } : {}, autoStart: toolKind === existing?.tool.kind ? existing.tool.autoStart : true }
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 } }
601
631
  });
602
632
  await loadConfig(opts.configPath);
603
633
  console.log(`\u5DF2\u5199\u5165\u914D\u7F6E\uFF1A${opts.configPath}\uFF080600\uFF09`);
@@ -626,7 +656,7 @@ async function runOnboard(opts) {
626
656
  }
627
657
 
628
658
  // src/projects-command.ts
629
- import { randomUUID } from "node:crypto";
659
+ import { randomUUID as randomUUID2 } from "node:crypto";
630
660
  import { readFile as readFile2, writeFile as writeFile3 } from "node:fs/promises";
631
661
  function asConfig(value) {
632
662
  if (typeof value !== "object" || value === null || Array.isArray(value) || !Array.isArray(value.projects)) {
@@ -653,7 +683,7 @@ async function manageProjects(configPath2, remove) {
653
683
  const index = Number(remove) - 1;
654
684
  if (!Number.isSafeInteger(index) || index >= config.projects.length) throw new Error(`\u9879\u76EE\u5E8F\u53F7\u8D85\u51FA\u8303\u56F4\uFF1A${remove}`);
655
685
  if (config.projects.length === 1) throw new Error("\u81F3\u5C11\u9700\u8981\u4FDD\u7559\u4E00\u4E2A\u6388\u6743\u9879\u76EE\uFF1B\u65E0\u6CD5\u5220\u9664\u6700\u540E\u4E00\u9879");
656
- const backupPath = `${configPath2}.backup-${randomUUID()}`;
686
+ const backupPath = `${configPath2}.backup-${randomUUID2()}`;
657
687
  await writeFile3(backupPath, original, { mode: 384, flag: "wx" });
658
688
  config.projects.splice(index, 1);
659
689
  await atomicJson(configPath2, config);
package/dist/main.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  ConfigError,
4
4
  runDaemon
5
- } from "./chunk-JWIM27KY.js";
5
+ } from "./chunk-AGWRSSBQ.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.7",
3
+ "version": "0.1.11",
4
4
  "type": "module",
5
5
  "description": "Octopus host proxy: OpenCode, Codex, ZCode and Pi adapters with service installer CLI",
6
6
  "bin": {
@@ -4,5 +4,5 @@
4
4
  "botId": "00000000-0000-4000-8000-000000000000",
5
5
  "stateDirectory": "/home/developer/.local/state/octopus",
6
6
  "projects": [{ "id": "example", "name": "Example project", "directory": "/home/developer/projects/example", "specDirectories": ["specs", "docs"] }],
7
- "tool": { "kind": "opencode", "command": "/usr/local/bin/opencode", "baseUrl": "http://127.0.0.1:4096", "mode": "managed", "autoStart": true }
7
+ "tool": { "kind": "opencode", "command": "/usr/local/bin/opencode", "baseUrl": "http://127.0.0.1:4096", "mode": "external" }
8
8
  }