@diologue/local-agent 0.3.0 → 0.6.0
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/cli.mjs +263 -12
- package/dist/cli.mjs.map +4 -4
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -726,9 +726,9 @@ var getDiff = async (cwd) => {
|
|
|
726
726
|
return [trackedDiff, ...untrackedDiffs].filter((part) => part.trim().length > 0).join("\n");
|
|
727
727
|
};
|
|
728
728
|
var runGitWithStdin = async (cwd, args, input) => {
|
|
729
|
-
const { execFile:
|
|
729
|
+
const { execFile: execFile3 } = await import("node:child_process");
|
|
730
730
|
return new Promise((resolve, reject) => {
|
|
731
|
-
const child =
|
|
731
|
+
const child = execFile3(
|
|
732
732
|
"git",
|
|
733
733
|
args,
|
|
734
734
|
{
|
|
@@ -848,6 +848,73 @@ var validateRepoPath = async (raw) => {
|
|
|
848
848
|
return resolved;
|
|
849
849
|
};
|
|
850
850
|
|
|
851
|
+
// src/lib/pick-directory.ts
|
|
852
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
853
|
+
import { homedir } from "node:os";
|
|
854
|
+
var PROMPT = "Select a git repository";
|
|
855
|
+
var dialogSpec = (platform) => {
|
|
856
|
+
switch (platform) {
|
|
857
|
+
case "darwin":
|
|
858
|
+
return {
|
|
859
|
+
cmd: "osascript",
|
|
860
|
+
args: [
|
|
861
|
+
"-e",
|
|
862
|
+
`set theFolder to choose folder with prompt "${PROMPT}"`,
|
|
863
|
+
"-e",
|
|
864
|
+
"POSIX path of theFolder"
|
|
865
|
+
]
|
|
866
|
+
};
|
|
867
|
+
case "linux":
|
|
868
|
+
return {
|
|
869
|
+
cmd: "zenity",
|
|
870
|
+
args: ["--file-selection", "--directory", `--title=${PROMPT}`]
|
|
871
|
+
};
|
|
872
|
+
case "win32":
|
|
873
|
+
return {
|
|
874
|
+
cmd: "powershell",
|
|
875
|
+
args: [
|
|
876
|
+
"-NoProfile",
|
|
877
|
+
"-Command",
|
|
878
|
+
`Add-Type -AssemblyName System.Windows.Forms;$d = New-Object System.Windows.Forms.FolderBrowserDialog;$d.Description = '${PROMPT}';if ($d.ShowDialog() -eq 'OK') { Write-Output $d.SelectedPath }`
|
|
879
|
+
]
|
|
880
|
+
};
|
|
881
|
+
default:
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
var run = (cmd, args) => new Promise((resolve, reject) => {
|
|
886
|
+
execFile2(
|
|
887
|
+
cmd,
|
|
888
|
+
args,
|
|
889
|
+
// Folder dialogs can sit open a while; cap it so the request can't hang
|
|
890
|
+
// forever if the user wanders off. Killing it reads as "cancelled".
|
|
891
|
+
{ timeout: 12e4, windowsHide: true },
|
|
892
|
+
(err, stdout) => {
|
|
893
|
+
if (err) reject(err);
|
|
894
|
+
else resolve(stdout);
|
|
895
|
+
}
|
|
896
|
+
);
|
|
897
|
+
});
|
|
898
|
+
var pickDirectory = async () => {
|
|
899
|
+
const spec = dialogSpec(process.platform);
|
|
900
|
+
if (!spec) return { ok: false, reason: "unsupported" };
|
|
901
|
+
const attempt = async (cmd, args) => {
|
|
902
|
+
try {
|
|
903
|
+
const out = (await run(cmd, args)).trim();
|
|
904
|
+
return out ? { ok: true, path: out } : { ok: false, reason: "cancelled" };
|
|
905
|
+
} catch (err) {
|
|
906
|
+
const code = err.code;
|
|
907
|
+
if (code === "ENOENT") return { ok: false, reason: "no_gui" };
|
|
908
|
+
return { ok: false, reason: "cancelled" };
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
const result = await attempt(spec.cmd, spec.args);
|
|
912
|
+
if (!result.ok && result.reason === "no_gui" && process.platform === "linux") {
|
|
913
|
+
return attempt("kdialog", ["--getexistingdirectory", homedir()]);
|
|
914
|
+
}
|
|
915
|
+
return result;
|
|
916
|
+
};
|
|
917
|
+
|
|
851
918
|
// src/routes/repo.ts
|
|
852
919
|
var selectRepoSchema = z.object({
|
|
853
920
|
path: z.string().min(1)
|
|
@@ -917,6 +984,23 @@ var createRepoRouter = (state) => {
|
|
|
917
984
|
throw err;
|
|
918
985
|
}
|
|
919
986
|
});
|
|
987
|
+
router.post("/browse", async (_req, res) => {
|
|
988
|
+
const result = await pickDirectory();
|
|
989
|
+
if (result.ok) {
|
|
990
|
+
const body = { path: result.path };
|
|
991
|
+
res.json(body);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
if (result.reason === "cancelled") {
|
|
995
|
+
const body = { cancelled: true };
|
|
996
|
+
res.json(body);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
res.status(501).json({
|
|
1000
|
+
error: result.reason,
|
|
1001
|
+
message: "No desktop folder dialog is available on this machine. Type the repo path instead."
|
|
1002
|
+
});
|
|
1003
|
+
});
|
|
920
1004
|
router.get("/status", async (_req, res) => {
|
|
921
1005
|
const repo = state.getSelectedRepo();
|
|
922
1006
|
if (!repo) {
|
|
@@ -1266,7 +1350,10 @@ var agentMessageBodySchema = z2.object({
|
|
|
1266
1350
|
/** Optional user-chosen routing for this turn. When absent the cloud
|
|
1267
1351
|
* picks the default (CODING_AGENT_DEFAULT_PROVIDER/MODEL). */
|
|
1268
1352
|
preferredProvider: z2.string().min(1).max(100).optional(),
|
|
1269
|
-
preferredModel: z2.string().min(1).max(200).optional()
|
|
1353
|
+
preferredModel: z2.string().min(1).max(200).optional(),
|
|
1354
|
+
/** Tool-gating policy. Absent → "auto" (run everything) so older clients
|
|
1355
|
+
* keep their current behaviour. */
|
|
1356
|
+
permissionMode: z2.enum(["auto", "confirm"]).optional()
|
|
1270
1357
|
});
|
|
1271
1358
|
var writeEvent = (res, event) => {
|
|
1272
1359
|
res.write(`data: ${JSON.stringify(event)}
|
|
@@ -1322,7 +1409,8 @@ var createAgentRouter = (deps) => {
|
|
|
1322
1409
|
signal: controller.signal,
|
|
1323
1410
|
broker: (payload, observer) => broker.request(payload, observer),
|
|
1324
1411
|
preferredProvider: parsed.data.preferredProvider,
|
|
1325
|
-
preferredModel: parsed.data.preferredModel
|
|
1412
|
+
preferredModel: parsed.data.preferredModel,
|
|
1413
|
+
permissionMode: parsed.data.permissionMode
|
|
1326
1414
|
})) {
|
|
1327
1415
|
if (controller.signal.aborted) {
|
|
1328
1416
|
logAgentRoute(
|
|
@@ -1493,6 +1581,36 @@ var createLlmChunkRouter = (deps) => {
|
|
|
1493
1581
|
});
|
|
1494
1582
|
return router;
|
|
1495
1583
|
};
|
|
1584
|
+
var permissionDecisionBodySchema = z2.object({
|
|
1585
|
+
sessionId: z2.string().min(1),
|
|
1586
|
+
permissionId: z2.string().min(1),
|
|
1587
|
+
response: z2.enum(["once", "always", "reject"])
|
|
1588
|
+
});
|
|
1589
|
+
var createPermissionRouter = (deps) => {
|
|
1590
|
+
const router = createRouter2();
|
|
1591
|
+
router.post("/", async (req, res) => {
|
|
1592
|
+
const parsed = permissionDecisionBodySchema.safeParse(req.body);
|
|
1593
|
+
if (!parsed.success) {
|
|
1594
|
+
res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
if (!deps.adapter.replyPermission) {
|
|
1598
|
+
res.status(501).json({ error: "permissions_unsupported" });
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
const matched = await deps.adapter.replyPermission(
|
|
1602
|
+
parsed.data.sessionId,
|
|
1603
|
+
parsed.data.permissionId,
|
|
1604
|
+
parsed.data.response
|
|
1605
|
+
);
|
|
1606
|
+
if (!matched) {
|
|
1607
|
+
res.status(404).json({ error: "no_active_permission" });
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
res.json({ ok: true });
|
|
1611
|
+
});
|
|
1612
|
+
return router;
|
|
1613
|
+
};
|
|
1496
1614
|
|
|
1497
1615
|
// src/routes/llm-shim.ts
|
|
1498
1616
|
import { Router as createRouter3 } from "express";
|
|
@@ -2359,13 +2477,30 @@ var MockOpenCodeAdapter = class {
|
|
|
2359
2477
|
};
|
|
2360
2478
|
|
|
2361
2479
|
// src/adapters/opencode-event-mapper.ts
|
|
2480
|
+
var MAX_TOOL_OUTPUT_CHARS = 4e3;
|
|
2481
|
+
var truncateOutput = (raw) => {
|
|
2482
|
+
if (!raw) return void 0;
|
|
2483
|
+
if (raw.length <= MAX_TOOL_OUTPUT_CHARS) return raw;
|
|
2484
|
+
const omitted = raw.length - MAX_TOOL_OUTPUT_CHARS;
|
|
2485
|
+
return `${raw.slice(0, MAX_TOOL_OUTPUT_CHARS)}
|
|
2486
|
+
\u2026 [${omitted} more characters truncated]`;
|
|
2487
|
+
};
|
|
2362
2488
|
var createMapState = (ourSessionId, openCodeSessionId) => ({
|
|
2363
2489
|
ourSessionId,
|
|
2364
2490
|
openCodeSessionId,
|
|
2365
2491
|
startedTools: /* @__PURE__ */ new Set(),
|
|
2366
2492
|
endedTools: /* @__PURE__ */ new Set(),
|
|
2367
|
-
announcedPatches: /* @__PURE__ */ new Set()
|
|
2493
|
+
announcedPatches: /* @__PURE__ */ new Set(),
|
|
2494
|
+
announcedPermissions: /* @__PURE__ */ new Set()
|
|
2368
2495
|
});
|
|
2496
|
+
var extractPermissionCommand = (metadata) => {
|
|
2497
|
+
if (!metadata) return void 0;
|
|
2498
|
+
for (const key of ["command", "cmd", "url", "pattern", "filePath"]) {
|
|
2499
|
+
const v = metadata[key];
|
|
2500
|
+
if (typeof v === "string" && v) return v;
|
|
2501
|
+
}
|
|
2502
|
+
return void 0;
|
|
2503
|
+
};
|
|
2369
2504
|
var EMPTY = { events: [], done: false };
|
|
2370
2505
|
var mapEvent = (item, state) => {
|
|
2371
2506
|
const sid = item.properties?.sessionID;
|
|
@@ -2375,6 +2510,25 @@ var mapEvent = (item, state) => {
|
|
|
2375
2510
|
if (item.type === "session.idle" && sid === state.openCodeSessionId) {
|
|
2376
2511
|
return { events: [{ type: "done" }], done: true };
|
|
2377
2512
|
}
|
|
2513
|
+
if (item.type === "permission.updated") {
|
|
2514
|
+
const permissionId = item.properties?.id;
|
|
2515
|
+
if (!permissionId || state.announcedPermissions.has(permissionId)) {
|
|
2516
|
+
return EMPTY;
|
|
2517
|
+
}
|
|
2518
|
+
state.announcedPermissions.add(permissionId);
|
|
2519
|
+
return {
|
|
2520
|
+
events: [
|
|
2521
|
+
{
|
|
2522
|
+
type: "permission_request",
|
|
2523
|
+
permissionId,
|
|
2524
|
+
tool: item.properties?.type ?? "tool",
|
|
2525
|
+
title: item.properties?.title,
|
|
2526
|
+
command: extractPermissionCommand(item.properties?.metadata)
|
|
2527
|
+
}
|
|
2528
|
+
],
|
|
2529
|
+
done: false
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2378
2532
|
if (item.type !== "message.part.updated") {
|
|
2379
2533
|
return EMPTY;
|
|
2380
2534
|
}
|
|
@@ -2413,7 +2567,10 @@ var mapEvent = (item, state) => {
|
|
|
2413
2567
|
type: "tool_call_end",
|
|
2414
2568
|
toolCallId: callId,
|
|
2415
2569
|
ok: status === "completed",
|
|
2416
|
-
summary: status === "error" ? part.state?.error ?? "Tool failed" : part.state?.title
|
|
2570
|
+
summary: status === "error" ? part.state?.error ?? "Tool failed" : part.state?.title,
|
|
2571
|
+
output: truncateOutput(
|
|
2572
|
+
status === "error" ? part.state?.error : part.state?.output
|
|
2573
|
+
)
|
|
2417
2574
|
};
|
|
2418
2575
|
return { events: [event], done: false };
|
|
2419
2576
|
}
|
|
@@ -2472,6 +2629,7 @@ init_engine_locator_npm();
|
|
|
2472
2629
|
var DIOLOGUE_PROVIDER_ID = "diologue";
|
|
2473
2630
|
var DIOLOGUE_MODEL_ID = "diologue-routed";
|
|
2474
2631
|
var END_OF_TURN_GRACE_MS = 600;
|
|
2632
|
+
var PERMISSION_DECISION_TIMEOUT_MS = 18e4;
|
|
2475
2633
|
var EDIT_DIRECTIVE = "You are an autonomous coding agent operating on a real git repository. Carry out the request by editing files directly with your tools (write / edit / patch / bash) \u2014 do not just describe the change, outline steps, or print code for the user to copy. Read only what you need, make the edits on disk, and finish once the change has actually been written.";
|
|
2476
2634
|
var logAdapter = (message) => {
|
|
2477
2635
|
console.error(`[opencode-adapter] ${message}`);
|
|
@@ -2485,6 +2643,9 @@ var OpenCodeProcessAdapter = class {
|
|
|
2485
2643
|
/** Maps our sessionId → opencode session id so successive turns within
|
|
2486
2644
|
* one coding-agent session reuse the same opencode conversation. */
|
|
2487
2645
|
sessionMap = /* @__PURE__ */ new Map();
|
|
2646
|
+
/** In-flight turns by our sessionId, so replyPermission() can reach the
|
|
2647
|
+
* engine client to resolve a paused tool call. */
|
|
2648
|
+
activeTurns = /* @__PURE__ */ new Map();
|
|
2488
2649
|
async resolveLocator() {
|
|
2489
2650
|
if (this.options.engineLocator) {
|
|
2490
2651
|
return this.options.engineLocator;
|
|
@@ -2527,14 +2688,17 @@ var OpenCodeProcessAdapter = class {
|
|
|
2527
2688
|
}
|
|
2528
2689
|
},
|
|
2529
2690
|
model: `${DIOLOGUE_PROVIDER_ID}/${DIOLOGUE_MODEL_ID}`,
|
|
2530
|
-
//
|
|
2531
|
-
//
|
|
2532
|
-
//
|
|
2533
|
-
//
|
|
2691
|
+
// Permission policy. The ADAPTER is the policy engine, not this config:
|
|
2692
|
+
// - edit stays "allow" — edits land in the working tree and are
|
|
2693
|
+
// reversible via the chat's Keep/Revert, so we never prompt on them.
|
|
2694
|
+
// - bash + webfetch are "ask" so opencode pauses them. In "auto" mode
|
|
2695
|
+
// the adapter auto-approves instantly (behaves like allow); in
|
|
2696
|
+
// "confirm" mode it forwards an AgentPermissionRequest to the browser
|
|
2697
|
+
// and resumes only once the user replies. See streamMessage().
|
|
2534
2698
|
permission: {
|
|
2535
2699
|
edit: "allow",
|
|
2536
|
-
bash: "
|
|
2537
|
-
webfetch: "
|
|
2700
|
+
bash: "ask",
|
|
2701
|
+
webfetch: "ask"
|
|
2538
2702
|
}
|
|
2539
2703
|
};
|
|
2540
2704
|
}
|
|
@@ -2579,6 +2743,44 @@ var OpenCodeProcessAdapter = class {
|
|
|
2579
2743
|
);
|
|
2580
2744
|
return id;
|
|
2581
2745
|
}
|
|
2746
|
+
/** Send a decision for a paused permission to opencode. Best-effort: a
|
|
2747
|
+
* failure is logged but not thrown — the worst case is the turn hangs
|
|
2748
|
+
* until the grace/abort path tears it down. */
|
|
2749
|
+
async replyToOpencode(client, openCodeSessionId, repoPath, permissionId, response) {
|
|
2750
|
+
try {
|
|
2751
|
+
await client.postSessionIdPermissionsPermissionId({
|
|
2752
|
+
path: { id: openCodeSessionId, permissionID: permissionId },
|
|
2753
|
+
query: { directory: repoPath },
|
|
2754
|
+
body: { response }
|
|
2755
|
+
});
|
|
2756
|
+
logAdapter(
|
|
2757
|
+
`permission reply id=${permissionId.slice(0, 8)} response=${response}`
|
|
2758
|
+
);
|
|
2759
|
+
} catch (err) {
|
|
2760
|
+
logAdapter(
|
|
2761
|
+
`permission reply FAILED id=${permissionId.slice(0, 8)} response=${response} error=${err instanceof Error ? err.message : String(err)}`
|
|
2762
|
+
);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
/** Apply a browser decision to a paused permission. Invoked by the
|
|
2766
|
+
* /agent/permission route on a separate HTTP request from the SSE stream. */
|
|
2767
|
+
async replyPermission(sessionId, permissionId, response) {
|
|
2768
|
+
const turn = this.activeTurns.get(sessionId);
|
|
2769
|
+
if (!turn) return false;
|
|
2770
|
+
const timer = turn.pending.get(permissionId);
|
|
2771
|
+
if (timer) {
|
|
2772
|
+
clearTimeout(timer);
|
|
2773
|
+
turn.pending.delete(permissionId);
|
|
2774
|
+
}
|
|
2775
|
+
await this.replyToOpencode(
|
|
2776
|
+
turn.client,
|
|
2777
|
+
turn.openCodeSessionId,
|
|
2778
|
+
turn.repoPath,
|
|
2779
|
+
permissionId,
|
|
2780
|
+
response
|
|
2781
|
+
);
|
|
2782
|
+
return true;
|
|
2783
|
+
}
|
|
2582
2784
|
async *streamMessage(request) {
|
|
2583
2785
|
let handle;
|
|
2584
2786
|
try {
|
|
@@ -2613,6 +2815,14 @@ var OpenCodeProcessAdapter = class {
|
|
|
2613
2815
|
);
|
|
2614
2816
|
let sawAnyPatch = false;
|
|
2615
2817
|
let lastPatchHash = "";
|
|
2818
|
+
const permissionMode = request.permissionMode ?? "auto";
|
|
2819
|
+
const activeTurn = {
|
|
2820
|
+
client: handle.client,
|
|
2821
|
+
openCodeSessionId,
|
|
2822
|
+
repoPath: request.repoPath,
|
|
2823
|
+
pending: /* @__PURE__ */ new Map()
|
|
2824
|
+
};
|
|
2825
|
+
this.activeTurns.set(request.sessionId, activeTurn);
|
|
2616
2826
|
let activeBrokerHandle = null;
|
|
2617
2827
|
let streamScopedBroker = null;
|
|
2618
2828
|
if (request.broker) {
|
|
@@ -2725,6 +2935,34 @@ ${request.prompt}` }
|
|
|
2725
2935
|
);
|
|
2726
2936
|
}
|
|
2727
2937
|
for (const event of mapped.events) {
|
|
2938
|
+
if (event.type === "permission_request") {
|
|
2939
|
+
if (permissionMode === "confirm") {
|
|
2940
|
+
const timer = setTimeout(() => {
|
|
2941
|
+
activeTurn.pending.delete(event.permissionId);
|
|
2942
|
+
void this.replyToOpencode(
|
|
2943
|
+
handle.client,
|
|
2944
|
+
openCodeSessionId,
|
|
2945
|
+
request.repoPath,
|
|
2946
|
+
event.permissionId,
|
|
2947
|
+
"reject"
|
|
2948
|
+
);
|
|
2949
|
+
logAdapter(
|
|
2950
|
+
`permission auto-rejected (timeout) id=${event.permissionId.slice(0, 8)} session=${request.sessionId.slice(0, 8)}`
|
|
2951
|
+
);
|
|
2952
|
+
}, PERMISSION_DECISION_TIMEOUT_MS);
|
|
2953
|
+
activeTurn.pending.set(event.permissionId, timer);
|
|
2954
|
+
yield event;
|
|
2955
|
+
} else {
|
|
2956
|
+
void this.replyToOpencode(
|
|
2957
|
+
handle.client,
|
|
2958
|
+
openCodeSessionId,
|
|
2959
|
+
request.repoPath,
|
|
2960
|
+
event.permissionId,
|
|
2961
|
+
"once"
|
|
2962
|
+
);
|
|
2963
|
+
}
|
|
2964
|
+
continue;
|
|
2965
|
+
}
|
|
2728
2966
|
if (event.type === "diff_proposed" && mapped.patchToFetch) {
|
|
2729
2967
|
sawAnyPatch = true;
|
|
2730
2968
|
lastPatchHash = mapped.patchToFetch.hash;
|
|
@@ -2743,6 +2981,18 @@ ${request.prompt}` }
|
|
|
2743
2981
|
} catch {
|
|
2744
2982
|
}
|
|
2745
2983
|
request.signal?.removeEventListener("abort", stopOnAbort);
|
|
2984
|
+
for (const [permId, timer] of activeTurn.pending) {
|
|
2985
|
+
clearTimeout(timer);
|
|
2986
|
+
void this.replyToOpencode(
|
|
2987
|
+
handle.client,
|
|
2988
|
+
openCodeSessionId,
|
|
2989
|
+
request.repoPath,
|
|
2990
|
+
permId,
|
|
2991
|
+
"reject"
|
|
2992
|
+
);
|
|
2993
|
+
}
|
|
2994
|
+
activeTurn.pending.clear();
|
|
2995
|
+
this.activeTurns.delete(request.sessionId);
|
|
2746
2996
|
activeBrokerHandle?.release();
|
|
2747
2997
|
streamScopedBroker?.close("turn_ended");
|
|
2748
2998
|
}
|
|
@@ -2850,6 +3100,7 @@ var createApp = (options) => {
|
|
|
2850
3100
|
app.use("/agent", createAgentRouter({ state, adapter, brokerRegistry }));
|
|
2851
3101
|
app.use("/agent/llm-response", createLlmResponseRouter({ brokerRegistry }));
|
|
2852
3102
|
app.use("/agent/llm-chunk", createLlmChunkRouter({ brokerRegistry }));
|
|
3103
|
+
app.use("/agent/permission", createPermissionRouter({ adapter }));
|
|
2853
3104
|
app.use("/llm-shim", createLlmShimRouter());
|
|
2854
3105
|
app.use((req, res) => {
|
|
2855
3106
|
res.status(404).json({ error: "not_found", method: req.method, path: req.path });
|