agent-sh 0.4.0 → 0.6.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 +37 -115
- package/dist/agent/agent-loop.d.ts +86 -0
- package/dist/agent/agent-loop.js +704 -0
- package/dist/agent/conversation-state.d.ts +27 -0
- package/dist/agent/conversation-state.js +59 -0
- package/dist/agent/index.d.ts +11 -0
- package/dist/agent/index.js +9 -0
- package/dist/agent/skills.d.ts +25 -0
- package/dist/agent/skills.js +186 -0
- package/dist/agent/subagent.d.ts +37 -0
- package/dist/agent/subagent.js +119 -0
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +103 -0
- package/dist/agent/tool-registry.d.ts +15 -0
- package/dist/agent/tool-registry.js +30 -0
- package/dist/agent/tools/bash.d.ts +7 -0
- package/dist/agent/tools/bash.js +71 -0
- package/dist/agent/tools/display.d.ts +13 -0
- package/dist/agent/tools/display.js +70 -0
- package/dist/agent/tools/edit-file.d.ts +2 -0
- package/dist/agent/tools/edit-file.js +148 -0
- package/dist/agent/tools/glob.d.ts +2 -0
- package/dist/agent/tools/glob.js +87 -0
- package/dist/agent/tools/grep.d.ts +2 -0
- package/dist/agent/tools/grep.js +168 -0
- package/dist/agent/tools/list-skills.d.ts +2 -0
- package/dist/agent/tools/list-skills.js +28 -0
- package/dist/agent/tools/ls.d.ts +2 -0
- package/dist/agent/tools/ls.js +72 -0
- package/dist/agent/tools/read-file.d.ts +10 -0
- package/dist/agent/tools/read-file.js +101 -0
- package/dist/agent/tools/user-shell.d.ts +13 -0
- package/dist/agent/tools/user-shell.js +84 -0
- package/dist/agent/tools/write-file.d.ts +2 -0
- package/dist/agent/tools/write-file.js +82 -0
- package/dist/agent/types.d.ts +78 -0
- package/dist/agent/types.js +1 -0
- package/dist/core.d.ts +22 -14
- package/dist/core.js +256 -36
- package/dist/event-bus.d.ts +98 -17
- package/dist/event-bus.js +10 -1
- package/dist/extension-loader.d.ts +1 -1
- package/dist/extension-loader.js +10 -1
- package/dist/extensions/command-suggest.d.ts +10 -0
- package/dist/extensions/command-suggest.js +41 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +161 -64
- package/dist/extensions/tui-renderer.js +426 -126
- package/dist/index.js +110 -129
- package/dist/input-handler.js +78 -9
- package/dist/output-parser.d.ts +7 -0
- package/dist/output-parser.js +27 -0
- package/dist/settings.d.ts +53 -2
- package/dist/settings.js +46 -3
- package/dist/shell.js +35 -28
- package/dist/types.d.ts +33 -6
- package/dist/utils/box-frame.d.ts +3 -1
- package/dist/utils/box-frame.js +12 -5
- package/dist/utils/diff.js +10 -0
- package/dist/utils/llm-client.d.ts +45 -0
- package/dist/utils/llm-client.js +60 -0
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +25 -3
- package/dist/utils/stream-transform.js +20 -47
- package/dist/utils/tool-display.d.ts +4 -0
- package/dist/utils/tool-display.js +35 -8
- package/examples/extensions/claude-code-bridge/README.md +35 -0
- package/examples/extensions/claude-code-bridge/index.ts +194 -0
- package/examples/extensions/claude-code-bridge/package.json +11 -0
- package/examples/extensions/openrouter.ts +87 -0
- package/examples/extensions/pi-bridge/README.md +35 -0
- package/examples/extensions/pi-bridge/index.ts +263 -0
- package/examples/extensions/pi-bridge/package.json +13 -0
- package/examples/extensions/secret-guard.ts +100 -0
- package/examples/extensions/subagents.ts +87 -0
- package/package.json +3 -5
- package/dist/acp-client.d.ts +0 -105
- package/dist/acp-client.js +0 -684
- package/dist/extensions/shell-exec.d.ts +0 -24
- package/dist/extensions/shell-exec.js +0 -188
- package/dist/mcp-server.d.ts +0 -13
- package/dist/mcp-server.js +0 -234
- package/examples/pi-agent-sh.ts +0 -166
package/dist/core.js
CHANGED
|
@@ -1,86 +1,306 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Core kernel — the minimum viable agent-sh.
|
|
3
3
|
*
|
|
4
|
-
* Wires up EventBus + ContextManager +
|
|
4
|
+
* Wires up EventBus + ContextManager + AgentBackend without any frontend.
|
|
5
5
|
* Consumers attach their own I/O (Shell, WebSocket, REST, tests) by
|
|
6
|
-
* subscribing to bus events
|
|
6
|
+
* subscribing to bus events.
|
|
7
7
|
*
|
|
8
|
-
* The
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* The default backend (AgentLoop) is created eagerly but wired lazily —
|
|
9
|
+
* extensions can register alternative backends via agent:register-backend
|
|
10
|
+
* before activateBackend() is called.
|
|
11
11
|
*
|
|
12
12
|
* Usage:
|
|
13
13
|
* import { createCore } from "agent-sh";
|
|
14
|
-
* const core = createCore({
|
|
15
|
-
* core.bus.on("agent:response-chunk", ({
|
|
16
|
-
*
|
|
17
|
-
* core.
|
|
14
|
+
* const core = createCore({ apiKey: "...", model: "gpt-4o" });
|
|
15
|
+
* core.bus.on("agent:response-chunk", ({ blocks }) => { ... });
|
|
16
|
+
* core.activateBackend();
|
|
17
|
+
* const response = await core.query("hello");
|
|
18
18
|
*/
|
|
19
19
|
import { EventBus } from "./event-bus.js";
|
|
20
20
|
import { ContextManager } from "./context-manager.js";
|
|
21
|
-
import {
|
|
21
|
+
import { AgentLoop } from "./agent/agent-loop.js";
|
|
22
|
+
import { LlmClient } from "./utils/llm-client.js";
|
|
22
23
|
import { setPalette } from "./utils/palette.js";
|
|
23
24
|
import * as streamTransform from "./utils/stream-transform.js";
|
|
24
25
|
import * as settingsMod from "./settings.js";
|
|
26
|
+
import { resolveProvider, getProviderNames } from "./settings.js";
|
|
25
27
|
import { HandlerRegistry } from "./utils/handler-registry.js";
|
|
26
28
|
// Re-export types that library consumers need
|
|
27
29
|
export { EventBus } from "./event-bus.js";
|
|
28
30
|
export { palette, setPalette, resetPalette } from "./utils/palette.js";
|
|
31
|
+
export { runSubagent } from "./agent/subagent.js";
|
|
32
|
+
export { LlmClient } from "./utils/llm-client.js";
|
|
29
33
|
export function createCore(config) {
|
|
30
34
|
const bus = new EventBus();
|
|
31
35
|
const handlers = new HandlerRegistry();
|
|
32
36
|
const contextManager = new ContextManager(bus);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
// ── Resolve provider ─────────────────────────────────────────
|
|
38
|
+
const settings = settingsMod.getSettings();
|
|
39
|
+
let activeProvider = null;
|
|
40
|
+
const providerRegistry = new Map();
|
|
41
|
+
for (const name of getProviderNames()) {
|
|
42
|
+
const p = resolveProvider(name);
|
|
43
|
+
if (p)
|
|
44
|
+
providerRegistry.set(name, p);
|
|
45
|
+
}
|
|
46
|
+
const providerName = config.provider ?? settings.defaultProvider;
|
|
47
|
+
if (providerName) {
|
|
48
|
+
activeProvider = providerRegistry.get(providerName) ?? null;
|
|
49
|
+
}
|
|
50
|
+
// Build flat modes list across all providers
|
|
51
|
+
const buildModes = () => {
|
|
52
|
+
const allModes = [];
|
|
53
|
+
for (const [id, p] of providerRegistry) {
|
|
54
|
+
if (!p.apiKey)
|
|
55
|
+
continue;
|
|
56
|
+
for (const model of p.models) {
|
|
57
|
+
const mc = p.modelCapabilities?.get(model);
|
|
58
|
+
allModes.push({
|
|
59
|
+
model,
|
|
60
|
+
provider: id,
|
|
61
|
+
providerConfig: { apiKey: p.apiKey, baseURL: p.baseURL },
|
|
62
|
+
contextWindow: mc?.contextWindow ?? p.contextWindow,
|
|
63
|
+
reasoning: mc?.reasoning,
|
|
64
|
+
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
65
|
+
});
|
|
44
66
|
}
|
|
45
|
-
|
|
46
|
-
|
|
67
|
+
}
|
|
68
|
+
return allModes;
|
|
69
|
+
};
|
|
70
|
+
const effectiveApiKey = config.apiKey ?? activeProvider?.apiKey;
|
|
71
|
+
const effectiveBaseURL = config.baseURL ?? activeProvider?.baseURL;
|
|
72
|
+
const effectiveModel = config.model ?? activeProvider?.defaultModel;
|
|
73
|
+
let modes = buildModes();
|
|
74
|
+
if (modes.length === 0 && effectiveApiKey && effectiveModel) {
|
|
75
|
+
modes = [{ model: effectiveModel }];
|
|
76
|
+
}
|
|
77
|
+
const initialModeIndex = Math.max(0, modes.findIndex((m) => m.model === effectiveModel && (!activeProvider || m.provider === activeProvider.id)));
|
|
78
|
+
// Shared LLM client — used by agent loop AND fast-path features
|
|
79
|
+
let llmClient = null;
|
|
80
|
+
if (effectiveApiKey) {
|
|
81
|
+
if (!effectiveModel) {
|
|
82
|
+
throw new Error("No model specified. Use --model or configure a provider with defaultModel in ~/.agent-sh/settings.json");
|
|
83
|
+
}
|
|
84
|
+
llmClient = new LlmClient({
|
|
85
|
+
apiKey: effectiveApiKey,
|
|
86
|
+
baseURL: effectiveBaseURL,
|
|
87
|
+
model: effectiveModel,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// Create AgentLoop (unwired — tools only, no bus subscriptions yet)
|
|
91
|
+
const agentLoop = llmClient
|
|
92
|
+
? new AgentLoop(bus, contextManager, llmClient, handlers, modes, initialModeIndex)
|
|
93
|
+
: null;
|
|
94
|
+
const backends = new Map();
|
|
95
|
+
let activeBackendName = null;
|
|
96
|
+
const activateByName = async (name, silent = false) => {
|
|
97
|
+
const backend = name === "agent-sh" ? null : backends.get(name);
|
|
98
|
+
if (name !== "agent-sh" && !backend) {
|
|
99
|
+
bus.emit("ui:error", { message: `Unknown backend: ${name}` });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// Deactivate current backend
|
|
103
|
+
if (activeBackendName === "agent-sh") {
|
|
104
|
+
agentLoop?.unwire();
|
|
105
|
+
}
|
|
106
|
+
else if (activeBackendName) {
|
|
107
|
+
backends.get(activeBackendName)?.kill();
|
|
108
|
+
}
|
|
109
|
+
// Activate new backend
|
|
110
|
+
if (name === "agent-sh") {
|
|
111
|
+
if (!agentLoop) {
|
|
112
|
+
bus.emit("ui:error", { message: "No LLM provider configured for built-in backend" });
|
|
47
113
|
return;
|
|
48
114
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
bus.emit("agent:
|
|
52
|
-
|
|
53
|
-
|
|
115
|
+
agentLoop.wire();
|
|
116
|
+
activeBackendName = "agent-sh";
|
|
117
|
+
bus.emit("agent:info", { name: "agent-sh", version: "0.4", model: llmClient?.model, provider: activeProvider?.id, contextWindow: activeProvider?.contextWindow });
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
await backend.start?.();
|
|
121
|
+
activeBackendName = name;
|
|
122
|
+
}
|
|
123
|
+
if (!silent) {
|
|
124
|
+
bus.emit("ui:info", { message: `Backend: ${name}` });
|
|
125
|
+
}
|
|
126
|
+
bus.emit("config:changed", {});
|
|
127
|
+
};
|
|
128
|
+
bus.on("agent:register-backend", (backend) => {
|
|
129
|
+
backends.set(backend.name, backend);
|
|
130
|
+
});
|
|
131
|
+
bus.on("config:switch-backend", ({ name }) => {
|
|
132
|
+
activateByName(name);
|
|
133
|
+
});
|
|
134
|
+
bus.on("config:list-backends", () => {
|
|
135
|
+
const names = [];
|
|
136
|
+
if (agentLoop)
|
|
137
|
+
names.push("agent-sh");
|
|
138
|
+
for (const name of backends.keys())
|
|
139
|
+
names.push(name);
|
|
140
|
+
const list = names
|
|
141
|
+
.map((n) => n === activeBackendName ? `${n} (active)` : n)
|
|
142
|
+
.join(", ");
|
|
143
|
+
bus.emit("ui:info", { message: `Backends: ${list}` });
|
|
144
|
+
});
|
|
145
|
+
bus.onPipe("config:get-backends", (payload) => {
|
|
146
|
+
const names = [];
|
|
147
|
+
if (agentLoop)
|
|
148
|
+
names.push("agent-sh");
|
|
149
|
+
for (const name of backends.keys())
|
|
150
|
+
names.push(name);
|
|
151
|
+
return { names, active: activeBackendName };
|
|
152
|
+
});
|
|
153
|
+
// ── Runtime provider management ──────────────────────────────
|
|
154
|
+
bus.on("provider:register", (p) => {
|
|
155
|
+
const rawModels = p.models ?? (p.defaultModel ? [p.defaultModel] : []);
|
|
156
|
+
const modelIds = [];
|
|
157
|
+
const caps = new Map();
|
|
158
|
+
for (const m of rawModels) {
|
|
159
|
+
if (typeof m === "string") {
|
|
160
|
+
modelIds.push(m);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
modelIds.push(m.id);
|
|
164
|
+
caps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
providerRegistry.set(p.id, {
|
|
168
|
+
id: p.id,
|
|
169
|
+
apiKey: p.apiKey,
|
|
170
|
+
baseURL: p.baseURL,
|
|
171
|
+
defaultModel: p.defaultModel,
|
|
172
|
+
models: modelIds,
|
|
173
|
+
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
174
|
+
modelCapabilities: caps.size > 0 ? caps : undefined,
|
|
54
175
|
});
|
|
55
176
|
});
|
|
56
|
-
bus.on("
|
|
57
|
-
|
|
177
|
+
bus.on("config:switch-provider", ({ provider: name }) => {
|
|
178
|
+
const p = providerRegistry.get(name);
|
|
179
|
+
if (!p) {
|
|
180
|
+
bus.emit("ui:error", { message: `Unknown provider: ${name}` });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (!llmClient) {
|
|
184
|
+
bus.emit("ui:error", { message: `Provider switching requires internal agent mode` });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const newApiKey = p.apiKey;
|
|
188
|
+
if (!newApiKey) {
|
|
189
|
+
bus.emit("ui:error", { message: `Provider "${name}" has no API key configured` });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const switchModel = p.defaultModel ?? p.models[0];
|
|
193
|
+
if (!switchModel) {
|
|
194
|
+
bus.emit("ui:error", { message: `Provider "${name}" has no models configured` });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
llmClient.reconfigure({
|
|
198
|
+
apiKey: newApiKey,
|
|
199
|
+
baseURL: p.baseURL,
|
|
200
|
+
model: switchModel,
|
|
201
|
+
});
|
|
202
|
+
const newModes = p.models.map((m) => {
|
|
203
|
+
const mc = p.modelCapabilities?.get(m);
|
|
204
|
+
return {
|
|
205
|
+
model: m,
|
|
206
|
+
provider: name,
|
|
207
|
+
providerConfig: { apiKey: newApiKey, baseURL: p.baseURL },
|
|
208
|
+
contextWindow: mc?.contextWindow ?? p.contextWindow,
|
|
209
|
+
reasoning: mc?.reasoning,
|
|
210
|
+
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
bus.emit("config:set-modes", { modes: newModes });
|
|
214
|
+
activeProvider = p;
|
|
215
|
+
bus.emit("agent:info", { name: "agent-sh", version: "0.4", model: switchModel, provider: name, contextWindow: p.contextWindow });
|
|
216
|
+
bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
|
|
217
|
+
bus.emit("config:changed", {});
|
|
58
218
|
});
|
|
59
219
|
return {
|
|
60
220
|
bus,
|
|
61
221
|
contextManager,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
222
|
+
llmClient,
|
|
223
|
+
activateBackend() {
|
|
224
|
+
// Silent — backend info is shown in the startup banner.
|
|
225
|
+
// Runtime switches (config:switch-backend) still emit ui:info.
|
|
226
|
+
const preferred = settings.defaultBackend;
|
|
227
|
+
if (preferred && backends.has(preferred)) {
|
|
228
|
+
activateByName(preferred, true);
|
|
229
|
+
}
|
|
230
|
+
else if (backends.size > 0 && !agentLoop) {
|
|
231
|
+
activateByName(backends.keys().next().value, true);
|
|
232
|
+
}
|
|
233
|
+
else if (agentLoop) {
|
|
234
|
+
agentLoop.wire();
|
|
235
|
+
activeBackendName = "agent-sh";
|
|
236
|
+
bus.emit("agent:info", { name: "agent-sh", version: "0.4", model: llmClient?.model, provider: activeProvider?.id, contextWindow: activeProvider?.contextWindow });
|
|
237
|
+
}
|
|
238
|
+
else if (backends.size > 0) {
|
|
239
|
+
activateByName(backends.keys().next().value, true);
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
async query(text) {
|
|
243
|
+
return new Promise((resolve, reject) => {
|
|
244
|
+
let response = "";
|
|
245
|
+
let settled = false;
|
|
246
|
+
const onChunk = (e) => {
|
|
247
|
+
for (const b of e.blocks)
|
|
248
|
+
if (b.type === "text")
|
|
249
|
+
response += b.text;
|
|
250
|
+
};
|
|
251
|
+
const onDone = () => {
|
|
252
|
+
if (settled)
|
|
253
|
+
return;
|
|
254
|
+
settled = true;
|
|
255
|
+
cleanup();
|
|
256
|
+
resolve(response);
|
|
257
|
+
};
|
|
258
|
+
const onError = (e) => {
|
|
259
|
+
if (settled)
|
|
260
|
+
return;
|
|
261
|
+
settled = true;
|
|
262
|
+
cleanup();
|
|
263
|
+
reject(new Error(e.message));
|
|
264
|
+
};
|
|
265
|
+
const cleanup = () => {
|
|
266
|
+
bus.off("agent:response-chunk", onChunk);
|
|
267
|
+
bus.off("agent:processing-done", onDone);
|
|
268
|
+
bus.off("agent:error", onError);
|
|
269
|
+
};
|
|
270
|
+
bus.on("agent:response-chunk", onChunk);
|
|
271
|
+
bus.on("agent:processing-done", onDone);
|
|
272
|
+
bus.on("agent:error", onError);
|
|
273
|
+
bus.emit("agent:submit", { query: text });
|
|
274
|
+
});
|
|
275
|
+
},
|
|
276
|
+
cancel() {
|
|
277
|
+
bus.emit("agent:cancel-request", {});
|
|
66
278
|
},
|
|
67
279
|
extensionContext(opts) {
|
|
68
280
|
return {
|
|
69
281
|
bus,
|
|
70
282
|
contextManager,
|
|
71
|
-
|
|
283
|
+
llmClient,
|
|
72
284
|
quit: opts.quit,
|
|
73
285
|
setPalette,
|
|
74
286
|
createBlockTransform: (o) => streamTransform.createBlockTransform(bus, o),
|
|
75
287
|
createFencedBlockTransform: (o) => streamTransform.createFencedBlockTransform(bus, o),
|
|
76
288
|
getExtensionSettings: settingsMod.getExtensionSettings,
|
|
289
|
+
registerCommand: (name, description, handler) => bus.emit("command:register", { name, description, handler }),
|
|
290
|
+
registerTool: (tool) => agentLoop?.registerTool(tool),
|
|
291
|
+
getTools: () => agentLoop?.getTools() ?? [],
|
|
77
292
|
define: (name, fn) => handlers.define(name, fn),
|
|
78
293
|
advise: (name, wrapper) => handlers.advise(name, wrapper),
|
|
79
294
|
call: (name, ...args) => handlers.call(name, ...args),
|
|
80
295
|
};
|
|
81
296
|
},
|
|
82
297
|
kill() {
|
|
83
|
-
|
|
298
|
+
if (activeBackendName === "agent-sh") {
|
|
299
|
+
agentLoop?.kill();
|
|
300
|
+
}
|
|
301
|
+
else if (activeBackendName) {
|
|
302
|
+
backends.get(activeBackendName)?.kill();
|
|
303
|
+
}
|
|
84
304
|
},
|
|
85
305
|
};
|
|
86
306
|
}
|
package/dist/event-bus.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { AgentMode } from "./types.js";
|
|
2
|
+
import type { ToolResultDisplay } from "./agent/types.js";
|
|
1
3
|
/**
|
|
2
4
|
* Typed event map — every event has a known payload shape.
|
|
3
5
|
*/
|
|
@@ -22,25 +24,28 @@ export interface ShellEvents {
|
|
|
22
24
|
"shell:agent-exec-done": Record<string, never>;
|
|
23
25
|
"agent:submit": {
|
|
24
26
|
query: string;
|
|
25
|
-
modeInstruction?: string;
|
|
26
|
-
modeLabel?: string;
|
|
27
27
|
};
|
|
28
|
-
"agent:cancel-request":
|
|
28
|
+
"agent:cancel-request": {
|
|
29
|
+
silent?: boolean;
|
|
30
|
+
};
|
|
29
31
|
"input-mode:register": import("./types.js").InputModeConfig;
|
|
30
32
|
"agent:query": {
|
|
31
33
|
query: string;
|
|
32
|
-
modeLabel?: string;
|
|
33
34
|
};
|
|
34
35
|
"agent:thinking-chunk": {
|
|
35
36
|
text: string;
|
|
36
37
|
};
|
|
37
38
|
"agent:response-chunk": {
|
|
38
|
-
|
|
39
|
-
blocks?: ContentBlock[];
|
|
39
|
+
blocks: ContentBlock[];
|
|
40
40
|
};
|
|
41
41
|
"agent:response-done": {
|
|
42
42
|
response: string;
|
|
43
43
|
};
|
|
44
|
+
"agent:usage": {
|
|
45
|
+
prompt_tokens: number;
|
|
46
|
+
completion_tokens: number;
|
|
47
|
+
total_tokens: number;
|
|
48
|
+
};
|
|
44
49
|
"agent:processing-start": Record<string, never>;
|
|
45
50
|
"agent:processing-done": Record<string, never>;
|
|
46
51
|
"agent:cancelled": Record<string, never>;
|
|
@@ -56,20 +61,37 @@ export interface ShellEvents {
|
|
|
56
61
|
output: string;
|
|
57
62
|
exitCode: number | null;
|
|
58
63
|
};
|
|
64
|
+
"agent:tool-batch": {
|
|
65
|
+
groups: Array<{
|
|
66
|
+
kind: string;
|
|
67
|
+
tools: Array<{
|
|
68
|
+
name: string;
|
|
69
|
+
displayDetail?: string;
|
|
70
|
+
}>;
|
|
71
|
+
}>;
|
|
72
|
+
};
|
|
59
73
|
"agent:tool-started": {
|
|
60
74
|
title: string;
|
|
61
75
|
toolCallId?: string;
|
|
62
76
|
kind?: string;
|
|
77
|
+
icon?: string;
|
|
63
78
|
locations?: {
|
|
64
79
|
path: string;
|
|
65
80
|
line?: number | null;
|
|
66
81
|
}[];
|
|
67
82
|
rawInput?: unknown;
|
|
83
|
+
/** Pre-formatted display detail from tool's formatCall(). */
|
|
84
|
+
displayDetail?: string;
|
|
85
|
+
batchIndex?: number;
|
|
86
|
+
batchTotal?: number;
|
|
68
87
|
};
|
|
69
88
|
"agent:tool-completed": {
|
|
70
89
|
toolCallId?: string;
|
|
71
90
|
exitCode: number | null;
|
|
72
91
|
rawOutput?: unknown;
|
|
92
|
+
kind?: string;
|
|
93
|
+
/** Structured result display — set by formatResult or defaults, overridable via onPipe. */
|
|
94
|
+
resultDisplay?: ToolResultDisplay;
|
|
73
95
|
};
|
|
74
96
|
"agent:tool-output-chunk": {
|
|
75
97
|
chunk: string;
|
|
@@ -80,6 +102,11 @@ export interface ShellEvents {
|
|
|
80
102
|
metadata: Record<string, unknown>;
|
|
81
103
|
decision: Record<string, unknown>;
|
|
82
104
|
};
|
|
105
|
+
"command:register": {
|
|
106
|
+
name: string;
|
|
107
|
+
description: string;
|
|
108
|
+
handler: (args: string) => Promise<void> | void;
|
|
109
|
+
};
|
|
83
110
|
"command:execute": {
|
|
84
111
|
name: string;
|
|
85
112
|
args: string;
|
|
@@ -90,6 +117,9 @@ export interface ShellEvents {
|
|
|
90
117
|
"ui:error": {
|
|
91
118
|
message: string;
|
|
92
119
|
};
|
|
120
|
+
"ui:suggestion": {
|
|
121
|
+
text: string;
|
|
122
|
+
};
|
|
93
123
|
"input:keypress": {
|
|
94
124
|
key: string;
|
|
95
125
|
};
|
|
@@ -107,24 +137,75 @@ export interface ShellEvents {
|
|
|
107
137
|
command: string;
|
|
108
138
|
output: string;
|
|
109
139
|
cwd: string;
|
|
140
|
+
exitCode: number | null;
|
|
110
141
|
done: boolean;
|
|
111
142
|
};
|
|
112
|
-
"
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
143
|
+
"agent:info": {
|
|
144
|
+
name: string;
|
|
145
|
+
version: string;
|
|
146
|
+
model?: string;
|
|
147
|
+
provider?: string;
|
|
148
|
+
contextWindow?: number;
|
|
149
|
+
};
|
|
150
|
+
"agent:reset-session": Record<string, never>;
|
|
151
|
+
"agent:register-backend": {
|
|
152
|
+
name: string;
|
|
153
|
+
kill: () => void;
|
|
154
|
+
start?: () => Promise<void>;
|
|
155
|
+
};
|
|
156
|
+
"config:switch-backend": {
|
|
157
|
+
name: string;
|
|
158
|
+
};
|
|
159
|
+
"config:list-backends": Record<string, never>;
|
|
160
|
+
"config:get-backends": {
|
|
161
|
+
names: string[];
|
|
162
|
+
active: string | null;
|
|
123
163
|
};
|
|
124
164
|
"config:changed": Record<string, never>;
|
|
125
165
|
"config:cycle": Record<string, never>;
|
|
166
|
+
"config:switch-model": {
|
|
167
|
+
model: string;
|
|
168
|
+
};
|
|
169
|
+
"config:get-models": {
|
|
170
|
+
models: {
|
|
171
|
+
model: string;
|
|
172
|
+
provider: string;
|
|
173
|
+
}[];
|
|
174
|
+
active: string | null;
|
|
175
|
+
};
|
|
176
|
+
"config:set-thinking": {
|
|
177
|
+
level: string;
|
|
178
|
+
};
|
|
179
|
+
"config:get-thinking": {
|
|
180
|
+
level: string;
|
|
181
|
+
levels: string[];
|
|
182
|
+
supported: boolean;
|
|
183
|
+
};
|
|
184
|
+
"config:switch-provider": {
|
|
185
|
+
provider: string;
|
|
186
|
+
};
|
|
187
|
+
"config:set-modes": {
|
|
188
|
+
modes: AgentMode[];
|
|
189
|
+
};
|
|
190
|
+
"provider:register": {
|
|
191
|
+
id: string;
|
|
192
|
+
apiKey?: string;
|
|
193
|
+
baseURL?: string;
|
|
194
|
+
defaultModel: string;
|
|
195
|
+
models?: (string | {
|
|
196
|
+
id: string;
|
|
197
|
+
reasoning?: boolean;
|
|
198
|
+
contextWindow?: number;
|
|
199
|
+
})[];
|
|
200
|
+
/** Provider supports the reasoning_effort parameter. Default: true. */
|
|
201
|
+
supportsReasoningEffort?: boolean;
|
|
202
|
+
};
|
|
126
203
|
"autocomplete:request": {
|
|
127
204
|
buffer: string;
|
|
205
|
+
/** Parsed slash command name (e.g. "/backend"), or null if not a command. */
|
|
206
|
+
command: string | null;
|
|
207
|
+
/** Text after the command name (e.g. "clau" for "/backend clau"), or null. */
|
|
208
|
+
commandArgs: string | null;
|
|
128
209
|
items: {
|
|
129
210
|
name: string;
|
|
130
211
|
description: string;
|
package/dist/event-bus.js
CHANGED
|
@@ -28,7 +28,16 @@ export class EventBus {
|
|
|
28
28
|
* modify data (e.g. render LaTeX → terminal image) before renderers see it.
|
|
29
29
|
*/
|
|
30
30
|
emitTransform(event, payload) {
|
|
31
|
-
|
|
31
|
+
let transformed;
|
|
32
|
+
try {
|
|
33
|
+
transformed = this.emitPipe(event, payload);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
if (process.env.DEBUG) {
|
|
37
|
+
process.stderr.write(`[event-bus] pipe error on ${String(event)}: ${err}\n`);
|
|
38
|
+
}
|
|
39
|
+
transformed = payload; // fall back to untransformed
|
|
40
|
+
}
|
|
32
41
|
this.emitter.emit(event, transformed);
|
|
33
42
|
}
|
|
34
43
|
/** Register a transform listener for a pipeline event. */
|
|
@@ -13,4 +13,4 @@ import type { ExtensionContext } from "./types.js";
|
|
|
13
13
|
* Each module should export a default or named `activate(ctx)` function.
|
|
14
14
|
* Errors are non-fatal — logged via ui:error and skipped.
|
|
15
15
|
*/
|
|
16
|
-
export declare function loadExtensions(ctx: ExtensionContext, cliExtensions?: string[]): Promise<
|
|
16
|
+
export declare function loadExtensions(ctx: ExtensionContext, cliExtensions?: string[]): Promise<string[]>;
|
package/dist/extension-loader.js
CHANGED
|
@@ -47,7 +47,10 @@ export async function loadExtensions(ctx, cliExtensions) {
|
|
|
47
47
|
const entries = await fs.readdir(EXT_DIR, { withFileTypes: true });
|
|
48
48
|
for (const entry of entries) {
|
|
49
49
|
const fullPath = path.join(EXT_DIR, entry.name);
|
|
50
|
-
if
|
|
50
|
+
// Resolve symlinks to check if they point to directories
|
|
51
|
+
const isDir = entry.isDirectory() ||
|
|
52
|
+
(entry.isSymbolicLink() && (await fs.stat(fullPath)).isDirectory());
|
|
53
|
+
if (isDir) {
|
|
51
54
|
// Directory extension: look for index.{ts,js,mjs,...}
|
|
52
55
|
const indexFile = await findIndex(fullPath);
|
|
53
56
|
if (indexFile) {
|
|
@@ -71,6 +74,7 @@ export async function loadExtensions(ctx, cliExtensions) {
|
|
|
71
74
|
return true;
|
|
72
75
|
});
|
|
73
76
|
// Load each extension
|
|
77
|
+
const loaded = [];
|
|
74
78
|
for (const specifier of unique) {
|
|
75
79
|
try {
|
|
76
80
|
const importPath = await resolveSpecifier(specifier);
|
|
@@ -86,6 +90,10 @@ export async function loadExtensions(ctx, cliExtensions) {
|
|
|
86
90
|
: mod.activate;
|
|
87
91
|
if (typeof activate === "function") {
|
|
88
92
|
activate(ctx);
|
|
93
|
+
// Extract a short name from the specifier
|
|
94
|
+
const base = path.basename(specifier).replace(/\.(ts|js|mjs|mts|tsx)$/, "");
|
|
95
|
+
const name = base === "index" ? path.basename(path.dirname(specifier)) : base;
|
|
96
|
+
loaded.push(name);
|
|
89
97
|
}
|
|
90
98
|
}
|
|
91
99
|
catch (err) {
|
|
@@ -94,6 +102,7 @@ export async function loadExtensions(ctx, cliExtensions) {
|
|
|
94
102
|
});
|
|
95
103
|
}
|
|
96
104
|
}
|
|
105
|
+
return loaded;
|
|
97
106
|
}
|
|
98
107
|
/**
|
|
99
108
|
* Find an index file in a directory extension.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command suggestion extension (fast-path LLM feature).
|
|
3
|
+
*
|
|
4
|
+
* After a shell command fails (non-zero exit), uses llmClient.complete()
|
|
5
|
+
* to suggest a fix. Shows the suggestion below the prompt.
|
|
6
|
+
*
|
|
7
|
+
* Only active when llmClient is available (internal agent mode).
|
|
8
|
+
*/
|
|
9
|
+
import type { ExtensionContext } from "../types.js";
|
|
10
|
+
export default function activate({ bus, llmClient }: ExtensionContext): void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export default function activate({ bus, llmClient }) {
|
|
2
|
+
if (!llmClient)
|
|
3
|
+
return;
|
|
4
|
+
let suggesting = false;
|
|
5
|
+
bus.on("shell:command-done", ({ command, output, exitCode, cwd }) => {
|
|
6
|
+
if (exitCode === null || exitCode === 0)
|
|
7
|
+
return;
|
|
8
|
+
if (!command.trim())
|
|
9
|
+
return;
|
|
10
|
+
if (suggesting)
|
|
11
|
+
return; // don't stack suggestions
|
|
12
|
+
suggesting = true;
|
|
13
|
+
// Truncate output to avoid blowing up the prompt
|
|
14
|
+
const truncated = output.length > 1000
|
|
15
|
+
? output.slice(-1000)
|
|
16
|
+
: output;
|
|
17
|
+
llmClient.complete({
|
|
18
|
+
messages: [
|
|
19
|
+
{
|
|
20
|
+
role: "system",
|
|
21
|
+
content: "You are a shell assistant. The user's command failed. " +
|
|
22
|
+
"Suggest a fix as a single command. Just the command, no explanation, no backticks, no prefix. " +
|
|
23
|
+
"If you can't suggest anything useful, reply with an empty string.",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
role: "user",
|
|
27
|
+
content: `cwd: ${cwd}\n$ ${command}\n${truncated}\nexit code: ${exitCode}`,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
max_tokens: 150,
|
|
31
|
+
}).then((suggestion) => {
|
|
32
|
+
suggesting = false;
|
|
33
|
+
const trimmed = suggestion.trim().replace(/^`+|`+$/g, ""); // strip backticks
|
|
34
|
+
if (trimmed && trimmed.length < 500) {
|
|
35
|
+
bus.emit("ui:suggestion", { text: trimmed });
|
|
36
|
+
}
|
|
37
|
+
}).catch(() => {
|
|
38
|
+
suggesting = false;
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ExtensionContext } from "../types.js";
|
|
2
|
-
export default function activate({ bus,
|
|
2
|
+
export default function activate({ bus, contextManager }: ExtensionContext): void;
|