@skylandnpm/octopus-proxy 0.1.8 → 0.1.12

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,79 +14541,27 @@ 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
- lastError = "";
14442
- async identity(pid) {
14443
- try {
14444
- return (await promisify2(execFile2)("ps", ["-p", String(pid), "-o", "lstart=", "-o", "command="])).stdout.trim();
14445
- } catch {
14446
- return "";
14447
- }
14448
- }
14449
- async save() {
14450
- 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 });
14451
- }
14452
- async stillOwned() {
14453
- const owned = this.owned;
14454
- return !!owned && await this.identity(owned.pid) === owned.identity && this.owned === owned;
14455
- }
14550
+ state = "external";
14456
14551
  async init() {
14457
- const saved = await readJson(join5(this.config.stateDirectory, "runtime.json"));
14458
- this.password = this.config.tool.password ?? saved?.password ?? randomBytes(32).toString("hex");
14459
- if (this.config.tool.mode === "external") {
14460
- this.password = this.config.tool.password ?? "";
14461
- this.state = "external";
14462
- return;
14463
- }
14464
- this.wanted = saved?.wanted ?? this.config.tool.autoStart;
14465
- if (saved?.pid && saved.identity && saved.baseUrl === this.config.tool.baseUrl && saved.command === this.config.tool.command) {
14466
- this.owned = { pid: saved.pid, identity: saved.identity };
14467
- if (await this.stillOwned() && await this.health()) this.state = "running";
14468
- else this.owned = void 0;
14469
- }
14470
- await this.save();
14471
- this.monitor = setInterval(() => {
14472
- if (this.owned && !this.child) void this.stillOwned().then((alive) => {
14473
- if (!alive) {
14474
- this.owned = void 0;
14475
- this.exited();
14476
- }
14477
- });
14478
- }, 3e3);
14479
- this.monitor.unref();
14480
- if (this.wanted && !this.owned) await this.start();
14552
+ await this.health();
14481
14553
  }
14482
14554
  headers() {
14483
- 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")}` } : {};
14484
14557
  }
14485
14558
  async health() {
14486
14559
  try {
14487
- const r = await fetch(`${this.config.tool.baseUrl}/global/health`, { headers: this.headers(), signal: AbortSignal.timeout(2e3) });
14488
- if (!r.ok) return false;
14489
- 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();
14490
14563
  this.version = data.version ?? this.version;
14491
- return true;
14564
+ return data.healthy === true;
14492
14565
  } catch {
14493
14566
  return false;
14494
14567
  }
@@ -14497,145 +14570,49 @@ var Runtime = class {
14497
14570
  const healthy = await this.health();
14498
14571
  return {
14499
14572
  tool: this.kind,
14500
- mode: this.config.tool.mode,
14573
+ mode: "external",
14501
14574
  state: this.state,
14502
14575
  healthy,
14503
14576
  version: this.version,
14504
- pid: this.owned?.pid,
14505
- attempts: this.attempts,
14506
- ...healthy ? {} : { error: this.lastError || `OpenCode at ${this.config.tool.baseUrl} did not pass its health check` }
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` }
14507
14578
  };
14508
14579
  }
14509
- start() {
14510
- if (this.detached) throw new Error("Runtime supervisor is detached");
14511
- if (this.config.tool.mode !== "managed") throw new Error("External instance is not owned; configure managed mode after stopping it explicitly");
14512
- if (this.starting) return this.starting;
14513
- this.starting = this.startOwned().then((result) => {
14514
- this.lastError = "";
14515
- return result;
14516
- }).catch((error) => {
14517
- this.lastError = error instanceof Error ? error.message : String(error);
14518
- if (this.wanted && this.state !== "unowned") this.state = "failed";
14519
- throw error;
14520
- }).finally(() => {
14521
- this.starting = void 0;
14522
- });
14523
- return this.starting;
14524
- }
14525
- exited() {
14526
- if (this.detached) return;
14527
- this.state = this.wanted ? "failed" : "stopped";
14528
- if (this.wanted && !this.lastError) this.lastError = "OpenCode process exited unexpectedly";
14529
- void this.save();
14530
- if (this.wanted && this.attempts < 5) {
14531
- const delay = Math.min(3e4, 1e3 * 2 ** this.attempts++);
14532
- this.timer = setTimeout(() => this.start().catch((e) => this.lines.push(String(e))), delay);
14533
- }
14534
- }
14535
- // Detach supervision when Proxy exits; the independently running tool is kept alive.
14536
- detach() {
14537
- this.detached = true;
14538
- clearTimeout(this.timer);
14539
- clearInterval(this.monitor);
14540
- }
14541
- async startOwned() {
14542
- this.wanted = true;
14543
- await this.save();
14544
- if (await this.stillOwned()) return this.status();
14545
- if (await this.health()) {
14546
- this.state = "unowned";
14547
- throw new Error("A server already occupies the endpoint; refusing implicit takeover");
14548
- }
14549
- const { stdout } = await promisify2(execFile2)(this.config.tool.command, ["--version"], { timeout: 15e3 });
14550
- this.version = stdout.trim();
14551
- if (this.version !== "1.1.32") throw new Error(`Unsupported OpenCode ${this.version}; verified baseline is 1.1.32`);
14552
- const url = new URL(this.config.tool.baseUrl);
14553
- this.state = "starting";
14554
- const log = await open4(join5(this.config.stateDirectory, "opencode.log"), "a", 384);
14555
- 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] });
14556
- await log.close();
14557
- this.child = child;
14558
- child.unref();
14559
- child.once("error", (e) => {
14560
- this.lines.push(e.message);
14561
- if (this.child === child) {
14562
- this.child = void 0;
14563
- this.owned = void 0;
14564
- this.exited();
14565
- }
14566
- });
14567
- child.once("exit", () => {
14568
- if (this.child === child) {
14569
- this.child = void 0;
14570
- this.owned = void 0;
14571
- this.exited();
14572
- }
14573
- });
14574
- if (child.pid) {
14575
- const identity = await this.identity(child.pid);
14576
- if (identity) {
14577
- this.owned = { pid: child.pid, identity };
14578
- await this.save();
14579
- }
14580
- }
14581
- for (let i = 0; i < 100; i++) {
14582
- if (await this.health()) {
14583
- this.state = "running";
14584
- return this.status();
14585
- }
14586
- if (!this.child) throw new Error("OpenCode exited during startup");
14587
- await new Promise((resolve4) => setTimeout(resolve4, 200));
14588
- }
14589
- await this.stop();
14590
- 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");
14591
14582
  }
14592
14583
  async stop() {
14593
- if (this.config.tool.mode !== "managed") throw new Error("Cannot stop an externally managed instance");
14594
- this.wanted = false;
14595
- clearTimeout(this.timer);
14596
- await this.save();
14597
- if (this.owned && await this.stillOwned()) {
14598
- const pid = this.owned.pid;
14599
- process.kill(pid, "SIGTERM");
14600
- for (let i = 0; i < 50 && await this.stillOwned(); i++) await new Promise((resolve4) => setTimeout(resolve4, 100));
14601
- if (await this.stillOwned()) process.kill(pid, "SIGKILL");
14602
- }
14603
- this.child = void 0;
14604
- this.owned = void 0;
14605
- this.state = "stopped";
14606
- await this.save();
14607
- return this.status();
14584
+ throw new Error("OpenCode \u670D\u52A1\u7531\u5916\u90E8\u7BA1\u7406\uFF0CProxy \u4E0D\u652F\u6301\u505C\u6B62");
14608
14585
  }
14609
14586
  async restart() {
14610
- await this.stop();
14611
- this.attempts = 0;
14612
- return this.start();
14587
+ throw new Error("OpenCode \u670D\u52A1\u7531\u5916\u90E8\u7BA1\u7406\uFF0CProxy \u4E0D\u652F\u6301\u91CD\u542F");
14613
14588
  }
14614
14589
  async logs() {
14615
- let log = "";
14616
- const file = await open4(join5(this.config.stateDirectory, "opencode.log"), "r").catch(() => void 0);
14617
- if (file) {
14618
- try {
14619
- const size = (await file.stat()).size, bytes = Buffer.alloc(Math.min(size, 1e5));
14620
- await file.read(bytes, 0, bytes.length, Math.max(0, size - bytes.length));
14621
- log = bytes.toString("utf8");
14622
- } finally {
14623
- await file.close();
14624
- }
14625
- }
14626
- const result = log + this.lines.slice(-100).join("\n");
14627
- 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() {
14628
14593
  }
14629
14594
  };
14630
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
+
14631
14608
  // src/tools/stdio-runtime.ts
14632
- import { spawn as spawn2, execFile as execFile3 } from "node:child_process";
14633
- import { promisify as promisify3 } from "node:util";
14634
- 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";
14635
14612
  import { join as join6 } from "node:path";
14636
14613
 
14637
14614
  // src/tools/line-rpc.ts
14638
- import { randomUUID as randomUUID7 } from "node:crypto";
14615
+ import { randomUUID as randomUUID8 } from "node:crypto";
14639
14616
  import { crc32 } from "node:zlib";
14640
14617
  import { StringDecoder } from "node:string_decoder";
14641
14618
  function interrupted(message) {
@@ -14820,11 +14797,11 @@ var LineRpc = class {
14820
14797
  }
14821
14798
  };
14822
14799
  function wrapEnvelope(frame) {
14823
- 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 };
14824
14801
  }
14825
14802
 
14826
14803
  // src/tools/stdio-runtime.ts
14827
- var run = promisify3(execFile3);
14804
+ var run = promisify2(execFile2);
14828
14805
  var StdioRuntime = class {
14829
14806
  constructor(config) {
14830
14807
  this.config = config;
@@ -14842,6 +14819,7 @@ var StdioRuntime = class {
14842
14819
  launching = /* @__PURE__ */ new Map();
14843
14820
  attempts = /* @__PURE__ */ new Map();
14844
14821
  timers = /* @__PURE__ */ new Map();
14822
+ refreshing = /* @__PURE__ */ new Set();
14845
14823
  wanted = false;
14846
14824
  detached = false;
14847
14825
  async init() {
@@ -14873,10 +14851,22 @@ var StdioRuntime = class {
14873
14851
  await Promise.allSettled(this.config.projects.map((project) => this.track(project.id, project.directory)));
14874
14852
  return this.status();
14875
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
+ }
14876
14866
  async logs() {
14877
14867
  const chunks = [];
14878
14868
  for (const [projectId, logPath] of this.logPaths) {
14879
- const file = await open5(logPath, "r").catch(() => void 0);
14869
+ const file = await open4(logPath, "r").catch(() => void 0);
14880
14870
  if (!file) continue;
14881
14871
  try {
14882
14872
  const size = (await file.stat()).size, bytes = Buffer.alloc(Math.min(size, 3e4));
@@ -14943,8 +14933,8 @@ ${bytes.toString("utf8")}`);
14943
14933
  if (inFlight) return inFlight;
14944
14934
  const spawnOnce = (async () => {
14945
14935
  const logPath = join6(this.config.stateDirectory, `${this.kind}-${projectId}.log`);
14946
- const log = await open5(logPath, "a", 384).catch(() => void 0);
14947
- 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"] });
14948
14938
  if (log) child.once("exit", () => {
14949
14939
  void log.close();
14950
14940
  });
@@ -14960,7 +14950,7 @@ ${bytes.toString("utf8")}`);
14960
14950
  return spawnOnce;
14961
14951
  }
14962
14952
  onExited(projectId, _reason) {
14963
- if (this.detached || !this.wanted) return;
14953
+ if (this.detached || !this.wanted || this.refreshing.has(projectId)) return;
14964
14954
  const attempts = (this.attempts.get(projectId) ?? 0) + 1;
14965
14955
  if (attempts > 5) {
14966
14956
  console.error(`${projectId}: app server exited; retry budget exhausted`);
@@ -15023,9 +15013,10 @@ function unsupportedOperation(kind, operation) {
15023
15013
  }
15024
15014
 
15025
15015
  // src/tools/codex.ts
15026
- import { randomUUID as randomUUID8 } from "node:crypto";
15016
+ import { randomUUID as randomUUID9 } from "node:crypto";
15027
15017
  var OPERATIONS = [
15028
15018
  ...DAEMON_OPERATIONS,
15019
+ ...AUTH_OPERATIONS,
15029
15020
  "sessions.list",
15030
15021
  "sessions.create",
15031
15022
  "sessions.get",
@@ -15049,6 +15040,10 @@ var CodexAdapter = class {
15049
15040
  this.emit = emit;
15050
15041
  this.media = new RemoteMedia(workspace, () => uploads.limits.imageBytes);
15051
15042
  }
15043
+ auth = new AuthFlows();
15044
+ authDirty = /* @__PURE__ */ new Set();
15045
+ authReloading = /* @__PURE__ */ new Map();
15046
+ logins = /* @__PURE__ */ new Map();
15052
15047
  media;
15053
15048
  liveTurns = /* @__PURE__ */ new Map();
15054
15049
  approvals = /* @__PURE__ */ new Map();
@@ -15058,10 +15053,36 @@ var CodexAdapter = class {
15058
15053
  supportedOperations() {
15059
15054
  return OPERATIONS;
15060
15055
  }
15056
+ accountChanged(project) {
15057
+ for (const entry of this.runtime.config.projects) if (entry.id !== project) this.authDirty.add(entry.id);
15058
+ }
15061
15059
  async rpc(project) {
15062
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
+ }
15063
15073
  const rpc = await this.runtime.connect(project, {
15064
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;
15065
15086
  if (method === "deprecationNotice") return;
15066
15087
  this.trackNotification(project, method, params);
15067
15088
  if (method === "turn/completed") {
@@ -15087,7 +15108,7 @@ var CodexAdapter = class {
15087
15108
  }
15088
15109
  onServerRequest(project, id, method, params, respond) {
15089
15110
  const detail = params ?? {};
15090
- const requestId = randomUUID8();
15111
+ const requestId = randomUUID9();
15091
15112
  const threadId = String(detail.threadId ?? "");
15092
15113
  if (["item/permissions/requestApproval", "item/commandExecution/requestApproval", "item/fileChange/requestApproval"].includes(method)) {
15093
15114
  this.approvals.set(requestId, { project, method, requestId, threadId, detail, respond });
@@ -15264,6 +15285,7 @@ var CodexAdapter = class {
15264
15285
  await this.rpc(project);
15265
15286
  }
15266
15287
  close() {
15288
+ this.auth.close();
15267
15289
  for (const approval of this.approvals.values()) approval.respond(this.approvalResponse(approval, "reject"));
15268
15290
  for (const input of this.inputs.values()) input.respond({ answers: {} });
15269
15291
  this.approvals.clear();
@@ -15275,8 +15297,47 @@ var CodexAdapter = class {
15275
15297
  if (!project) throw new Error("Project required");
15276
15298
  this.workspace.project(project);
15277
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);
15278
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
+ }
15279
15338
  if (op === "configuration.models") {
15339
+ const account = await rpc.request("account/read", { refreshToken: false });
15340
+ if (!account.account && account.requiresOpenaiAuth) return { providers: [] };
15280
15341
  const models = [];
15281
15342
  let cursor = null;
15282
15343
  const seenCursors = /* @__PURE__ */ new Set();
@@ -15416,10 +15477,13 @@ var CodexAdapter = class {
15416
15477
  };
15417
15478
 
15418
15479
  // src/tools/zcode.ts
15419
- import { randomUUID as randomUUID9 } from "node:crypto";
15480
+ import { spawn as spawn2 } from "node:child_process";
15481
+ import { randomUUID as randomUUID10 } from "node:crypto";
15420
15482
  import { realpath as realpath5 } from "node:fs/promises";
15421
15483
  var OPERATIONS2 = [
15422
15484
  ...DAEMON_OPERATIONS,
15485
+ ...AUTH_OPERATIONS,
15486
+ "configuration.models",
15423
15487
  "sessions.list",
15424
15488
  "sessions.create",
15425
15489
  "sessions.get",
@@ -15442,6 +15506,7 @@ var ZcodeAdapter = class {
15442
15506
  this.emit = emit;
15443
15507
  this.media = new RemoteMedia(workspace, () => uploads.limits.imageBytes);
15444
15508
  }
15509
+ auth = new AuthFlows();
15445
15510
  media;
15446
15511
  permissions = /* @__PURE__ */ new Map();
15447
15512
  inputs = /* @__PURE__ */ new Map();
@@ -15468,7 +15533,7 @@ var ZcodeAdapter = class {
15468
15533
  }
15469
15534
  onServerRequest(project, id, method, params, respond, respondError) {
15470
15535
  const detail = params ?? {};
15471
- const requestId = randomUUID9();
15536
+ const requestId = randomUUID10();
15472
15537
  const sessionId = String(detail.sessionId ?? "");
15473
15538
  if (method === "session/requestRuntimePreferences") {
15474
15539
  respond({ nativeSearchEnhancementsEnabled: false });
@@ -15569,6 +15634,7 @@ var ZcodeAdapter = class {
15569
15634
  await this.rpc(project);
15570
15635
  }
15571
15636
  close() {
15637
+ this.auth.close();
15572
15638
  for (const entry of this.permissions.values()) entry.respond({ decision: "deny" });
15573
15639
  for (const entry of this.inputs.values()) entry.respond({ action: "decline" });
15574
15640
  this.permissions.clear();
@@ -15579,7 +15645,54 @@ var ZcodeAdapter = class {
15579
15645
  if (!project) throw new Error("Project required");
15580
15646
  this.workspace.project(project);
15581
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);
15582
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
+ }
15583
15696
  if (op === "sessions.list") {
15584
15697
  const result = await rpc.request("session/list", { workspace: await this.workspaceRef(project) });
15585
15698
  return (result.sessions ?? []).map((session) => this.sessionProjection(session));
@@ -15618,6 +15731,8 @@ var ZcodeAdapter = class {
15618
15731
  lines.push(`Attached file: ${upload.destination} (${upload.name}, ${upload.mime}). Read this file if relevant to the task.`);
15619
15732
  }
15620
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 } });
15621
15736
  return await rpc.request("session/send", { sessionId: id, content: lines.join("\n") });
15622
15737
  }
15623
15738
  if (op === "sessions.abort") {
@@ -15693,9 +15808,9 @@ var ZcodeAdapter = class {
15693
15808
  };
15694
15809
 
15695
15810
  // src/tools/pi.ts
15696
- import { randomUUID as randomUUID10 } from "node:crypto";
15811
+ import { randomUUID as randomUUID11 } from "node:crypto";
15697
15812
  import { readFile as readFile5, readdir as readdir3, rm } from "node:fs/promises";
15698
- import { dirname as dirname4 } from "node:path";
15813
+ import { dirname as dirname5 } from "node:path";
15699
15814
  var OPERATIONS3 = [
15700
15815
  ...DAEMON_OPERATIONS,
15701
15816
  "sessions.list",
@@ -15706,6 +15821,7 @@ var OPERATIONS3 = [
15706
15821
  "sessions.prompt",
15707
15822
  "sessions.abort",
15708
15823
  "sessions.delete",
15824
+ "auth.status",
15709
15825
  "configuration.models",
15710
15826
  "permissions.list",
15711
15827
  "questions.list",
@@ -15787,7 +15903,7 @@ var PiAdapter = class {
15787
15903
  }
15788
15904
  onQuestion(project, sessionId, detail) {
15789
15905
  if (!["select", "confirm", "input", "editor"].includes(String(detail.method))) return;
15790
- const id = randomUUID10();
15906
+ const id = randomUUID11();
15791
15907
  void this.rpc(project, sessionId).then((rpc) => {
15792
15908
  if (rpc.exited) return;
15793
15909
  const question = { id, project, sessionId, detail, rpc };
@@ -15866,6 +15982,11 @@ var PiAdapter = class {
15866
15982
  async execute(command, project) {
15867
15983
  const op = command.operation, p = command.payload, id = command.sessionId;
15868
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
+ }
15869
15990
  if (op === "configuration.models") {
15870
15991
  const rpc2 = await this.runtime.connect(project);
15871
15992
  const { models } = await rpc2.request("get_available_models");
@@ -15878,7 +15999,7 @@ var PiAdapter = class {
15878
15999
  return { providers: [...providers.values()] };
15879
16000
  }
15880
16001
  if (op === "sessions.list") {
15881
- const directory = dirname4(this.metadataPath(project, "00000000-0000-0000-0000-000000000000"));
16002
+ const directory = dirname5(this.metadataPath(project, "00000000-0000-0000-0000-000000000000"));
15882
16003
  const files = await readdir3(directory).catch((error) => {
15883
16004
  if (error.code === "ENOENT") return [];
15884
16005
  throw error;
@@ -15887,7 +16008,7 @@ var PiAdapter = class {
15887
16008
  return sessions.sort((a, b) => b.time.updated - a.time.updated);
15888
16009
  }
15889
16010
  if (op === "sessions.create") {
15890
- 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() } };
15891
16012
  try {
15892
16013
  const rpc2 = await this.rpc(project, session2.id);
15893
16014
  await rpc2.request("set_session_name", { name: session2.title });
@@ -15985,11 +16106,11 @@ var PiAdapter = class {
15985
16106
  };
15986
16107
 
15987
16108
  // src/tools/pi-runtime.ts
15988
- import { execFile as execFile4, spawn as spawn3 } from "node:child_process";
15989
- 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";
15990
16111
  import { join as join8 } from "node:path";
15991
- import { promisify as promisify4 } from "node:util";
15992
- var run2 = promisify4(execFile4);
16112
+ import { promisify as promisify3 } from "node:util";
16113
+ var run2 = promisify3(execFile3);
15993
16114
  var PiRuntime = class {
15994
16115
  constructor(config) {
15995
16116
  this.config = config;
@@ -16049,7 +16170,7 @@ var PiRuntime = class {
16049
16170
  async logs() {
16050
16171
  const chunks = [];
16051
16172
  for (const path of this.logPaths) {
16052
- const file = await open6(path, "r").catch(() => void 0);
16173
+ const file = await open5(path, "r").catch(() => void 0);
16053
16174
  if (!file) continue;
16054
16175
  try {
16055
16176
  const size = (await file.stat()).size, bytes = Buffer.alloc(Math.min(size, 3e4));
@@ -16098,7 +16219,7 @@ ${bytes.toString("utf8")}`);
16098
16219
  const directory = this.project(projectId).directory;
16099
16220
  await mkdir4(join8(this.config.stateDirectory, "pi-sessions", projectId), { recursive: true, mode: 448 });
16100
16221
  const logPath = join8(this.config.stateDirectory, `pi-${key}.log`);
16101
- const log = await open6(logPath, "a", 384);
16222
+ const log = await open5(logPath, "a", 384);
16102
16223
  if (generation !== this.generation) {
16103
16224
  await log.close();
16104
16225
  throw new Error("Pi runtime stopped during startup");
@@ -16163,7 +16284,7 @@ async function runDaemon(configPath) {
16163
16284
  const config = await loadConfig(configPath);
16164
16285
  await mkdir5(config.stateDirectory, { recursive: true, mode: 448 });
16165
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"));
16166
- const epoch = randomUUID11();
16287
+ const epoch = randomUUID12();
16167
16288
  let stopping = false, botId = "";
16168
16289
  const sockets = /* @__PURE__ */ new Set();
16169
16290
  const events = new EventStream(epoch, () => {
@@ -16174,6 +16295,7 @@ async function runDaemon(configPath) {
16174
16295
  }
16175
16296
  const adapter = createAdapter(runtime, workspace, uploads, emit), artifacts = new Artifacts(workspace, join9(config.stateDirectory, "artifacts"), emit);
16176
16297
  async function dispatch(c) {
16298
+ if (!adapter.supportedOperations().includes(c.operation)) throw new Error(`Unsupported ${runtime.kind} operation: ${c.operation}`);
16177
16299
  const p = c.payload;
16178
16300
  if (c.operation === "runtime.status") return { ...await runtime.status(), capabilities: adapter.supportedOperations() };
16179
16301
  if (c.operation === "runtime.snapshot") return buildProxySnapshot(config, runtime, adapter, c.projectId);
@@ -16303,6 +16425,10 @@ export {
16303
16425
  describeTool,
16304
16426
  ConfigError,
16305
16427
  loadConfig,
16428
+ Workspace,
16306
16429
  atomicJson,
16430
+ Uploads,
16431
+ createRuntime,
16432
+ createAdapter,
16307
16433
  runDaemon
16308
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-2SGNDVCO.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-2SGNDVCO.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.8",
3
+ "version": "0.1.12",
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
  }