agent-sh 0.8.0 → 0.10.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 +27 -43
- package/dist/agent/agent-loop.d.ts +69 -6
- package/dist/agent/agent-loop.js +954 -153
- package/dist/agent/conversation-state.d.ts +74 -21
- package/dist/agent/conversation-state.js +361 -150
- package/dist/agent/history-file.d.ts +13 -4
- package/dist/agent/history-file.js +110 -36
- package/dist/agent/nuclear-form.d.ts +28 -3
- package/dist/agent/nuclear-form.js +88 -6
- package/dist/agent/skills.d.ts +2 -4
- package/dist/agent/skills.js +10 -4
- package/dist/agent/subagent.d.ts +23 -0
- package/dist/agent/subagent.js +53 -11
- package/dist/agent/system-prompt.d.ts +37 -5
- package/dist/agent/system-prompt.js +100 -67
- package/dist/{token-budget.d.ts → agent/token-budget.d.ts} +5 -4
- package/dist/{token-budget.js → agent/token-budget.js} +15 -20
- package/dist/agent/tool-protocol.d.ts +105 -0
- package/dist/agent/tool-protocol.js +551 -0
- package/dist/agent/tools/bash.js +3 -3
- package/dist/agent/tools/edit-file.js +9 -6
- package/dist/agent/tools/glob.js +4 -2
- package/dist/agent/tools/grep.js +27 -3
- package/dist/agent/tools/ls.js +5 -6
- package/dist/agent/types.d.ts +22 -2
- package/dist/context-manager.d.ts +17 -0
- package/dist/context-manager.js +37 -4
- package/dist/core.d.ts +7 -7
- package/dist/core.js +99 -196
- package/dist/event-bus.d.ts +85 -2
- package/dist/event-bus.js +20 -1
- package/dist/executor.d.ts +4 -3
- package/dist/executor.js +18 -15
- package/dist/extension-loader.d.ts +5 -0
- package/dist/extension-loader.js +143 -19
- package/dist/extensions/agent-backend.d.ts +14 -0
- package/dist/extensions/agent-backend.js +188 -0
- package/dist/extensions/command-suggest.d.ts +3 -3
- package/dist/extensions/command-suggest.js +4 -3
- package/dist/extensions/index.d.ts +19 -0
- package/dist/extensions/index.js +24 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +30 -10
- package/dist/extensions/tui-renderer.js +117 -113
- package/dist/index.js +39 -26
- package/dist/settings.d.ts +40 -3
- package/dist/settings.js +57 -10
- package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +3 -2
- package/dist/{input-handler.js → shell/input-handler.js} +111 -85
- package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
- package/dist/{output-parser.js → shell/output-parser.js} +1 -1
- package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
- package/dist/{shell.js → shell/shell.js} +39 -8
- package/dist/types.d.ts +61 -10
- package/dist/utils/ansi.d.ts +5 -0
- package/dist/utils/ansi.js +1 -1
- package/dist/utils/compositor.d.ts +67 -0
- package/dist/utils/compositor.js +116 -0
- package/dist/utils/diff-renderer.d.ts +9 -0
- package/dist/utils/diff-renderer.js +312 -146
- package/dist/utils/diff.d.ts +21 -2
- package/dist/utils/diff.js +165 -89
- package/dist/utils/floating-panel.d.ts +2 -0
- package/dist/utils/floating-panel.js +30 -14
- package/dist/utils/handler-registry.d.ts +31 -10
- package/dist/utils/handler-registry.js +58 -16
- package/dist/utils/line-editor.d.ts +33 -3
- package/dist/utils/line-editor.js +221 -44
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +1 -1
- package/dist/utils/message-utils.d.ts +35 -0
- package/dist/utils/message-utils.js +75 -0
- package/dist/utils/terminal-buffer.d.ts +5 -1
- package/dist/utils/terminal-buffer.js +18 -2
- package/dist/utils/tool-display.d.ts +1 -1
- package/dist/utils/tool-display.js +4 -4
- package/dist/utils/tool-interactive.d.ts +12 -0
- package/dist/utils/tool-interactive.js +53 -0
- package/examples/extensions/ash-acp-bridge/README.md +39 -0
- package/examples/extensions/ash-acp-bridge/package.json +23 -0
- package/examples/extensions/ash-acp-bridge/src/index.ts +574 -0
- package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
- package/examples/extensions/ash-mcp-bridge/README.md +72 -0
- package/examples/extensions/ash-mcp-bridge/index.ts +164 -0
- package/examples/extensions/ash-mcp-bridge/package.json +9 -0
- package/examples/extensions/claude-code-bridge/index.ts +198 -51
- package/examples/extensions/claude-code-bridge/package.json +1 -0
- package/examples/extensions/interactive-prompts.ts +98 -112
- package/examples/extensions/overlay-agent.ts +84 -38
- package/examples/extensions/peer-mesh.ts +565 -0
- package/examples/extensions/pi-bridge/index.ts +2 -2
- package/examples/extensions/questionnaire.ts +260 -0
- package/examples/extensions/subagents.ts +19 -4
- package/examples/extensions/terminal-buffer.ts +32 -53
- package/examples/extensions/tmux-pane.ts +307 -0
- package/examples/extensions/user-shell.ts +136 -0
- package/examples/extensions/web-access.ts +335 -0
- package/package.json +44 -2
- package/dist/agent/tools/display.d.ts +0 -13
- package/dist/agent/tools/display.js +0 -70
- package/dist/agent/tools/user-shell.d.ts +0 -13
- package/dist/agent/tools/user-shell.js +0 -87
- package/dist/extensions/overlay-agent.d.ts +0 -14
- package/dist/extensions/overlay-agent.js +0 -147
- package/dist/extensions/terminal-buffer.d.ts +0 -14
- package/dist/extensions/terminal-buffer.js +0 -125
package/dist/event-bus.d.ts
CHANGED
|
@@ -89,6 +89,28 @@ export interface ShellEvents {
|
|
|
89
89
|
}>;
|
|
90
90
|
}>;
|
|
91
91
|
};
|
|
92
|
+
"agent:tool-batch-complete": {
|
|
93
|
+
results: Array<{
|
|
94
|
+
name: string;
|
|
95
|
+
isError: boolean;
|
|
96
|
+
errorSummary?: string;
|
|
97
|
+
}>;
|
|
98
|
+
};
|
|
99
|
+
"conversation:message-appended": {
|
|
100
|
+
role: "user" | "assistant" | "tool" | "system";
|
|
101
|
+
content: string;
|
|
102
|
+
/** For role="tool": name of the tool whose result this is. */
|
|
103
|
+
toolName?: string;
|
|
104
|
+
/** For role="tool": parsed arguments passed to the tool. */
|
|
105
|
+
toolArgs?: Record<string, unknown>;
|
|
106
|
+
/** For role="tool": whether the tool errored. */
|
|
107
|
+
isError?: boolean;
|
|
108
|
+
};
|
|
109
|
+
"conversation:after-compact": {
|
|
110
|
+
beforeTokens: number;
|
|
111
|
+
afterTokens: number;
|
|
112
|
+
evictedCount: number;
|
|
113
|
+
};
|
|
92
114
|
"agent:tool-started": {
|
|
93
115
|
title: string;
|
|
94
116
|
toolCallId?: string;
|
|
@@ -115,10 +137,24 @@ export interface ShellEvents {
|
|
|
115
137
|
"agent:tool-output-chunk": {
|
|
116
138
|
chunk: string;
|
|
117
139
|
};
|
|
140
|
+
"agent:subagent-started": {
|
|
141
|
+
taskId: string;
|
|
142
|
+
task: string;
|
|
143
|
+
};
|
|
144
|
+
"agent:subagent-completed": {
|
|
145
|
+
taskId: string;
|
|
146
|
+
task: string;
|
|
147
|
+
result: string;
|
|
148
|
+
isError: boolean;
|
|
149
|
+
};
|
|
150
|
+
"tool:interactive-start": Record<string, never>;
|
|
151
|
+
"tool:interactive-end": Record<string, never>;
|
|
118
152
|
"permission:request": {
|
|
119
153
|
kind: string;
|
|
120
154
|
title: string;
|
|
121
155
|
metadata: Record<string, unknown>;
|
|
156
|
+
/** Interactive UI capability — available when the built-in agent is active. */
|
|
157
|
+
ui?: unknown;
|
|
122
158
|
decision: Record<string, unknown>;
|
|
123
159
|
};
|
|
124
160
|
"command:register": {
|
|
@@ -126,6 +162,9 @@ export interface ShellEvents {
|
|
|
126
162
|
description: string;
|
|
127
163
|
handler: (args: string) => Promise<void> | void;
|
|
128
164
|
};
|
|
165
|
+
"command:unregister": {
|
|
166
|
+
name: string;
|
|
167
|
+
};
|
|
129
168
|
"command:execute": {
|
|
130
169
|
name: string;
|
|
131
170
|
args: string;
|
|
@@ -139,6 +178,10 @@ export interface ShellEvents {
|
|
|
139
178
|
"ui:suggestion": {
|
|
140
179
|
text: string;
|
|
141
180
|
};
|
|
181
|
+
"compositor:write": {
|
|
182
|
+
stream: string;
|
|
183
|
+
text: string;
|
|
184
|
+
};
|
|
142
185
|
"input:keypress": {
|
|
143
186
|
key: string;
|
|
144
187
|
};
|
|
@@ -178,8 +221,7 @@ export interface ShellEvents {
|
|
|
178
221
|
"agent:compact-request": Record<string, never>;
|
|
179
222
|
"context:get-stats": {
|
|
180
223
|
activeTokens: number;
|
|
181
|
-
|
|
182
|
-
recallArchiveSize: number;
|
|
224
|
+
totalTokens: number;
|
|
183
225
|
budgetTokens: number;
|
|
184
226
|
};
|
|
185
227
|
"agent:register-backend": {
|
|
@@ -218,12 +260,18 @@ export interface ShellEvents {
|
|
|
218
260
|
"config:switch-provider": {
|
|
219
261
|
provider: string;
|
|
220
262
|
};
|
|
263
|
+
"config:get-initial-modes": {
|
|
264
|
+
modes: AgentMode[];
|
|
265
|
+
initialModeIndex: number;
|
|
266
|
+
};
|
|
221
267
|
"config:set-modes": {
|
|
222
268
|
modes: AgentMode[];
|
|
269
|
+
activeIndex?: number;
|
|
223
270
|
};
|
|
224
271
|
"config:add-modes": {
|
|
225
272
|
modes: AgentMode[];
|
|
226
273
|
};
|
|
274
|
+
"core:extensions-loaded": Record<string, never>;
|
|
227
275
|
"provider:register": {
|
|
228
276
|
id: string;
|
|
229
277
|
apiKey?: string;
|
|
@@ -237,6 +285,39 @@ export interface ShellEvents {
|
|
|
237
285
|
/** Provider supports the reasoning_effort parameter. Default: true. */
|
|
238
286
|
supportsReasoningEffort?: boolean;
|
|
239
287
|
};
|
|
288
|
+
"agent:register-tool": {
|
|
289
|
+
tool: import("./agent/types.js").ToolDefinition;
|
|
290
|
+
extensionName?: string;
|
|
291
|
+
};
|
|
292
|
+
"agent:unregister-tool": {
|
|
293
|
+
name: string;
|
|
294
|
+
};
|
|
295
|
+
"agent:get-tools": {
|
|
296
|
+
tools: import("./agent/types.js").ToolDefinition[];
|
|
297
|
+
};
|
|
298
|
+
"agent:register-instruction": {
|
|
299
|
+
name: string;
|
|
300
|
+
text: string;
|
|
301
|
+
extensionName: string;
|
|
302
|
+
};
|
|
303
|
+
"agent:remove-instruction": {
|
|
304
|
+
name: string;
|
|
305
|
+
};
|
|
306
|
+
"agent:register-skill": {
|
|
307
|
+
name: string;
|
|
308
|
+
description: string;
|
|
309
|
+
filePath: string;
|
|
310
|
+
extensionName: string;
|
|
311
|
+
};
|
|
312
|
+
"agent:remove-skill": {
|
|
313
|
+
name: string;
|
|
314
|
+
};
|
|
315
|
+
"banner:collect": {
|
|
316
|
+
sections: Array<{
|
|
317
|
+
label: string;
|
|
318
|
+
items: string[];
|
|
319
|
+
}>;
|
|
320
|
+
};
|
|
240
321
|
"autocomplete:request": {
|
|
241
322
|
buffer: string;
|
|
242
323
|
/** Parsed slash command name (e.g. "/backend"), or null if not a command. */
|
|
@@ -291,6 +372,8 @@ export declare class EventBus {
|
|
|
291
372
|
emitTransform<K extends keyof ShellEvents>(event: K, payload: ShellEvents[K]): void;
|
|
292
373
|
/** Register a transform listener for a pipeline event. */
|
|
293
374
|
onPipe<K extends keyof ShellEvents>(event: K, fn: PipeListener<ShellEvents[K]>): void;
|
|
375
|
+
/** Remove a transform listener from a pipeline event. */
|
|
376
|
+
offPipe<K extends keyof ShellEvents>(event: K, fn: PipeListener<ShellEvents[K]>): void;
|
|
294
377
|
/**
|
|
295
378
|
* Emit a pipeline event — each registered pipe listener receives the
|
|
296
379
|
* output of the previous one. Returns the final transformed payload.
|
package/dist/event-bus.js
CHANGED
|
@@ -49,6 +49,15 @@ export class EventBus {
|
|
|
49
49
|
}
|
|
50
50
|
listeners.push(fn);
|
|
51
51
|
}
|
|
52
|
+
/** Remove a transform listener from a pipeline event. */
|
|
53
|
+
offPipe(event, fn) {
|
|
54
|
+
const listeners = this.pipeListeners.get(event);
|
|
55
|
+
if (!listeners)
|
|
56
|
+
return;
|
|
57
|
+
const idx = listeners.indexOf(fn);
|
|
58
|
+
if (idx !== -1)
|
|
59
|
+
listeners.splice(idx, 1);
|
|
60
|
+
}
|
|
52
61
|
/**
|
|
53
62
|
* Emit a pipeline event — each registered pipe listener receives the
|
|
54
63
|
* output of the previous one. Returns the final transformed payload.
|
|
@@ -60,7 +69,17 @@ export class EventBus {
|
|
|
60
69
|
return payload;
|
|
61
70
|
let result = payload;
|
|
62
71
|
for (const fn of listeners) {
|
|
63
|
-
|
|
72
|
+
try {
|
|
73
|
+
const out = fn(result);
|
|
74
|
+
if (out && typeof out.then === "function") {
|
|
75
|
+
console.error(`[event-bus] Warning: async handler in sync pipe "${String(event)}" — use onPipeAsync instead`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
result = out;
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
console.error(`[event-bus] Pipe handler error in "${String(event)}":`, err instanceof Error ? err.message : err);
|
|
82
|
+
}
|
|
64
83
|
}
|
|
65
84
|
return result;
|
|
66
85
|
}
|
package/dist/executor.d.ts
CHANGED
|
@@ -25,7 +25,8 @@ export declare function executeCommand(opts: {
|
|
|
25
25
|
done: Promise<void>;
|
|
26
26
|
};
|
|
27
27
|
/**
|
|
28
|
-
* Kill a running session's process group.
|
|
29
|
-
*
|
|
28
|
+
* Kill a running session's process group: SIGTERM, then SIGKILL after 5s.
|
|
29
|
+
* Returns a cleanup that cancels the pending SIGKILL — callers should invoke
|
|
30
|
+
* it once the process has exited.
|
|
30
31
|
*/
|
|
31
|
-
export declare function killSession(session: ExecutorSession): void;
|
|
32
|
+
export declare function killSession(session: ExecutorSession): () => void;
|
package/dist/executor.js
CHANGED
|
@@ -60,14 +60,15 @@ export function executeCommand(opts) {
|
|
|
60
60
|
};
|
|
61
61
|
child.stdout?.on("data", handleData);
|
|
62
62
|
child.stderr?.on("data", handleData);
|
|
63
|
-
|
|
63
|
+
let cancelKill;
|
|
64
64
|
const timer = setTimeout(() => {
|
|
65
65
|
if (!session.done) {
|
|
66
|
-
killSession(session);
|
|
66
|
+
cancelKill = killSession(session);
|
|
67
67
|
}
|
|
68
68
|
}, timeout);
|
|
69
69
|
child.on("exit", (code, signal) => {
|
|
70
70
|
clearTimeout(timer);
|
|
71
|
+
cancelKill?.();
|
|
71
72
|
session.exitCode = code ?? (signal ? -1 : null);
|
|
72
73
|
session.done = true;
|
|
73
74
|
session.process = null;
|
|
@@ -75,6 +76,7 @@ export function executeCommand(opts) {
|
|
|
75
76
|
});
|
|
76
77
|
child.on("error", (err) => {
|
|
77
78
|
clearTimeout(timer);
|
|
79
|
+
cancelKill?.();
|
|
78
80
|
if (!session.done) {
|
|
79
81
|
session.exitCode = -1;
|
|
80
82
|
session.output += `\nProcess error: ${err.message}`;
|
|
@@ -86,31 +88,32 @@ export function executeCommand(opts) {
|
|
|
86
88
|
return { session, done };
|
|
87
89
|
}
|
|
88
90
|
/**
|
|
89
|
-
* Kill a running session's process group.
|
|
90
|
-
*
|
|
91
|
+
* Kill a running session's process group: SIGTERM, then SIGKILL after 5s.
|
|
92
|
+
* Returns a cleanup that cancels the pending SIGKILL — callers should invoke
|
|
93
|
+
* it once the process has exited.
|
|
91
94
|
*/
|
|
92
95
|
export function killSession(session) {
|
|
93
96
|
const proc = session.process;
|
|
94
97
|
if (!proc || !proc.pid)
|
|
95
|
-
return;
|
|
98
|
+
return () => { };
|
|
96
99
|
try {
|
|
97
|
-
// Kill the entire process group
|
|
98
100
|
process.kill(-proc.pid, "SIGTERM");
|
|
99
101
|
}
|
|
100
|
-
catch {
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
// Fallback: SIGKILL after 5 seconds
|
|
102
|
+
catch { }
|
|
103
|
+
let settled = false;
|
|
104
104
|
const fallback = setTimeout(() => {
|
|
105
|
-
if (!session.done && proc.pid) {
|
|
105
|
+
if (!settled && !session.done && proc.pid) {
|
|
106
106
|
try {
|
|
107
107
|
process.kill(-proc.pid, "SIGKILL");
|
|
108
108
|
}
|
|
109
|
-
catch {
|
|
110
|
-
// Ignore
|
|
111
|
-
}
|
|
109
|
+
catch { }
|
|
112
110
|
}
|
|
113
111
|
}, 5000);
|
|
114
|
-
// Don't let the timer keep the process alive
|
|
115
112
|
fallback.unref();
|
|
113
|
+
return () => {
|
|
114
|
+
if (!settled) {
|
|
115
|
+
settled = true;
|
|
116
|
+
clearTimeout(fallback);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
116
119
|
}
|
|
@@ -14,3 +14,8 @@ import type { ExtensionContext } from "./types.js";
|
|
|
14
14
|
* Errors are non-fatal — logged via ui:error and skipped.
|
|
15
15
|
*/
|
|
16
16
|
export declare function loadExtensions(ctx: ExtensionContext, cliExtensions?: string[]): Promise<string[]>;
|
|
17
|
+
/**
|
|
18
|
+
* Reload user extensions (from ~/.agent-sh/extensions/).
|
|
19
|
+
* Tears down old registrations, busts the module cache, and re-activates.
|
|
20
|
+
*/
|
|
21
|
+
export declare function reloadExtensions(ctx: ExtensionContext): Promise<string[]>;
|
package/dist/extension-loader.js
CHANGED
|
@@ -17,6 +17,78 @@ async function ensureTsSupport() {
|
|
|
17
17
|
// tsx not available — TS extensions will fail with a clear error
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Wrap an ExtensionContext to track all registrations (bus.on, bus.onPipe,
|
|
22
|
+
* advise, command:register). Returns the wrapped context and a dispose()
|
|
23
|
+
* function that tears down everything registered through it.
|
|
24
|
+
*/
|
|
25
|
+
function createScopedContext(ctx, extensionName) {
|
|
26
|
+
const cleanups = [];
|
|
27
|
+
const bus = ctx.bus;
|
|
28
|
+
const scopedBus = Object.create(bus);
|
|
29
|
+
// Track bus.on registrations
|
|
30
|
+
scopedBus.on = ((event, fn) => {
|
|
31
|
+
bus.on(event, fn);
|
|
32
|
+
cleanups.push(() => bus.off(event, fn));
|
|
33
|
+
});
|
|
34
|
+
// Track bus.onPipe registrations
|
|
35
|
+
scopedBus.onPipe = ((event, fn) => {
|
|
36
|
+
bus.onPipe(event, fn);
|
|
37
|
+
cleanups.push(() => bus.offPipe(event, fn));
|
|
38
|
+
});
|
|
39
|
+
// Track advise registrations
|
|
40
|
+
const scopedAdvise = (name, wrapper) => {
|
|
41
|
+
const unadvise = ctx.advise(name, wrapper);
|
|
42
|
+
cleanups.push(unadvise);
|
|
43
|
+
return unadvise;
|
|
44
|
+
};
|
|
45
|
+
// Track instruction registrations — extension name captured in scope
|
|
46
|
+
const scopedRegisterInstruction = (name, text) => {
|
|
47
|
+
bus.emit("agent:register-instruction", { name, text, extensionName });
|
|
48
|
+
cleanups.push(() => bus.emit("agent:remove-instruction", { name }));
|
|
49
|
+
};
|
|
50
|
+
// Track skill registrations — extension name captured in scope
|
|
51
|
+
const scopedRegisterSkill = (name, description, filePath) => {
|
|
52
|
+
bus.emit("agent:register-skill", { name, description, filePath, extensionName });
|
|
53
|
+
cleanups.push(() => bus.emit("agent:remove-skill", { name }));
|
|
54
|
+
};
|
|
55
|
+
// Track tool registrations — extension name captured in scope
|
|
56
|
+
const scopedRegisterTool = (tool) => {
|
|
57
|
+
bus.emit("agent:register-tool", { tool, extensionName });
|
|
58
|
+
cleanups.push(() => bus.emit("agent:unregister-tool", { name: tool.name }));
|
|
59
|
+
};
|
|
60
|
+
// Track slash command registrations — without this, reloading an
|
|
61
|
+
// extension stacks its commands (old `/status` + new `/status`) in
|
|
62
|
+
// the slash-commands registry.
|
|
63
|
+
const scopedRegisterCommand = (name, description, handler) => {
|
|
64
|
+
ctx.registerCommand(name, description, handler);
|
|
65
|
+
cleanups.push(() => bus.emit("command:unregister", { name }));
|
|
66
|
+
};
|
|
67
|
+
const scoped = {
|
|
68
|
+
...ctx,
|
|
69
|
+
bus: scopedBus,
|
|
70
|
+
advise: scopedAdvise,
|
|
71
|
+
registerInstruction: scopedRegisterInstruction,
|
|
72
|
+
removeInstruction: ctx.removeInstruction,
|
|
73
|
+
registerSkill: scopedRegisterSkill,
|
|
74
|
+
removeSkill: ctx.removeSkill,
|
|
75
|
+
registerTool: scopedRegisterTool,
|
|
76
|
+
unregisterTool: ctx.unregisterTool,
|
|
77
|
+
registerCommand: scopedRegisterCommand,
|
|
78
|
+
};
|
|
79
|
+
const dispose = () => {
|
|
80
|
+
for (const fn of cleanups) {
|
|
81
|
+
try {
|
|
82
|
+
fn();
|
|
83
|
+
}
|
|
84
|
+
catch { /* ignore */ }
|
|
85
|
+
}
|
|
86
|
+
cleanups.length = 0;
|
|
87
|
+
};
|
|
88
|
+
return { scoped, dispose };
|
|
89
|
+
}
|
|
90
|
+
// Track disposers for user extensions so reload can tear them down
|
|
91
|
+
const extensionDisposers = new Map();
|
|
20
92
|
/**
|
|
21
93
|
* Load extensions from three sources (merged, deduplicated):
|
|
22
94
|
*
|
|
@@ -43,19 +115,39 @@ export async function loadExtensions(ctx, cliExtensions) {
|
|
|
43
115
|
specifiers.push(...settings.extensions);
|
|
44
116
|
}
|
|
45
117
|
// 3. ~/.agent-sh/extensions/ directory
|
|
118
|
+
const userSpecifiers = await discoverUserExtensions();
|
|
119
|
+
specifiers.push(...userSpecifiers);
|
|
120
|
+
// Deduplicate
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
const unique = specifiers.filter((s) => {
|
|
123
|
+
if (seen.has(s))
|
|
124
|
+
return false;
|
|
125
|
+
seen.add(s);
|
|
126
|
+
return true;
|
|
127
|
+
});
|
|
128
|
+
// Load each extension (user extensions get scoped contexts for reloadability)
|
|
129
|
+
const loaded = await loadSpecifiers(unique, ctx, false, userSpecifiers);
|
|
130
|
+
return loaded;
|
|
131
|
+
}
|
|
132
|
+
async function discoverUserExtensions() {
|
|
133
|
+
const specifiers = [];
|
|
134
|
+
const disabled = new Set(getSettings().disabledExtensions ?? []);
|
|
46
135
|
try {
|
|
47
136
|
const entries = await fs.readdir(EXT_DIR, { withFileTypes: true });
|
|
48
137
|
for (const entry of entries) {
|
|
138
|
+
// Disable check: directory name for dir-extensions, or basename sans
|
|
139
|
+
// extension for file-extensions. Lets settings.json turn one off
|
|
140
|
+
// without renaming it.
|
|
141
|
+
const nameForDisable = entry.name.replace(/\.[^.]+$/, "");
|
|
142
|
+
if (disabled.has(nameForDisable))
|
|
143
|
+
continue;
|
|
49
144
|
const fullPath = path.join(EXT_DIR, entry.name);
|
|
50
|
-
// Resolve symlinks to check if they point to directories
|
|
51
145
|
const isDir = entry.isDirectory() ||
|
|
52
146
|
(entry.isSymbolicLink() && (await fs.stat(fullPath)).isDirectory());
|
|
53
147
|
if (isDir) {
|
|
54
|
-
// Directory extension: look for index.{ts,js,mjs,...}
|
|
55
148
|
const indexFile = await findIndex(fullPath);
|
|
56
|
-
if (indexFile)
|
|
149
|
+
if (indexFile)
|
|
57
150
|
specifiers.push(indexFile);
|
|
58
|
-
}
|
|
59
151
|
}
|
|
60
152
|
else if (SCRIPT_EXTS.some((ext) => entry.name.endsWith(ext))) {
|
|
61
153
|
specifiers.push(fullPath);
|
|
@@ -65,22 +157,22 @@ export async function loadExtensions(ctx, cliExtensions) {
|
|
|
65
157
|
catch {
|
|
66
158
|
// Directory doesn't exist — no user extensions
|
|
67
159
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return false;
|
|
73
|
-
seen.add(s);
|
|
74
|
-
return true;
|
|
75
|
-
});
|
|
76
|
-
// Load each extension
|
|
160
|
+
return specifiers;
|
|
161
|
+
}
|
|
162
|
+
async function loadSpecifiers(specifiers, ctx, bustCache, userSpecifiers) {
|
|
163
|
+
const userSet = new Set(userSpecifiers ?? []);
|
|
77
164
|
const loaded = [];
|
|
78
|
-
for (const specifier of
|
|
165
|
+
for (const specifier of specifiers) {
|
|
79
166
|
try {
|
|
80
|
-
|
|
167
|
+
let importPath = await resolveSpecifier(specifier);
|
|
81
168
|
if (TS_EXTS.some((ext) => importPath.endsWith(ext))) {
|
|
82
169
|
await ensureTsSupport();
|
|
83
170
|
}
|
|
171
|
+
// Append timestamp query to bust Node's module cache on reload
|
|
172
|
+
if (bustCache) {
|
|
173
|
+
const sep = importPath.includes("?") ? "&" : "?";
|
|
174
|
+
importPath += `${sep}t=${Date.now()}`;
|
|
175
|
+
}
|
|
84
176
|
const mod = await import(importPath);
|
|
85
177
|
// tsx may double-wrap default exports: mod.default.default
|
|
86
178
|
const activate = typeof mod.default === "function"
|
|
@@ -89,10 +181,25 @@ export async function loadExtensions(ctx, cliExtensions) {
|
|
|
89
181
|
? mod.default.default
|
|
90
182
|
: mod.activate;
|
|
91
183
|
if (typeof activate === "function") {
|
|
92
|
-
activate(ctx);
|
|
93
|
-
// Extract a short name from the specifier
|
|
94
184
|
const base = path.basename(specifier).replace(/\.(ts|js|mjs|mts|tsx)$/, "");
|
|
95
185
|
const name = base === "index" ? path.basename(path.dirname(specifier)) : base;
|
|
186
|
+
// Scoped context so /reload can tear user extensions down.
|
|
187
|
+
// Awaiting activate() lets extensions with async setup (e.g.
|
|
188
|
+
// openrouter fetching its model catalog) finish before we move
|
|
189
|
+
// on; a 10s outer timeout in index.ts guards against hangs.
|
|
190
|
+
if (userSet.has(specifier)) {
|
|
191
|
+
// Dispose previous load if reloading
|
|
192
|
+
extensionDisposers.get(name)?.();
|
|
193
|
+
const { scoped, dispose } = createScopedContext(ctx, name);
|
|
194
|
+
await activate(scoped);
|
|
195
|
+
extensionDisposers.set(name, dispose);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
const { scoped, dispose } = createScopedContext(ctx, name);
|
|
199
|
+
await activate(scoped);
|
|
200
|
+
// Non-user extensions aren't reloadable, but track for cleanup on shutdown
|
|
201
|
+
extensionDisposers.set(name, dispose);
|
|
202
|
+
}
|
|
96
203
|
loaded.push(name);
|
|
97
204
|
}
|
|
98
205
|
}
|
|
@@ -104,6 +211,14 @@ export async function loadExtensions(ctx, cliExtensions) {
|
|
|
104
211
|
}
|
|
105
212
|
return loaded;
|
|
106
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Reload user extensions (from ~/.agent-sh/extensions/).
|
|
216
|
+
* Tears down old registrations, busts the module cache, and re-activates.
|
|
217
|
+
*/
|
|
218
|
+
export async function reloadExtensions(ctx) {
|
|
219
|
+
const specifiers = await discoverUserExtensions();
|
|
220
|
+
return loadSpecifiers(specifiers, ctx, true, specifiers);
|
|
221
|
+
}
|
|
107
222
|
/**
|
|
108
223
|
* Find an index file in a directory extension.
|
|
109
224
|
*/
|
|
@@ -136,8 +251,17 @@ async function resolveSpecifier(specifier) {
|
|
|
136
251
|
resolved = specifier;
|
|
137
252
|
}
|
|
138
253
|
else {
|
|
139
|
-
//
|
|
140
|
-
|
|
254
|
+
// Distinguish bare npm specifier from a relative path lacking "./".
|
|
255
|
+
// Scoped packages ("@scope/pkg") contain "/" but are npm specifiers,
|
|
256
|
+
// so the "@" prefix takes precedence over the "/" heuristic.
|
|
257
|
+
if (specifier.includes("/") && !specifier.startsWith("@")) {
|
|
258
|
+
// Treat as relative path from cwd
|
|
259
|
+
resolved = path.resolve(process.cwd(), specifier);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
// Bare specifier — npm package (including @scope/pkg)
|
|
263
|
+
return specifier;
|
|
264
|
+
}
|
|
141
265
|
}
|
|
142
266
|
// If it's a directory, find the index file
|
|
143
267
|
try {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in agent backend extension.
|
|
3
|
+
*
|
|
4
|
+
* Constructs the AgentLoop synchronously with a placeholder LlmClient,
|
|
5
|
+
* so core handlers (history:append, system-prompt:build, conversation:*)
|
|
6
|
+
* are defined before user extensions activate. Mode resolution is
|
|
7
|
+
* deferred to `core:extensions-loaded`, giving runtime-registered
|
|
8
|
+
* providers (e.g. openrouter) a chance to register before we look up
|
|
9
|
+
* settings.defaultProvider. Without this deferral, a persisted
|
|
10
|
+
* `defaultProvider: "openrouter"` loses to a cold-start race and the
|
|
11
|
+
* backend bails silently.
|
|
12
|
+
*/
|
|
13
|
+
import type { ExtensionContext } from "../types.js";
|
|
14
|
+
export default function agentBackend(ctx: ExtensionContext): void;
|