offhands 0.1.3 → 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 +369 -106
- 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,102 +5308,71 @@ 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
|
|
5311
|
-
const
|
|
5312
|
-
if (
|
|
5313
|
-
const
|
|
5314
|
-
return
|
|
5315
|
-
}
|
|
5316
|
-
if (
|
|
5317
|
-
const
|
|
5318
|
-
return
|
|
5319
|
-
}
|
|
5320
|
-
if (
|
|
5321
|
-
const name =
|
|
5322
|
-
const
|
|
5311
|
+
const part = v2.part ?? {};
|
|
5312
|
+
const partType = typeof part.type === "string" ? part.type : typeof v2.type === "string" ? v2.type : "";
|
|
5313
|
+
if (partType === "text") {
|
|
5314
|
+
const text = typeof part.text === "string" ? part.text : "";
|
|
5315
|
+
return text ? [{ type: "text", chunk: text }] : [];
|
|
5316
|
+
}
|
|
5317
|
+
if (partType === "reasoning") {
|
|
5318
|
+
const thinking = typeof part.text === "string" ? part.text : "";
|
|
5319
|
+
return thinking ? [{ type: "thinking", chunk: thinking }] : [];
|
|
5320
|
+
}
|
|
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
|
+
}
|
|
5323
5331
|
return [{ type: "tool", name, summary: summary ? `${name}: ${truncate3(summary, 120)}` : name }];
|
|
5324
5332
|
}
|
|
5325
|
-
if (
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
const question = data.question ? mapQuestion(data.question) : void 0;
|
|
5335
|
-
return [{
|
|
5336
|
-
type: "approval",
|
|
5337
|
-
id,
|
|
5338
|
-
action,
|
|
5339
|
-
detail,
|
|
5340
|
-
risk,
|
|
5341
|
-
preview,
|
|
5342
|
-
question
|
|
5343
|
-
}];
|
|
5344
|
-
}
|
|
5345
|
-
if (type === "result" || type === "completed" || type === "done") {
|
|
5346
|
-
const isError = v2.is_error === true || v2.error === true || typeof data.exitCode === "number" && data.exitCode !== 0;
|
|
5347
|
-
const text = firstString2(v2, "result", "text", "summary", "message") ?? "";
|
|
5348
|
-
if (isError) {
|
|
5349
|
-
return [{ type: "error", message: text || `run failed (${String(v2.subtype ?? v2.type ?? "unknown")})` }];
|
|
5333
|
+
if (partType === "step-start") return [];
|
|
5334
|
+
if (partType === "step-finish") {
|
|
5335
|
+
const reason = typeof part.reason === "string" ? part.reason : "stop";
|
|
5336
|
+
const usage = extractUsageFromPart(part);
|
|
5337
|
+
if (reason === "error") {
|
|
5338
|
+
const message = firstString2(part, "error", "message") ?? "run failed";
|
|
5339
|
+
const events2 = [{ type: "error", message }];
|
|
5340
|
+
if (usage) events2.push(usage);
|
|
5341
|
+
return events2;
|
|
5350
5342
|
}
|
|
5351
|
-
|
|
5352
|
-
const
|
|
5343
|
+
if (reason !== "stop") return usage ? [usage] : [];
|
|
5344
|
+
const events = [{ type: "done", summary: "" }];
|
|
5353
5345
|
if (usage) events.push(usage);
|
|
5354
5346
|
return events;
|
|
5355
5347
|
}
|
|
5356
|
-
if (type === "
|
|
5348
|
+
if (!part.type && typeof v2.type === "string" && v2.type === "error") {
|
|
5357
5349
|
const message = firstString2(v2, "message", "error", "detail", "result") ?? "unknown error";
|
|
5358
5350
|
return [{ type: "error", message }];
|
|
5359
5351
|
}
|
|
5360
|
-
if (type === "rate_limit" || type === "usage") {
|
|
5361
|
-
const usage = extractUsage(v2, data);
|
|
5362
|
-
return usage ? [usage] : [];
|
|
5363
|
-
}
|
|
5364
|
-
if (type === "session.created" || type === "session.updated") {
|
|
5365
|
-
return [];
|
|
5366
|
-
}
|
|
5367
5352
|
return [];
|
|
5368
5353
|
}
|
|
5369
5354
|
function extractOpenCodeSessionId(value) {
|
|
5370
5355
|
if (typeof value !== "object" || value === null) return null;
|
|
5371
5356
|
const v2 = value;
|
|
5372
|
-
const
|
|
5373
|
-
return firstString2(v2, "
|
|
5374
|
-
}
|
|
5375
|
-
function
|
|
5376
|
-
|
|
5377
|
-
const
|
|
5378
|
-
|
|
5379
|
-
const
|
|
5380
|
-
|
|
5381
|
-
description: typeof o2.description === "string" ? o2.description : void 0
|
|
5382
|
-
})) : [];
|
|
5383
|
-
const multiSelect = typeof qq.multiSelect === "boolean" ? qq.multiSelect : void 0;
|
|
5384
|
-
if (!text && options.length === 0) return void 0;
|
|
5385
|
-
return { text, options, multiSelect };
|
|
5386
|
-
}
|
|
5387
|
-
function extractUsage(v2, data) {
|
|
5388
|
-
const usage = v2.usage ?? data.usage;
|
|
5389
|
-
const costUsd = typeof v2.costUsd === "number" ? v2.costUsd : typeof data.costUsd === "number" ? data.costUsd : typeof v2.total_cost_usd === "number" ? v2.total_cost_usd : void 0;
|
|
5390
|
-
if (!usage && costUsd === void 0) return null;
|
|
5391
|
-
const n2 = (k2) => {
|
|
5392
|
-
const val = usage?.[k2] ?? data[k2];
|
|
5393
|
-
return typeof val === "number" ? val : 0;
|
|
5394
|
-
};
|
|
5395
|
-
const contextTokens = n2("inputTokens") + n2("input_tokens") + n2("cacheReadInputTokens") + n2("cache_read_input_tokens") + n2("cacheCreationInputTokens") + n2("cache_creation_input_tokens") + n2("outputTokens") + n2("output_tokens") + n2("reasoningTokens") + n2("reasoning_tokens");
|
|
5357
|
+
const part = v2.part ?? {};
|
|
5358
|
+
return firstString2(v2, "sessionID", "sessionId", "session_id") ?? firstString2(part, "sessionID", "sessionId", "session_id") ?? null;
|
|
5359
|
+
}
|
|
5360
|
+
function extractUsageFromPart(part) {
|
|
5361
|
+
const tokens = part.tokens;
|
|
5362
|
+
const costUsd = typeof part.cost === "number" ? part.cost : void 0;
|
|
5363
|
+
if (!tokens && costUsd === void 0) return null;
|
|
5364
|
+
const n2 = (k2) => typeof tokens?.[k2] === "number" ? tokens[k2] : 0;
|
|
5365
|
+
const contextTokens = tokens && typeof tokens.total === "number" ? tokens.total : n2("input") + n2("output") + n2("reasoning");
|
|
5396
5366
|
if (contextTokens <= 0 && costUsd === void 0) return null;
|
|
5397
5367
|
return {
|
|
5398
5368
|
type: "usage",
|
|
5399
5369
|
contextTokens,
|
|
5400
5370
|
contextWindow: 2e5,
|
|
5401
|
-
// OpenCode doesn't expose window; use reasonable default
|
|
5402
5371
|
...costUsd !== void 0 ? { costUsd } : {}
|
|
5403
5372
|
};
|
|
5404
5373
|
}
|
|
5405
5374
|
function firstString2(obj, ...keys) {
|
|
5375
|
+
if (!obj) return void 0;
|
|
5406
5376
|
for (const k2 of keys) {
|
|
5407
5377
|
const val = obj[k2];
|
|
5408
5378
|
if (typeof val === "string" && val !== "") return val;
|
|
@@ -5414,7 +5384,11 @@ function truncate3(s2, max) {
|
|
|
5414
5384
|
}
|
|
5415
5385
|
|
|
5416
5386
|
// ../daemon/src/runners/opencode-cli.ts
|
|
5387
|
+
var POLL_INTERVAL_MS = 500;
|
|
5417
5388
|
var OpenCodeRunner = class {
|
|
5389
|
+
constructor(broker2) {
|
|
5390
|
+
this.broker = broker2;
|
|
5391
|
+
}
|
|
5418
5392
|
id = "opencode";
|
|
5419
5393
|
name = "OpenCode";
|
|
5420
5394
|
supportsApprovals = true;
|
|
@@ -5423,6 +5397,10 @@ var OpenCodeRunner = class {
|
|
|
5423
5397
|
/** [command, ...prefixArgs] resolved once. */
|
|
5424
5398
|
resolved = null;
|
|
5425
5399
|
modelsCache = [];
|
|
5400
|
+
// Lazily-started, shared `opencode serve` instance backing startViaHttpApi.
|
|
5401
|
+
serverPort = null;
|
|
5402
|
+
serverProcess = null;
|
|
5403
|
+
serverStarting = null;
|
|
5426
5404
|
resolveCommand() {
|
|
5427
5405
|
if (this.resolved) return this.resolved;
|
|
5428
5406
|
if (process.platform === "win32") {
|
|
@@ -5493,7 +5471,7 @@ var OpenCodeRunner = class {
|
|
|
5493
5471
|
if (code === 0) {
|
|
5494
5472
|
try {
|
|
5495
5473
|
const lines = output.trim().split("\n");
|
|
5496
|
-
const models = lines.map((l2) => l2.trim()).filter((l2) => l2
|
|
5474
|
+
const models = lines.map((l2) => l2.trim()).filter((l2) => l2.length > 0);
|
|
5497
5475
|
this.modelsCache = models;
|
|
5498
5476
|
Object.defineProperty(this, "models", {
|
|
5499
5477
|
value: models,
|
|
@@ -5515,6 +5493,13 @@ var OpenCodeRunner = class {
|
|
|
5515
5493
|
return existsSync3(join3(homedir3(), ".local", "share", "opencode", "auth.json"));
|
|
5516
5494
|
}
|
|
5517
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) {
|
|
5518
5503
|
const queue = new AsyncEventQueue();
|
|
5519
5504
|
const parser = new NdjsonParser();
|
|
5520
5505
|
let child;
|
|
@@ -5538,10 +5523,8 @@ var OpenCodeRunner = class {
|
|
|
5538
5523
|
const args2 = [
|
|
5539
5524
|
...cmd.slice(1),
|
|
5540
5525
|
"run",
|
|
5541
|
-
run.prompt,
|
|
5542
5526
|
"--format",
|
|
5543
|
-
"json"
|
|
5544
|
-
"--no-color"
|
|
5527
|
+
"json"
|
|
5545
5528
|
];
|
|
5546
5529
|
if (run.model) {
|
|
5547
5530
|
args2.push("--model", run.model);
|
|
@@ -5549,12 +5532,10 @@ var OpenCodeRunner = class {
|
|
|
5549
5532
|
if (run.resumeConversationId) {
|
|
5550
5533
|
args2.push("--session", run.resumeConversationId);
|
|
5551
5534
|
}
|
|
5552
|
-
|
|
5553
|
-
if (mode === "bypass") {
|
|
5554
|
-
args2.push("--auto");
|
|
5555
|
-
} else if (mode === "plan") {
|
|
5535
|
+
if (mode === "plan") {
|
|
5556
5536
|
args2.push("--agent", "plan");
|
|
5557
5537
|
} else {
|
|
5538
|
+
args2.push("--auto");
|
|
5558
5539
|
}
|
|
5559
5540
|
if (run.effort) {
|
|
5560
5541
|
const variantMap = { low: "minimal", medium: "high", high: "max", max: "max" };
|
|
@@ -5570,6 +5551,8 @@ var OpenCodeRunner = class {
|
|
|
5570
5551
|
}
|
|
5571
5552
|
}
|
|
5572
5553
|
args2.push("--dir", run.workspace);
|
|
5554
|
+
const promptArg = process.platform === "win32" ? `"${run.prompt.replace(/"/g, '\\"')}"` : run.prompt;
|
|
5555
|
+
args2.push(promptArg);
|
|
5573
5556
|
try {
|
|
5574
5557
|
child = spawn4(cmd[0], args2, {
|
|
5575
5558
|
cwd: run.workspace,
|
|
@@ -5623,12 +5606,283 @@ stderr: ${stderrTail}` : ""}`
|
|
|
5623
5606
|
});
|
|
5624
5607
|
return {
|
|
5625
5608
|
events: queue,
|
|
5626
|
-
|
|
5609
|
+
// No broker (or bypass/plan, which never ask) — nothing to answer.
|
|
5610
|
+
respond: () => {
|
|
5627
5611
|
},
|
|
5628
5612
|
cancel: () => child.kill()
|
|
5629
5613
|
};
|
|
5630
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
|
+
}
|
|
5631
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
|
+
}
|
|
5632
5886
|
|
|
5633
5887
|
// ../daemon/src/session-manager.ts
|
|
5634
5888
|
import { randomUUID } from "node:crypto";
|
|
@@ -5749,7 +6003,7 @@ function summarizeConversation(path, workspace) {
|
|
|
5749
6003
|
return {
|
|
5750
6004
|
conversationId: basename(path, ".jsonl"),
|
|
5751
6005
|
workspace,
|
|
5752
|
-
firstPrompt:
|
|
6006
|
+
firstPrompt: truncate5(firstPrompt || "(empty prompt)", 120),
|
|
5753
6007
|
lastActiveMs: Math.floor(statSync(path).mtimeMs),
|
|
5754
6008
|
messageCount
|
|
5755
6009
|
};
|
|
@@ -5775,7 +6029,7 @@ function userMessageText(value) {
|
|
|
5775
6029
|
function collapseText(text) {
|
|
5776
6030
|
return text.replace(/\s+/g, " ").trim();
|
|
5777
6031
|
}
|
|
5778
|
-
function
|
|
6032
|
+
function truncate5(text, max) {
|
|
5779
6033
|
return text.length <= max ? text : `${text.slice(0, max - 1)}\u2026`;
|
|
5780
6034
|
}
|
|
5781
6035
|
function safeReadDir(path) {
|
|
@@ -6558,7 +6812,7 @@ function searchableText(msg) {
|
|
|
6558
6812
|
}
|
|
6559
6813
|
|
|
6560
6814
|
// ../daemon/src/local-server.ts
|
|
6561
|
-
import { createServer } from "node:http";
|
|
6815
|
+
import { createServer as createServer2 } from "node:http";
|
|
6562
6816
|
|
|
6563
6817
|
// ../node_modules/.pnpm/ws@8.21.3/node_modules/ws/wrapper.mjs
|
|
6564
6818
|
var import_stream = __toESM(require_stream(), 1);
|
|
@@ -11015,9 +11269,9 @@ if (typeof Module.getRandomValue === "undefined") {
|
|
|
11015
11269
|
Module.getRandomValue = randomValuesStandard;
|
|
11016
11270
|
} catch (e) {
|
|
11017
11271
|
try {
|
|
11018
|
-
|
|
11272
|
+
crypto = null;
|
|
11019
11273
|
randomValueNodeJS = function() {
|
|
11020
|
-
var buf =
|
|
11274
|
+
var buf = crypto["randomBytes"](4);
|
|
11021
11275
|
return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
|
|
11022
11276
|
};
|
|
11023
11277
|
randomValueNodeJS();
|
|
@@ -11030,7 +11284,7 @@ if (typeof Module.getRandomValue === "undefined") {
|
|
|
11030
11284
|
var window_;
|
|
11031
11285
|
var crypto_;
|
|
11032
11286
|
var randomValuesStandard;
|
|
11033
|
-
var
|
|
11287
|
+
var crypto;
|
|
11034
11288
|
var randomValueNodeJS;
|
|
11035
11289
|
var _Module = Module;
|
|
11036
11290
|
Module.ready = new Promise(function(resolve6, reject) {
|
|
@@ -37924,7 +38178,7 @@ Module.ready = new Promise(function(resolve6, reject) {
|
|
|
37924
38178
|
try {
|
|
37925
38179
|
var window_ = "object" === typeof window ? window : self;
|
|
37926
38180
|
var crypto_ = typeof window_.crypto !== "undefined" ? window_.crypto : window_.msCrypto;
|
|
37927
|
-
crypto_ = crypto_ === void 0 ?
|
|
38181
|
+
crypto_ = crypto_ === void 0 ? crypto : crypto_;
|
|
37928
38182
|
var randomValuesStandard = function() {
|
|
37929
38183
|
var buf = new Uint32Array(1);
|
|
37930
38184
|
crypto_.getRandomValues(buf);
|
|
@@ -37934,9 +38188,9 @@ Module.ready = new Promise(function(resolve6, reject) {
|
|
|
37934
38188
|
Module3.getRandomValue = randomValuesStandard;
|
|
37935
38189
|
} catch (e) {
|
|
37936
38190
|
try {
|
|
37937
|
-
var
|
|
38191
|
+
var crypto = null;
|
|
37938
38192
|
var randomValueNodeJS = function() {
|
|
37939
|
-
var buf =
|
|
38193
|
+
var buf = crypto["randomBytes"](4);
|
|
37940
38194
|
return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
|
|
37941
38195
|
};
|
|
37942
38196
|
randomValueNodeJS();
|
|
@@ -38582,7 +38836,7 @@ Module.ready = new Promise(function(resolve6, reject) {
|
|
|
38582
38836
|
try {
|
|
38583
38837
|
var window_ = "object" === typeof window ? window : self;
|
|
38584
38838
|
var crypto_ = typeof window_.crypto !== "undefined" ? window_.crypto : window_.msCrypto;
|
|
38585
|
-
crypto_ = crypto_ === void 0 ?
|
|
38839
|
+
crypto_ = crypto_ === void 0 ? crypto : crypto_;
|
|
38586
38840
|
var randomValuesStandard = function() {
|
|
38587
38841
|
var buf = new Uint32Array(1);
|
|
38588
38842
|
crypto_.getRandomValues(buf);
|
|
@@ -38592,9 +38846,9 @@ Module.ready = new Promise(function(resolve6, reject) {
|
|
|
38592
38846
|
Module2.getRandomValue = randomValuesStandard;
|
|
38593
38847
|
} catch (e) {
|
|
38594
38848
|
try {
|
|
38595
|
-
var
|
|
38849
|
+
var crypto = null;
|
|
38596
38850
|
var randomValueNodeJS = function() {
|
|
38597
|
-
var buf =
|
|
38851
|
+
var buf = crypto["randomBytes"](4);
|
|
38598
38852
|
return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
|
|
38599
38853
|
};
|
|
38600
38854
|
randomValueNodeJS();
|
|
@@ -41516,7 +41770,7 @@ var LocalSessionServer = class {
|
|
|
41516
41770
|
constructor(manager2, port2, broker2) {
|
|
41517
41771
|
this.manager = manager2;
|
|
41518
41772
|
this.broker = broker2;
|
|
41519
|
-
const http =
|
|
41773
|
+
const http = createServer2((req, res) => void this.onRequest(req, res));
|
|
41520
41774
|
this.wss = new import_websocket_server.default({ server: http });
|
|
41521
41775
|
this.wss.on("connection", (ws) => this.onConnection(ws));
|
|
41522
41776
|
http.on("error", (err) => {
|
|
@@ -41865,7 +42119,7 @@ var ApprovalBroker = class {
|
|
|
41865
42119
|
};
|
|
41866
42120
|
function classifyRisk(toolName, input) {
|
|
41867
42121
|
const i2 = input ?? {};
|
|
41868
|
-
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(" ");
|
|
41869
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(
|
|
41870
42124
|
text
|
|
41871
42125
|
) ? "high" : "low";
|
|
@@ -41874,11 +42128,11 @@ function summariseInput(input) {
|
|
|
41874
42128
|
if (typeof input !== "object" || input === null) return String(input ?? "");
|
|
41875
42129
|
const i2 = input;
|
|
41876
42130
|
const q2 = firstQuestion(i2);
|
|
41877
|
-
if (q2) return
|
|
41878
|
-
for (const key of ["command", "file_path", "path", "url", "pattern", "description"]) {
|
|
41879
|
-
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);
|
|
41880
42134
|
}
|
|
41881
|
-
return
|
|
42135
|
+
return truncate6(JSON.stringify(input), 200);
|
|
41882
42136
|
}
|
|
41883
42137
|
function firstQuestion(i2) {
|
|
41884
42138
|
if (!Array.isArray(i2.questions) || i2.questions.length === 0) return void 0;
|
|
@@ -41896,7 +42150,7 @@ function buildPreview(toolName, input) {
|
|
|
41896
42150
|
const q2 = firstQuestion(i2);
|
|
41897
42151
|
if (q2) {
|
|
41898
42152
|
const lines = [q2.text, ...q2.options.map((o2, n2) => `${n2 + 1}. ${o2.label}${o2.description ? ` \u2014 ${o2.description}` : ""}`)];
|
|
41899
|
-
return
|
|
42153
|
+
return truncate6(lines.join("\n"), 600);
|
|
41900
42154
|
}
|
|
41901
42155
|
if (/^(Edit|MultiEdit)/.test(toolName) && typeof i2.new_string === "string") {
|
|
41902
42156
|
const oldS = typeof i2.old_string === "string" ? i2.old_string : "";
|
|
@@ -41904,15 +42158,24 @@ function buildPreview(toolName, input) {
|
|
|
41904
42158
|
...oldS.split("\n").map((l2) => `- ${l2}`),
|
|
41905
42159
|
...i2.new_string.split("\n").map((l2) => `+ ${l2}`)
|
|
41906
42160
|
];
|
|
41907
|
-
return
|
|
42161
|
+
return truncate6(lines.join("\n"), 600);
|
|
41908
42162
|
}
|
|
41909
42163
|
if (/^Write/.test(toolName) && typeof i2.content === "string") {
|
|
41910
|
-
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);
|
|
41911
42168
|
}
|
|
41912
|
-
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);
|
|
41913
42171
|
return void 0;
|
|
41914
42172
|
}
|
|
41915
|
-
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) {
|
|
41916
42179
|
return s2.length <= max ? s2 : s2.slice(0, max - 1) + "\u2026";
|
|
41917
42180
|
}
|
|
41918
42181
|
|
|
@@ -42103,7 +42366,7 @@ var approvalUrl = `http://127.0.0.1:${port}/approval`;
|
|
|
42103
42366
|
var runners = [
|
|
42104
42367
|
new ClaudeCodeRunner(broker, approvalUrl),
|
|
42105
42368
|
new CopilotCliRunner(),
|
|
42106
|
-
new OpenCodeRunner(),
|
|
42369
|
+
new OpenCodeRunner(broker),
|
|
42107
42370
|
new CodexCliRunner(),
|
|
42108
42371
|
new CursorAgentRunner(),
|
|
42109
42372
|
new GeminiCliRunner()
|