agent-sh 0.7.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 (86) hide show
  1. package/README.md +28 -33
  2. package/dist/agent/agent-loop.d.ts +31 -8
  3. package/dist/agent/agent-loop.js +277 -66
  4. package/dist/agent/conversation-state.d.ts +41 -9
  5. package/dist/agent/conversation-state.js +340 -17
  6. package/dist/agent/history-file.d.ts +36 -0
  7. package/dist/agent/history-file.js +167 -0
  8. package/dist/agent/nuclear-form.d.ts +41 -0
  9. package/dist/agent/nuclear-form.js +176 -0
  10. package/dist/agent/system-prompt.d.ts +4 -5
  11. package/dist/agent/system-prompt.js +16 -11
  12. package/dist/agent/token-budget.d.ts +13 -0
  13. package/dist/agent/token-budget.js +50 -0
  14. package/dist/agent/tool-protocol.d.ts +83 -0
  15. package/dist/agent/tool-protocol.js +386 -0
  16. package/dist/agent/tools/user-shell.js +4 -1
  17. package/dist/agent/types.d.ts +21 -1
  18. package/dist/context-manager.d.ts +0 -1
  19. package/dist/context-manager.js +5 -110
  20. package/dist/core.d.ts +7 -7
  21. package/dist/core.js +76 -180
  22. package/dist/event-bus.d.ts +40 -0
  23. package/dist/event-bus.js +20 -1
  24. package/dist/extension-loader.d.ts +5 -0
  25. package/dist/extension-loader.js +104 -17
  26. package/dist/extensions/agent-backend.d.ts +13 -0
  27. package/dist/extensions/agent-backend.js +167 -0
  28. package/dist/extensions/command-suggest.d.ts +3 -3
  29. package/dist/extensions/command-suggest.js +4 -3
  30. package/dist/extensions/index.d.ts +19 -0
  31. package/dist/extensions/index.js +25 -0
  32. package/dist/extensions/slash-commands.d.ts +1 -1
  33. package/dist/extensions/slash-commands.js +44 -1
  34. package/dist/extensions/terminal-buffer.d.ts +1 -1
  35. package/dist/extensions/terminal-buffer.js +22 -8
  36. package/dist/extensions/tui-renderer.js +177 -122
  37. package/dist/index.js +14 -20
  38. package/dist/settings.d.ts +25 -2
  39. package/dist/settings.js +25 -4
  40. package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +1 -1
  41. package/dist/{input-handler.js → shell/input-handler.js} +60 -43
  42. package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
  43. package/dist/{output-parser.js → shell/output-parser.js} +1 -1
  44. package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
  45. package/dist/{shell.js → shell/shell.js} +24 -6
  46. package/dist/types.d.ts +49 -32
  47. package/dist/utils/ansi.d.ts +10 -0
  48. package/dist/utils/ansi.js +27 -0
  49. package/dist/utils/compositor.d.ts +62 -0
  50. package/dist/utils/compositor.js +88 -0
  51. package/dist/utils/diff-renderer.js +92 -4
  52. package/dist/utils/floating-panel.d.ts +34 -3
  53. package/dist/utils/floating-panel.js +315 -82
  54. package/dist/utils/handler-registry.d.ts +26 -10
  55. package/dist/utils/handler-registry.js +52 -16
  56. package/dist/utils/line-editor.d.ts +32 -3
  57. package/dist/utils/line-editor.js +218 -36
  58. package/dist/utils/markdown.d.ts +1 -0
  59. package/dist/utils/markdown.js +4 -4
  60. package/dist/utils/message-utils.d.ts +35 -0
  61. package/dist/utils/message-utils.js +75 -0
  62. package/dist/utils/terminal-buffer.d.ts +9 -1
  63. package/dist/utils/terminal-buffer.js +31 -2
  64. package/dist/utils/tool-display.d.ts +1 -0
  65. package/dist/utils/tool-display.js +1 -1
  66. package/dist/utils/tool-interactive.d.ts +12 -0
  67. package/dist/utils/tool-interactive.js +53 -0
  68. package/examples/extensions/ash-acp-bridge/README.md +39 -0
  69. package/examples/extensions/ash-acp-bridge/package.json +23 -0
  70. package/examples/extensions/ash-acp-bridge/src/index.ts +571 -0
  71. package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
  72. package/examples/extensions/ash-mcp-bridge/README.md +72 -0
  73. package/examples/extensions/ash-mcp-bridge/index.ts +154 -0
  74. package/examples/extensions/ash-mcp-bridge/package.json +9 -0
  75. package/examples/extensions/claude-code-bridge/index.ts +77 -1
  76. package/examples/extensions/interactive-prompts.ts +82 -110
  77. package/examples/extensions/overlay-agent.ts +84 -38
  78. package/examples/extensions/peer-mesh.ts +450 -0
  79. package/examples/extensions/pi-bridge/index.ts +87 -2
  80. package/examples/extensions/questionnaire.ts +249 -0
  81. package/examples/extensions/tmux-pane.ts +307 -0
  82. package/examples/extensions/web-access.ts +327 -0
  83. package/package.json +9 -1
  84. package/dist/extensions/overlay-agent.d.ts +0 -11
  85. package/dist/extensions/overlay-agent.js +0 -43
  86. package/examples/extensions/terminal-buffer.ts +0 -184
@@ -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
+ }
@@ -0,0 +1,72 @@
1
+ # ash-mcp-bridge
2
+
3
+ Connects any MCP (Model Context Protocol) server to ash. Spawns servers as child processes over stdio, discovers their tools, and registers each as a native ash tool.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ cp -r examples/extensions/ash-mcp-bridge ~/.agent-sh/extensions/
9
+ cd ~/.agent-sh/extensions/ash-mcp-bridge && npm install
10
+ ```
11
+
12
+ ## Configuration
13
+
14
+ Add server definitions to `~/.agent-sh/settings.json`:
15
+
16
+ ```json
17
+ {
18
+ "mcp-bridge": {
19
+ "servers": {
20
+ "vision": {
21
+ "command": "npx",
22
+ "args": ["-y", "@z_ai/mcp-server"],
23
+ "env": {
24
+ "Z_AI_API_KEY": "your-key",
25
+ "Z_AI_MODE": "ZAI"
26
+ }
27
+ }
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ Each server entry:
34
+
35
+ | Field | Type | Description |
36
+ |-------|------|-------------|
37
+ | `command` | `string` | Executable to spawn (e.g. `npx`, `node`) |
38
+ | `args` | `string[]` | Command arguments |
39
+ | `env` | `Record<string, string>` | Extra environment variables (merged with `process.env`) |
40
+
41
+ ## How it works
42
+
43
+ On activation, the extension:
44
+
45
+ 1. Reads `mcp-bridge.servers` from settings
46
+ 2. Spawns each server as a child process with stdio transport
47
+ 3. Connects via the MCP SDK client
48
+ 4. Calls `listTools()` to discover available tools
49
+ 5. Registers each tool as `mcp_{server}_{tool}` (e.g. `mcp_vision_image_analysis`)
50
+
51
+ Tools are then available to the agent like any built-in tool.
52
+
53
+ ## Example: Z.AI Vision
54
+
55
+ ```json
56
+ {
57
+ "mcp-bridge": {
58
+ "servers": {
59
+ "vision": {
60
+ "command": "npx",
61
+ "args": ["-y", "@z_ai/mcp-server"],
62
+ "env": {
63
+ "Z_AI_API_KEY": "your-key",
64
+ "Z_AI_MODE": "ZAI"
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ This gives the agent access to tools like `mcp_vision_image_analysis`, `mcp_vision_ui_to_artifact`, `mcp_vision_extract_text_from_screenshot`, etc.