@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 +6 -0
- package/dist/{chunk-2SGNDVCO.js → chunk-AGWRSSBQ.js} +369 -243
- package/dist/cli.js +53 -23
- package/dist/main.js +1 -1
- package/package.json +1 -1
- package/proxy.example.json +1 -1
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"]).
|
|
11606
|
-
autoStart: z.boolean().
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
12513
|
+
modelCatalog: { projectId, status: modelCatalogStatus, models }
|
|
12466
12514
|
};
|
|
12467
12515
|
}
|
|
12468
12516
|
|
|
12469
12517
|
// src/media.ts
|
|
12470
|
-
import { createHash as createHash5, randomUUID as
|
|
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-" +
|
|
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
|
|
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-${
|
|
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)
|
|
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 = "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
14488
|
-
if (!
|
|
14489
|
-
const data = await
|
|
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:
|
|
14573
|
+
mode: "external",
|
|
14501
14574
|
state: this.state,
|
|
14502
14575
|
healthy,
|
|
14503
14576
|
version: this.version,
|
|
14504
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14616
|
-
|
|
14617
|
-
|
|
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
|
|
14633
|
-
import { promisify as
|
|
14634
|
-
import { open as
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
14947
|
-
const child =
|
|
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
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
|
15989
|
-
import { mkdir as mkdir4, open as
|
|
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
|
|
15992
|
-
var run2 =
|
|
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
|
|
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
|
|
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 =
|
|
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-
|
|
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
|
-
|
|
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 (
|
|
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\
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
|
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-${
|
|
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
package/package.json
CHANGED
package/proxy.example.json
CHANGED
|
@@ -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": "
|
|
7
|
+
"tool": { "kind": "opencode", "command": "/usr/local/bin/opencode", "baseUrl": "http://127.0.0.1:4096", "mode": "external" }
|
|
8
8
|
}
|