agent-sh 0.9.0 → 0.10.1
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 +25 -30
- package/dist/agent/agent-loop.d.ts +43 -6
- package/dist/agent/agent-loop.js +817 -157
- package/dist/agent/conversation-state.d.ts +72 -21
- package/dist/agent/conversation-state.js +364 -151
- 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 +10 -13
- package/dist/agent/token-budget.js +6 -46
- 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 -2
- package/dist/context-manager.d.ts +16 -19
- package/dist/context-manager.js +48 -152
- package/dist/core.js +27 -6
- package/dist/event-bus.d.ts +59 -3
- package/dist/executor.d.ts +4 -3
- package/dist/executor.js +18 -15
- package/dist/extension-loader.js +75 -17
- package/dist/extensions/agent-backend.d.ts +8 -7
- package/dist/extensions/agent-backend.js +72 -50
- package/dist/extensions/index.js +0 -2
- package/dist/extensions/slash-commands.js +14 -9
- package/dist/extensions/tui-renderer.js +67 -80
- package/dist/index.js +25 -6
- package/dist/settings.d.ts +39 -16
- package/dist/settings.js +51 -11
- package/dist/shell/input-handler.d.ts +2 -1
- package/dist/shell/input-handler.js +84 -76
- package/dist/shell/shell.js +19 -2
- package/dist/types.d.ts +15 -0
- package/dist/utils/ansi.d.ts +7 -0
- package/dist/utils/ansi.js +69 -8
- package/dist/utils/box-frame.js +8 -2
- 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/markdown.js +23 -8
- package/dist/utils/package-version.d.ts +1 -0
- package/dist/utils/package-version.js +10 -0
- package/dist/utils/shell-output-spill.d.ts +2 -0
- package/dist/utils/shell-output-spill.js +81 -0
- 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/README.md +14 -0
- package/examples/extensions/claude-code-bridge/index.ts +204 -145
- 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/README.md +16 -0
- package/examples/extensions/pi-bridge/index.ts +9 -155
- 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/shell-recall.d.ts +0 -9
- package/dist/extensions/shell-recall.js +0 -8
- package/dist/extensions/terminal-buffer.d.ts +0 -14
- package/dist/extensions/terminal-buffer.js +0 -134
package/dist/extension-loader.js
CHANGED
|
@@ -5,12 +5,33 @@ const EXT_DIR = path.join(CONFIG_DIR, "extensions");
|
|
|
5
5
|
const TS_EXTS = [".ts", ".tsx", ".mts"];
|
|
6
6
|
const SCRIPT_EXTS = [".js", ".mjs", ".ts", ".tsx", ".mts"];
|
|
7
7
|
let tsRegistered = false;
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
let tsxUnregister = null;
|
|
9
|
+
/**
|
|
10
|
+
* Register tsx's ESM loader for .ts file support.
|
|
11
|
+
*
|
|
12
|
+
* Called before importing .ts extensions. The tsx loader uses Node's
|
|
13
|
+
* module.register() which creates a background thread with a MessageChannel.
|
|
14
|
+
* On reload, the old loader may become stale (the MessageChannel port can be
|
|
15
|
+
* GC'd or the loader thread can stop responding), so we unregister the old
|
|
16
|
+
* handle and re-register on each reload.
|
|
17
|
+
*
|
|
18
|
+
* Initial load: registers fresh.
|
|
19
|
+
* Reload: unregisters old handle, registers new one.
|
|
20
|
+
* Non-reload calls within the same load: no-op (tsRegistered guard).
|
|
21
|
+
*/
|
|
22
|
+
async function ensureTsSupport(force = false) {
|
|
23
|
+
if (tsRegistered && !force)
|
|
10
24
|
return;
|
|
11
25
|
try {
|
|
26
|
+
// Unregister previous loader if reloading
|
|
27
|
+
if (tsxUnregister) {
|
|
28
|
+
try {
|
|
29
|
+
await tsxUnregister();
|
|
30
|
+
}
|
|
31
|
+
catch { /* ignore stale handle */ }
|
|
32
|
+
}
|
|
12
33
|
const { register } = await import("tsx/esm/api");
|
|
13
|
-
register();
|
|
34
|
+
tsxUnregister = register();
|
|
14
35
|
tsRegistered = true;
|
|
15
36
|
}
|
|
16
37
|
catch {
|
|
@@ -22,7 +43,7 @@ async function ensureTsSupport() {
|
|
|
22
43
|
* advise, command:register). Returns the wrapped context and a dispose()
|
|
23
44
|
* function that tears down everything registered through it.
|
|
24
45
|
*/
|
|
25
|
-
function createScopedContext(ctx) {
|
|
46
|
+
function createScopedContext(ctx, extensionName) {
|
|
26
47
|
const cleanups = [];
|
|
27
48
|
const bus = ctx.bus;
|
|
28
49
|
const scopedBus = Object.create(bus);
|
|
@@ -42,15 +63,27 @@ function createScopedContext(ctx) {
|
|
|
42
63
|
cleanups.push(unadvise);
|
|
43
64
|
return unadvise;
|
|
44
65
|
};
|
|
45
|
-
// Track instruction registrations
|
|
66
|
+
// Track instruction registrations — extension name captured in scope
|
|
46
67
|
const scopedRegisterInstruction = (name, text) => {
|
|
47
|
-
|
|
48
|
-
cleanups.push(() =>
|
|
68
|
+
bus.emit("agent:register-instruction", { name, text, extensionName });
|
|
69
|
+
cleanups.push(() => bus.emit("agent:remove-instruction", { name }));
|
|
70
|
+
};
|
|
71
|
+
// Track skill registrations — extension name captured in scope
|
|
72
|
+
const scopedRegisterSkill = (name, description, filePath) => {
|
|
73
|
+
bus.emit("agent:register-skill", { name, description, filePath, extensionName });
|
|
74
|
+
cleanups.push(() => bus.emit("agent:remove-skill", { name }));
|
|
49
75
|
};
|
|
50
|
-
// Track tool registrations
|
|
76
|
+
// Track tool registrations — extension name captured in scope
|
|
51
77
|
const scopedRegisterTool = (tool) => {
|
|
52
|
-
|
|
53
|
-
cleanups.push(() =>
|
|
78
|
+
bus.emit("agent:register-tool", { tool, extensionName });
|
|
79
|
+
cleanups.push(() => bus.emit("agent:unregister-tool", { name: tool.name }));
|
|
80
|
+
};
|
|
81
|
+
// Track slash command registrations — without this, reloading an
|
|
82
|
+
// extension stacks its commands (old `/status` + new `/status`) in
|
|
83
|
+
// the slash-commands registry.
|
|
84
|
+
const scopedRegisterCommand = (name, description, handler) => {
|
|
85
|
+
ctx.registerCommand(name, description, handler);
|
|
86
|
+
cleanups.push(() => bus.emit("command:unregister", { name }));
|
|
54
87
|
};
|
|
55
88
|
const scoped = {
|
|
56
89
|
...ctx,
|
|
@@ -58,8 +91,11 @@ function createScopedContext(ctx) {
|
|
|
58
91
|
advise: scopedAdvise,
|
|
59
92
|
registerInstruction: scopedRegisterInstruction,
|
|
60
93
|
removeInstruction: ctx.removeInstruction,
|
|
94
|
+
registerSkill: scopedRegisterSkill,
|
|
95
|
+
removeSkill: ctx.removeSkill,
|
|
61
96
|
registerTool: scopedRegisterTool,
|
|
62
97
|
unregisterTool: ctx.unregisterTool,
|
|
98
|
+
registerCommand: scopedRegisterCommand,
|
|
63
99
|
};
|
|
64
100
|
const dispose = () => {
|
|
65
101
|
for (const fn of cleanups) {
|
|
@@ -116,9 +152,16 @@ export async function loadExtensions(ctx, cliExtensions) {
|
|
|
116
152
|
}
|
|
117
153
|
async function discoverUserExtensions() {
|
|
118
154
|
const specifiers = [];
|
|
155
|
+
const disabled = new Set(getSettings().disabledExtensions ?? []);
|
|
119
156
|
try {
|
|
120
157
|
const entries = await fs.readdir(EXT_DIR, { withFileTypes: true });
|
|
121
158
|
for (const entry of entries) {
|
|
159
|
+
// Disable check: directory name for dir-extensions, or basename sans
|
|
160
|
+
// extension for file-extensions. Lets settings.json turn one off
|
|
161
|
+
// without renaming it.
|
|
162
|
+
const nameForDisable = entry.name.replace(/\.[^.]+$/, "");
|
|
163
|
+
if (disabled.has(nameForDisable))
|
|
164
|
+
continue;
|
|
122
165
|
const fullPath = path.join(EXT_DIR, entry.name);
|
|
123
166
|
const isDir = entry.isDirectory() ||
|
|
124
167
|
(entry.isSymbolicLink() && (await fs.stat(fullPath)).isDirectory());
|
|
@@ -144,7 +187,7 @@ async function loadSpecifiers(specifiers, ctx, bustCache, userSpecifiers) {
|
|
|
144
187
|
try {
|
|
145
188
|
let importPath = await resolveSpecifier(specifier);
|
|
146
189
|
if (TS_EXTS.some((ext) => importPath.endsWith(ext))) {
|
|
147
|
-
await ensureTsSupport();
|
|
190
|
+
await ensureTsSupport(bustCache);
|
|
148
191
|
}
|
|
149
192
|
// Append timestamp query to bust Node's module cache on reload
|
|
150
193
|
if (bustCache) {
|
|
@@ -161,16 +204,22 @@ async function loadSpecifiers(specifiers, ctx, bustCache, userSpecifiers) {
|
|
|
161
204
|
if (typeof activate === "function") {
|
|
162
205
|
const base = path.basename(specifier).replace(/\.(ts|js|mjs|mts|tsx)$/, "");
|
|
163
206
|
const name = base === "index" ? path.basename(path.dirname(specifier)) : base;
|
|
164
|
-
//
|
|
207
|
+
// Scoped context so /reload can tear user extensions down.
|
|
208
|
+
// Awaiting activate() lets extensions with async setup (e.g.
|
|
209
|
+
// openrouter fetching its model catalog) finish before we move
|
|
210
|
+
// on; a 10s outer timeout in index.ts guards against hangs.
|
|
165
211
|
if (userSet.has(specifier)) {
|
|
166
212
|
// Dispose previous load if reloading
|
|
167
213
|
extensionDisposers.get(name)?.();
|
|
168
|
-
const { scoped, dispose } = createScopedContext(ctx);
|
|
169
|
-
activate(scoped);
|
|
214
|
+
const { scoped, dispose } = createScopedContext(ctx, name);
|
|
215
|
+
await activate(scoped);
|
|
170
216
|
extensionDisposers.set(name, dispose);
|
|
171
217
|
}
|
|
172
218
|
else {
|
|
173
|
-
|
|
219
|
+
const { scoped, dispose } = createScopedContext(ctx, name);
|
|
220
|
+
await activate(scoped);
|
|
221
|
+
// Non-user extensions aren't reloadable, but track for cleanup on shutdown
|
|
222
|
+
extensionDisposers.set(name, dispose);
|
|
174
223
|
}
|
|
175
224
|
loaded.push(name);
|
|
176
225
|
}
|
|
@@ -223,8 +272,17 @@ async function resolveSpecifier(specifier) {
|
|
|
223
272
|
resolved = specifier;
|
|
224
273
|
}
|
|
225
274
|
else {
|
|
226
|
-
//
|
|
227
|
-
|
|
275
|
+
// Distinguish bare npm specifier from a relative path lacking "./".
|
|
276
|
+
// Scoped packages ("@scope/pkg") contain "/" but are npm specifiers,
|
|
277
|
+
// so the "@" prefix takes precedence over the "/" heuristic.
|
|
278
|
+
if (specifier.includes("/") && !specifier.startsWith("@")) {
|
|
279
|
+
// Treat as relative path from cwd
|
|
280
|
+
resolved = path.resolve(process.cwd(), specifier);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
// Bare specifier — npm package (including @scope/pkg)
|
|
284
|
+
return specifier;
|
|
285
|
+
}
|
|
228
286
|
}
|
|
229
287
|
// If it's a directory, find the index file
|
|
230
288
|
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;
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import { AgentLoop } from "../agent/agent-loop.js";
|
|
2
2
|
import { LlmClient } from "../utils/llm-client.js";
|
|
3
3
|
import { resolveProvider, getProviderNames, getSettings } from "../settings.js";
|
|
4
|
+
import { PACKAGE_VERSION } from "../utils/package-version.js";
|
|
5
|
+
/** Read the user's persisted defaultModel for a provider, if any. */
|
|
6
|
+
function persistedModelFor(providerName) {
|
|
7
|
+
if (!providerName)
|
|
8
|
+
return undefined;
|
|
9
|
+
return getSettings().providers?.[providerName]?.defaultModel;
|
|
10
|
+
}
|
|
4
11
|
export default function agentBackend(ctx) {
|
|
5
12
|
const { bus } = ctx;
|
|
6
|
-
// ── Resolve providers ──────────────────────────────────────
|
|
7
13
|
const config = ctx.call("config:get-shell-config") ?? {};
|
|
8
|
-
|
|
9
|
-
let activeProvider = null;
|
|
14
|
+
// Seed from settings.json; runtime provider:register events add more.
|
|
10
15
|
const providerRegistry = new Map();
|
|
11
16
|
for (const name of getProviderNames()) {
|
|
12
17
|
const p = resolveProvider(name);
|
|
13
18
|
if (p)
|
|
14
19
|
providerRegistry.set(name, p);
|
|
15
20
|
}
|
|
16
|
-
const providerName = config.provider ?? settings.defaultProvider;
|
|
17
|
-
if (providerName) {
|
|
18
|
-
activeProvider = providerRegistry.get(providerName) ?? null;
|
|
19
|
-
}
|
|
20
|
-
// ── Build modes ────────────────────────────────────────────
|
|
21
21
|
const buildModes = () => {
|
|
22
22
|
const allModes = [];
|
|
23
23
|
for (const [id, p] of providerRegistry) {
|
|
@@ -37,59 +37,76 @@ export default function agentBackend(ctx) {
|
|
|
37
37
|
}
|
|
38
38
|
return allModes;
|
|
39
39
|
};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (modes.length === 0 && effectiveApiKey && effectiveModel) {
|
|
45
|
-
modes = [{ model: effectiveModel }];
|
|
46
|
-
}
|
|
47
|
-
const initialModeIndex = Math.max(0, modes.findIndex((m) => m.model === effectiveModel && (!activeProvider || m.provider === activeProvider.id)));
|
|
48
|
-
// ── Create LLM client ─────────────────────────────────────
|
|
49
|
-
if (!effectiveApiKey)
|
|
50
|
-
return; // No LLM provider configured — skip
|
|
51
|
-
if (!effectiveModel) {
|
|
52
|
-
bus.emit("ui:error", { message: "No model specified. Use --model or configure a provider with defaultModel in ~/.agent-sh/settings.json" });
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
const llmClient = new LlmClient({
|
|
56
|
-
apiKey: effectiveApiKey,
|
|
57
|
-
baseURL: effectiveBaseURL,
|
|
58
|
-
model: effectiveModel,
|
|
59
|
-
});
|
|
60
|
-
// Expose LLM client for other extensions (e.g. command-suggest)
|
|
40
|
+
// Placeholder client — reconfigured at core:extensions-loaded. Any
|
|
41
|
+
// stream() call before then fails from the OpenAI SDK; start() won't
|
|
42
|
+
// wire the loop until we've resolved, so users never hit that path.
|
|
43
|
+
const llmClient = new LlmClient({ apiKey: "not-configured", model: "not-configured" });
|
|
61
44
|
ctx.define("llm:get-client", () => llmClient);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
//
|
|
45
|
+
let modes = [];
|
|
46
|
+
let initialModeIndex = 0;
|
|
47
|
+
let resolved = false;
|
|
48
|
+
bus.onPipe("config:get-initial-modes", () => ({ modes, initialModeIndex }));
|
|
49
|
+
// AgentLoop must be constructed *before* user extensions activate,
|
|
50
|
+
// because its ctor defines handlers (history:append, etc.) that
|
|
51
|
+
// extensions like superash call synchronously during their own
|
|
52
|
+
// activate. Advise-before-define works for advisers, but plain calls
|
|
53
|
+
// would hit a no-op stub.
|
|
68
54
|
const agentLoop = new AgentLoop({
|
|
69
55
|
bus,
|
|
70
56
|
contextManager: ctx.contextManager,
|
|
71
57
|
llmClient,
|
|
72
|
-
handlers: { define: ctx.define, advise: ctx.advise, call: ctx.call },
|
|
58
|
+
handlers: { define: ctx.define, advise: ctx.advise, call: ctx.call, list: ctx.list },
|
|
73
59
|
modes,
|
|
74
60
|
initialModeIndex,
|
|
75
61
|
compositor: ctx.compositor,
|
|
62
|
+
instanceId: ctx.instanceId,
|
|
76
63
|
});
|
|
77
|
-
// Register as backend
|
|
78
64
|
bus.emit("agent:register-backend", {
|
|
79
65
|
name: "ash",
|
|
80
66
|
kill: () => agentLoop.kill(),
|
|
81
67
|
start: async () => {
|
|
68
|
+
if (!resolved) {
|
|
69
|
+
bus.emit("ui:error", { message: "Agent backend not started — no LLM provider available. See earlier messages." });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
82
72
|
agentLoop.wire();
|
|
83
73
|
bus.emit("agent:info", {
|
|
84
74
|
name: "ash",
|
|
85
|
-
version:
|
|
75
|
+
version: PACKAGE_VERSION,
|
|
86
76
|
model: llmClient.model,
|
|
87
77
|
provider: modes[initialModeIndex]?.provider,
|
|
88
78
|
contextWindow: modes[initialModeIndex]?.contextWindow,
|
|
89
79
|
});
|
|
90
80
|
},
|
|
91
81
|
});
|
|
92
|
-
|
|
82
|
+
bus.on("core:extensions-loaded", () => {
|
|
83
|
+
const settings = getSettings();
|
|
84
|
+
const providerName = config.provider ?? settings.defaultProvider;
|
|
85
|
+
const activeProvider = providerName ? providerRegistry.get(providerName) ?? null : null;
|
|
86
|
+
// User's persisted defaultModel wins over the provider's declared
|
|
87
|
+
// default. Dynamic providers (openrouter) re-register with their
|
|
88
|
+
// hardcoded DEFAULT_MODELS[0] each startup, which would otherwise
|
|
89
|
+
// clobber the user's /model selection.
|
|
90
|
+
const effectiveApiKey = config.apiKey ?? activeProvider?.apiKey;
|
|
91
|
+
const effectiveBaseURL = config.baseURL ?? activeProvider?.baseURL;
|
|
92
|
+
const effectiveModel = config.model ?? persistedModelFor(providerName) ?? activeProvider?.defaultModel;
|
|
93
|
+
if (!effectiveApiKey) {
|
|
94
|
+
bus.emit("ui:error", { message: "No LLM provider configured. Set --api-key, configure a provider in ~/.agent-sh/settings.json, or load a provider extension (e.g. openrouter) that sets OPENROUTER_API_KEY." });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!effectiveModel) {
|
|
98
|
+
bus.emit("ui:error", { message: "No model specified. Use --model or configure a provider with defaultModel in ~/.agent-sh/settings.json" });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
modes = buildModes();
|
|
102
|
+
if (modes.length === 0)
|
|
103
|
+
modes = [{ model: effectiveModel }];
|
|
104
|
+
initialModeIndex = Math.max(0, modes.findIndex((m) => m.model === effectiveModel && (!activeProvider || m.provider === activeProvider.id)));
|
|
105
|
+
llmClient.reconfigure({ apiKey: effectiveApiKey, baseURL: effectiveBaseURL, model: effectiveModel });
|
|
106
|
+
bus.emit("config:set-modes", { modes, activeIndex: initialModeIndex });
|
|
107
|
+
resolved = true;
|
|
108
|
+
// start() emits agent:info after wiring.
|
|
109
|
+
});
|
|
93
110
|
bus.on("provider:register", (p) => {
|
|
94
111
|
const rawModels = p.models ?? (p.defaultModel ? [p.defaultModel] : []);
|
|
95
112
|
const modelIds = [];
|
|
@@ -124,16 +141,26 @@ export default function agentBackend(ctx) {
|
|
|
124
141
|
};
|
|
125
142
|
});
|
|
126
143
|
bus.emit("config:add-modes", { modes: addModes });
|
|
144
|
+
// Late-registration reconcile: if this completes the user's persisted
|
|
145
|
+
// default (openrouter's async fetch delivers the full catalog after
|
|
146
|
+
// we've already fallen back to mode 0), quietly switch to it.
|
|
147
|
+
if (!resolved)
|
|
148
|
+
return;
|
|
149
|
+
const pendingProvider = getSettings().defaultProvider;
|
|
150
|
+
if (pendingProvider !== p.id)
|
|
151
|
+
return;
|
|
152
|
+
const pendingModel = persistedModelFor(pendingProvider);
|
|
153
|
+
if (pendingModel && modelIds.includes(pendingModel) && llmClient.model !== pendingModel) {
|
|
154
|
+
bus.emit("config:switch-model", { model: pendingModel });
|
|
155
|
+
}
|
|
127
156
|
});
|
|
128
|
-
// ── Runtime provider switching ─────────────────────────────
|
|
129
157
|
bus.on("config:switch-provider", ({ provider: name }) => {
|
|
130
158
|
const p = providerRegistry.get(name);
|
|
131
159
|
if (!p) {
|
|
132
160
|
bus.emit("ui:error", { message: `Unknown provider: ${name}` });
|
|
133
161
|
return;
|
|
134
162
|
}
|
|
135
|
-
|
|
136
|
-
if (!newApiKey) {
|
|
163
|
+
if (!p.apiKey) {
|
|
137
164
|
bus.emit("ui:error", { message: `Provider "${name}" has no API key configured` });
|
|
138
165
|
return;
|
|
139
166
|
}
|
|
@@ -142,25 +169,20 @@ export default function agentBackend(ctx) {
|
|
|
142
169
|
bus.emit("ui:error", { message: `Provider "${name}" has no models configured` });
|
|
143
170
|
return;
|
|
144
171
|
}
|
|
145
|
-
llmClient.reconfigure({
|
|
146
|
-
apiKey: newApiKey,
|
|
147
|
-
baseURL: p.baseURL,
|
|
148
|
-
model: switchModel,
|
|
149
|
-
});
|
|
172
|
+
llmClient.reconfigure({ apiKey: p.apiKey, baseURL: p.baseURL, model: switchModel });
|
|
150
173
|
const newModes = p.models.map((m) => {
|
|
151
174
|
const mc = p.modelCapabilities?.get(m);
|
|
152
175
|
return {
|
|
153
176
|
model: m,
|
|
154
177
|
provider: name,
|
|
155
|
-
providerConfig: { apiKey:
|
|
178
|
+
providerConfig: { apiKey: p.apiKey, baseURL: p.baseURL },
|
|
156
179
|
contextWindow: mc?.contextWindow ?? p.contextWindow,
|
|
157
180
|
reasoning: mc?.reasoning,
|
|
158
181
|
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
159
182
|
};
|
|
160
183
|
});
|
|
161
184
|
bus.emit("config:set-modes", { modes: newModes });
|
|
162
|
-
|
|
163
|
-
bus.emit("agent:info", { name: "ash", version: "0.4", model: switchModel, provider: name, contextWindow: p.contextWindow });
|
|
185
|
+
bus.emit("agent:info", { name: "ash", version: PACKAGE_VERSION, model: switchModel, provider: name, contextWindow: p.contextWindow });
|
|
164
186
|
bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
|
|
165
187
|
bus.emit("config:changed", {});
|
|
166
188
|
});
|
package/dist/extensions/index.js
CHANGED
|
@@ -3,9 +3,7 @@ export const BUILTIN_EXTENSIONS = [
|
|
|
3
3
|
{ name: "tui-renderer", load: () => import("./tui-renderer.js").then(m => m.default) },
|
|
4
4
|
{ name: "slash-commands", load: () => import("./slash-commands.js").then(m => m.default) },
|
|
5
5
|
{ name: "file-autocomplete", load: () => import("./file-autocomplete.js").then(m => m.default) },
|
|
6
|
-
{ name: "shell-recall", load: () => import("./shell-recall.js").then(m => m.default) },
|
|
7
6
|
{ name: "command-suggest", load: () => import("./command-suggest.js").then(m => m.default) },
|
|
8
|
-
{ name: "terminal-buffer", load: () => import("./terminal-buffer.js").then(m => m.default) },
|
|
9
7
|
];
|
|
10
8
|
/**
|
|
11
9
|
* Load built-in extensions sequentially, skipping any in the disabled list.
|
|
@@ -79,7 +79,7 @@ export default function activate(ctx) {
|
|
|
79
79
|
});
|
|
80
80
|
register({
|
|
81
81
|
name: "/compact",
|
|
82
|
-
description: "Compact conversation
|
|
82
|
+
description: "Compact conversation via the active compaction strategy",
|
|
83
83
|
handler: () => {
|
|
84
84
|
bus.emit("agent:compact-request", {});
|
|
85
85
|
},
|
|
@@ -90,19 +90,15 @@ export default function activate(ctx) {
|
|
|
90
90
|
handler: () => {
|
|
91
91
|
const stats = bus.emitPipe("context:get-stats", {
|
|
92
92
|
activeTokens: 0,
|
|
93
|
-
|
|
94
|
-
recallArchiveSize: 0,
|
|
93
|
+
totalTokens: 0,
|
|
95
94
|
budgetTokens: 0,
|
|
96
95
|
});
|
|
97
96
|
const pct = stats.budgetTokens > 0
|
|
98
97
|
? Math.round((stats.activeTokens / stats.budgetTokens) * 100)
|
|
99
98
|
: 0;
|
|
100
|
-
|
|
101
|
-
`Active context: ~${stats.activeTokens.toLocaleString()} tokens / ${stats.budgetTokens.toLocaleString()} budget (${pct}%)`,
|
|
102
|
-
|
|
103
|
-
`Recall archive: ${stats.recallArchiveSize} entries`,
|
|
104
|
-
];
|
|
105
|
-
bus.emit("ui:info", { message: lines.join("\n") });
|
|
99
|
+
bus.emit("ui:info", {
|
|
100
|
+
message: `Active context: ~${stats.activeTokens.toLocaleString()} tokens / ${stats.budgetTokens.toLocaleString()} budget (${pct}%)`,
|
|
101
|
+
});
|
|
106
102
|
},
|
|
107
103
|
});
|
|
108
104
|
register({
|
|
@@ -118,10 +114,19 @@ export default function activate(ctx) {
|
|
|
118
114
|
}
|
|
119
115
|
},
|
|
120
116
|
});
|
|
117
|
+
// Handler form so extensions can trigger reload programmatically
|
|
118
|
+
// (e.g. an ash-callable reload_extensions tool in superash).
|
|
119
|
+
ctx.define("extensions:reload", async () => {
|
|
120
|
+
return await reloadExtensions(ctx);
|
|
121
|
+
});
|
|
121
122
|
// ── Extension registration ────────────────────────────────────
|
|
122
123
|
bus.on("command:register", (cmd) => {
|
|
123
124
|
register(cmd);
|
|
124
125
|
});
|
|
126
|
+
bus.on("command:unregister", ({ name }) => {
|
|
127
|
+
const key = name.startsWith("/") ? name : `/${name}`;
|
|
128
|
+
commands.delete(key);
|
|
129
|
+
});
|
|
125
130
|
// ── Skill commands (/skill:<name>) ────────────────────────────
|
|
126
131
|
const getSkills = () => {
|
|
127
132
|
const cwd = contextManager?.getCwd() ?? process.cwd();
|