offhands 0.1.4 → 0.1.5
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/dist/daemon.mjs +350 -86
- package/package.json +1 -1
package/dist/daemon.mjs
CHANGED
|
@@ -5299,6 +5299,7 @@ stderr: ${stderrTail}` : ""}`
|
|
|
5299
5299
|
|
|
5300
5300
|
// ../daemon/src/runners/opencode-cli.ts
|
|
5301
5301
|
import { spawn as spawn4 } from "node:child_process";
|
|
5302
|
+
import { createServer } from "node:net";
|
|
5302
5303
|
import { existsSync as existsSync3 } from "node:fs";
|
|
5303
5304
|
import { homedir as homedir3 } from "node:os";
|
|
5304
5305
|
import { delimiter as delimiter2, join as join3 } from "node:path";
|
|
@@ -5307,79 +5308,61 @@ import { delimiter as delimiter2, join as join3 } from "node:path";
|
|
|
5307
5308
|
function mapOpenCodeEvent(value) {
|
|
5308
5309
|
if (typeof value !== "object" || value === null) return [];
|
|
5309
5310
|
const v2 = value;
|
|
5310
|
-
const type = typeof v2.type === "string" ? v2.type : "";
|
|
5311
5311
|
const part = v2.part ?? {};
|
|
5312
|
-
const partType = typeof part.type === "string" ? part.type : "";
|
|
5313
|
-
if (
|
|
5312
|
+
const partType = typeof part.type === "string" ? part.type : typeof v2.type === "string" ? v2.type : "";
|
|
5313
|
+
if (partType === "text") {
|
|
5314
5314
|
const text = typeof part.text === "string" ? part.text : "";
|
|
5315
5315
|
return text ? [{ type: "text", chunk: text }] : [];
|
|
5316
5316
|
}
|
|
5317
|
-
if (
|
|
5318
|
-
const thinking = typeof part.
|
|
5317
|
+
if (partType === "reasoning") {
|
|
5318
|
+
const thinking = typeof part.text === "string" ? part.text : "";
|
|
5319
5319
|
return thinking ? [{ type: "thinking", chunk: thinking }] : [];
|
|
5320
5320
|
}
|
|
5321
|
-
if (
|
|
5322
|
-
const name =
|
|
5323
|
-
const
|
|
5324
|
-
const
|
|
5321
|
+
if (partType === "tool") {
|
|
5322
|
+
const name = typeof part.tool === "string" ? part.tool : "tool";
|
|
5323
|
+
const state = part.state ?? {};
|
|
5324
|
+
const input = state.input;
|
|
5325
|
+
const summary = input ? firstString2(input, "command", "filePath", "path", "file", "pattern", "query", "url") ?? "" : "";
|
|
5326
|
+
const status = typeof state.status === "string" ? state.status : void 0;
|
|
5327
|
+
if (status === "error") {
|
|
5328
|
+
const errMsg = typeof state.error === "string" ? state.error : "tool call failed";
|
|
5329
|
+
return [{ type: "tool", name, summary: `${name}: ${truncate3(errMsg, 120)}` }];
|
|
5330
|
+
}
|
|
5325
5331
|
return [{ type: "tool", name, summary: summary ? `${name}: ${truncate3(summary, 120)}` : name }];
|
|
5326
5332
|
}
|
|
5327
|
-
if (
|
|
5328
|
-
|
|
5329
|
-
}
|
|
5330
|
-
if (type === "step_finish" || partType === "step-finish") {
|
|
5333
|
+
if (partType === "step-start") return [];
|
|
5334
|
+
if (partType === "step-finish") {
|
|
5331
5335
|
const reason = typeof part.reason === "string" ? part.reason : "stop";
|
|
5332
|
-
const isError = reason === "error" || reason === "failed";
|
|
5333
5336
|
const usage = extractUsageFromPart(part);
|
|
5334
|
-
if (
|
|
5335
|
-
const message =
|
|
5337
|
+
if (reason === "error") {
|
|
5338
|
+
const message = firstString2(part, "error", "message") ?? "run failed";
|
|
5336
5339
|
const events2 = [{ type: "error", message }];
|
|
5337
5340
|
if (usage) events2.push(usage);
|
|
5338
5341
|
return events2;
|
|
5339
5342
|
}
|
|
5343
|
+
if (reason !== "stop") return usage ? [usage] : [];
|
|
5340
5344
|
const events = [{ type: "done", summary: "" }];
|
|
5341
5345
|
if (usage) events.push(usage);
|
|
5342
5346
|
return events;
|
|
5343
5347
|
}
|
|
5344
|
-
if (type === "
|
|
5345
|
-
const
|
|
5346
|
-
const id = firstString2(partData, "id", "permissionId", "id") ?? crypto.randomUUID();
|
|
5347
|
-
const action = firstString2(partData, "toolName", "action", "command") ?? "action";
|
|
5348
|
-
const detail = firstString2(partData, "detail", "description", "message") ?? "";
|
|
5349
|
-
const risk = firstString2(partData, "risk") === "high" ? "high" : "low";
|
|
5350
|
-
const preview = firstString2(partData, "preview", "diff", "command") ?? void 0;
|
|
5351
|
-
const question = partData?.question ? mapQuestion(partData.question) : void 0;
|
|
5352
|
-
return [{
|
|
5353
|
-
type: "approval",
|
|
5354
|
-
id,
|
|
5355
|
-
action,
|
|
5356
|
-
detail,
|
|
5357
|
-
risk,
|
|
5358
|
-
preview,
|
|
5359
|
-
question
|
|
5360
|
-
}];
|
|
5361
|
-
}
|
|
5362
|
-
if (type === "error") {
|
|
5363
|
-
const message = firstString2(v2, "message", "error", "detail", "result") ?? firstString2(part, "message", "error") ?? "unknown error";
|
|
5348
|
+
if (!part.type && typeof v2.type === "string" && v2.type === "error") {
|
|
5349
|
+
const message = firstString2(v2, "message", "error", "detail", "result") ?? "unknown error";
|
|
5364
5350
|
return [{ type: "error", message }];
|
|
5365
5351
|
}
|
|
5366
|
-
if (type === "session_created" || type === "session.updated" || type === "session.created") {
|
|
5367
|
-
return [];
|
|
5368
|
-
}
|
|
5369
5352
|
return [];
|
|
5370
5353
|
}
|
|
5371
5354
|
function extractOpenCodeSessionId(value) {
|
|
5372
5355
|
if (typeof value !== "object" || value === null) return null;
|
|
5373
5356
|
const v2 = value;
|
|
5374
5357
|
const part = v2.part ?? {};
|
|
5375
|
-
return firstString2(v2, "
|
|
5358
|
+
return firstString2(v2, "sessionID", "sessionId", "session_id") ?? firstString2(part, "sessionID", "sessionId", "session_id") ?? null;
|
|
5376
5359
|
}
|
|
5377
5360
|
function extractUsageFromPart(part) {
|
|
5378
5361
|
const tokens = part.tokens;
|
|
5379
|
-
const costUsd = typeof part.cost === "number" ? part.cost :
|
|
5362
|
+
const costUsd = typeof part.cost === "number" ? part.cost : void 0;
|
|
5380
5363
|
if (!tokens && costUsd === void 0) return null;
|
|
5381
5364
|
const n2 = (k2) => typeof tokens?.[k2] === "number" ? tokens[k2] : 0;
|
|
5382
|
-
const contextTokens =
|
|
5365
|
+
const contextTokens = tokens && typeof tokens.total === "number" ? tokens.total : n2("input") + n2("output") + n2("reasoning");
|
|
5383
5366
|
if (contextTokens <= 0 && costUsd === void 0) return null;
|
|
5384
5367
|
return {
|
|
5385
5368
|
type: "usage",
|
|
@@ -5388,19 +5371,8 @@ function extractUsageFromPart(part) {
|
|
|
5388
5371
|
...costUsd !== void 0 ? { costUsd } : {}
|
|
5389
5372
|
};
|
|
5390
5373
|
}
|
|
5391
|
-
function mapQuestion(q2) {
|
|
5392
|
-
if (typeof q2 !== "object" || q2 === null) return void 0;
|
|
5393
|
-
const qq = q2;
|
|
5394
|
-
const text = typeof qq.text === "string" ? qq.text : "";
|
|
5395
|
-
const options = Array.isArray(qq.options) ? qq.options.filter((o2) => typeof o2 === "object" && o2 !== null).map((o2) => ({
|
|
5396
|
-
label: typeof o2.label === "string" ? o2.label : "",
|
|
5397
|
-
description: typeof o2.description === "string" ? o2.description : void 0
|
|
5398
|
-
})) : [];
|
|
5399
|
-
const multiSelect = typeof qq.multiSelect === "boolean" ? qq.multiSelect : void 0;
|
|
5400
|
-
if (!text && options.length === 0) return void 0;
|
|
5401
|
-
return { text, options, multiSelect };
|
|
5402
|
-
}
|
|
5403
5374
|
function firstString2(obj, ...keys) {
|
|
5375
|
+
if (!obj) return void 0;
|
|
5404
5376
|
for (const k2 of keys) {
|
|
5405
5377
|
const val = obj[k2];
|
|
5406
5378
|
if (typeof val === "string" && val !== "") return val;
|
|
@@ -5412,7 +5384,11 @@ function truncate3(s2, max) {
|
|
|
5412
5384
|
}
|
|
5413
5385
|
|
|
5414
5386
|
// ../daemon/src/runners/opencode-cli.ts
|
|
5387
|
+
var POLL_INTERVAL_MS = 500;
|
|
5415
5388
|
var OpenCodeRunner = class {
|
|
5389
|
+
constructor(broker2) {
|
|
5390
|
+
this.broker = broker2;
|
|
5391
|
+
}
|
|
5416
5392
|
id = "opencode";
|
|
5417
5393
|
name = "OpenCode";
|
|
5418
5394
|
supportsApprovals = true;
|
|
@@ -5421,6 +5397,10 @@ var OpenCodeRunner = class {
|
|
|
5421
5397
|
/** [command, ...prefixArgs] resolved once. */
|
|
5422
5398
|
resolved = null;
|
|
5423
5399
|
modelsCache = [];
|
|
5400
|
+
// Lazily-started, shared `opencode serve` instance backing startViaHttpApi.
|
|
5401
|
+
serverPort = null;
|
|
5402
|
+
serverProcess = null;
|
|
5403
|
+
serverStarting = null;
|
|
5424
5404
|
resolveCommand() {
|
|
5425
5405
|
if (this.resolved) return this.resolved;
|
|
5426
5406
|
if (process.platform === "win32") {
|
|
@@ -5491,7 +5471,7 @@ var OpenCodeRunner = class {
|
|
|
5491
5471
|
if (code === 0) {
|
|
5492
5472
|
try {
|
|
5493
5473
|
const lines = output.trim().split("\n");
|
|
5494
|
-
const models = lines.map((l2) => l2.trim()).filter((l2) => l2
|
|
5474
|
+
const models = lines.map((l2) => l2.trim()).filter((l2) => l2.length > 0);
|
|
5495
5475
|
this.modelsCache = models;
|
|
5496
5476
|
Object.defineProperty(this, "models", {
|
|
5497
5477
|
value: models,
|
|
@@ -5513,6 +5493,13 @@ var OpenCodeRunner = class {
|
|
|
5513
5493
|
return existsSync3(join3(homedir3(), ".local", "share", "opencode", "auth.json"));
|
|
5514
5494
|
}
|
|
5515
5495
|
start(run, callbacks) {
|
|
5496
|
+
const mode = run.permissionMode ?? "guarded";
|
|
5497
|
+
if (this.broker && (mode === "guarded" || mode === "acceptEdits")) {
|
|
5498
|
+
return this.startViaHttpApi(run, mode, callbacks);
|
|
5499
|
+
}
|
|
5500
|
+
return this.startViaCli(run, mode, callbacks);
|
|
5501
|
+
}
|
|
5502
|
+
startViaCli(run, mode, callbacks) {
|
|
5516
5503
|
const queue = new AsyncEventQueue();
|
|
5517
5504
|
const parser = new NdjsonParser();
|
|
5518
5505
|
let child;
|
|
@@ -5537,8 +5524,7 @@ var OpenCodeRunner = class {
|
|
|
5537
5524
|
...cmd.slice(1),
|
|
5538
5525
|
"run",
|
|
5539
5526
|
"--format",
|
|
5540
|
-
"json"
|
|
5541
|
-
"--no-color"
|
|
5527
|
+
"json"
|
|
5542
5528
|
];
|
|
5543
5529
|
if (run.model) {
|
|
5544
5530
|
args2.push("--model", run.model);
|
|
@@ -5546,12 +5532,10 @@ var OpenCodeRunner = class {
|
|
|
5546
5532
|
if (run.resumeConversationId) {
|
|
5547
5533
|
args2.push("--session", run.resumeConversationId);
|
|
5548
5534
|
}
|
|
5549
|
-
|
|
5550
|
-
if (mode === "bypass") {
|
|
5551
|
-
args2.push("--auto");
|
|
5552
|
-
} else if (mode === "plan") {
|
|
5535
|
+
if (mode === "plan") {
|
|
5553
5536
|
args2.push("--agent", "plan");
|
|
5554
5537
|
} else {
|
|
5538
|
+
args2.push("--auto");
|
|
5555
5539
|
}
|
|
5556
5540
|
if (run.effort) {
|
|
5557
5541
|
const variantMap = { low: "minimal", medium: "high", high: "max", max: "max" };
|
|
@@ -5622,12 +5606,283 @@ stderr: ${stderrTail}` : ""}`
|
|
|
5622
5606
|
});
|
|
5623
5607
|
return {
|
|
5624
5608
|
events: queue,
|
|
5625
|
-
|
|
5609
|
+
// No broker (or bypass/plan, which never ask) — nothing to answer.
|
|
5610
|
+
respond: () => {
|
|
5626
5611
|
},
|
|
5627
5612
|
cancel: () => child.kill()
|
|
5628
5613
|
};
|
|
5629
5614
|
}
|
|
5615
|
+
startViaHttpApi(run, mode, callbacks) {
|
|
5616
|
+
const queue = new AsyncEventQueue();
|
|
5617
|
+
const emit = (events) => {
|
|
5618
|
+
for (const e of events) queue.push(e);
|
|
5619
|
+
};
|
|
5620
|
+
const broker2 = this.broker;
|
|
5621
|
+
const directory = run.workspace;
|
|
5622
|
+
const encDir = encodeURIComponent(directory);
|
|
5623
|
+
let cancelled = false;
|
|
5624
|
+
let sessionId = run.resumeConversationId ?? null;
|
|
5625
|
+
let port2 = null;
|
|
5626
|
+
let detachBroker;
|
|
5627
|
+
const pendingQuestions = /* @__PURE__ */ new Map();
|
|
5628
|
+
const cleanup = () => {
|
|
5629
|
+
detachBroker?.();
|
|
5630
|
+
queue.close();
|
|
5631
|
+
};
|
|
5632
|
+
void (async () => {
|
|
5633
|
+
try {
|
|
5634
|
+
port2 = await this.ensureServer();
|
|
5635
|
+
} catch (e) {
|
|
5636
|
+
emit([{ type: "error", message: `failed to start opencode server: ${String(e)}` }]);
|
|
5637
|
+
cleanup();
|
|
5638
|
+
return;
|
|
5639
|
+
}
|
|
5640
|
+
if (cancelled) {
|
|
5641
|
+
cleanup();
|
|
5642
|
+
return;
|
|
5643
|
+
}
|
|
5644
|
+
const base = `http://127.0.0.1:${port2}`;
|
|
5645
|
+
detachBroker = broker2.attach((ev) => queue.push(ev));
|
|
5646
|
+
try {
|
|
5647
|
+
if (!sessionId) {
|
|
5648
|
+
const modelRef2 = parseModelRef(run.model);
|
|
5649
|
+
const body = { permission: buildPermissionRuleset(mode) };
|
|
5650
|
+
if (modelRef2) body.model = { id: modelRef2.modelId, providerID: modelRef2.providerId };
|
|
5651
|
+
const session = await httpJson(`${base}/session?directory=${encDir}`, "POST", body);
|
|
5652
|
+
sessionId = session.id;
|
|
5653
|
+
}
|
|
5654
|
+
callbacks?.onConversationId?.(sessionId);
|
|
5655
|
+
const parts = [{ type: "text", text: run.prompt }];
|
|
5656
|
+
for (const file of run.attachments ?? []) {
|
|
5657
|
+
parts.push({ type: "file", mime: "application/octet-stream", url: fileUrl(file) });
|
|
5658
|
+
}
|
|
5659
|
+
const promptBody = { parts };
|
|
5660
|
+
const modelRef = parseModelRef(run.model);
|
|
5661
|
+
if (modelRef) promptBody.model = { providerID: modelRef.providerId, modelID: modelRef.modelId };
|
|
5662
|
+
await httpJson(`${base}/session/${sessionId}/prompt_async?directory=${encDir}`, "POST", promptBody, true);
|
|
5663
|
+
await this.pollUntilDone(base, sessionId, encDir, broker2, emit, pendingQuestions, () => cancelled);
|
|
5664
|
+
} catch (e) {
|
|
5665
|
+
if (!cancelled) emit([{ type: "error", message: `opencode HTTP API error: ${String(e)}` }]);
|
|
5666
|
+
} finally {
|
|
5667
|
+
cleanup();
|
|
5668
|
+
}
|
|
5669
|
+
})();
|
|
5670
|
+
return {
|
|
5671
|
+
events: queue,
|
|
5672
|
+
respond: (approvalId, ok, answer) => {
|
|
5673
|
+
const settleQuestion = pendingQuestions.get(approvalId);
|
|
5674
|
+
if (settleQuestion) {
|
|
5675
|
+
pendingQuestions.delete(approvalId);
|
|
5676
|
+
settleQuestion(ok, answer);
|
|
5677
|
+
return;
|
|
5678
|
+
}
|
|
5679
|
+
broker2.resolve(approvalId, ok, answer);
|
|
5680
|
+
},
|
|
5681
|
+
cancel: () => {
|
|
5682
|
+
cancelled = true;
|
|
5683
|
+
if (port2 && sessionId) {
|
|
5684
|
+
const abortUrl = `http://127.0.0.1:${port2}/session/${sessionId}/abort?directory=${encDir}`;
|
|
5685
|
+
void httpJson(abortUrl, "POST", {}, true).catch(() => {
|
|
5686
|
+
});
|
|
5687
|
+
}
|
|
5688
|
+
}
|
|
5689
|
+
};
|
|
5690
|
+
}
|
|
5691
|
+
/** Poll session messages + pending permissions/questions until the turn finishes. */
|
|
5692
|
+
async pollUntilDone(base, sessionId, encDir, broker2, emit, pendingQuestions, isCancelled) {
|
|
5693
|
+
const seenPermissions = /* @__PURE__ */ new Set();
|
|
5694
|
+
const seenQuestions = /* @__PURE__ */ new Set();
|
|
5695
|
+
const emittedParts = /* @__PURE__ */ new Set();
|
|
5696
|
+
let sawTerminal = false;
|
|
5697
|
+
while (!isCancelled() && !sawTerminal) {
|
|
5698
|
+
await sleep(POLL_INTERVAL_MS);
|
|
5699
|
+
if (isCancelled()) break;
|
|
5700
|
+
try {
|
|
5701
|
+
const pending = await httpJson(`${base}/permission?directory=${encDir}`, "GET");
|
|
5702
|
+
for (const p2 of pending) {
|
|
5703
|
+
if (p2.sessionID !== sessionId || seenPermissions.has(p2.id)) continue;
|
|
5704
|
+
seenPermissions.add(p2.id);
|
|
5705
|
+
void this.answerPermission(base, encDir, p2, broker2);
|
|
5706
|
+
}
|
|
5707
|
+
} catch {
|
|
5708
|
+
}
|
|
5709
|
+
try {
|
|
5710
|
+
const pendingQ = await httpJson(`${base}/question?directory=${encDir}`, "GET");
|
|
5711
|
+
for (const q2 of pendingQ) {
|
|
5712
|
+
if (q2.sessionID !== sessionId || seenQuestions.has(q2.id)) continue;
|
|
5713
|
+
seenQuestions.add(q2.id);
|
|
5714
|
+
void this.answerQuestion(base, encDir, q2, emit, pendingQuestions);
|
|
5715
|
+
}
|
|
5716
|
+
} catch {
|
|
5717
|
+
}
|
|
5718
|
+
let messages;
|
|
5719
|
+
try {
|
|
5720
|
+
messages = await httpJson(`${base}/session/${sessionId}/message?directory=${encDir}`, "GET");
|
|
5721
|
+
} catch {
|
|
5722
|
+
continue;
|
|
5723
|
+
}
|
|
5724
|
+
for (const row of messages) {
|
|
5725
|
+
if (row.info.role !== "assistant") continue;
|
|
5726
|
+
for (const part of row.parts) {
|
|
5727
|
+
if (!part.id || emittedParts.has(part.id)) continue;
|
|
5728
|
+
if ((part.type === "text" || part.type === "reasoning") && !part.time?.end) continue;
|
|
5729
|
+
emittedParts.add(part.id);
|
|
5730
|
+
const events = mapOpenCodeEvent({ part });
|
|
5731
|
+
emit(events);
|
|
5732
|
+
if (events.some((e) => e.type === "done" || e.type === "error")) sawTerminal = true;
|
|
5733
|
+
}
|
|
5734
|
+
if (!sawTerminal && row.info.error) {
|
|
5735
|
+
sawTerminal = true;
|
|
5736
|
+
emit([{ type: "error", message: row.info.error.data?.message ?? "run failed" }]);
|
|
5737
|
+
}
|
|
5738
|
+
}
|
|
5739
|
+
}
|
|
5740
|
+
}
|
|
5741
|
+
/** Route one pending permission through the shared broker, then reply. */
|
|
5742
|
+
async answerPermission(base, encDir, p2, broker2) {
|
|
5743
|
+
const input = { ...p2.metadata };
|
|
5744
|
+
const verdict = await broker2.submit(p2.permission, input);
|
|
5745
|
+
try {
|
|
5746
|
+
await httpJson(`${base}/permission/${p2.id}/reply?directory=${encDir}`, "POST", {
|
|
5747
|
+
reply: verdict.approve ? "once" : "reject",
|
|
5748
|
+
...verdict.message ? { message: verdict.message } : {}
|
|
5749
|
+
}, true);
|
|
5750
|
+
} catch {
|
|
5751
|
+
}
|
|
5752
|
+
}
|
|
5753
|
+
/** Surface an OpenCode "question" (its AskUserQuestion equivalent) as a
|
|
5754
|
+
* real approval-with-options card, wait for the phone's pick, then answer
|
|
5755
|
+
* through the question API's own reply/reject endpoints. */
|
|
5756
|
+
async answerQuestion(base, encDir, q2, emit, pendingQuestions) {
|
|
5757
|
+
const first = q2.questions[0];
|
|
5758
|
+
if (!first) return;
|
|
5759
|
+
const options = first.options.map((o2) => ({ label: o2.label, description: o2.description }));
|
|
5760
|
+
const preview = [first.question, ...options.map((o2, n2) => `${n2 + 1}. ${o2.label}${o2.description ? ` \u2014 ${o2.description}` : ""}`)].join("\n");
|
|
5761
|
+
emit([{
|
|
5762
|
+
type: "approval",
|
|
5763
|
+
id: q2.id,
|
|
5764
|
+
action: "question",
|
|
5765
|
+
detail: truncate4(first.question, 200),
|
|
5766
|
+
risk: "low",
|
|
5767
|
+
preview: truncate4(preview, 600),
|
|
5768
|
+
question: { text: first.question, options, multiSelect: first.multiple }
|
|
5769
|
+
}]);
|
|
5770
|
+
const verdict = await new Promise((resolve6) => {
|
|
5771
|
+
pendingQuestions.set(q2.id, (ok, answer) => resolve6({ ok, answer }));
|
|
5772
|
+
});
|
|
5773
|
+
emit([{
|
|
5774
|
+
type: "approval-result",
|
|
5775
|
+
id: q2.id,
|
|
5776
|
+
approve: verdict.ok,
|
|
5777
|
+
...verdict.answer ? { answer: verdict.answer } : {}
|
|
5778
|
+
}]);
|
|
5779
|
+
try {
|
|
5780
|
+
if (verdict.ok && verdict.answer) {
|
|
5781
|
+
await httpJson(`${base}/question/${q2.id}/reply?directory=${encDir}`, "POST", { answers: [[verdict.answer]] }, true);
|
|
5782
|
+
} else {
|
|
5783
|
+
await httpJson(`${base}/question/${q2.id}/reject?directory=${encDir}`, "POST", {}, true);
|
|
5784
|
+
}
|
|
5785
|
+
} catch {
|
|
5786
|
+
}
|
|
5787
|
+
}
|
|
5788
|
+
/** Lazily start (once) the shared `opencode serve` instance used for
|
|
5789
|
+
* broker-driven runs, and wait for it to accept connections. */
|
|
5790
|
+
async ensureServer() {
|
|
5791
|
+
if (this.serverPort) return this.serverPort;
|
|
5792
|
+
if (this.serverStarting) return this.serverStarting;
|
|
5793
|
+
this.serverStarting = (async () => {
|
|
5794
|
+
const cmd = this.resolveCommand();
|
|
5795
|
+
if (!cmd) throw new Error("opencode CLI not found");
|
|
5796
|
+
const port2 = await findFreePort();
|
|
5797
|
+
const proc = spawn4(cmd[0], [...cmd.slice(1), "serve", "--port", String(port2), "--hostname", "127.0.0.1"], {
|
|
5798
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
5799
|
+
shell: process.platform === "win32"
|
|
5800
|
+
});
|
|
5801
|
+
proc.on("exit", () => {
|
|
5802
|
+
if (this.serverProcess === proc) {
|
|
5803
|
+
this.serverProcess = null;
|
|
5804
|
+
this.serverPort = null;
|
|
5805
|
+
}
|
|
5806
|
+
});
|
|
5807
|
+
await waitForServerReady(port2);
|
|
5808
|
+
this.serverProcess = proc;
|
|
5809
|
+
this.serverPort = port2;
|
|
5810
|
+
return port2;
|
|
5811
|
+
})();
|
|
5812
|
+
try {
|
|
5813
|
+
return await this.serverStarting;
|
|
5814
|
+
} finally {
|
|
5815
|
+
this.serverStarting = null;
|
|
5816
|
+
}
|
|
5817
|
+
}
|
|
5630
5818
|
};
|
|
5819
|
+
function buildPermissionRuleset(mode) {
|
|
5820
|
+
const base = [{ permission: "*", pattern: "*", action: "allow" }];
|
|
5821
|
+
if (mode === "acceptEdits") {
|
|
5822
|
+
return [
|
|
5823
|
+
...base,
|
|
5824
|
+
{ permission: "edit", pattern: "*", action: "allow" },
|
|
5825
|
+
{ permission: "bash", pattern: "*", action: "ask" },
|
|
5826
|
+
{ permission: "webfetch", pattern: "*", action: "ask" }
|
|
5827
|
+
];
|
|
5828
|
+
}
|
|
5829
|
+
return [
|
|
5830
|
+
...base,
|
|
5831
|
+
{ permission: "edit", pattern: "*", action: "ask" },
|
|
5832
|
+
{ permission: "bash", pattern: "*", action: "ask" },
|
|
5833
|
+
{ permission: "webfetch", pattern: "*", action: "ask" }
|
|
5834
|
+
];
|
|
5835
|
+
}
|
|
5836
|
+
function parseModelRef(model) {
|
|
5837
|
+
if (!model) return null;
|
|
5838
|
+
const idx = model.indexOf("/");
|
|
5839
|
+
if (idx < 0) return null;
|
|
5840
|
+
return { providerId: model.slice(0, idx), modelId: model.slice(idx + 1) };
|
|
5841
|
+
}
|
|
5842
|
+
function fileUrl(path) {
|
|
5843
|
+
const normalised = path.replace(/\\/g, "/");
|
|
5844
|
+
return `file://${normalised.startsWith("/") ? "" : "/"}${normalised}`;
|
|
5845
|
+
}
|
|
5846
|
+
function truncate4(s2, max) {
|
|
5847
|
+
return s2.length <= max ? s2 : s2.slice(0, max - 1) + "\u2026";
|
|
5848
|
+
}
|
|
5849
|
+
async function httpJson(url, method, body, expectNoContent = false) {
|
|
5850
|
+
const res = await fetch(url, {
|
|
5851
|
+
method,
|
|
5852
|
+
headers: body !== void 0 ? { "content-type": "application/json" } : void 0,
|
|
5853
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
5854
|
+
});
|
|
5855
|
+
if (!res.ok) throw new Error(`opencode API ${method} ${url} -> ${res.status}`);
|
|
5856
|
+
if (expectNoContent || res.status === 204) return void 0;
|
|
5857
|
+
return res.json();
|
|
5858
|
+
}
|
|
5859
|
+
function findFreePort() {
|
|
5860
|
+
return new Promise((resolve6, reject) => {
|
|
5861
|
+
const srv = createServer();
|
|
5862
|
+
srv.unref();
|
|
5863
|
+
srv.on("error", reject);
|
|
5864
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
5865
|
+
const address = srv.address();
|
|
5866
|
+
const port2 = typeof address === "object" && address ? address.port : 0;
|
|
5867
|
+
srv.close(() => resolve6(port2));
|
|
5868
|
+
});
|
|
5869
|
+
});
|
|
5870
|
+
}
|
|
5871
|
+
async function waitForServerReady(port2, timeoutMs = 2e4) {
|
|
5872
|
+
const deadline = Date.now() + timeoutMs;
|
|
5873
|
+
while (Date.now() < deadline) {
|
|
5874
|
+
try {
|
|
5875
|
+
const res = await fetch(`http://127.0.0.1:${port2}/doc`);
|
|
5876
|
+
if (res.ok) return;
|
|
5877
|
+
} catch {
|
|
5878
|
+
}
|
|
5879
|
+
await sleep(150);
|
|
5880
|
+
}
|
|
5881
|
+
throw new Error(`opencode serve did not become ready on port ${port2}`);
|
|
5882
|
+
}
|
|
5883
|
+
function sleep(ms) {
|
|
5884
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
5885
|
+
}
|
|
5631
5886
|
|
|
5632
5887
|
// ../daemon/src/session-manager.ts
|
|
5633
5888
|
import { randomUUID } from "node:crypto";
|
|
@@ -5748,7 +6003,7 @@ function summarizeConversation(path, workspace) {
|
|
|
5748
6003
|
return {
|
|
5749
6004
|
conversationId: basename(path, ".jsonl"),
|
|
5750
6005
|
workspace,
|
|
5751
|
-
firstPrompt:
|
|
6006
|
+
firstPrompt: truncate5(firstPrompt || "(empty prompt)", 120),
|
|
5752
6007
|
lastActiveMs: Math.floor(statSync(path).mtimeMs),
|
|
5753
6008
|
messageCount
|
|
5754
6009
|
};
|
|
@@ -5774,7 +6029,7 @@ function userMessageText(value) {
|
|
|
5774
6029
|
function collapseText(text) {
|
|
5775
6030
|
return text.replace(/\s+/g, " ").trim();
|
|
5776
6031
|
}
|
|
5777
|
-
function
|
|
6032
|
+
function truncate5(text, max) {
|
|
5778
6033
|
return text.length <= max ? text : `${text.slice(0, max - 1)}\u2026`;
|
|
5779
6034
|
}
|
|
5780
6035
|
function safeReadDir(path) {
|
|
@@ -6557,7 +6812,7 @@ function searchableText(msg) {
|
|
|
6557
6812
|
}
|
|
6558
6813
|
|
|
6559
6814
|
// ../daemon/src/local-server.ts
|
|
6560
|
-
import { createServer } from "node:http";
|
|
6815
|
+
import { createServer as createServer2 } from "node:http";
|
|
6561
6816
|
|
|
6562
6817
|
// ../node_modules/.pnpm/ws@8.21.3/node_modules/ws/wrapper.mjs
|
|
6563
6818
|
var import_stream = __toESM(require_stream(), 1);
|
|
@@ -11014,9 +11269,9 @@ if (typeof Module.getRandomValue === "undefined") {
|
|
|
11014
11269
|
Module.getRandomValue = randomValuesStandard;
|
|
11015
11270
|
} catch (e) {
|
|
11016
11271
|
try {
|
|
11017
|
-
|
|
11272
|
+
crypto = null;
|
|
11018
11273
|
randomValueNodeJS = function() {
|
|
11019
|
-
var buf =
|
|
11274
|
+
var buf = crypto["randomBytes"](4);
|
|
11020
11275
|
return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
|
|
11021
11276
|
};
|
|
11022
11277
|
randomValueNodeJS();
|
|
@@ -11029,7 +11284,7 @@ if (typeof Module.getRandomValue === "undefined") {
|
|
|
11029
11284
|
var window_;
|
|
11030
11285
|
var crypto_;
|
|
11031
11286
|
var randomValuesStandard;
|
|
11032
|
-
var
|
|
11287
|
+
var crypto;
|
|
11033
11288
|
var randomValueNodeJS;
|
|
11034
11289
|
var _Module = Module;
|
|
11035
11290
|
Module.ready = new Promise(function(resolve6, reject) {
|
|
@@ -37923,7 +38178,7 @@ Module.ready = new Promise(function(resolve6, reject) {
|
|
|
37923
38178
|
try {
|
|
37924
38179
|
var window_ = "object" === typeof window ? window : self;
|
|
37925
38180
|
var crypto_ = typeof window_.crypto !== "undefined" ? window_.crypto : window_.msCrypto;
|
|
37926
|
-
crypto_ = crypto_ === void 0 ?
|
|
38181
|
+
crypto_ = crypto_ === void 0 ? crypto : crypto_;
|
|
37927
38182
|
var randomValuesStandard = function() {
|
|
37928
38183
|
var buf = new Uint32Array(1);
|
|
37929
38184
|
crypto_.getRandomValues(buf);
|
|
@@ -37933,9 +38188,9 @@ Module.ready = new Promise(function(resolve6, reject) {
|
|
|
37933
38188
|
Module3.getRandomValue = randomValuesStandard;
|
|
37934
38189
|
} catch (e) {
|
|
37935
38190
|
try {
|
|
37936
|
-
var
|
|
38191
|
+
var crypto = null;
|
|
37937
38192
|
var randomValueNodeJS = function() {
|
|
37938
|
-
var buf =
|
|
38193
|
+
var buf = crypto["randomBytes"](4);
|
|
37939
38194
|
return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
|
|
37940
38195
|
};
|
|
37941
38196
|
randomValueNodeJS();
|
|
@@ -38581,7 +38836,7 @@ Module.ready = new Promise(function(resolve6, reject) {
|
|
|
38581
38836
|
try {
|
|
38582
38837
|
var window_ = "object" === typeof window ? window : self;
|
|
38583
38838
|
var crypto_ = typeof window_.crypto !== "undefined" ? window_.crypto : window_.msCrypto;
|
|
38584
|
-
crypto_ = crypto_ === void 0 ?
|
|
38839
|
+
crypto_ = crypto_ === void 0 ? crypto : crypto_;
|
|
38585
38840
|
var randomValuesStandard = function() {
|
|
38586
38841
|
var buf = new Uint32Array(1);
|
|
38587
38842
|
crypto_.getRandomValues(buf);
|
|
@@ -38591,9 +38846,9 @@ Module.ready = new Promise(function(resolve6, reject) {
|
|
|
38591
38846
|
Module2.getRandomValue = randomValuesStandard;
|
|
38592
38847
|
} catch (e) {
|
|
38593
38848
|
try {
|
|
38594
|
-
var
|
|
38849
|
+
var crypto = null;
|
|
38595
38850
|
var randomValueNodeJS = function() {
|
|
38596
|
-
var buf =
|
|
38851
|
+
var buf = crypto["randomBytes"](4);
|
|
38597
38852
|
return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
|
|
38598
38853
|
};
|
|
38599
38854
|
randomValueNodeJS();
|
|
@@ -41515,7 +41770,7 @@ var LocalSessionServer = class {
|
|
|
41515
41770
|
constructor(manager2, port2, broker2) {
|
|
41516
41771
|
this.manager = manager2;
|
|
41517
41772
|
this.broker = broker2;
|
|
41518
|
-
const http =
|
|
41773
|
+
const http = createServer2((req, res) => void this.onRequest(req, res));
|
|
41519
41774
|
this.wss = new import_websocket_server.default({ server: http });
|
|
41520
41775
|
this.wss.on("connection", (ws) => this.onConnection(ws));
|
|
41521
41776
|
http.on("error", (err) => {
|
|
@@ -41864,7 +42119,7 @@ var ApprovalBroker = class {
|
|
|
41864
42119
|
};
|
|
41865
42120
|
function classifyRisk(toolName, input) {
|
|
41866
42121
|
const i2 = input ?? {};
|
|
41867
|
-
const text = [toolName, i2.command, i2.file_path, i2.path].filter((x2) => typeof x2 === "string").join(" ");
|
|
42122
|
+
const text = [toolName, i2.command, i2.file_path, i2.path, i2.filepath].filter((x2) => typeof x2 === "string").join(" ");
|
|
41868
42123
|
return /\b(rm|del|rmdir|rd|format|mkfs|shutdown|reboot|kill|drop\s+table|truncate|git\s+push\s+--force|--hard)\b/i.test(
|
|
41869
42124
|
text
|
|
41870
42125
|
) ? "high" : "low";
|
|
@@ -41873,11 +42128,11 @@ function summariseInput(input) {
|
|
|
41873
42128
|
if (typeof input !== "object" || input === null) return String(input ?? "");
|
|
41874
42129
|
const i2 = input;
|
|
41875
42130
|
const q2 = firstQuestion(i2);
|
|
41876
|
-
if (q2) return
|
|
41877
|
-
for (const key of ["command", "file_path", "path", "url", "pattern", "description"]) {
|
|
41878
|
-
if (typeof i2[key] === "string" && i2[key] !== "") return
|
|
42131
|
+
if (q2) return truncate6(q2.text, 200);
|
|
42132
|
+
for (const key of ["command", "file_path", "filepath", "path", "url", "pattern", "description"]) {
|
|
42133
|
+
if (typeof i2[key] === "string" && i2[key] !== "") return truncate6(`${key}: ${i2[key]}`, 200);
|
|
41879
42134
|
}
|
|
41880
|
-
return
|
|
42135
|
+
return truncate6(JSON.stringify(input), 200);
|
|
41881
42136
|
}
|
|
41882
42137
|
function firstQuestion(i2) {
|
|
41883
42138
|
if (!Array.isArray(i2.questions) || i2.questions.length === 0) return void 0;
|
|
@@ -41895,7 +42150,7 @@ function buildPreview(toolName, input) {
|
|
|
41895
42150
|
const q2 = firstQuestion(i2);
|
|
41896
42151
|
if (q2) {
|
|
41897
42152
|
const lines = [q2.text, ...q2.options.map((o2, n2) => `${n2 + 1}. ${o2.label}${o2.description ? ` \u2014 ${o2.description}` : ""}`)];
|
|
41898
|
-
return
|
|
42153
|
+
return truncate6(lines.join("\n"), 600);
|
|
41899
42154
|
}
|
|
41900
42155
|
if (/^(Edit|MultiEdit)/.test(toolName) && typeof i2.new_string === "string") {
|
|
41901
42156
|
const oldS = typeof i2.old_string === "string" ? i2.old_string : "";
|
|
@@ -41903,15 +42158,24 @@ function buildPreview(toolName, input) {
|
|
|
41903
42158
|
...oldS.split("\n").map((l2) => `- ${l2}`),
|
|
41904
42159
|
...i2.new_string.split("\n").map((l2) => `+ ${l2}`)
|
|
41905
42160
|
];
|
|
41906
|
-
return
|
|
42161
|
+
return truncate6(lines.join("\n"), 600);
|
|
41907
42162
|
}
|
|
41908
42163
|
if (/^Write/.test(toolName) && typeof i2.content === "string") {
|
|
41909
|
-
return
|
|
42164
|
+
return truncate6(i2.content.split("\n").map((l2) => `+ ${l2}`).join("\n"), 600);
|
|
42165
|
+
}
|
|
42166
|
+
if (typeof i2.diff === "string" && i2.diff.trim()) {
|
|
42167
|
+
return truncate6(formatUnifiedDiff(i2.diff), 600);
|
|
41910
42168
|
}
|
|
41911
|
-
if (typeof i2.
|
|
42169
|
+
if (typeof i2.url === "string") return truncate6(`\u2192 ${i2.url}`, 600);
|
|
42170
|
+
if (typeof i2.command === "string") return truncate6(`$ ${i2.command}`, 600);
|
|
41912
42171
|
return void 0;
|
|
41913
42172
|
}
|
|
41914
|
-
function
|
|
42173
|
+
function formatUnifiedDiff(diff) {
|
|
42174
|
+
const lines = diff.split("\n");
|
|
42175
|
+
const hunkStart = lines.findIndex((l2) => l2.startsWith("@@"));
|
|
42176
|
+
return (hunkStart >= 0 ? lines.slice(hunkStart) : lines).join("\n").trimEnd();
|
|
42177
|
+
}
|
|
42178
|
+
function truncate6(s2, max) {
|
|
41915
42179
|
return s2.length <= max ? s2 : s2.slice(0, max - 1) + "\u2026";
|
|
41916
42180
|
}
|
|
41917
42181
|
|
|
@@ -42102,7 +42366,7 @@ var approvalUrl = `http://127.0.0.1:${port}/approval`;
|
|
|
42102
42366
|
var runners = [
|
|
42103
42367
|
new ClaudeCodeRunner(broker, approvalUrl),
|
|
42104
42368
|
new CopilotCliRunner(),
|
|
42105
|
-
new OpenCodeRunner(),
|
|
42369
|
+
new OpenCodeRunner(broker),
|
|
42106
42370
|
new CodexCliRunner(),
|
|
42107
42371
|
new CursorAgentRunner(),
|
|
42108
42372
|
new GeminiCliRunner()
|