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.
Files changed (83) hide show
  1. package/README.md +37 -115
  2. package/dist/agent/agent-loop.d.ts +86 -0
  3. package/dist/agent/agent-loop.js +704 -0
  4. package/dist/agent/conversation-state.d.ts +27 -0
  5. package/dist/agent/conversation-state.js +59 -0
  6. package/dist/agent/index.d.ts +11 -0
  7. package/dist/agent/index.js +9 -0
  8. package/dist/agent/skills.d.ts +25 -0
  9. package/dist/agent/skills.js +186 -0
  10. package/dist/agent/subagent.d.ts +37 -0
  11. package/dist/agent/subagent.js +119 -0
  12. package/dist/agent/system-prompt.d.ts +14 -0
  13. package/dist/agent/system-prompt.js +103 -0
  14. package/dist/agent/tool-registry.d.ts +15 -0
  15. package/dist/agent/tool-registry.js +30 -0
  16. package/dist/agent/tools/bash.d.ts +7 -0
  17. package/dist/agent/tools/bash.js +71 -0
  18. package/dist/agent/tools/display.d.ts +13 -0
  19. package/dist/agent/tools/display.js +70 -0
  20. package/dist/agent/tools/edit-file.d.ts +2 -0
  21. package/dist/agent/tools/edit-file.js +148 -0
  22. package/dist/agent/tools/glob.d.ts +2 -0
  23. package/dist/agent/tools/glob.js +87 -0
  24. package/dist/agent/tools/grep.d.ts +2 -0
  25. package/dist/agent/tools/grep.js +168 -0
  26. package/dist/agent/tools/list-skills.d.ts +2 -0
  27. package/dist/agent/tools/list-skills.js +28 -0
  28. package/dist/agent/tools/ls.d.ts +2 -0
  29. package/dist/agent/tools/ls.js +72 -0
  30. package/dist/agent/tools/read-file.d.ts +10 -0
  31. package/dist/agent/tools/read-file.js +101 -0
  32. package/dist/agent/tools/user-shell.d.ts +13 -0
  33. package/dist/agent/tools/user-shell.js +84 -0
  34. package/dist/agent/tools/write-file.d.ts +2 -0
  35. package/dist/agent/tools/write-file.js +82 -0
  36. package/dist/agent/types.d.ts +78 -0
  37. package/dist/agent/types.js +1 -0
  38. package/dist/core.d.ts +22 -14
  39. package/dist/core.js +256 -36
  40. package/dist/event-bus.d.ts +98 -17
  41. package/dist/event-bus.js +10 -1
  42. package/dist/extension-loader.d.ts +1 -1
  43. package/dist/extension-loader.js +10 -1
  44. package/dist/extensions/command-suggest.d.ts +10 -0
  45. package/dist/extensions/command-suggest.js +41 -0
  46. package/dist/extensions/slash-commands.d.ts +1 -1
  47. package/dist/extensions/slash-commands.js +161 -64
  48. package/dist/extensions/tui-renderer.js +426 -126
  49. package/dist/index.js +110 -129
  50. package/dist/input-handler.js +78 -9
  51. package/dist/output-parser.d.ts +7 -0
  52. package/dist/output-parser.js +27 -0
  53. package/dist/settings.d.ts +53 -2
  54. package/dist/settings.js +46 -3
  55. package/dist/shell.js +35 -28
  56. package/dist/types.d.ts +33 -6
  57. package/dist/utils/box-frame.d.ts +3 -1
  58. package/dist/utils/box-frame.js +12 -5
  59. package/dist/utils/diff.js +10 -0
  60. package/dist/utils/llm-client.d.ts +45 -0
  61. package/dist/utils/llm-client.js +60 -0
  62. package/dist/utils/markdown.d.ts +1 -0
  63. package/dist/utils/markdown.js +25 -3
  64. package/dist/utils/stream-transform.js +20 -47
  65. package/dist/utils/tool-display.d.ts +4 -0
  66. package/dist/utils/tool-display.js +35 -8
  67. package/examples/extensions/claude-code-bridge/README.md +35 -0
  68. package/examples/extensions/claude-code-bridge/index.ts +194 -0
  69. package/examples/extensions/claude-code-bridge/package.json +11 -0
  70. package/examples/extensions/openrouter.ts +87 -0
  71. package/examples/extensions/pi-bridge/README.md +35 -0
  72. package/examples/extensions/pi-bridge/index.ts +263 -0
  73. package/examples/extensions/pi-bridge/package.json +13 -0
  74. package/examples/extensions/secret-guard.ts +100 -0
  75. package/examples/extensions/subagents.ts +87 -0
  76. package/package.json +3 -5
  77. package/dist/acp-client.d.ts +0 -105
  78. package/dist/acp-client.js +0 -684
  79. package/dist/extensions/shell-exec.d.ts +0 -24
  80. package/dist/extensions/shell-exec.js +0 -188
  81. package/dist/mcp-server.d.ts +0 -13
  82. package/dist/mcp-server.js +0 -234
  83. 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 + AcpClient without any frontend.
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 and calling client methods.
6
+ * subscribing to bus events.
7
7
  *
8
- * The core listens for `agent:submit` and `agent:cancel-request` events
9
- * from any frontend, routing them to the AcpClient. This means frontends
10
- * never need a direct reference to AcpClient — they just emit events.
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({ agentCommand: "pi-acp" });
15
- * core.bus.on("agent:response-chunk", ({ text }) => ws.send(text));
16
- * await core.start();
17
- * core.bus.emit("agent:submit", { query: "hello" });
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 { AcpClient } from "./acp-client.js";
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
- const client = new AcpClient({ bus, contextManager, config });
34
- let connected = false;
35
- // Route frontend events to the agent — any frontend (Shell, WebSocket,
36
- // REST handler, test harness) can emit these without knowing about AcpClient.
37
- bus.on("agent:submit", ({ query, modeInstruction, modeLabel }) => {
38
- (async () => {
39
- // Wait briefly for agent connection if start() is still in progress
40
- if (!connected) {
41
- for (let i = 0; i < 30 && !connected; i++) {
42
- await new Promise((r) => setTimeout(r, 100));
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
- if (!connected) {
46
- bus.emit("ui:error", { message: "Agent not connected. Please wait a moment and try again." });
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
- await client.sendPrompt(query, { modeInstruction, modeLabel });
50
- })().catch((err) => {
51
- bus.emit("agent:error", {
52
- message: err instanceof Error ? err.message : String(err),
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("agent:cancel-request", () => {
57
- client.cancel().catch(() => { });
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
- client,
63
- async start() {
64
- await client.start();
65
- connected = true;
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
- getAcpClient: () => client,
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
- client.kill();
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
  }
@@ -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": Record<string, never>;
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
- text: string;
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
- "session:configure": {
113
- cwd: string;
114
- mcpServers: {
115
- name: string;
116
- command: string;
117
- args: string[];
118
- env: {
119
- name: string;
120
- value: string;
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
- const transformed = this.emitPipe(event, payload);
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<void>;
16
+ export declare function loadExtensions(ctx: ExtensionContext, cliExtensions?: string[]): Promise<string[]>;
@@ -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 (entry.isDirectory()) {
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, getAcpClient, quit }: ExtensionContext): void;
2
+ export default function activate({ bus, contextManager }: ExtensionContext): void;