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.
- package/README.md +13 -2
- package/dist/agent/agent-loop.d.ts +3 -5
- package/dist/agent/agent-loop.js +44 -100
- package/dist/agent/conversation-state.d.ts +9 -0
- package/dist/agent/conversation-state.js +38 -1
- package/dist/agent/history-file.d.ts +6 -0
- package/dist/agent/history-file.js +1 -1
- package/dist/agent/host-types.d.ts +125 -0
- package/dist/agent/index.d.ts +12 -4
- package/dist/agent/index.js +357 -6
- package/dist/agent/nuclear-form.d.ts +7 -0
- package/dist/{extensions → agent}/providers/deepseek.d.ts +2 -2
- package/dist/{extensions → agent}/providers/deepseek.js +5 -4
- package/dist/{extensions → agent}/providers/openai-compatible.d.ts +2 -2
- package/dist/{extensions → agent}/providers/openai.d.ts +2 -2
- package/dist/{extensions → agent}/providers/openai.js +3 -2
- package/dist/{extensions → agent}/providers/openrouter.d.ts +2 -2
- package/dist/{extensions → agent}/providers/openrouter.js +4 -3
- package/dist/agent/skills.js +51 -7
- package/dist/agent/subagent.d.ts +1 -1
- package/dist/agent/system-prompt.js +14 -17
- package/dist/agent/tool-protocol.d.ts +1 -1
- package/dist/agent/tool-protocol.js +5 -3
- package/dist/agent/tool-registry.d.ts +9 -4
- package/dist/agent/tool-registry.js +27 -4
- package/dist/agent/tools/bash.d.ts +1 -1
- package/dist/agent/tools/bash.js +3 -2
- package/dist/agent/tools/edit-file.js +0 -1
- package/dist/agent/tools/glob.js +1 -1
- package/dist/agent/tools/grep.js +1 -1
- package/dist/agent/tools/pwsh.d.ts +1 -1
- package/dist/agent/tools/pwsh.js +1 -2
- package/dist/agent/tools/read-file.js +7 -4
- package/dist/agent/tools/write-file.js +0 -1
- package/dist/agent/types.d.ts +17 -2
- package/dist/cli/auth/cli.d.ts +1 -0
- package/dist/cli/auth/cli.js +216 -0
- package/dist/cli/auth/keys.d.ts +31 -0
- package/dist/cli/auth/keys.js +102 -0
- package/dist/{index.js → cli/index.js} +29 -32
- package/dist/{init.js → cli/init.js} +1 -1
- package/dist/{install.js → cli/install.js} +114 -5
- package/dist/cli/subcommands.d.ts +1 -0
- package/dist/cli/subcommands.js +17 -0
- package/dist/{event-bus.d.ts → core/event-bus.d.ts} +7 -13
- package/dist/{extension-loader.d.ts → core/extension-loader.d.ts} +1 -1
- package/dist/{extension-loader.js → core/extension-loader.js} +62 -70
- package/dist/{core.d.ts → core/index.d.ts} +18 -15
- package/dist/{core.js → core/index.js} +18 -92
- package/dist/{settings.d.ts → core/settings.d.ts} +7 -0
- package/dist/{settings.js → core/settings.js} +1 -0
- package/dist/core/types.d.ts +49 -0
- package/dist/core/types.js +1 -0
- package/dist/extensions/file-autocomplete.d.ts +1 -1
- package/dist/extensions/index.d.ts +7 -14
- package/dist/extensions/index.js +2 -19
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +7 -2
- package/dist/shell/host-types.d.ts +114 -0
- package/dist/shell/host-types.js +1 -0
- package/dist/shell/index.d.ts +8 -7
- package/dist/shell/index.js +58 -9
- package/dist/shell/input-handler.d.ts +7 -1
- package/dist/shell/input-handler.js +5 -2
- package/dist/shell/output-parser.d.ts +1 -1
- package/dist/{extensions → shell}/shell-context.d.ts +1 -1
- package/dist/{extensions → shell}/shell-context.js +18 -12
- package/dist/shell/shell.d.ts +6 -4
- package/dist/shell/shell.js +33 -109
- package/dist/shell/strategies/bash.d.ts +2 -0
- package/dist/shell/strategies/bash.js +68 -0
- package/dist/shell/strategies/fish.d.ts +2 -0
- package/dist/shell/strategies/fish.js +65 -0
- package/dist/shell/strategies/index.d.ts +13 -0
- package/dist/shell/strategies/index.js +17 -0
- package/dist/shell/strategies/types.d.ts +50 -0
- package/dist/shell/strategies/types.js +9 -0
- package/dist/shell/strategies/zsh.d.ts +2 -0
- package/dist/shell/strategies/zsh.js +72 -0
- package/dist/shell/tui-input-view.js +14 -3
- package/dist/{extensions → shell}/tui-renderer.d.ts +1 -1
- package/dist/{extensions → shell}/tui-renderer.js +27 -55
- package/dist/utils/box-frame.d.ts +4 -0
- package/dist/utils/box-frame.js +17 -6
- package/dist/utils/compositor.d.ts +1 -1
- package/dist/utils/compositor.js +2 -1
- package/dist/{executor.js → utils/executor.js} +1 -1
- package/dist/utils/floating-panel.d.ts +17 -5
- package/dist/utils/floating-panel.js +218 -70
- package/dist/utils/llm-facade.d.ts +7 -3
- package/dist/utils/stream-transform.d.ts +1 -1
- package/dist/utils/terminal-buffer.d.ts +1 -1
- package/dist/utils/tool-display.js +4 -0
- package/dist/utils/tool-interactive.d.ts +1 -1
- package/dist/utils/tty.d.ts +7 -0
- package/dist/utils/tty.js +15 -0
- package/examples/extensions/ash-acp-bridge/README.md +4 -1
- package/examples/extensions/ash-acp-bridge/src/index.ts +654 -0
- package/examples/extensions/ash-mcp-bridge/index.ts +1 -1
- package/examples/extensions/ashi/README.md +250 -0
- package/examples/extensions/ashi/package.json +60 -0
- package/examples/extensions/ashi/src/autocomplete.ts +91 -0
- package/examples/extensions/ashi/src/capture.ts +34 -0
- package/examples/extensions/ashi/src/cli.ts +126 -0
- package/examples/extensions/ashi/src/commands.ts +82 -0
- package/examples/extensions/ashi/src/compaction.ts +157 -0
- package/examples/extensions/ashi/src/components.ts +332 -0
- package/examples/extensions/ashi/src/default-renderers.ts +153 -0
- package/examples/extensions/ashi/src/display-config.ts +62 -0
- package/examples/extensions/ashi/src/frontend.ts +735 -0
- package/examples/extensions/ashi/src/hooks.ts +136 -0
- package/examples/extensions/ashi/src/multi-session-store.ts +146 -0
- package/examples/extensions/ashi/src/session-commands.ts +76 -0
- package/examples/extensions/ashi/src/session-store.ts +264 -0
- package/examples/extensions/ashi/src/status-footer.ts +66 -0
- package/examples/extensions/ashi/src/theme.ts +151 -0
- package/examples/extensions/ashi/tsconfig.json +14 -0
- package/examples/extensions/emacs-buffer.ts +364 -0
- package/examples/extensions/interactive-prompts.ts +114 -69
- package/examples/extensions/latex-images.ts +3 -3
- package/examples/extensions/opencode-bridge/index.ts +1 -1
- package/examples/extensions/overlay-agent.ts +35 -10
- package/examples/extensions/peer-mesh.ts +1 -1
- package/examples/extensions/pi-bridge/index.ts +0 -1
- package/examples/extensions/questionnaire.ts +2 -1
- package/examples/extensions/rtk-proxy.ts +3 -3
- package/examples/extensions/solarized-theme.ts +3 -3
- package/examples/extensions/subagents.ts +6 -6
- package/examples/extensions/terminal-buffer.ts +174 -33
- package/examples/extensions/tmux-pane.ts +6 -4
- package/examples/extensions/tunnel-vision.ts +405 -0
- package/examples/extensions/user-shell.ts +1 -1
- package/examples/extensions/web-access.ts +8 -113
- package/package.json +26 -22
- package/dist/extensions/agent-backend.d.ts +0 -14
- package/dist/extensions/agent-backend.js +0 -307
- package/dist/types.d.ts +0 -227
- /package/dist/{types.js → agent/host-types.js} +0 -0
- /package/dist/{extensions → agent}/providers/openai-compatible.js +0 -0
- /package/dist/{index.d.ts → cli/index.d.ts} +0 -0
- /package/dist/{init.d.ts → cli/init.d.ts} +0 -0
- /package/dist/{install.d.ts → cli/install.d.ts} +0 -0
- /package/dist/{event-bus.js → core/event-bus.js} +0 -0
- /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 ?? ""}`,
|