agent-sh 0.8.0 → 0.9.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 (74) hide show
  1. package/README.md +25 -34
  2. package/dist/agent/agent-loop.d.ts +29 -6
  3. package/dist/agent/agent-loop.js +177 -59
  4. package/dist/agent/conversation-state.d.ts +3 -1
  5. package/dist/agent/conversation-state.js +6 -2
  6. package/dist/agent/nuclear-form.js +5 -4
  7. package/dist/agent/system-prompt.d.ts +4 -5
  8. package/dist/agent/system-prompt.js +12 -28
  9. package/dist/{token-budget.js → agent/token-budget.js} +1 -1
  10. package/dist/agent/tool-protocol.d.ts +83 -0
  11. package/dist/agent/tool-protocol.js +386 -0
  12. package/dist/agent/types.d.ts +21 -1
  13. package/dist/core.d.ts +7 -7
  14. package/dist/core.js +76 -194
  15. package/dist/event-bus.d.ts +26 -0
  16. package/dist/event-bus.js +20 -1
  17. package/dist/extension-loader.d.ts +5 -0
  18. package/dist/extension-loader.js +104 -17
  19. package/dist/extensions/agent-backend.d.ts +13 -0
  20. package/dist/extensions/agent-backend.js +167 -0
  21. package/dist/extensions/command-suggest.d.ts +3 -3
  22. package/dist/extensions/command-suggest.js +4 -3
  23. package/dist/extensions/index.d.ts +19 -0
  24. package/dist/extensions/index.js +25 -0
  25. package/dist/extensions/slash-commands.d.ts +1 -1
  26. package/dist/extensions/slash-commands.js +16 -1
  27. package/dist/extensions/terminal-buffer.d.ts +1 -1
  28. package/dist/extensions/terminal-buffer.js +13 -4
  29. package/dist/extensions/tui-renderer.js +63 -43
  30. package/dist/index.js +14 -20
  31. package/dist/settings.d.ts +6 -0
  32. package/dist/settings.js +4 -1
  33. package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +1 -1
  34. package/dist/{input-handler.js → shell/input-handler.js} +60 -43
  35. package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
  36. package/dist/{output-parser.js → shell/output-parser.js} +1 -1
  37. package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
  38. package/dist/{shell.js → shell/shell.js} +20 -6
  39. package/dist/types.d.ts +49 -10
  40. package/dist/utils/compositor.d.ts +62 -0
  41. package/dist/utils/compositor.js +88 -0
  42. package/dist/utils/diff-renderer.js +92 -4
  43. package/dist/utils/floating-panel.d.ts +2 -0
  44. package/dist/utils/floating-panel.js +30 -14
  45. package/dist/utils/handler-registry.d.ts +26 -10
  46. package/dist/utils/handler-registry.js +52 -16
  47. package/dist/utils/line-editor.d.ts +23 -3
  48. package/dist/utils/line-editor.js +180 -42
  49. package/dist/utils/markdown.d.ts +1 -0
  50. package/dist/utils/markdown.js +1 -1
  51. package/dist/utils/message-utils.d.ts +35 -0
  52. package/dist/utils/message-utils.js +75 -0
  53. package/dist/utils/terminal-buffer.d.ts +5 -1
  54. package/dist/utils/terminal-buffer.js +18 -2
  55. package/dist/utils/tool-interactive.d.ts +12 -0
  56. package/dist/utils/tool-interactive.js +53 -0
  57. package/examples/extensions/ash-acp-bridge/README.md +39 -0
  58. package/examples/extensions/ash-acp-bridge/package.json +23 -0
  59. package/examples/extensions/ash-acp-bridge/src/index.ts +571 -0
  60. package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
  61. package/examples/extensions/ash-mcp-bridge/README.md +72 -0
  62. package/examples/extensions/ash-mcp-bridge/index.ts +154 -0
  63. package/examples/extensions/ash-mcp-bridge/package.json +9 -0
  64. package/examples/extensions/interactive-prompts.ts +82 -110
  65. package/examples/extensions/overlay-agent.ts +84 -38
  66. package/examples/extensions/peer-mesh.ts +450 -0
  67. package/examples/extensions/questionnaire.ts +249 -0
  68. package/examples/extensions/tmux-pane.ts +307 -0
  69. package/examples/extensions/web-access.ts +327 -0
  70. package/package.json +9 -1
  71. package/dist/extensions/overlay-agent.d.ts +0 -14
  72. package/dist/extensions/overlay-agent.js +0 -147
  73. package/examples/extensions/terminal-buffer.ts +0 -184
  74. /package/dist/{token-budget.d.ts → agent/token-budget.d.ts} +0 -0
@@ -0,0 +1,39 @@
1
+ # ash-acp-bridge
2
+
3
+ ACP (Agent Client Protocol) server that wraps agent-sh's headless core, allowing any ACP-compatible client to use ash as a backend.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ cd ash-acp-bridge
9
+ npm install
10
+ npm run build # or use `npx tsx src/index.ts` for dev
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```bash
16
+ ash-acp-bridge # use ~/.agent-sh/settings.json defaults
17
+ ash-acp-bridge --model gpt-4o # override model
18
+ ash-acp-bridge --provider anthropic # override provider
19
+ ```
20
+
21
+ ## How it works
22
+
23
+ ```
24
+ ACP client
25
+ ↕ JSON-RPC over stdin/stdout (ACP)
26
+ ash-acp-bridge
27
+ ↕ EventBus
28
+ agent-sh core (headless)
29
+ ↕ OpenAI-compatible API
30
+ LLM provider
31
+ ```
32
+
33
+ The adapter translates between ACP methods and agent-sh's event bus:
34
+
35
+ - `initialize` → return capabilities
36
+ - `session/new` → create core, set cwd
37
+ - `session/prompt` → `agent:submit` event
38
+ - `session/update` notifications ← `agent:response-chunk`, `agent:tool-started`, etc.
39
+ - `session/request_permission` ↔ `permission:request` async pipe
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "ash-acp-bridge",
3
+ "version": "0.1.0",
4
+ "description": "ACP server that wraps agent-sh's headless core for any ACP-compatible client",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "ash-acp-bridge": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "dev": "tsx src/index.ts",
12
+ "build": "tsc",
13
+ "start": "node dist/index.js"
14
+ },
15
+ "dependencies": {
16
+ "agent-sh": "file:../../..",
17
+ "tsx": "^4.19.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^22.0.0",
21
+ "typescript": "^5.7.0"
22
+ }
23
+ }
@@ -0,0 +1,571 @@
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 { getSettings } from "agent-sh/settings";
18
+ import type { ContentBlock } from "agent-sh/types";
19
+
20
+ // ── JSON-RPC types ──────────────────────────────────────────────────
21
+
22
+ interface JsonRpcRequest {
23
+ jsonrpc: "2.0";
24
+ method: string;
25
+ params?: Record<string, unknown>;
26
+ id?: number | string;
27
+ }
28
+
29
+ interface JsonRpcResponse {
30
+ jsonrpc: "2.0";
31
+ id: number | string;
32
+ result?: unknown;
33
+ error?: { code: number; message: string; data?: unknown };
34
+ }
35
+
36
+ interface JsonRpcNotification {
37
+ jsonrpc: "2.0";
38
+ method: string;
39
+ params?: Record<string, unknown>;
40
+ }
41
+
42
+ // ── ACP content block ───────────────────────────────────────────────
43
+
44
+ interface AcpContentBlock {
45
+ type: string;
46
+ text?: string;
47
+ data?: string;
48
+ mimeType?: string;
49
+ }
50
+
51
+ // ── Stdio transport ─────────────────────────────────────────────────
52
+
53
+ function send(msg: JsonRpcResponse | JsonRpcNotification): void {
54
+ const line = JSON.stringify(msg) + "\n";
55
+ process.stdout.write(line);
56
+ }
57
+
58
+ function sendResult(id: number | string, result: unknown): void {
59
+ send({ jsonrpc: "2.0", id, result });
60
+ }
61
+
62
+ function sendError(id: number | string, code: number, message: string, data?: unknown): void {
63
+ send({ jsonrpc: "2.0", id, error: { code, message, data } });
64
+ }
65
+
66
+ function sendNotification(method: string, params: Record<string, unknown>): void {
67
+ send({ jsonrpc: "2.0", method, params });
68
+ }
69
+
70
+ // ── ACP session/update helpers ──────────────────────────────────────
71
+
72
+ function sendSessionUpdate(update: Record<string, unknown>): void {
73
+ sendNotification("session/update", { update });
74
+ }
75
+
76
+ function sendTextChunk(text: string): void {
77
+ sendSessionUpdate({
78
+ sessionUpdate: "agent_message_chunk",
79
+ content: { type: "text", text },
80
+ });
81
+ }
82
+
83
+ function sendThinkingChunk(text: string): void {
84
+ sendSessionUpdate({
85
+ sessionUpdate: "agent_thought_chunk",
86
+ content: { type: "text", text },
87
+ });
88
+ }
89
+
90
+ function sendToolCall(
91
+ toolCallId: string,
92
+ title: string,
93
+ kind: string,
94
+ rawInput?: unknown,
95
+ ): void {
96
+ sendSessionUpdate({
97
+ sessionUpdate: "tool_call",
98
+ toolCallId,
99
+ title,
100
+ status: "pending",
101
+ kind,
102
+ content: [],
103
+ rawInput,
104
+ });
105
+ }
106
+
107
+ function sendToolCallUpdate(
108
+ toolCallId: string,
109
+ status: string,
110
+ content: AcpContentBlock[],
111
+ kind?: string,
112
+ ): void {
113
+ sendSessionUpdate({
114
+ sessionUpdate: "tool_call_update",
115
+ toolCallId,
116
+ status,
117
+ content,
118
+ kind,
119
+ });
120
+ }
121
+
122
+ function sendUsageUpdate(
123
+ inputTokens: number,
124
+ outputTokens: number,
125
+ ): void {
126
+ sendSessionUpdate({
127
+ sessionUpdate: "usage_update",
128
+ inputTokens,
129
+ outputTokens,
130
+ cacheCreationInputTokens: 0,
131
+ cacheReadInputTokens: 0,
132
+ });
133
+ }
134
+
135
+ // ── Permission bridge ───────────────────────────────────────────────
136
+
137
+ let nextPermissionId = 1;
138
+ const pendingPermissions = new Map<
139
+ number,
140
+ { resolve: (outcome: string) => void }
141
+ >();
142
+
143
+ function buildPermissionToolCall(
144
+ title: string,
145
+ kind: string,
146
+ metadata: Record<string, unknown>,
147
+ toolCallId: string,
148
+ ): { toolCall: Record<string, unknown> } {
149
+ const args = (metadata.args ?? {}) as Record<string, unknown>;
150
+
151
+ // Map agent-sh permission kinds → ACP tool call shapes
152
+ if (kind === "file-write") {
153
+ // File edit/write — send diff content block + rawInput for agent-shell
154
+ const content: unknown[] = [];
155
+ const rawInput: Record<string, unknown> = {};
156
+
157
+ // Set path for title display
158
+ const filePath = (args.path as string) ?? "";
159
+ rawInput.path = filePath;
160
+ rawInput.file_path = filePath;
161
+
162
+ // For edit_file: old_str/new_str so agent-shell can render a diff
163
+ if (typeof args.old_text === "string") {
164
+ rawInput.old_str = args.old_text;
165
+ rawInput.new_str = args.new_text ?? "";
166
+ content.push({
167
+ type: "diff",
168
+ oldText: args.old_text,
169
+ newText: args.new_text ?? "",
170
+ path: filePath,
171
+ });
172
+ } else if (typeof args.content === "string") {
173
+ // write_file (new file or full overwrite)
174
+ rawInput.new_str = args.content;
175
+ rawInput.old_str = "";
176
+ content.push({
177
+ type: "diff",
178
+ oldText: "",
179
+ newText: args.content,
180
+ path: filePath,
181
+ });
182
+ }
183
+
184
+ if (typeof args.description === "string") {
185
+ rawInput.description = args.description;
186
+ }
187
+
188
+ return {
189
+ toolCall: {
190
+ toolCallId,
191
+ title,
192
+ status: "pending",
193
+ kind: "diff",
194
+ content,
195
+ rawInput,
196
+ },
197
+ };
198
+ }
199
+
200
+ // Generic tool call (bash, etc.)
201
+ const rawInput: Record<string, unknown> = {};
202
+ if (typeof args.command === "string") {
203
+ rawInput.command = args.command;
204
+ }
205
+ if (typeof args.description === "string") {
206
+ rawInput.description = args.description;
207
+ }
208
+
209
+ return {
210
+ toolCall: {
211
+ toolCallId,
212
+ title,
213
+ status: "pending",
214
+ kind: kind === "tool-call" ? "execute" : kind,
215
+ content: [],
216
+ rawInput,
217
+ },
218
+ };
219
+ }
220
+
221
+ function requestPermission(
222
+ title: string,
223
+ kind: string,
224
+ metadata: Record<string, unknown>,
225
+ toolCallId?: string,
226
+ ): Promise<string> {
227
+ const id = nextPermissionId++;
228
+ const tcId = toolCallId ?? `perm-${id}`;
229
+ return new Promise((resolve) => {
230
+ pendingPermissions.set(id, { resolve });
231
+ const { toolCall } = buildPermissionToolCall(title, kind, metadata, tcId);
232
+ send({
233
+ jsonrpc: "2.0",
234
+ method: "session/request_permission",
235
+ id,
236
+ params: {
237
+ toolCall,
238
+ options: [
239
+ { id: "accepted", name: "Accept", description: "Accept this action" },
240
+ { id: "rejected", name: "Reject", description: "Reject this action" },
241
+ { id: "always", name: "Always allow", description: "Always allow for this session" },
242
+ ],
243
+ },
244
+ } as any);
245
+ });
246
+ }
247
+
248
+ // ── Core setup ──────────────────────────────────────────────────────
249
+
250
+ function parseArgs(): { model?: string; provider?: string } {
251
+ const args = process.argv.slice(2);
252
+ const result: Record<string, string> = {};
253
+ for (let i = 0; i < args.length; i++) {
254
+ if (args[i] === "--model" && args[i + 1]) result.model = args[++i];
255
+ if (args[i] === "--provider" && args[i + 1]) result.provider = args[++i];
256
+ }
257
+ return result;
258
+ }
259
+
260
+ const cliArgs = parseArgs();
261
+ let core: AgentShellCore | null = null;
262
+ let sessionId: string | null = null;
263
+ let sessionCwd: string = process.cwd();
264
+
265
+ // Track tool output chunks per toolCallId so we can send accumulated content
266
+ const toolOutputBuffers = new Map<string, string>();
267
+
268
+ // Track the active prompt request id so we can respond when processing is done
269
+ let activePromptRequestId: number | string | null = null;
270
+
271
+ // Track always-allowed permission kinds
272
+ const alwaysAllowed = new Set<string>();
273
+
274
+ // Track in-flight async operations so stdin end can wait
275
+ let pendingOp: Promise<void> = Promise.resolve();
276
+
277
+ // ── Wire agent-sh events → ACP notifications ───────────────────────
278
+
279
+ function wireEvents(core: AgentShellCore): void {
280
+ const { bus } = core;
281
+
282
+ bus.on("agent:response-chunk", ({ blocks }) => {
283
+ for (const block of blocks) {
284
+ if (block.type === "text") {
285
+ sendTextChunk(block.text);
286
+ }
287
+ // code-block blocks are sent as text (agent-shell renders markdown)
288
+ if (block.type === "code-block") {
289
+ sendTextChunk("```" + block.language + "\n" + block.code + "\n```");
290
+ }
291
+ }
292
+ });
293
+
294
+ bus.on("agent:thinking-chunk", ({ text }) => {
295
+ sendThinkingChunk(text);
296
+ });
297
+
298
+ bus.on("agent:tool-started", (e) => {
299
+ const id = e.toolCallId ?? `tool-${Date.now()}`;
300
+ toolOutputBuffers.set(id, "");
301
+ sendToolCall(id, e.title, e.kind ?? "tool", e.rawInput);
302
+ });
303
+
304
+ bus.on("agent:tool-output-chunk", ({ chunk }) => {
305
+ // Accumulate — we don't know toolCallId here, but only one tool runs at a time
306
+ // in sequential mode. For parallel tools this is best-effort.
307
+ for (const [id, buf] of toolOutputBuffers) {
308
+ toolOutputBuffers.set(id, buf + chunk);
309
+ }
310
+ });
311
+
312
+ bus.on("agent:tool-completed", (e) => {
313
+ const id = e.toolCallId ?? [...toolOutputBuffers.keys()].pop() ?? "unknown";
314
+ const output = toolOutputBuffers.get(id) ?? "";
315
+ toolOutputBuffers.delete(id);
316
+
317
+ const status = e.exitCode === 0 || e.exitCode === null ? "completed" : "failed";
318
+ const content: AcpContentBlock[] = output
319
+ ? [{ type: "text", text: output }]
320
+ : [];
321
+ sendToolCallUpdate(id, status, content, e.kind);
322
+ });
323
+
324
+ bus.on("agent:usage", ({ prompt_tokens, completion_tokens }) => {
325
+ sendUsageUpdate(prompt_tokens, completion_tokens);
326
+ });
327
+
328
+ bus.on("agent:processing-done", () => {
329
+ if (activePromptRequestId !== null) {
330
+ sendResult(activePromptRequestId, { stopReason: "end_turn" });
331
+ activePromptRequestId = null;
332
+ }
333
+ });
334
+
335
+ bus.on("agent:error", ({ message }) => {
336
+ if (activePromptRequestId !== null) {
337
+ sendError(activePromptRequestId, -32603, message);
338
+ activePromptRequestId = null;
339
+ }
340
+ });
341
+
342
+ bus.on("agent:cancelled", () => {
343
+ if (activePromptRequestId !== null) {
344
+ sendResult(activePromptRequestId, { stopReason: "cancelled" });
345
+ activePromptRequestId = null;
346
+ }
347
+ });
348
+
349
+ // Permission gating — auto-approve all tool calls.
350
+ // agent-sh's built-in tools handle their own safety; the ACP layer
351
+ // doesn't add a second permission gate. If you want to bridge
352
+ // permissions to agent-shell's UI, replace this with the
353
+ // requestPermission() flow.
354
+ bus.onPipeAsync("permission:request", async (payload) => {
355
+ payload.decision = { outcome: "approved" };
356
+ return payload;
357
+ });
358
+ }
359
+
360
+ // ── ACP method handlers ─────────────────────────────────────────────
361
+
362
+ function getModelsPayload(): Record<string, unknown> | undefined {
363
+ if (!core) return undefined;
364
+ const info = core.bus.emitPipe("config:get-models", { models: [], active: null });
365
+ if (!info.models.length) return undefined;
366
+ return {
367
+ currentModelId: info.active ?? info.models[0]?.model,
368
+ availableModels: info.models.map((m) => ({
369
+ modelId: m.model,
370
+ name: m.provider ? `${m.provider}/${m.model}` : m.model,
371
+ description: m.provider ? `Provider: ${m.provider}` : "",
372
+ })),
373
+ };
374
+ }
375
+
376
+ function handleInitialize(id: number | string): void {
377
+ sendResult(id, {
378
+ agentCapabilities: {
379
+ promptCapabilities: {
380
+ image: false,
381
+ embeddedContext: true,
382
+ },
383
+ sessionCapabilities: {},
384
+ },
385
+ modes: {
386
+ currentModeId: "default",
387
+ availableModes: [
388
+ { id: "default", name: "Default", description: "Standard mode" },
389
+ ],
390
+ },
391
+ });
392
+ }
393
+
394
+ async function handleSessionNew(id: number | string, params: Record<string, unknown>): Promise<void> {
395
+ sessionCwd = (params.cwd as string) ?? process.cwd();
396
+ process.chdir(sessionCwd);
397
+
398
+ // Create core lazily on first session
399
+ if (!core) {
400
+ core = createCore({
401
+ model: cliArgs.model,
402
+ provider: cliArgs.provider,
403
+ });
404
+ wireEvents(core);
405
+
406
+ const extCtx = core.extensionContext({ quit: () => process.exit(0) });
407
+ const settings = getSettings();
408
+
409
+ // Load built-in extensions first (agent-backend, slash-commands, etc.)
410
+ // Skip TUI-only extensions that don't apply in headless mode
411
+ const headlessDisabled = [
412
+ "tui-renderer",
413
+ "file-autocomplete",
414
+ "terminal-buffer",
415
+ "overlay-agent",
416
+ ...(settings.disabledBuiltins ?? []),
417
+ ];
418
+ await loadBuiltinExtensions(extCtx, headlessDisabled);
419
+
420
+ // Load user extensions with a timeout (some may hang in headless mode)
421
+ const TIMEOUT_MS = 10000;
422
+ await Promise.race([
423
+ loadExtensions(extCtx),
424
+ new Promise<void>((_, reject) =>
425
+ setTimeout(() => reject(new Error(`Extension loading timeout after ${TIMEOUT_MS}ms`)), TIMEOUT_MS),
426
+ ),
427
+ ]).catch((err) => {
428
+ process.stderr.write(`Warning: ${err instanceof Error ? err.message : err}\n`);
429
+ });
430
+
431
+ core.activateBackend();
432
+ }
433
+
434
+ sessionId = `session-${Date.now()}`;
435
+ const result: Record<string, unknown> = {
436
+ sessionId,
437
+ modes: {
438
+ currentModeId: "default",
439
+ availableModes: [
440
+ { id: "default", name: "Default", description: "Standard mode" },
441
+ ],
442
+ },
443
+ };
444
+ const models = getModelsPayload();
445
+ if (models) result.models = models;
446
+ sendResult(id, result);
447
+ }
448
+
449
+ function handleSessionPrompt(id: number | string, params: Record<string, unknown>): void {
450
+ if (!core) {
451
+ sendError(id, -32603, "No active session");
452
+ return;
453
+ }
454
+
455
+ // Extract text from prompt content blocks
456
+ const prompt = params.prompt as Array<{ type: string; text?: string; resource?: { text?: string } }>;
457
+ const parts: string[] = [];
458
+ for (const block of prompt) {
459
+ if (block.type === "text" && block.text) {
460
+ parts.push(block.text);
461
+ } else if (block.type === "resource" && block.resource?.text) {
462
+ parts.push(block.resource.text);
463
+ }
464
+ }
465
+
466
+ const query = parts.join("\n");
467
+ if (!query) {
468
+ sendResult(id, { stopReason: "end_turn" });
469
+ return;
470
+ }
471
+
472
+ // Store the request id — we'll respond when agent:processing-done fires
473
+ activePromptRequestId = id;
474
+ core.bus.emit("agent:submit", { query });
475
+ }
476
+
477
+ function handleSessionSetMode(id: number | string, _params: Record<string, unknown>): void {
478
+ // Acknowledge — agent-sh doesn't have distinct modes yet
479
+ sendResult(id, {});
480
+ }
481
+
482
+ // ── Message dispatcher ──────────────────────────────────────────────
483
+
484
+ function dispatch(msg: JsonRpcRequest): void {
485
+ const { method, params, id } = msg;
486
+
487
+ // Handle responses to our outgoing requests (permission responses)
488
+ if (!method && id !== undefined && (msg as any).result !== undefined) {
489
+ const pending = pendingPermissions.get(id as number);
490
+ if (pending) {
491
+ pendingPermissions.delete(id as number);
492
+ const result = (msg as any).result;
493
+ const outcome = result?.outcome?.optionId ?? result?.outcome?.outcome ?? "rejected";
494
+ pending.resolve(outcome);
495
+ }
496
+ return;
497
+ }
498
+
499
+ if (!id && !method) return; // ignore malformed
500
+
501
+ switch (method) {
502
+ case "initialize":
503
+ handleInitialize(id!);
504
+ break;
505
+ case "session/new":
506
+ pendingOp = handleSessionNew(id!, params ?? {}).catch((err) => {
507
+ sendError(id!, -32603, err instanceof Error ? err.message : String(err));
508
+ });
509
+ break;
510
+ case "session/prompt":
511
+ handleSessionPrompt(id!, params ?? {});
512
+ break;
513
+ case "session/set_mode":
514
+ handleSessionSetMode(id!, params ?? {});
515
+ break;
516
+ case "session/set_model":
517
+ if (core && params?.modelId) {
518
+ core.bus.emit("config:switch-model", { model: params.modelId as string });
519
+ }
520
+ sendResult(id!, {
521
+ models: getModelsPayload() ?? {},
522
+ });
523
+ break;
524
+ case "session/cancel":
525
+ if (core) {
526
+ core.bus.emit("agent:cancel-request", {});
527
+ }
528
+ // Notification — no response needed
529
+ break;
530
+ default:
531
+ if (id !== undefined) {
532
+ sendError(id, -32601, `Method not found: ${method}`);
533
+ }
534
+ }
535
+ }
536
+
537
+ // ── Stdin line reader ───────────────────────────────────────────────
538
+
539
+ let buffer = "";
540
+
541
+ process.stdin.setEncoding("utf-8");
542
+ process.stdin.on("data", (chunk: string) => {
543
+ buffer += chunk;
544
+ let newlineIdx: number;
545
+ while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
546
+ const line = buffer.slice(0, newlineIdx).trim();
547
+ buffer = buffer.slice(newlineIdx + 1);
548
+ if (!line) continue;
549
+ try {
550
+ const msg = JSON.parse(line) as JsonRpcRequest;
551
+ dispatch(msg);
552
+ } catch {
553
+ // Skip malformed JSON
554
+ }
555
+ }
556
+ });
557
+
558
+ process.stdin.on("end", async () => {
559
+ // Wait for any in-flight async operations (e.g. session/new) to settle
560
+ await pendingOp;
561
+ core?.kill();
562
+ process.exit(0);
563
+ });
564
+
565
+ // Log unhandled rejections to stderr (don't crash, but don't swallow silently)
566
+ process.on("unhandledRejection", (err) => {
567
+ process.stderr.write(`[ash-acp-bridge] unhandled rejection: ${err instanceof Error ? err.message : err}\n`);
568
+ });
569
+
570
+ // Redirect stderr from agent-sh internals so it doesn't pollute the protocol
571
+ // (agent-shell reads stdout only; stderr goes to its log)
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "declaration": true,
11
+ "skipLibCheck": true
12
+ },
13
+ "include": ["src"]
14
+ }