agent-sh 0.9.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 +14 -21
- package/dist/agent/agent-loop.d.ts +43 -3
- package/dist/agent/agent-loop.js +811 -128
- package/dist/agent/conversation-state.d.ts +72 -21
- package/dist/agent/conversation-state.js +357 -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 +84 -3
- 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 +34 -1
- package/dist/agent/system-prompt.js +96 -47
- package/dist/agent/token-budget.d.ts +5 -4
- package/dist/agent/token-budget.js +14 -19
- package/dist/agent/tool-protocol.d.ts +23 -1
- package/dist/agent/tool-protocol.js +169 -4
- 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 +1 -1
- package/dist/context-manager.d.ts +17 -0
- package/dist/context-manager.js +37 -4
- package/dist/core.js +27 -6
- package/dist/event-bus.d.ts +59 -2
- package/dist/executor.d.ts +4 -3
- package/dist/executor.js +18 -15
- package/dist/extension-loader.js +50 -13
- package/dist/extensions/agent-backend.d.ts +8 -7
- package/dist/extensions/agent-backend.js +69 -48
- package/dist/extensions/index.js +0 -1
- package/dist/extensions/slash-commands.js +14 -9
- package/dist/extensions/tui-renderer.js +62 -78
- package/dist/index.js +25 -6
- package/dist/settings.d.ts +36 -5
- package/dist/settings.js +53 -9
- package/dist/shell/input-handler.d.ts +2 -1
- package/dist/shell/input-handler.js +82 -73
- package/dist/shell/shell.js +19 -2
- package/dist/types.d.ts +12 -0
- package/dist/utils/ansi.d.ts +5 -0
- package/dist/utils/ansi.js +1 -1
- package/dist/utils/compositor.d.ts +5 -0
- package/dist/utils/compositor.js +31 -3
- package/dist/utils/diff-renderer.d.ts +9 -0
- package/dist/utils/diff-renderer.js +221 -143
- package/dist/utils/diff.d.ts +21 -2
- package/dist/utils/diff.js +165 -89
- package/dist/utils/handler-registry.d.ts +5 -0
- package/dist/utils/handler-registry.js +6 -0
- package/dist/utils/line-editor.d.ts +11 -1
- package/dist/utils/line-editor.js +44 -5
- package/dist/utils/tool-display.d.ts +1 -1
- package/dist/utils/tool-display.js +4 -4
- package/examples/extensions/ash-acp-bridge/src/index.ts +4 -1
- package/examples/extensions/ash-mcp-bridge/index.ts +13 -3
- 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 +39 -25
- package/examples/extensions/overlay-agent.ts +3 -3
- package/examples/extensions/peer-mesh.ts +115 -0
- package/examples/extensions/pi-bridge/index.ts +2 -2
- package/examples/extensions/questionnaire.ts +16 -5
- package/examples/extensions/subagents.ts +19 -4
- package/examples/extensions/terminal-buffer.ts +163 -0
- package/examples/extensions/user-shell.ts +136 -0
- package/examples/extensions/web-access.ts +8 -0
- package/package.json +36 -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/terminal-buffer.d.ts +0 -14
- package/dist/extensions/terminal-buffer.js +0 -134
package/dist/agent/types.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export type ToolResultBody = {
|
|
|
38
38
|
maxLines?: number;
|
|
39
39
|
};
|
|
40
40
|
export interface ToolDisplayInfo {
|
|
41
|
-
kind: "read" | "write" | "execute" | "search"
|
|
41
|
+
kind: "read" | "write" | "execute" | "search";
|
|
42
42
|
locations?: {
|
|
43
43
|
path: string;
|
|
44
44
|
line?: number | null;
|
|
@@ -24,6 +24,23 @@ export declare class ContextManager {
|
|
|
24
24
|
* Optional start/end restrict to a line range (1-indexed).
|
|
25
25
|
*/
|
|
26
26
|
expand(ids: number[], start?: number, end?: number): string;
|
|
27
|
+
/**
|
|
28
|
+
* Return shell events with id > afterId, formatted as an incremental
|
|
29
|
+
* delta suitable for injection into conversation history. Skips
|
|
30
|
+
* agent-source commands (already visible in tool results). Returns
|
|
31
|
+
* null when nothing new exists.
|
|
32
|
+
*
|
|
33
|
+
* The motivation: resending the full <shell_context> every turn wastes
|
|
34
|
+
* tokens — N turns × full history = O(N²) cost for O(N) information.
|
|
35
|
+
* Instead we inject only new events as regular conversation messages,
|
|
36
|
+
* so the provider's prefix cache amortizes them to O(N).
|
|
37
|
+
*/
|
|
38
|
+
getEventsSince(afterId: number): {
|
|
39
|
+
text: string;
|
|
40
|
+
lastSeq: number;
|
|
41
|
+
} | null;
|
|
42
|
+
/** Highest exchange id seen so far (0 if none). */
|
|
43
|
+
lastSeq(): number;
|
|
27
44
|
/**
|
|
28
45
|
* One-line summaries of last N exchanges.
|
|
29
46
|
*/
|
package/dist/context-manager.js
CHANGED
|
@@ -140,6 +140,43 @@ export class ContextManager {
|
|
|
140
140
|
}
|
|
141
141
|
return results.join("\n\n");
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Return shell events with id > afterId, formatted as an incremental
|
|
145
|
+
* delta suitable for injection into conversation history. Skips
|
|
146
|
+
* agent-source commands (already visible in tool results). Returns
|
|
147
|
+
* null when nothing new exists.
|
|
148
|
+
*
|
|
149
|
+
* The motivation: resending the full <shell_context> every turn wastes
|
|
150
|
+
* tokens — N turns × full history = O(N²) cost for O(N) information.
|
|
151
|
+
* Instead we inject only new events as regular conversation messages,
|
|
152
|
+
* so the provider's prefix cache amortizes them to O(N).
|
|
153
|
+
*/
|
|
154
|
+
getEventsSince(afterId) {
|
|
155
|
+
const fresh = this.exchanges.filter((e) => e.id > afterId && !(e.type === "shell_command" && e.source === "agent"));
|
|
156
|
+
if (fresh.length === 0)
|
|
157
|
+
return null;
|
|
158
|
+
const lastSeq = this.exchanges[this.exchanges.length - 1].id;
|
|
159
|
+
// Apply per-type truncation so giant outputs don't blow up the turn.
|
|
160
|
+
const truncated = fresh.map((ex) => {
|
|
161
|
+
if (ex.type === "shell_command") {
|
|
162
|
+
const s = getSettings();
|
|
163
|
+
return {
|
|
164
|
+
...ex,
|
|
165
|
+
output: truncateOutput(ex.output, s.shellTruncateThreshold, s.shellHeadLines, s.shellTailLines, ex.id),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return { ...ex };
|
|
169
|
+
});
|
|
170
|
+
const body = truncated.map((ex) => this.formatExchangeTruncated(ex)).join("\n");
|
|
171
|
+
return {
|
|
172
|
+
text: `<shell-events>\n${body}</shell-events>`,
|
|
173
|
+
lastSeq,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/** Highest exchange id seen so far (0 if none). */
|
|
177
|
+
lastSeq() {
|
|
178
|
+
return this.exchanges.length === 0 ? 0 : this.exchanges[this.exchanges.length - 1].id;
|
|
179
|
+
}
|
|
143
180
|
/**
|
|
144
181
|
* One-line summaries of last N exchanges.
|
|
145
182
|
*/
|
|
@@ -229,13 +266,9 @@ export class ContextManager {
|
|
|
229
266
|
out += `The user interacts with a real shell (PTY) and sends you queries inline. You are there to help them with their tasks.\n`;
|
|
230
267
|
out += `\n`;
|
|
231
268
|
out += `IMPORTANT tool usage rules:\n`;
|
|
232
|
-
out += `- user_shell runs commands in the user's live shell (PTY). The user sees output directly — no summary needed.\n`;
|
|
233
269
|
out += `- Your internal tools (bash, read, write, ls, etc.) run in an isolated subprocess. The user CANNOT see their output.\n`;
|
|
234
|
-
out += `- When the user asks to see, list, view, or display anything, ALWAYS use user_shell. NEVER use internal tools like ls/read/bash for display — the user won't see it.\n`;
|
|
235
270
|
out += `- Only use internal tools when YOU need to reason about content silently (e.g. reading a file to answer a question about it).\n`;
|
|
236
|
-
out += `- After a user_shell command, the user already saw the output. Do NOT repeat or summarize it.\n`;
|
|
237
271
|
out += `- You can browse or search shell history with shell_recall.\n`;
|
|
238
|
-
out += `- You can browse or search evicted conversation turns with conversation_recall.\n`;
|
|
239
272
|
out += `\n`;
|
|
240
273
|
this.firstPrompt = false;
|
|
241
274
|
}
|
package/dist/core.js
CHANGED
|
@@ -24,7 +24,11 @@ import * as settingsMod from "./settings.js";
|
|
|
24
24
|
import { HandlerRegistry } from "./utils/handler-registry.js";
|
|
25
25
|
import { TerminalBuffer } from "./utils/terminal-buffer.js";
|
|
26
26
|
import crypto from "node:crypto";
|
|
27
|
+
import * as fs from "node:fs";
|
|
28
|
+
import * as path from "node:path";
|
|
29
|
+
import * as os from "node:os";
|
|
27
30
|
import { DefaultCompositor, StdoutSurface } from "./utils/compositor.js";
|
|
31
|
+
const STORAGE_ROOT = path.join(os.homedir(), ".agent-sh");
|
|
28
32
|
// Re-export types that library consumers need
|
|
29
33
|
export { EventBus } from "./event-bus.js";
|
|
30
34
|
export { palette, setPalette, resetPalette } from "./utils/palette.js";
|
|
@@ -34,7 +38,10 @@ export function createCore(config) {
|
|
|
34
38
|
const bus = new EventBus();
|
|
35
39
|
const handlers = new HandlerRegistry();
|
|
36
40
|
const contextManager = new ContextManager(bus, handlers);
|
|
37
|
-
|
|
41
|
+
// 3 bytes = 6 hex chars, ~16M values — ample for per-lineage uniqueness and
|
|
42
|
+
// short enough to read/remember. Legacy content may have 16-char iids; any
|
|
43
|
+
// parsers should accept ≥6 hex chars.
|
|
44
|
+
const instanceId = crypto.randomBytes(3).toString("hex");
|
|
38
45
|
const settings = settingsMod.getSettings();
|
|
39
46
|
// Expose raw CLI config so the agent backend extension can resolve
|
|
40
47
|
// providers and create the LLM client.
|
|
@@ -63,7 +70,12 @@ export function createCore(config) {
|
|
|
63
70
|
backends.set(backend.name, backend);
|
|
64
71
|
});
|
|
65
72
|
bus.on("config:switch-backend", ({ name }) => {
|
|
66
|
-
activateByName(name)
|
|
73
|
+
activateByName(name).then(() => {
|
|
74
|
+
if (activeBackendName === name) {
|
|
75
|
+
settingsMod.updateSettings({ defaultBackend: name });
|
|
76
|
+
bus.emit("ui:info", { message: `Saved '${name}' as default backend.` });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
67
79
|
});
|
|
68
80
|
bus.on("config:list-backends", () => {
|
|
69
81
|
const names = [...backends.keys()];
|
|
@@ -77,7 +89,7 @@ export function createCore(config) {
|
|
|
77
89
|
return { names, active: activeBackendName };
|
|
78
90
|
});
|
|
79
91
|
// ── Compositor ──────────────────────────────────────────────
|
|
80
|
-
const compositor = new DefaultCompositor();
|
|
92
|
+
const compositor = new DefaultCompositor(bus);
|
|
81
93
|
const stdoutSurface = new StdoutSurface();
|
|
82
94
|
compositor.setDefault("agent", stdoutSurface);
|
|
83
95
|
compositor.setDefault("query", stdoutSurface);
|
|
@@ -145,7 +157,7 @@ export function createCore(config) {
|
|
|
145
157
|
bus.emit("agent:cancel-request", {});
|
|
146
158
|
},
|
|
147
159
|
extensionContext(opts) {
|
|
148
|
-
|
|
160
|
+
const ctx = {
|
|
149
161
|
bus,
|
|
150
162
|
contextManager,
|
|
151
163
|
instanceId,
|
|
@@ -154,15 +166,23 @@ export function createCore(config) {
|
|
|
154
166
|
createBlockTransform: (o) => streamTransform.createBlockTransform(bus, o),
|
|
155
167
|
createFencedBlockTransform: (o) => streamTransform.createFencedBlockTransform(bus, o),
|
|
156
168
|
getExtensionSettings: settingsMod.getExtensionSettings,
|
|
169
|
+
getStoragePath: (namespace) => {
|
|
170
|
+
const dir = path.join(STORAGE_ROOT, namespace);
|
|
171
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
172
|
+
return dir;
|
|
173
|
+
},
|
|
157
174
|
registerCommand: (name, description, handler) => bus.emit("command:register", { name, description, handler }),
|
|
158
|
-
registerTool: (tool) => bus.emit("agent:register-tool", { tool }),
|
|
175
|
+
registerTool: (tool) => bus.emit("agent:register-tool", { tool, extensionName: "" }),
|
|
159
176
|
unregisterTool: (name) => bus.emit("agent:unregister-tool", { name }),
|
|
160
177
|
getTools: () => bus.emitPipe("agent:get-tools", { tools: [] }).tools,
|
|
161
|
-
registerInstruction: (name, text) => bus.emit("agent:register-instruction", { name, text }),
|
|
178
|
+
registerInstruction: (name, text) => bus.emit("agent:register-instruction", { name, text, extensionName: "" }),
|
|
162
179
|
removeInstruction: (name) => bus.emit("agent:remove-instruction", { name }),
|
|
180
|
+
registerSkill: (name, description, filePath) => bus.emit("agent:register-skill", { name, description, filePath, extensionName: "" }),
|
|
181
|
+
removeSkill: (name) => bus.emit("agent:remove-skill", { name }),
|
|
163
182
|
define: (name, fn) => handlers.define(name, fn),
|
|
164
183
|
advise: (name, wrapper) => handlers.advise(name, wrapper),
|
|
165
184
|
call: (name, ...args) => handlers.call(name, ...args),
|
|
185
|
+
list: () => handlers.list(),
|
|
166
186
|
get terminalBuffer() { return getTerminalBuffer(); },
|
|
167
187
|
compositor,
|
|
168
188
|
createRemoteSession: (opts) => {
|
|
@@ -207,6 +227,7 @@ export function createCore(config) {
|
|
|
207
227
|
};
|
|
208
228
|
},
|
|
209
229
|
};
|
|
230
|
+
return ctx;
|
|
210
231
|
},
|
|
211
232
|
kill() {
|
|
212
233
|
if (activeBackendName) {
|
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,6 +137,16 @@ 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
|
+
};
|
|
118
150
|
"tool:interactive-start": Record<string, never>;
|
|
119
151
|
"tool:interactive-end": Record<string, never>;
|
|
120
152
|
"permission:request": {
|
|
@@ -130,6 +162,9 @@ export interface ShellEvents {
|
|
|
130
162
|
description: string;
|
|
131
163
|
handler: (args: string) => Promise<void> | void;
|
|
132
164
|
};
|
|
165
|
+
"command:unregister": {
|
|
166
|
+
name: string;
|
|
167
|
+
};
|
|
133
168
|
"command:execute": {
|
|
134
169
|
name: string;
|
|
135
170
|
args: string;
|
|
@@ -143,6 +178,10 @@ export interface ShellEvents {
|
|
|
143
178
|
"ui:suggestion": {
|
|
144
179
|
text: string;
|
|
145
180
|
};
|
|
181
|
+
"compositor:write": {
|
|
182
|
+
stream: string;
|
|
183
|
+
text: string;
|
|
184
|
+
};
|
|
146
185
|
"input:keypress": {
|
|
147
186
|
key: string;
|
|
148
187
|
};
|
|
@@ -182,8 +221,7 @@ export interface ShellEvents {
|
|
|
182
221
|
"agent:compact-request": Record<string, never>;
|
|
183
222
|
"context:get-stats": {
|
|
184
223
|
activeTokens: number;
|
|
185
|
-
|
|
186
|
-
recallArchiveSize: number;
|
|
224
|
+
totalTokens: number;
|
|
187
225
|
budgetTokens: number;
|
|
188
226
|
};
|
|
189
227
|
"agent:register-backend": {
|
|
@@ -228,10 +266,12 @@ export interface ShellEvents {
|
|
|
228
266
|
};
|
|
229
267
|
"config:set-modes": {
|
|
230
268
|
modes: AgentMode[];
|
|
269
|
+
activeIndex?: number;
|
|
231
270
|
};
|
|
232
271
|
"config:add-modes": {
|
|
233
272
|
modes: AgentMode[];
|
|
234
273
|
};
|
|
274
|
+
"core:extensions-loaded": Record<string, never>;
|
|
235
275
|
"provider:register": {
|
|
236
276
|
id: string;
|
|
237
277
|
apiKey?: string;
|
|
@@ -247,6 +287,7 @@ export interface ShellEvents {
|
|
|
247
287
|
};
|
|
248
288
|
"agent:register-tool": {
|
|
249
289
|
tool: import("./agent/types.js").ToolDefinition;
|
|
290
|
+
extensionName?: string;
|
|
250
291
|
};
|
|
251
292
|
"agent:unregister-tool": {
|
|
252
293
|
name: string;
|
|
@@ -257,10 +298,26 @@ export interface ShellEvents {
|
|
|
257
298
|
"agent:register-instruction": {
|
|
258
299
|
name: string;
|
|
259
300
|
text: string;
|
|
301
|
+
extensionName: string;
|
|
260
302
|
};
|
|
261
303
|
"agent:remove-instruction": {
|
|
262
304
|
name: string;
|
|
263
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
|
+
};
|
|
264
321
|
"autocomplete:request": {
|
|
265
322
|
buffer: string;
|
|
266
323
|
/** Parsed slash command name (e.g. "/backend"), or null if not a command. */
|
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
|
}
|
package/dist/extension-loader.js
CHANGED
|
@@ -22,7 +22,7 @@ async function ensureTsSupport() {
|
|
|
22
22
|
* advise, command:register). Returns the wrapped context and a dispose()
|
|
23
23
|
* function that tears down everything registered through it.
|
|
24
24
|
*/
|
|
25
|
-
function createScopedContext(ctx) {
|
|
25
|
+
function createScopedContext(ctx, extensionName) {
|
|
26
26
|
const cleanups = [];
|
|
27
27
|
const bus = ctx.bus;
|
|
28
28
|
const scopedBus = Object.create(bus);
|
|
@@ -42,15 +42,27 @@ function createScopedContext(ctx) {
|
|
|
42
42
|
cleanups.push(unadvise);
|
|
43
43
|
return unadvise;
|
|
44
44
|
};
|
|
45
|
-
// Track instruction registrations
|
|
45
|
+
// Track instruction registrations — extension name captured in scope
|
|
46
46
|
const scopedRegisterInstruction = (name, text) => {
|
|
47
|
-
|
|
48
|
-
cleanups.push(() =>
|
|
47
|
+
bus.emit("agent:register-instruction", { name, text, extensionName });
|
|
48
|
+
cleanups.push(() => bus.emit("agent:remove-instruction", { name }));
|
|
49
49
|
};
|
|
50
|
-
// Track
|
|
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
|
|
51
56
|
const scopedRegisterTool = (tool) => {
|
|
52
|
-
|
|
53
|
-
cleanups.push(() =>
|
|
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 }));
|
|
54
66
|
};
|
|
55
67
|
const scoped = {
|
|
56
68
|
...ctx,
|
|
@@ -58,8 +70,11 @@ function createScopedContext(ctx) {
|
|
|
58
70
|
advise: scopedAdvise,
|
|
59
71
|
registerInstruction: scopedRegisterInstruction,
|
|
60
72
|
removeInstruction: ctx.removeInstruction,
|
|
73
|
+
registerSkill: scopedRegisterSkill,
|
|
74
|
+
removeSkill: ctx.removeSkill,
|
|
61
75
|
registerTool: scopedRegisterTool,
|
|
62
76
|
unregisterTool: ctx.unregisterTool,
|
|
77
|
+
registerCommand: scopedRegisterCommand,
|
|
63
78
|
};
|
|
64
79
|
const dispose = () => {
|
|
65
80
|
for (const fn of cleanups) {
|
|
@@ -116,9 +131,16 @@ export async function loadExtensions(ctx, cliExtensions) {
|
|
|
116
131
|
}
|
|
117
132
|
async function discoverUserExtensions() {
|
|
118
133
|
const specifiers = [];
|
|
134
|
+
const disabled = new Set(getSettings().disabledExtensions ?? []);
|
|
119
135
|
try {
|
|
120
136
|
const entries = await fs.readdir(EXT_DIR, { withFileTypes: true });
|
|
121
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;
|
|
122
144
|
const fullPath = path.join(EXT_DIR, entry.name);
|
|
123
145
|
const isDir = entry.isDirectory() ||
|
|
124
146
|
(entry.isSymbolicLink() && (await fs.stat(fullPath)).isDirectory());
|
|
@@ -161,16 +183,22 @@ async function loadSpecifiers(specifiers, ctx, bustCache, userSpecifiers) {
|
|
|
161
183
|
if (typeof activate === "function") {
|
|
162
184
|
const base = path.basename(specifier).replace(/\.(ts|js|mjs|mts|tsx)$/, "");
|
|
163
185
|
const name = base === "index" ? path.basename(path.dirname(specifier)) : base;
|
|
164
|
-
//
|
|
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.
|
|
165
190
|
if (userSet.has(specifier)) {
|
|
166
191
|
// Dispose previous load if reloading
|
|
167
192
|
extensionDisposers.get(name)?.();
|
|
168
|
-
const { scoped, dispose } = createScopedContext(ctx);
|
|
169
|
-
activate(scoped);
|
|
193
|
+
const { scoped, dispose } = createScopedContext(ctx, name);
|
|
194
|
+
await activate(scoped);
|
|
170
195
|
extensionDisposers.set(name, dispose);
|
|
171
196
|
}
|
|
172
197
|
else {
|
|
173
|
-
|
|
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);
|
|
174
202
|
}
|
|
175
203
|
loaded.push(name);
|
|
176
204
|
}
|
|
@@ -223,8 +251,17 @@ async function resolveSpecifier(specifier) {
|
|
|
223
251
|
resolved = specifier;
|
|
224
252
|
}
|
|
225
253
|
else {
|
|
226
|
-
//
|
|
227
|
-
|
|
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
|
+
}
|
|
228
265
|
}
|
|
229
266
|
// If it's a directory, find the index file
|
|
230
267
|
try {
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in agent backend extension.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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.
|
|
11
12
|
*/
|
|
12
13
|
import type { ExtensionContext } from "../types.js";
|
|
13
14
|
export default function agentBackend(ctx: ExtensionContext): void;
|