agent-sh 0.12.26 → 0.13.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.
Files changed (144) hide show
  1. package/README.md +13 -2
  2. package/dist/agent/agent-loop.d.ts +3 -5
  3. package/dist/agent/agent-loop.js +44 -100
  4. package/dist/agent/conversation-state.d.ts +9 -0
  5. package/dist/agent/conversation-state.js +38 -1
  6. package/dist/agent/history-file.d.ts +6 -0
  7. package/dist/agent/history-file.js +1 -1
  8. package/dist/agent/host-types.d.ts +125 -0
  9. package/dist/agent/index.d.ts +12 -4
  10. package/dist/agent/index.js +357 -6
  11. package/dist/agent/nuclear-form.d.ts +7 -0
  12. package/dist/{extensions → agent}/providers/deepseek.d.ts +2 -2
  13. package/dist/{extensions → agent}/providers/deepseek.js +5 -4
  14. package/dist/{extensions → agent}/providers/openai-compatible.d.ts +2 -2
  15. package/dist/{extensions → agent}/providers/openai.d.ts +2 -2
  16. package/dist/{extensions → agent}/providers/openai.js +3 -2
  17. package/dist/{extensions → agent}/providers/openrouter.d.ts +2 -2
  18. package/dist/{extensions → agent}/providers/openrouter.js +4 -3
  19. package/dist/agent/skills.js +51 -7
  20. package/dist/agent/subagent.d.ts +1 -1
  21. package/dist/agent/system-prompt.js +14 -17
  22. package/dist/agent/tool-protocol.d.ts +1 -1
  23. package/dist/agent/tool-protocol.js +5 -3
  24. package/dist/agent/tool-registry.d.ts +9 -4
  25. package/dist/agent/tool-registry.js +27 -4
  26. package/dist/agent/tools/bash.d.ts +1 -1
  27. package/dist/agent/tools/bash.js +3 -2
  28. package/dist/agent/tools/edit-file.js +0 -1
  29. package/dist/agent/tools/glob.js +1 -1
  30. package/dist/agent/tools/grep.js +1 -1
  31. package/dist/agent/tools/pwsh.d.ts +1 -1
  32. package/dist/agent/tools/pwsh.js +1 -2
  33. package/dist/agent/tools/read-file.js +7 -4
  34. package/dist/agent/tools/write-file.js +0 -1
  35. package/dist/agent/types.d.ts +17 -2
  36. package/dist/cli/auth/cli.d.ts +1 -0
  37. package/dist/cli/auth/cli.js +216 -0
  38. package/dist/cli/auth/keys.d.ts +31 -0
  39. package/dist/cli/auth/keys.js +102 -0
  40. package/dist/{index.js → cli/index.js} +29 -32
  41. package/dist/{init.js → cli/init.js} +1 -1
  42. package/dist/{install.js → cli/install.js} +114 -5
  43. package/dist/cli/subcommands.d.ts +1 -0
  44. package/dist/cli/subcommands.js +17 -0
  45. package/dist/{event-bus.d.ts → core/event-bus.d.ts} +7 -13
  46. package/dist/{extension-loader.d.ts → core/extension-loader.d.ts} +1 -1
  47. package/dist/{extension-loader.js → core/extension-loader.js} +62 -70
  48. package/dist/{core.d.ts → core/index.d.ts} +18 -15
  49. package/dist/{core.js → core/index.js} +18 -92
  50. package/dist/{settings.d.ts → core/settings.d.ts} +7 -0
  51. package/dist/{settings.js → core/settings.js} +1 -0
  52. package/dist/core/types.d.ts +49 -0
  53. package/dist/core/types.js +1 -0
  54. package/dist/extensions/file-autocomplete.d.ts +1 -1
  55. package/dist/extensions/index.d.ts +7 -14
  56. package/dist/extensions/index.js +2 -19
  57. package/dist/extensions/slash-commands.d.ts +1 -1
  58. package/dist/extensions/slash-commands.js +7 -2
  59. package/dist/shell/host-types.d.ts +114 -0
  60. package/dist/shell/host-types.js +1 -0
  61. package/dist/shell/index.d.ts +8 -7
  62. package/dist/shell/index.js +58 -9
  63. package/dist/shell/input-handler.d.ts +7 -1
  64. package/dist/shell/input-handler.js +5 -2
  65. package/dist/shell/output-parser.d.ts +1 -1
  66. package/dist/{extensions → shell}/shell-context.d.ts +1 -1
  67. package/dist/{extensions → shell}/shell-context.js +18 -12
  68. package/dist/shell/shell.d.ts +6 -4
  69. package/dist/shell/shell.js +33 -109
  70. package/dist/shell/strategies/bash.d.ts +2 -0
  71. package/dist/shell/strategies/bash.js +68 -0
  72. package/dist/shell/strategies/fish.d.ts +2 -0
  73. package/dist/shell/strategies/fish.js +65 -0
  74. package/dist/shell/strategies/index.d.ts +13 -0
  75. package/dist/shell/strategies/index.js +17 -0
  76. package/dist/shell/strategies/types.d.ts +50 -0
  77. package/dist/shell/strategies/types.js +9 -0
  78. package/dist/shell/strategies/zsh.d.ts +2 -0
  79. package/dist/shell/strategies/zsh.js +72 -0
  80. package/dist/shell/tui-input-view.js +14 -3
  81. package/dist/{extensions → shell}/tui-renderer.d.ts +1 -1
  82. package/dist/{extensions → shell}/tui-renderer.js +27 -55
  83. package/dist/utils/box-frame.d.ts +4 -0
  84. package/dist/utils/box-frame.js +17 -6
  85. package/dist/utils/compositor.d.ts +1 -1
  86. package/dist/utils/compositor.js +2 -1
  87. package/dist/{executor.js → utils/executor.js} +1 -1
  88. package/dist/utils/floating-panel.d.ts +17 -5
  89. package/dist/utils/floating-panel.js +218 -70
  90. package/dist/utils/llm-facade.d.ts +7 -3
  91. package/dist/utils/stream-transform.d.ts +1 -1
  92. package/dist/utils/terminal-buffer.d.ts +1 -1
  93. package/dist/utils/tool-display.js +4 -0
  94. package/dist/utils/tool-interactive.d.ts +1 -1
  95. package/dist/utils/tty.d.ts +7 -0
  96. package/dist/utils/tty.js +15 -0
  97. package/examples/extensions/ash-acp-bridge/README.md +4 -1
  98. package/examples/extensions/ash-acp-bridge/src/index.ts +654 -0
  99. package/examples/extensions/ash-mcp-bridge/index.ts +1 -1
  100. package/examples/extensions/ashi/README.md +250 -0
  101. package/examples/extensions/ashi/package.json +60 -0
  102. package/examples/extensions/ashi/src/autocomplete.ts +91 -0
  103. package/examples/extensions/ashi/src/capture.ts +34 -0
  104. package/examples/extensions/ashi/src/cli.ts +126 -0
  105. package/examples/extensions/ashi/src/commands.ts +82 -0
  106. package/examples/extensions/ashi/src/compaction.ts +157 -0
  107. package/examples/extensions/ashi/src/components.ts +332 -0
  108. package/examples/extensions/ashi/src/default-renderers.ts +153 -0
  109. package/examples/extensions/ashi/src/display-config.ts +62 -0
  110. package/examples/extensions/ashi/src/frontend.ts +735 -0
  111. package/examples/extensions/ashi/src/hooks.ts +136 -0
  112. package/examples/extensions/ashi/src/multi-session-store.ts +146 -0
  113. package/examples/extensions/ashi/src/session-commands.ts +76 -0
  114. package/examples/extensions/ashi/src/session-store.ts +264 -0
  115. package/examples/extensions/ashi/src/status-footer.ts +66 -0
  116. package/examples/extensions/ashi/src/theme.ts +151 -0
  117. package/examples/extensions/ashi/tsconfig.json +14 -0
  118. package/examples/extensions/emacs-buffer.ts +364 -0
  119. package/examples/extensions/interactive-prompts.ts +114 -69
  120. package/examples/extensions/latex-images.ts +3 -3
  121. package/examples/extensions/opencode-bridge/index.ts +1 -1
  122. package/examples/extensions/overlay-agent.ts +35 -10
  123. package/examples/extensions/peer-mesh.ts +1 -1
  124. package/examples/extensions/pi-bridge/index.ts +0 -1
  125. package/examples/extensions/questionnaire.ts +2 -1
  126. package/examples/extensions/rtk-proxy.ts +3 -3
  127. package/examples/extensions/solarized-theme.ts +3 -3
  128. package/examples/extensions/subagents.ts +6 -6
  129. package/examples/extensions/terminal-buffer.ts +174 -33
  130. package/examples/extensions/tmux-pane.ts +6 -4
  131. package/examples/extensions/tunnel-vision.ts +405 -0
  132. package/examples/extensions/user-shell.ts +1 -1
  133. package/examples/extensions/web-access.ts +8 -113
  134. package/package.json +26 -22
  135. package/dist/extensions/agent-backend.d.ts +0 -14
  136. package/dist/extensions/agent-backend.js +0 -307
  137. package/dist/types.d.ts +0 -227
  138. /package/dist/{types.js → agent/host-types.js} +0 -0
  139. /package/dist/{extensions → agent}/providers/openai-compatible.js +0 -0
  140. /package/dist/{index.d.ts → cli/index.d.ts} +0 -0
  141. /package/dist/{init.d.ts → cli/init.d.ts} +0 -0
  142. /package/dist/{install.d.ts → cli/install.d.ts} +0 -0
  143. /package/dist/{event-bus.js → core/event-bus.js} +0 -0
  144. /package/dist/{executor.d.ts → utils/executor.d.ts} +0 -0
@@ -0,0 +1,654 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * agent-sh-acp — ACP (Agent Client Protocol) server wrapping agent-sh's
4
+ * headless core. Speaks JSON-RPC 2.0 over stdin/stdout so agent-shell
5
+ * (Emacs) can drive it as a backend.
6
+ *
7
+ * Usage:
8
+ * agent-sh-acp # uses settings from ~/.agent-sh/settings.json
9
+ * agent-sh-acp --model gpt-4o # override model
10
+ *
11
+ * In agent-shell (Emacs):
12
+ * (setq agent-shell-agentsh-acp-command '("agent-sh-acp"))
13
+ */
14
+ import { createCore, type AgentShellCore } from "agent-sh";
15
+ import { loadExtensions } from "agent-sh/extension-loader";
16
+ import { loadBuiltinExtensions } from "agent-sh/extensions";
17
+ import { activateAgent } from "agent-sh/agent";
18
+ import { getSettings } from "agent-sh/settings";
19
+ import type { ContentBlock } from "agent-sh/types";
20
+
21
+ // ── JSON-RPC types ──────────────────────────────────────────────────
22
+
23
+ interface JsonRpcRequest {
24
+ jsonrpc: "2.0";
25
+ method: string;
26
+ params?: Record<string, unknown>;
27
+ id?: number | string;
28
+ }
29
+
30
+ interface JsonRpcResponse {
31
+ jsonrpc: "2.0";
32
+ id: number | string;
33
+ result?: unknown;
34
+ error?: { code: number; message: string; data?: unknown };
35
+ }
36
+
37
+ interface JsonRpcNotification {
38
+ jsonrpc: "2.0";
39
+ method: string;
40
+ params?: Record<string, unknown>;
41
+ }
42
+
43
+ // ── ACP content block ───────────────────────────────────────────────
44
+
45
+ interface AcpContentBlock {
46
+ type: string;
47
+ text?: string;
48
+ data?: string;
49
+ mimeType?: string;
50
+ }
51
+
52
+ // ── Stdio transport ─────────────────────────────────────────────────
53
+
54
+ function send(msg: JsonRpcResponse | JsonRpcNotification): void {
55
+ const line = JSON.stringify(msg) + "\n";
56
+ process.stdout.write(line);
57
+ }
58
+
59
+ function sendResult(id: number | string, result: unknown): void {
60
+ send({ jsonrpc: "2.0", id, result });
61
+ }
62
+
63
+ function sendError(id: number | string, code: number, message: string, data?: unknown): void {
64
+ send({ jsonrpc: "2.0", id, error: { code, message, data } });
65
+ }
66
+
67
+ function sendNotification(method: string, params: Record<string, unknown>): void {
68
+ send({ jsonrpc: "2.0", method, params });
69
+ }
70
+
71
+ // ── ACP session/update helpers ──────────────────────────────────────
72
+
73
+ function sendSessionUpdate(update: Record<string, unknown>): void {
74
+ sendNotification("session/update", { update });
75
+ }
76
+
77
+ function sendTextChunk(text: string): void {
78
+ sendSessionUpdate({
79
+ sessionUpdate: "agent_message_chunk",
80
+ content: { type: "text", text },
81
+ });
82
+ }
83
+
84
+ function sendThinkingChunk(text: string): void {
85
+ sendSessionUpdate({
86
+ sessionUpdate: "agent_thought_chunk",
87
+ content: { type: "text", text },
88
+ });
89
+ }
90
+
91
+ function sendToolCall(
92
+ toolCallId: string,
93
+ title: string,
94
+ kind: string,
95
+ rawInput?: unknown,
96
+ locations?: { path: string; line?: number | null }[],
97
+ ): void {
98
+ const update: Record<string, unknown> = {
99
+ sessionUpdate: "tool_call",
100
+ toolCallId,
101
+ title,
102
+ status: "pending",
103
+ kind,
104
+ content: [],
105
+ rawInput,
106
+ };
107
+ if (locations && locations.length > 0) update.locations = locations;
108
+ sendSessionUpdate(update);
109
+ }
110
+
111
+ // ── Tool title enrichment ───────────────────────────────────────────
112
+ // ACP clients typically only render title + kind, so the bare tool name
113
+ // ("read_file") is unhelpful. Append path/command/pattern detail.
114
+
115
+ function shortenPath(p: string): string {
116
+ const cwd = process.cwd();
117
+ if (p.startsWith(cwd + "/")) return p.slice(cwd.length + 1);
118
+ const home = process.env.HOME;
119
+ if (home && p.startsWith(home + "/")) return "~/" + p.slice(home.length + 1);
120
+ return p;
121
+ }
122
+
123
+ function extractDetail(
124
+ displayDetail: string | undefined,
125
+ rawInput: unknown,
126
+ locations: { path: string; line?: number | null }[] | undefined,
127
+ ): string {
128
+ if (displayDetail) return displayDetail;
129
+ if (locations && locations.length > 0) {
130
+ const loc = locations.find((l) => l?.path) ?? locations[0]!;
131
+ const line = loc.line ? `:${loc.line}` : "";
132
+ return `${shortenPath(loc.path)}${line}`;
133
+ }
134
+ if (rawInput && typeof rawInput === "object") {
135
+ const raw = rawInput as Record<string, unknown>;
136
+ if (typeof raw.command === "string") return `$ ${raw.command}`;
137
+ if (typeof raw.pattern === "string") {
138
+ const target = typeof raw.path === "string" ? ` ${shortenPath(raw.path)}` : "";
139
+ return `${raw.pattern}${target}`;
140
+ }
141
+ if (typeof raw.path === "string") return shortenPath(raw.path);
142
+ if (typeof raw.file_path === "string") return shortenPath(raw.file_path);
143
+ if (typeof raw.url === "string") return raw.url;
144
+ if (typeof raw.query === "string") return `"${raw.query}"`;
145
+ }
146
+ return "";
147
+ }
148
+
149
+ function enrichTitle(
150
+ title: string,
151
+ displayDetail: string | undefined,
152
+ rawInput: unknown,
153
+ locations: { path: string; line?: number | null }[] | undefined,
154
+ ): string {
155
+ const detail = extractDetail(displayDetail, rawInput, locations);
156
+ if (!detail) return title;
157
+ if (title.includes(detail)) return title;
158
+ return `${title}: ${detail}`;
159
+ }
160
+
161
+ function sendToolCallUpdate(
162
+ toolCallId: string,
163
+ status: string,
164
+ content: AcpContentBlock[],
165
+ kind?: string,
166
+ ): void {
167
+ sendSessionUpdate({
168
+ sessionUpdate: "tool_call_update",
169
+ toolCallId,
170
+ status,
171
+ content,
172
+ kind,
173
+ });
174
+ }
175
+
176
+ function sendUsageUpdate(
177
+ inputTokens: number,
178
+ outputTokens: number,
179
+ ): void {
180
+ sendSessionUpdate({
181
+ sessionUpdate: "usage_update",
182
+ inputTokens,
183
+ outputTokens,
184
+ cacheCreationInputTokens: 0,
185
+ cacheReadInputTokens: 0,
186
+ });
187
+ }
188
+
189
+ // ── Permission bridge ───────────────────────────────────────────────
190
+
191
+ let nextPermissionId = 1;
192
+ const pendingPermissions = new Map<
193
+ number,
194
+ { resolve: (outcome: string) => void }
195
+ >();
196
+
197
+ function buildPermissionToolCall(
198
+ title: string,
199
+ kind: string,
200
+ metadata: Record<string, unknown>,
201
+ toolCallId: string,
202
+ ): { toolCall: Record<string, unknown> } {
203
+ const args = (metadata.args ?? {}) as Record<string, unknown>;
204
+
205
+ // Map agent-sh permission kinds → ACP tool call shapes
206
+ if (kind === "file-write") {
207
+ // File edit/write — send diff content block + rawInput for agent-shell
208
+ const content: unknown[] = [];
209
+ const rawInput: Record<string, unknown> = {};
210
+
211
+ // Set path for title display
212
+ const filePath = (args.path as string) ?? "";
213
+ rawInput.path = filePath;
214
+ rawInput.file_path = filePath;
215
+
216
+ // For edit_file: old_str/new_str so agent-shell can render a diff
217
+ if (typeof args.old_text === "string") {
218
+ rawInput.old_str = args.old_text;
219
+ rawInput.new_str = args.new_text ?? "";
220
+ content.push({
221
+ type: "diff",
222
+ oldText: args.old_text,
223
+ newText: args.new_text ?? "",
224
+ path: filePath,
225
+ });
226
+ } else if (typeof args.content === "string") {
227
+ // write_file (new file or full overwrite)
228
+ rawInput.new_str = args.content;
229
+ rawInput.old_str = "";
230
+ content.push({
231
+ type: "diff",
232
+ oldText: "",
233
+ newText: args.content,
234
+ path: filePath,
235
+ });
236
+ }
237
+
238
+ if (typeof args.description === "string") {
239
+ rawInput.description = args.description;
240
+ }
241
+
242
+ return {
243
+ toolCall: {
244
+ toolCallId,
245
+ title,
246
+ status: "pending",
247
+ kind: "diff",
248
+ content,
249
+ rawInput,
250
+ },
251
+ };
252
+ }
253
+
254
+ // Generic tool call (bash, etc.)
255
+ const rawInput: Record<string, unknown> = {};
256
+ if (typeof args.command === "string") {
257
+ rawInput.command = args.command;
258
+ }
259
+ if (typeof args.description === "string") {
260
+ rawInput.description = args.description;
261
+ }
262
+
263
+ return {
264
+ toolCall: {
265
+ toolCallId,
266
+ title,
267
+ status: "pending",
268
+ kind: kind === "tool-call" ? "execute" : kind,
269
+ content: [],
270
+ rawInput,
271
+ },
272
+ };
273
+ }
274
+
275
+ function requestPermission(
276
+ title: string,
277
+ kind: string,
278
+ metadata: Record<string, unknown>,
279
+ toolCallId?: string,
280
+ ): Promise<string> {
281
+ const id = nextPermissionId++;
282
+ const tcId = toolCallId ?? `perm-${id}`;
283
+ return new Promise((resolve) => {
284
+ pendingPermissions.set(id, { resolve });
285
+ const { toolCall } = buildPermissionToolCall(title, kind, metadata, tcId);
286
+ send({
287
+ jsonrpc: "2.0",
288
+ method: "session/request_permission",
289
+ id,
290
+ params: {
291
+ toolCall,
292
+ options: [
293
+ { id: "accepted", name: "Accept", description: "Accept this action" },
294
+ { id: "rejected", name: "Reject", description: "Reject this action" },
295
+ { id: "always", name: "Always allow", description: "Always allow for this session" },
296
+ ],
297
+ },
298
+ } as any);
299
+ });
300
+ }
301
+
302
+ // ── Core setup ──────────────────────────────────────────────────────
303
+
304
+ function parseArgs(): { model?: string; provider?: string } {
305
+ const args = process.argv.slice(2);
306
+ const result: Record<string, string> = {};
307
+ for (let i = 0; i < args.length; i++) {
308
+ if (args[i] === "--model" && args[i + 1]) result.model = args[++i];
309
+ if (args[i] === "--provider" && args[i + 1]) result.provider = args[++i];
310
+ }
311
+ return result;
312
+ }
313
+
314
+ const cliArgs = parseArgs();
315
+ let core: AgentShellCore | null = null;
316
+ let sessionId: string | null = null;
317
+ let sessionCwd: string = process.cwd();
318
+
319
+ // Track tool output chunks per toolCallId so we can send accumulated content
320
+ const toolOutputBuffers = new Map<string, string>();
321
+
322
+ // promptTurnInFlight binds the request id to the next turn that starts, so
323
+ // unsolicited turns (peer_send auto-wake, wakeups) don't satisfy it.
324
+ let activePromptRequestId: number | string | null = null;
325
+ let promptTurnInFlight = false;
326
+
327
+ // Track always-allowed permission kinds
328
+ const alwaysAllowed = new Set<string>();
329
+
330
+ // Track in-flight async operations so stdin end can wait
331
+ let pendingOp: Promise<void> = Promise.resolve();
332
+
333
+ // ── Wire agent-sh events → ACP notifications ───────────────────────
334
+
335
+ function wireEvents(core: AgentShellCore): void {
336
+ const { bus } = core;
337
+
338
+ bus.on("agent:response-chunk", ({ blocks }) => {
339
+ for (const block of blocks) {
340
+ if (block.type === "text") {
341
+ sendTextChunk(block.text);
342
+ }
343
+ // code-block blocks are sent as text (agent-shell renders markdown)
344
+ if (block.type === "code-block") {
345
+ sendTextChunk("```" + block.language + "\n" + block.code + "\n```");
346
+ }
347
+ }
348
+ });
349
+
350
+ bus.on("agent:thinking-chunk", ({ text }) => {
351
+ sendThinkingChunk(text);
352
+ });
353
+
354
+ bus.on("agent:tool-started", (e) => {
355
+ const id = e.toolCallId ?? `tool-${Date.now()}`;
356
+ toolOutputBuffers.set(id, "");
357
+ const title = enrichTitle(e.title, e.displayDetail, e.rawInput, e.locations);
358
+ sendToolCall(id, title, e.kind ?? "tool", e.rawInput, e.locations);
359
+ });
360
+
361
+ bus.on("agent:tool-output-chunk", ({ chunk }) => {
362
+ // Accumulate — we don't know toolCallId here, but only one tool runs at a time
363
+ // in sequential mode. For parallel tools this is best-effort.
364
+ for (const [id, buf] of toolOutputBuffers) {
365
+ toolOutputBuffers.set(id, buf + chunk);
366
+ }
367
+ });
368
+
369
+ bus.on("agent:tool-completed", (e) => {
370
+ const id = e.toolCallId ?? [...toolOutputBuffers.keys()].pop() ?? "unknown";
371
+ const output = toolOutputBuffers.get(id) ?? "";
372
+ toolOutputBuffers.delete(id);
373
+
374
+ const status = e.exitCode === 0 || e.exitCode === null ? "completed" : "failed";
375
+ const content: AcpContentBlock[] = output
376
+ ? [{ type: "text", text: output }]
377
+ : [];
378
+ sendToolCallUpdate(id, status, content, e.kind);
379
+ });
380
+
381
+ bus.on("agent:usage", ({ prompt_tokens, completion_tokens }) => {
382
+ sendUsageUpdate(prompt_tokens, completion_tokens);
383
+ });
384
+
385
+ bus.on("agent:processing-start", () => {
386
+ if (activePromptRequestId !== null && !promptTurnInFlight) {
387
+ promptTurnInFlight = true;
388
+ }
389
+ });
390
+
391
+ bus.on("agent:processing-done", () => {
392
+ if (promptTurnInFlight && activePromptRequestId !== null) {
393
+ sendResult(activePromptRequestId, { stopReason: "end_turn" });
394
+ activePromptRequestId = null;
395
+ promptTurnInFlight = false;
396
+ }
397
+ });
398
+
399
+ bus.on("agent:error", ({ message }) => {
400
+ if (promptTurnInFlight && activePromptRequestId !== null) {
401
+ sendError(activePromptRequestId, -32603, message);
402
+ activePromptRequestId = null;
403
+ promptTurnInFlight = false;
404
+ }
405
+ });
406
+
407
+ bus.on("agent:cancelled", () => {
408
+ if (promptTurnInFlight && activePromptRequestId !== null) {
409
+ sendResult(activePromptRequestId, { stopReason: "cancelled" });
410
+ activePromptRequestId = null;
411
+ promptTurnInFlight = false;
412
+ }
413
+ });
414
+
415
+ // Surface ui:error to stderr — extension load failures are otherwise silent.
416
+ bus.on("ui:error", ({ message }) => {
417
+ process.stderr.write(`[ash-acp-bridge] ${message}\n`);
418
+ });
419
+ }
420
+
421
+ // ── ACP method handlers ─────────────────────────────────────────────
422
+
423
+ function waitForModelsToSettle(
424
+ core: AgentShellCore,
425
+ quietMs: number,
426
+ maxMs: number,
427
+ ): Promise<void> {
428
+ return new Promise((resolve) => {
429
+ const start = Date.now();
430
+ let timer: NodeJS.Timeout;
431
+ const arm = () => {
432
+ clearTimeout(timer);
433
+ const remaining = maxMs - (Date.now() - start);
434
+ timer = setTimeout(done, Math.max(0, Math.min(quietMs, remaining)));
435
+ };
436
+ const done = () => {
437
+ core.bus.off("config:add-modes", arm);
438
+ resolve();
439
+ };
440
+ core.bus.on("config:add-modes", arm);
441
+ arm();
442
+ });
443
+ }
444
+
445
+ function getModelsPayload(): Record<string, unknown> | undefined {
446
+ if (!core) return undefined;
447
+ const info = core.bus.emitPipe("config:get-models", { models: [], active: null });
448
+ if (!info.models.length) return undefined;
449
+ return {
450
+ currentModelId: info.active?.model ?? info.models[0]?.model,
451
+ availableModels: info.models.map((m) => ({
452
+ modelId: m.model,
453
+ name: m.provider ? `${m.provider}/${m.model}` : m.model,
454
+ description: m.provider ? `Provider: ${m.provider}` : "",
455
+ })),
456
+ };
457
+ }
458
+
459
+ function handleInitialize(id: number | string): void {
460
+ sendResult(id, {
461
+ agentCapabilities: {
462
+ promptCapabilities: {
463
+ image: false,
464
+ embeddedContext: true,
465
+ },
466
+ sessionCapabilities: {},
467
+ },
468
+ modes: {
469
+ currentModeId: "default",
470
+ availableModes: [
471
+ { id: "default", name: "Default", description: "Standard mode" },
472
+ ],
473
+ },
474
+ });
475
+ }
476
+
477
+ async function handleSessionNew(id: number | string, params: Record<string, unknown>): Promise<void> {
478
+ sessionCwd = (params.cwd as string) ?? process.cwd();
479
+ process.chdir(sessionCwd);
480
+
481
+ // Create core lazily on first session
482
+ if (!core) {
483
+ core = createCore({
484
+ model: cliArgs.model,
485
+ provider: cliArgs.provider,
486
+ });
487
+ wireEvents(core);
488
+
489
+ const extCtx = core.extensionContext({ quit: () => process.exit(0) });
490
+ const settings = getSettings();
491
+
492
+ activateAgent(extCtx);
493
+ const headlessDisabled = ["file-autocomplete", ...(settings.disabledBuiltins ?? [])];
494
+ await loadBuiltinExtensions(extCtx, headlessDisabled);
495
+
496
+ // Load user extensions with a timeout (some may hang in headless mode)
497
+ const TIMEOUT_MS = 10000;
498
+ await Promise.race([
499
+ loadExtensions(extCtx),
500
+ new Promise<void>((_, reject) =>
501
+ setTimeout(() => reject(new Error(`Extension loading timeout after ${TIMEOUT_MS}ms`)), TIMEOUT_MS),
502
+ ),
503
+ ]).catch((err) => {
504
+ process.stderr.write(`Warning: ${err instanceof Error ? err.message : err}\n`);
505
+ });
506
+
507
+ // Signal deferred-init listeners (agent-backend) that the provider
508
+ // registry is complete — they resolve their LLM config on this event.
509
+ core.bus.emit("core:extensions-loaded", { names: [] });
510
+
511
+ core.activateBackend();
512
+
513
+ // Wait for async catalog registrations (e.g. openrouter's full list).
514
+ await waitForModelsToSettle(core, 300, 2500);
515
+ }
516
+
517
+ sessionId = `session-${Date.now()}`;
518
+ const result: Record<string, unknown> = {
519
+ sessionId,
520
+ modes: {
521
+ currentModeId: "default",
522
+ availableModes: [
523
+ { id: "default", name: "Default", description: "Standard mode" },
524
+ ],
525
+ },
526
+ };
527
+ const models = getModelsPayload();
528
+ if (models) result.models = models;
529
+ sendResult(id, result);
530
+ }
531
+
532
+ function handleSessionPrompt(id: number | string, params: Record<string, unknown>): void {
533
+ if (!core) {
534
+ sendError(id, -32603, "No active session");
535
+ return;
536
+ }
537
+
538
+ // Extract text from prompt content blocks
539
+ const prompt = params.prompt as Array<{ type: string; text?: string; resource?: { text?: string } }>;
540
+ const parts: string[] = [];
541
+ for (const block of prompt) {
542
+ if (block.type === "text" && block.text) {
543
+ parts.push(block.text);
544
+ } else if (block.type === "resource" && block.resource?.text) {
545
+ parts.push(block.resource.text);
546
+ }
547
+ }
548
+
549
+ const query = parts.join("\n");
550
+ if (!query) {
551
+ sendResult(id, { stopReason: "end_turn" });
552
+ return;
553
+ }
554
+
555
+ activePromptRequestId = id;
556
+ promptTurnInFlight = false;
557
+ core.bus.emit("agent:submit", { query });
558
+ }
559
+
560
+ function handleSessionSetMode(id: number | string, _params: Record<string, unknown>): void {
561
+ // Acknowledge — agent-sh doesn't have distinct modes yet
562
+ sendResult(id, {});
563
+ }
564
+
565
+ // ── Message dispatcher ──────────────────────────────────────────────
566
+
567
+ function dispatch(msg: JsonRpcRequest): void {
568
+ const { method, params, id } = msg;
569
+
570
+ // Handle responses to our outgoing requests (permission responses)
571
+ if (!method && id !== undefined && (msg as any).result !== undefined) {
572
+ const pending = pendingPermissions.get(id as number);
573
+ if (pending) {
574
+ pendingPermissions.delete(id as number);
575
+ const result = (msg as any).result;
576
+ const outcome = result?.outcome?.optionId ?? result?.outcome?.outcome ?? "rejected";
577
+ pending.resolve(outcome);
578
+ }
579
+ return;
580
+ }
581
+
582
+ if (!id && !method) return; // ignore malformed
583
+
584
+ switch (method) {
585
+ case "initialize":
586
+ handleInitialize(id!);
587
+ break;
588
+ case "session/new":
589
+ pendingOp = handleSessionNew(id!, params ?? {}).catch((err) => {
590
+ sendError(id!, -32603, err instanceof Error ? err.message : String(err));
591
+ });
592
+ break;
593
+ case "session/prompt":
594
+ handleSessionPrompt(id!, params ?? {});
595
+ break;
596
+ case "session/set_mode":
597
+ handleSessionSetMode(id!, params ?? {});
598
+ break;
599
+ case "session/set_model":
600
+ if (core && params?.modelId) {
601
+ core.bus.emit("config:switch-model", { model: params.modelId as string });
602
+ }
603
+ sendResult(id!, {
604
+ models: getModelsPayload() ?? {},
605
+ });
606
+ break;
607
+ case "session/cancel":
608
+ if (core) {
609
+ core.bus.emit("agent:cancel-request", {});
610
+ }
611
+ // Notification — no response needed
612
+ break;
613
+ default:
614
+ if (id !== undefined) {
615
+ sendError(id, -32601, `Method not found: ${method}`);
616
+ }
617
+ }
618
+ }
619
+
620
+ // ── Stdin line reader ───────────────────────────────────────────────
621
+
622
+ let buffer = "";
623
+
624
+ process.stdin.setEncoding("utf-8");
625
+ process.stdin.on("data", (chunk: string) => {
626
+ buffer += chunk;
627
+ let newlineIdx: number;
628
+ while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
629
+ const line = buffer.slice(0, newlineIdx).trim();
630
+ buffer = buffer.slice(newlineIdx + 1);
631
+ if (!line) continue;
632
+ try {
633
+ const msg = JSON.parse(line) as JsonRpcRequest;
634
+ dispatch(msg);
635
+ } catch {
636
+ // Skip malformed JSON
637
+ }
638
+ }
639
+ });
640
+
641
+ process.stdin.on("end", async () => {
642
+ // Wait for any in-flight async operations (e.g. session/new) to settle
643
+ await pendingOp;
644
+ core?.kill();
645
+ process.exit(0);
646
+ });
647
+
648
+ // Log unhandled rejections to stderr (don't crash, but don't swallow silently)
649
+ process.on("unhandledRejection", (err) => {
650
+ process.stderr.write(`[ash-acp-bridge] unhandled rejection: ${err instanceof Error ? err.message : err}\n`);
651
+ });
652
+
653
+ // Redirect stderr from agent-sh internals so it doesn't pollute the protocol
654
+ // (agent-shell reads stdout only; stderr goes to its log)
@@ -105,7 +105,7 @@ async function connectServer(
105
105
  const { tools } = await client.listTools();
106
106
  for (const tool of tools) {
107
107
  const toolName = `mcp_${name}_${tool.name}`;
108
- ctx.registerTool({
108
+ ctx.agent.registerTool({
109
109
  name: toolName,
110
110
  displayName: tool.name,
111
111
  description: `[${name}] ${tool.description ?? ""}`,