agent-sh 0.13.7 → 0.14.1
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 +7 -18
- package/dist/agent/agent-loop.d.ts +13 -17
- package/dist/agent/agent-loop.js +118 -224
- package/dist/agent/conversation-state.d.ts +1 -1
- package/dist/agent/events.d.ts +218 -0
- package/dist/agent/events.js +1 -0
- package/dist/agent/host-types.d.ts +20 -0
- package/dist/agent/index.d.ts +5 -9
- package/dist/agent/index.js +269 -167
- package/dist/{utils → agent}/llm-client.js +1 -0
- package/dist/agent/llm-facade.d.ts +13 -0
- package/dist/{utils → agent}/llm-facade.js +1 -1
- package/dist/agent/nuclear-form.d.ts +1 -1
- package/dist/agent/providers/deepseek.js +2 -5
- package/dist/agent/providers/openai-compatible.js +2 -2
- package/dist/agent/providers/openai.js +2 -5
- package/dist/agent/providers/openrouter.js +5 -5
- package/dist/agent/subagent.d.ts +1 -1
- package/dist/agent/tool-protocol.d.ts +1 -1
- package/dist/agent/tool-registry.d.ts +1 -1
- package/dist/agent/types.d.ts +2 -2
- package/dist/cli/args.js +3 -1
- package/dist/cli/auth/cli.js +11 -6
- package/dist/cli/auth/discover.d.ts +5 -0
- package/dist/cli/auth/discover.js +25 -0
- package/dist/cli/auth/keys.d.ts +5 -2
- package/dist/cli/auth/keys.js +22 -2
- package/dist/cli/index.d.ts +16 -0
- package/dist/cli/index.js +12 -2
- package/dist/cli/install.d.ts +1 -0
- package/dist/cli/install.js +86 -2
- package/dist/cli/subcommands.js +4 -1
- package/dist/core/event-bus.d.ts +28 -371
- package/dist/core/extension-loader.js +6 -6
- package/dist/core/index.d.ts +10 -29
- package/dist/core/index.js +32 -84
- package/dist/extensions/index.d.ts +2 -1
- package/dist/extensions/index.js +1 -1
- package/dist/extensions/slash-commands/events.d.ts +18 -0
- package/dist/extensions/slash-commands/events.js +1 -0
- package/dist/extensions/slash-commands/index.d.ts +15 -0
- package/dist/extensions/{slash-commands.js → slash-commands/index.js} +4 -3
- package/dist/shell/events.d.ts +85 -0
- package/dist/shell/events.js +1 -0
- package/dist/shell/index.d.ts +1 -0
- package/dist/shell/index.js +6 -0
- package/dist/shell/tui-renderer.js +0 -1
- package/dist/utils/tool-interactive.js +4 -2
- package/examples/extensions/ash-acp-bridge/src/index.ts +2 -2
- package/examples/extensions/ashi/package.json +2 -2
- package/examples/extensions/ashi/src/cli.ts +4 -1
- package/examples/extensions/claude-code-bridge/index.ts +7 -2
- package/examples/extensions/claude-code-bridge/package.json +1 -1
- package/examples/extensions/ollama.ts +47 -42
- package/examples/extensions/opencode-bridge/README.md +4 -0
- package/examples/extensions/opencode-bridge/index.ts +208 -54
- package/examples/extensions/opencode-bridge/package.json +1 -1
- package/examples/extensions/pi-bridge/index.ts +3 -4
- package/examples/extensions/zai-coding-plan.ts +2 -6
- package/package.json +1 -1
- package/dist/extensions/slash-commands.d.ts +0 -2
- package/dist/utils/llm-facade.d.ts +0 -11
- /package/dist/{utils → agent}/llm-client.d.ts +0 -0
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type ToolPart,
|
|
14
14
|
type QuestionRequest,
|
|
15
15
|
type QuestionInfo,
|
|
16
|
+
type PermissionRequest,
|
|
16
17
|
} from "@opencode-ai/sdk/v2";
|
|
17
18
|
import type { ExtensionContext } from "agent-sh/types";
|
|
18
19
|
import type { InteractiveSession } from "agent-sh/agent/types";
|
|
@@ -60,7 +61,8 @@ function parseUnifiedDiff(patch: string): DiffResult | null {
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
export default function activate(ctx: ExtensionContext): void {
|
|
63
|
-
const { bus, call } = ctx;
|
|
64
|
+
const { bus, call } = ctx;
|
|
65
|
+
const compositor = ctx.shell?.compositor;
|
|
64
66
|
|
|
65
67
|
const cwd = (): string => {
|
|
66
68
|
const v = call("cwd");
|
|
@@ -94,6 +96,18 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
94
96
|
let turnIdleSeen = false;
|
|
95
97
|
let turnError: string | null = null;
|
|
96
98
|
|
|
99
|
+
let pickerOpen = false;
|
|
100
|
+
const eventQueue: Event[] = [];
|
|
101
|
+
const drainQueue = (): void => {
|
|
102
|
+
const events = eventQueue.splice(0);
|
|
103
|
+
for (const ev of events) handleEvent(ev);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// After Ctrl+C, opencode emits a tail of tool / error state for the
|
|
107
|
+
// aborted turn. Suppress until the next turn so it doesn't restart the
|
|
108
|
+
// spinner or replay dead tool entries.
|
|
109
|
+
let cancelledTurn = false;
|
|
110
|
+
|
|
97
111
|
const listeners: Array<{ event: string; fn: Function }> = [];
|
|
98
112
|
|
|
99
113
|
function toolKind(name: string): string {
|
|
@@ -194,12 +208,24 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
194
208
|
|
|
195
209
|
|
|
196
210
|
function handleEvent(event: Event): void {
|
|
211
|
+
if (pickerOpen) { eventQueue.push(event); return; }
|
|
197
212
|
if (!sessionId) return;
|
|
198
213
|
const evType = (event as any).type as string;
|
|
199
214
|
const props = (event as any).properties ?? {};
|
|
200
215
|
const sid = props.sessionID;
|
|
201
216
|
if (typeof sid === "string" && sid !== sessionId) return;
|
|
202
217
|
|
|
218
|
+
if (cancelledTurn) {
|
|
219
|
+
// Only let through what unblocks onSubmit. Clearing the flag earlier
|
|
220
|
+
// (e.g. on session.error) lets the trailing bash part.updated slip
|
|
221
|
+
// past and restart the spinner.
|
|
222
|
+
if (evType === "session.idle" || evType === "session.error") {
|
|
223
|
+
turnIdleSeen = true;
|
|
224
|
+
pendingTurnEnd?.();
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
203
229
|
switch (evType) {
|
|
204
230
|
// message.part.delta is undocumented in the SDK's Event union but
|
|
205
231
|
// the SSE consumer yields it. Drop chunks for unknown partIDs —
|
|
@@ -242,72 +268,141 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
242
268
|
case "question.asked": {
|
|
243
269
|
const req = props as QuestionRequest;
|
|
244
270
|
if (!runtime) break;
|
|
271
|
+
if (!compositor) {
|
|
272
|
+
runtime.client.question
|
|
273
|
+
.reject({ requestID: req.id, directory: sessionDirectory ?? undefined })
|
|
274
|
+
.catch(() => { /* best-effort */ });
|
|
275
|
+
bus.emit("ui:error", {
|
|
276
|
+
message: `opencode-bridge: rejected interactive question (no shell host): ${req.questions.map((q) => q.question).join("; ")}`,
|
|
277
|
+
});
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
pickerOpen = true;
|
|
245
281
|
const ui = createToolUI(bus, compositor.surface("agent"));
|
|
246
282
|
ui.custom(createQuestionSession(req.questions)).then(async (result: QuestionResult) => {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
283
|
+
try {
|
|
284
|
+
if (!runtime) return;
|
|
285
|
+
// Record the question + answer as a synthetic tool entry so the
|
|
286
|
+
// timeline shows what was asked and what the user picked.
|
|
287
|
+
const callID = `question-${req.id}`;
|
|
288
|
+
const detail = req.questions.length === 1
|
|
289
|
+
? req.questions[0]!.question
|
|
290
|
+
: req.questions.map((q, i) => `${q.header || `Q${i + 1}`}: ${q.question}`).join("; ");
|
|
291
|
+
bus.emit("agent:tool-started", {
|
|
292
|
+
title: "question",
|
|
293
|
+
toolCallId: callID,
|
|
294
|
+
kind: "execute",
|
|
295
|
+
displayDetail: detail,
|
|
296
|
+
});
|
|
297
|
+
if (result.cancelled) {
|
|
298
|
+
bus.emitTransform("agent:tool-completed", {
|
|
299
|
+
toolCallId: callID,
|
|
300
|
+
exitCode: 1,
|
|
301
|
+
rawOutput: "cancelled",
|
|
302
|
+
kind: "execute",
|
|
303
|
+
resultDisplay: { summary: "cancelled" },
|
|
304
|
+
});
|
|
305
|
+
runtime.client.question
|
|
306
|
+
.reject({ requestID: req.id, directory: sessionDirectory ?? undefined })
|
|
307
|
+
.catch(() => { /* best-effort */ });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const summary = result.answers.length === 1
|
|
311
|
+
? result.answers[0]!.join(", ")
|
|
312
|
+
: result.answers
|
|
313
|
+
.map((ans, i) => `${req.questions[i]!.header || `Q${i + 1}`}: ${ans.join(", ")}`)
|
|
314
|
+
.join("; ");
|
|
315
|
+
bus.emitTransform("agent:tool-completed", {
|
|
316
|
+
toolCallId: callID,
|
|
317
|
+
exitCode: 0,
|
|
318
|
+
rawOutput: summary,
|
|
319
|
+
kind: "execute",
|
|
320
|
+
resultDisplay: { summary },
|
|
321
|
+
});
|
|
322
|
+
try {
|
|
323
|
+
await runtime.client.question.reply({
|
|
324
|
+
requestID: req.id,
|
|
325
|
+
answers: result.answers,
|
|
326
|
+
directory: sessionDirectory ?? undefined,
|
|
327
|
+
});
|
|
328
|
+
} catch (err) {
|
|
329
|
+
bus.emit("agent:error", {
|
|
330
|
+
message: err instanceof Error ? err.message : String(err),
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
} finally {
|
|
334
|
+
pickerOpen = false;
|
|
335
|
+
if (result.cancelled) {
|
|
336
|
+
eventQueue.length = 0;
|
|
337
|
+
cancelledTurn = true;
|
|
338
|
+
pendingTurnEnd?.();
|
|
339
|
+
} else {
|
|
340
|
+
drainQueue();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
case "permission.asked": {
|
|
347
|
+
const req = props as PermissionRequest;
|
|
348
|
+
if (!runtime) break;
|
|
349
|
+
const detail = req.patterns.length > 0
|
|
350
|
+
? `${req.permission}: ${req.patterns.join(", ")}`
|
|
351
|
+
: req.permission;
|
|
352
|
+
const finish = (reply: "once" | "always" | "reject", opts?: { note?: string; skipReply?: boolean }) => {
|
|
353
|
+
if (reply === "reject") {
|
|
354
|
+
const callID = `permission-${req.id}`;
|
|
355
|
+
const summary = opts?.note ? `denied (${opts.note})` : `denied: ${detail}`;
|
|
356
|
+
bus.emit("agent:tool-started", {
|
|
357
|
+
title: "permission",
|
|
358
|
+
toolCallId: callID,
|
|
359
|
+
kind: "execute",
|
|
360
|
+
displayDetail: detail,
|
|
361
|
+
});
|
|
261
362
|
bus.emitTransform("agent:tool-completed", {
|
|
262
363
|
toolCallId: callID,
|
|
263
364
|
exitCode: 1,
|
|
264
|
-
rawOutput:
|
|
365
|
+
rawOutput: summary,
|
|
265
366
|
kind: "execute",
|
|
266
|
-
resultDisplay: { summary
|
|
367
|
+
resultDisplay: { summary },
|
|
267
368
|
});
|
|
268
|
-
runtime.client.question
|
|
269
|
-
.reject({ requestID: req.id, directory: sessionDirectory ?? undefined })
|
|
270
|
-
.catch(() => { /* best-effort */ });
|
|
271
|
-
return;
|
|
272
369
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
:
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
370
|
+
if (!runtime || opts?.skipReply) return;
|
|
371
|
+
runtime.client.permission
|
|
372
|
+
.reply({ requestID: req.id, reply, directory: sessionDirectory ?? undefined })
|
|
373
|
+
.catch((err) => {
|
|
374
|
+
bus.emit("agent:error", {
|
|
375
|
+
message: err instanceof Error ? err.message : String(err),
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
};
|
|
379
|
+
if (!compositor) {
|
|
380
|
+
finish("reject", { note: "no shell host" });
|
|
381
|
+
bus.emit("ui:error", {
|
|
382
|
+
message: `opencode-bridge: rejected permission (no shell host): ${detail}`,
|
|
284
383
|
});
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
pickerOpen = true;
|
|
387
|
+
const ui = createToolUI(bus, compositor.surface("agent"));
|
|
388
|
+
ui.custom(createPermissionSession(req, bus)).then((result: PermissionResult) => {
|
|
285
389
|
try {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
390
|
+
finish(result.reply, result.cancelled ? { skipReply: true } : undefined);
|
|
391
|
+
} finally {
|
|
392
|
+
pickerOpen = false;
|
|
393
|
+
if (result.cancelled) {
|
|
394
|
+
// Let onSubmit's finally emit the single agent:processing-done;
|
|
395
|
+
// a second one here races and stacks prompts.
|
|
396
|
+
eventQueue.length = 0;
|
|
397
|
+
cancelledTurn = true;
|
|
398
|
+
pendingTurnEnd?.();
|
|
399
|
+
} else {
|
|
400
|
+
drainQueue();
|
|
401
|
+
}
|
|
295
402
|
}
|
|
296
403
|
});
|
|
297
404
|
break;
|
|
298
405
|
}
|
|
299
|
-
// Without a reply the gated tool hangs forever. The bridge has no
|
|
300
|
-
// interactive approval UI, so auto-approve — mirrors claude-code-
|
|
301
|
-
// bridge's permissionMode: "acceptEdits". Set permission.edit:
|
|
302
|
-
// "allow" in opencode.json to skip the round-trip entirely.
|
|
303
|
-
case "permission.asked": {
|
|
304
|
-
const requestID = props.id as string | undefined;
|
|
305
|
-
if (!requestID || !runtime) break;
|
|
306
|
-
runtime.client.permission
|
|
307
|
-
.reply({ requestID, reply: "once", directory: sessionDirectory ?? undefined })
|
|
308
|
-
.catch(() => { /* approval is best-effort */ });
|
|
309
|
-
break;
|
|
310
|
-
}
|
|
311
406
|
}
|
|
312
407
|
}
|
|
313
408
|
|
|
@@ -339,6 +434,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
339
434
|
bus.emit("agent:query", { query: userQuery });
|
|
340
435
|
bus.emit("agent:processing-start", {});
|
|
341
436
|
turnText = "";
|
|
437
|
+
cancelledTurn = false;
|
|
342
438
|
turnIdleSeen = false;
|
|
343
439
|
turnError = null;
|
|
344
440
|
// Set the idle waiter BEFORE prompt() so a fast session.idle can't
|
|
@@ -429,7 +525,9 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
429
525
|
start: async () => {
|
|
430
526
|
try {
|
|
431
527
|
serverAbort = new AbortController();
|
|
432
|
-
|
|
528
|
+
// port: 0 dodges collision with SDK default 4096 (override via OPENCODE_SDK_PORT).
|
|
529
|
+
const port = process.env.OPENCODE_SDK_PORT ? Number(process.env.OPENCODE_SDK_PORT) : 0;
|
|
530
|
+
runtime = await createOpencode({ signal: serverAbort.signal, port });
|
|
433
531
|
|
|
434
532
|
streamAbort = new AbortController();
|
|
435
533
|
// Subscribe before creating the session so we don't miss early events.
|
|
@@ -469,6 +567,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
469
567
|
// ── Interactive question picker ──────────────────────────────────
|
|
470
568
|
|
|
471
569
|
type QuestionResult = { answers: string[][]; cancelled: boolean };
|
|
570
|
+
type PermissionResult = { reply: "once" | "always" | "reject"; cancelled?: boolean };
|
|
472
571
|
|
|
473
572
|
function isKey(data: string, key: string): boolean {
|
|
474
573
|
switch (key) {
|
|
@@ -599,3 +698,58 @@ function createQuestionSession(questions: QuestionInfo[]): InteractiveSession<Qu
|
|
|
599
698
|
},
|
|
600
699
|
};
|
|
601
700
|
}
|
|
701
|
+
|
|
702
|
+
function createPermissionSession(
|
|
703
|
+
req: PermissionRequest,
|
|
704
|
+
bus: { on: (e: "agent:cancel-request", fn: () => void) => void; off: (e: "agent:cancel-request", fn: () => void) => void },
|
|
705
|
+
): InteractiveSession<PermissionResult> {
|
|
706
|
+
let cancelHandler: (() => void) | null = null;
|
|
707
|
+
// Cast widens onMount: the vendored agent-sh type still declares the 1-arg signature.
|
|
708
|
+
const onMount = ((_invalidate: () => void, done: (r: PermissionResult) => void): void => {
|
|
709
|
+
cancelHandler = () => done({ reply: "reject", cancelled: true });
|
|
710
|
+
bus.on("agent:cancel-request", cancelHandler);
|
|
711
|
+
}) as InteractiveSession<PermissionResult>["onMount"];
|
|
712
|
+
return {
|
|
713
|
+
onMount,
|
|
714
|
+
onUnmount() {
|
|
715
|
+
if (cancelHandler) bus.off("agent:cancel-request", cancelHandler);
|
|
716
|
+
cancelHandler = null;
|
|
717
|
+
},
|
|
718
|
+
render(_width) {
|
|
719
|
+
const lines: string[] = [];
|
|
720
|
+
lines.push(` ${p.warning}${p.bold}Permission required: ${req.permission}${p.reset}`);
|
|
721
|
+
|
|
722
|
+
const meta = req.metadata ?? {};
|
|
723
|
+
const cmd = typeof meta.command === "string" ? meta.command : null;
|
|
724
|
+
const file = typeof meta.file === "string"
|
|
725
|
+
? meta.file
|
|
726
|
+
: typeof meta.path === "string" ? meta.path : null;
|
|
727
|
+
if (cmd) {
|
|
728
|
+
for (const line of cmd.split("\n").slice(0, 6)) {
|
|
729
|
+
lines.push(` ${p.dim}${line}${p.reset}`);
|
|
730
|
+
}
|
|
731
|
+
} else if (file) {
|
|
732
|
+
lines.push(` ${p.dim}${file}${p.reset}`);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (req.patterns.length > 0 && !cmd && !file) {
|
|
736
|
+
for (const pat of req.patterns) {
|
|
737
|
+
lines.push(` ${p.dim}${pat}${p.reset}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
lines.push(` ${p.dim}[y] allow once [a] allow always [n] reject${p.reset}`);
|
|
742
|
+
return lines;
|
|
743
|
+
},
|
|
744
|
+
|
|
745
|
+
handleInput(data, done) {
|
|
746
|
+
if (isKey(data, "escape") || data === "n" || data === "N") {
|
|
747
|
+
done({ reply: "reject" });
|
|
748
|
+
} else if (data === "y" || data === "Y" || isKey(data, "enter")) {
|
|
749
|
+
done({ reply: "once" });
|
|
750
|
+
} else if (data === "a" || data === "A") {
|
|
751
|
+
done({ reply: "always" });
|
|
752
|
+
}
|
|
753
|
+
},
|
|
754
|
+
};
|
|
755
|
+
}
|
|
@@ -272,14 +272,13 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
272
272
|
}
|
|
273
273
|
});
|
|
274
274
|
|
|
275
|
-
|
|
275
|
+
booting = false;
|
|
276
|
+
const m = session.model;
|
|
276
277
|
bus.emit("agent:info", {
|
|
277
278
|
name: "pi",
|
|
278
279
|
version: "0.66",
|
|
279
|
-
model:
|
|
280
|
+
model: m ? `${m.provider}/${m.id}` : undefined,
|
|
280
281
|
});
|
|
281
|
-
|
|
282
|
-
booting = false;
|
|
283
282
|
} catch (err) {
|
|
284
283
|
booting = false;
|
|
285
284
|
bus.emit("ui:error", {
|
|
@@ -25,14 +25,10 @@ function buildReasoningParams(level: string, _model?: string): Record<string, un
|
|
|
25
25
|
|
|
26
26
|
export default function activate(ctx: AgentContext): void {
|
|
27
27
|
const { key } = resolveApiKey(ID);
|
|
28
|
-
const apiKey = key ?? process.env.ZAI_API_KEY ?? process.env.ZHIPU_API_KEY;
|
|
29
|
-
if (!apiKey) return;
|
|
30
|
-
|
|
31
28
|
ctx.agent.providers.configure(ID, { reasoningParams: buildReasoningParams });
|
|
32
|
-
|
|
33
|
-
ctx.bus.emit("provider:register", {
|
|
29
|
+
ctx.agent.providers.register({
|
|
34
30
|
id: ID,
|
|
35
|
-
apiKey:
|
|
31
|
+
apiKey: key ?? process.env.ZAI_API_KEY ?? process.env.ZHIPU_API_KEY,
|
|
36
32
|
baseURL: BASE_URL,
|
|
37
33
|
defaultModel: DEFAULT_MODELS[0].id,
|
|
38
34
|
models: DEFAULT_MODELS,
|
package/package.json
CHANGED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ctx.llm facade — delegates to an `llm:invoke` handler registered by the
|
|
3
|
-
* active backend. No handler → `available` is false and calls reject.
|
|
4
|
-
*/
|
|
5
|
-
import type { LlmInterface } from "../agent/host-types.js";
|
|
6
|
-
interface HandlerGate {
|
|
7
|
-
list: () => string[];
|
|
8
|
-
call: (name: string, ...args: unknown[]) => unknown;
|
|
9
|
-
}
|
|
10
|
-
export declare function createLlmFacade(handlers: HandlerGate): LlmInterface;
|
|
11
|
-
export {};
|
|
File without changes
|