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
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { AgentLoop } from "../agent/agent-loop.js";
|
|
2
|
+
import { LlmClient } from "../utils/llm-client.js";
|
|
3
|
+
import { resolveProvider, getProviderNames, getSettings } from "../settings.js";
|
|
4
|
+
/** Read the user's persisted defaultModel for a provider, if any. */
|
|
5
|
+
function persistedModelFor(providerName) {
|
|
6
|
+
if (!providerName)
|
|
7
|
+
return undefined;
|
|
8
|
+
return getSettings().providers?.[providerName]?.defaultModel;
|
|
9
|
+
}
|
|
10
|
+
export default function agentBackend(ctx) {
|
|
11
|
+
const { bus } = ctx;
|
|
12
|
+
const config = ctx.call("config:get-shell-config") ?? {};
|
|
13
|
+
// Seed from settings.json; runtime provider:register events add more.
|
|
14
|
+
const providerRegistry = new Map();
|
|
15
|
+
for (const name of getProviderNames()) {
|
|
16
|
+
const p = resolveProvider(name);
|
|
17
|
+
if (p)
|
|
18
|
+
providerRegistry.set(name, p);
|
|
19
|
+
}
|
|
20
|
+
const buildModes = () => {
|
|
21
|
+
const allModes = [];
|
|
22
|
+
for (const [id, p] of providerRegistry) {
|
|
23
|
+
if (!p.apiKey)
|
|
24
|
+
continue;
|
|
25
|
+
for (const model of p.models) {
|
|
26
|
+
const mc = p.modelCapabilities?.get(model);
|
|
27
|
+
allModes.push({
|
|
28
|
+
model,
|
|
29
|
+
provider: id,
|
|
30
|
+
providerConfig: { apiKey: p.apiKey, baseURL: p.baseURL },
|
|
31
|
+
contextWindow: mc?.contextWindow ?? p.contextWindow,
|
|
32
|
+
reasoning: mc?.reasoning,
|
|
33
|
+
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return allModes;
|
|
38
|
+
};
|
|
39
|
+
// Placeholder client — reconfigured at core:extensions-loaded. Any
|
|
40
|
+
// stream() call before then fails from the OpenAI SDK; start() won't
|
|
41
|
+
// wire the loop until we've resolved, so users never hit that path.
|
|
42
|
+
const llmClient = new LlmClient({ apiKey: "not-configured", model: "not-configured" });
|
|
43
|
+
ctx.define("llm:get-client", () => llmClient);
|
|
44
|
+
let modes = [];
|
|
45
|
+
let initialModeIndex = 0;
|
|
46
|
+
let resolved = false;
|
|
47
|
+
bus.onPipe("config:get-initial-modes", () => ({ modes, initialModeIndex }));
|
|
48
|
+
// AgentLoop must be constructed *before* user extensions activate,
|
|
49
|
+
// because its ctor defines handlers (history:append, etc.) that
|
|
50
|
+
// extensions like superash call synchronously during their own
|
|
51
|
+
// activate. Advise-before-define works for advisers, but plain calls
|
|
52
|
+
// would hit a no-op stub.
|
|
53
|
+
const agentLoop = new AgentLoop({
|
|
54
|
+
bus,
|
|
55
|
+
contextManager: ctx.contextManager,
|
|
56
|
+
llmClient,
|
|
57
|
+
handlers: { define: ctx.define, advise: ctx.advise, call: ctx.call, list: ctx.list },
|
|
58
|
+
modes,
|
|
59
|
+
initialModeIndex,
|
|
60
|
+
compositor: ctx.compositor,
|
|
61
|
+
instanceId: ctx.instanceId,
|
|
62
|
+
});
|
|
63
|
+
bus.emit("agent:register-backend", {
|
|
64
|
+
name: "ash",
|
|
65
|
+
kill: () => agentLoop.kill(),
|
|
66
|
+
start: async () => {
|
|
67
|
+
if (!resolved) {
|
|
68
|
+
bus.emit("ui:error", { message: "Agent backend not started — no LLM provider available. See earlier messages." });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
agentLoop.wire();
|
|
72
|
+
bus.emit("agent:info", {
|
|
73
|
+
name: "ash",
|
|
74
|
+
version: "0.4",
|
|
75
|
+
model: llmClient.model,
|
|
76
|
+
provider: modes[initialModeIndex]?.provider,
|
|
77
|
+
contextWindow: modes[initialModeIndex]?.contextWindow,
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
bus.on("core:extensions-loaded", () => {
|
|
82
|
+
const settings = getSettings();
|
|
83
|
+
const providerName = config.provider ?? settings.defaultProvider;
|
|
84
|
+
const activeProvider = providerName ? providerRegistry.get(providerName) ?? null : null;
|
|
85
|
+
// User's persisted defaultModel wins over the provider's declared
|
|
86
|
+
// default. Dynamic providers (openrouter) re-register with their
|
|
87
|
+
// hardcoded DEFAULT_MODELS[0] each startup, which would otherwise
|
|
88
|
+
// clobber the user's /model selection.
|
|
89
|
+
const effectiveApiKey = config.apiKey ?? activeProvider?.apiKey;
|
|
90
|
+
const effectiveBaseURL = config.baseURL ?? activeProvider?.baseURL;
|
|
91
|
+
const effectiveModel = config.model ?? persistedModelFor(providerName) ?? activeProvider?.defaultModel;
|
|
92
|
+
if (!effectiveApiKey) {
|
|
93
|
+
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." });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!effectiveModel) {
|
|
97
|
+
bus.emit("ui:error", { message: "No model specified. Use --model or configure a provider with defaultModel in ~/.agent-sh/settings.json" });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
modes = buildModes();
|
|
101
|
+
if (modes.length === 0)
|
|
102
|
+
modes = [{ model: effectiveModel }];
|
|
103
|
+
initialModeIndex = Math.max(0, modes.findIndex((m) => m.model === effectiveModel && (!activeProvider || m.provider === activeProvider.id)));
|
|
104
|
+
llmClient.reconfigure({ apiKey: effectiveApiKey, baseURL: effectiveBaseURL, model: effectiveModel });
|
|
105
|
+
bus.emit("config:set-modes", { modes, activeIndex: initialModeIndex });
|
|
106
|
+
resolved = true;
|
|
107
|
+
// start() emits agent:info after wiring.
|
|
108
|
+
});
|
|
109
|
+
bus.on("provider:register", (p) => {
|
|
110
|
+
const rawModels = p.models ?? (p.defaultModel ? [p.defaultModel] : []);
|
|
111
|
+
const modelIds = [];
|
|
112
|
+
const caps = new Map();
|
|
113
|
+
for (const m of rawModels) {
|
|
114
|
+
if (typeof m === "string") {
|
|
115
|
+
modelIds.push(m);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
modelIds.push(m.id);
|
|
119
|
+
caps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
providerRegistry.set(p.id, {
|
|
123
|
+
id: p.id,
|
|
124
|
+
apiKey: p.apiKey,
|
|
125
|
+
baseURL: p.baseURL,
|
|
126
|
+
defaultModel: p.defaultModel,
|
|
127
|
+
models: modelIds,
|
|
128
|
+
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
129
|
+
modelCapabilities: caps.size > 0 ? caps : undefined,
|
|
130
|
+
});
|
|
131
|
+
const addModes = modelIds.map((m) => {
|
|
132
|
+
const mc = caps.get(m);
|
|
133
|
+
return {
|
|
134
|
+
model: m,
|
|
135
|
+
provider: p.id,
|
|
136
|
+
providerConfig: { apiKey: p.apiKey ?? "", baseURL: p.baseURL },
|
|
137
|
+
contextWindow: mc?.contextWindow,
|
|
138
|
+
reasoning: mc?.reasoning,
|
|
139
|
+
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
bus.emit("config:add-modes", { modes: addModes });
|
|
143
|
+
// Late-registration reconcile: if this completes the user's persisted
|
|
144
|
+
// default (openrouter's async fetch delivers the full catalog after
|
|
145
|
+
// we've already fallen back to mode 0), quietly switch to it.
|
|
146
|
+
if (!resolved)
|
|
147
|
+
return;
|
|
148
|
+
const pendingProvider = getSettings().defaultProvider;
|
|
149
|
+
if (pendingProvider !== p.id)
|
|
150
|
+
return;
|
|
151
|
+
const pendingModel = persistedModelFor(pendingProvider);
|
|
152
|
+
if (pendingModel && modelIds.includes(pendingModel) && llmClient.model !== pendingModel) {
|
|
153
|
+
bus.emit("config:switch-model", { model: pendingModel });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
bus.on("config:switch-provider", ({ provider: name }) => {
|
|
157
|
+
const p = providerRegistry.get(name);
|
|
158
|
+
if (!p) {
|
|
159
|
+
bus.emit("ui:error", { message: `Unknown provider: ${name}` });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (!p.apiKey) {
|
|
163
|
+
bus.emit("ui:error", { message: `Provider "${name}" has no API key configured` });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const switchModel = p.defaultModel ?? p.models[0];
|
|
167
|
+
if (!switchModel) {
|
|
168
|
+
bus.emit("ui:error", { message: `Provider "${name}" has no models configured` });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
llmClient.reconfigure({ apiKey: p.apiKey, baseURL: p.baseURL, model: switchModel });
|
|
172
|
+
const newModes = p.models.map((m) => {
|
|
173
|
+
const mc = p.modelCapabilities?.get(m);
|
|
174
|
+
return {
|
|
175
|
+
model: m,
|
|
176
|
+
provider: name,
|
|
177
|
+
providerConfig: { apiKey: p.apiKey, baseURL: p.baseURL },
|
|
178
|
+
contextWindow: mc?.contextWindow ?? p.contextWindow,
|
|
179
|
+
reasoning: mc?.reasoning,
|
|
180
|
+
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
bus.emit("config:set-modes", { modes: newModes });
|
|
184
|
+
bus.emit("agent:info", { name: "ash", version: "0.4", model: switchModel, provider: name, contextWindow: p.contextWindow });
|
|
185
|
+
bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
|
|
186
|
+
bus.emit("config:changed", {});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Command suggestion extension (fast-path LLM feature).
|
|
3
3
|
*
|
|
4
|
-
* After a shell command fails (non-zero exit), uses
|
|
4
|
+
* After a shell command fails (non-zero exit), uses LlmClient.complete()
|
|
5
5
|
* to suggest a fix. Shows the suggestion below the prompt.
|
|
6
6
|
*
|
|
7
|
-
* Only active when
|
|
7
|
+
* Only active when an LLM client is available (registered by agent-backend).
|
|
8
8
|
*/
|
|
9
9
|
import type { ExtensionContext } from "../types.js";
|
|
10
|
-
export default function activate({ bus,
|
|
10
|
+
export default function activate({ bus, call }: ExtensionContext): void;
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
export default function activate({ bus,
|
|
2
|
-
if (!llmClient)
|
|
3
|
-
return;
|
|
1
|
+
export default function activate({ bus, call }) {
|
|
4
2
|
let suggesting = false;
|
|
5
3
|
bus.on("shell:command-done", ({ command, output, exitCode, cwd }) => {
|
|
6
4
|
if (exitCode === null || exitCode === 0)
|
|
@@ -9,6 +7,9 @@ export default function activate({ bus, llmClient }) {
|
|
|
9
7
|
return;
|
|
10
8
|
if (suggesting)
|
|
11
9
|
return; // don't stack suggestions
|
|
10
|
+
const llmClient = call("llm:get-client");
|
|
11
|
+
if (!llmClient)
|
|
12
|
+
return;
|
|
12
13
|
suggesting = true;
|
|
13
14
|
// Truncate output to avoid blowing up the prompt
|
|
14
15
|
const truncated = output.length > 1000
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in extension manifest.
|
|
3
|
+
*
|
|
4
|
+
* These extensions ship with agent-sh and load before user extensions.
|
|
5
|
+
* They receive unscoped contexts (not reloadable) and can be individually
|
|
6
|
+
* disabled via the `disabledBuiltins` setting in ~/.agent-sh/settings.json.
|
|
7
|
+
*/
|
|
8
|
+
import type { ExtensionContext } from "../types.js";
|
|
9
|
+
type ActivateFn = (ctx: ExtensionContext) => void;
|
|
10
|
+
export declare const BUILTIN_EXTENSIONS: Array<{
|
|
11
|
+
name: string;
|
|
12
|
+
load: () => Promise<ActivateFn>;
|
|
13
|
+
}>;
|
|
14
|
+
/**
|
|
15
|
+
* Load built-in extensions sequentially, skipping any in the disabled list.
|
|
16
|
+
* Returns the names of extensions that were loaded.
|
|
17
|
+
*/
|
|
18
|
+
export declare function loadBuiltinExtensions(ctx: ExtensionContext, disabled?: string[]): Promise<string[]>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const BUILTIN_EXTENSIONS = [
|
|
2
|
+
{ name: "agent-backend", load: () => import("./agent-backend.js").then(m => m.default) },
|
|
3
|
+
{ name: "tui-renderer", load: () => import("./tui-renderer.js").then(m => m.default) },
|
|
4
|
+
{ name: "slash-commands", load: () => import("./slash-commands.js").then(m => m.default) },
|
|
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
|
+
{ name: "command-suggest", load: () => import("./command-suggest.js").then(m => m.default) },
|
|
8
|
+
];
|
|
9
|
+
/**
|
|
10
|
+
* Load built-in extensions sequentially, skipping any in the disabled list.
|
|
11
|
+
* Returns the names of extensions that were loaded.
|
|
12
|
+
*/
|
|
13
|
+
export async function loadBuiltinExtensions(ctx, disabled = []) {
|
|
14
|
+
const disabledSet = new Set(disabled);
|
|
15
|
+
const loaded = [];
|
|
16
|
+
for (const ext of BUILTIN_EXTENSIONS) {
|
|
17
|
+
if (disabledSet.has(ext.name))
|
|
18
|
+
continue;
|
|
19
|
+
const activate = await ext.load();
|
|
20
|
+
activate(ctx);
|
|
21
|
+
loaded.push(ext.name);
|
|
22
|
+
}
|
|
23
|
+
return loaded;
|
|
24
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ExtensionContext } from "../types.js";
|
|
2
|
-
export default function activate(
|
|
2
|
+
export default function activate(ctx: ExtensionContext): void;
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { palette as p } from "../utils/palette.js";
|
|
14
14
|
import { discoverSkills, loadSkillContent } from "../agent/skills.js";
|
|
15
|
-
|
|
15
|
+
import { reloadExtensions } from "../extension-loader.js";
|
|
16
|
+
export default function activate(ctx) {
|
|
17
|
+
const { bus, contextManager } = ctx;
|
|
16
18
|
const commands = new Map();
|
|
17
19
|
const register = (cmd) => {
|
|
18
20
|
const name = cmd.name.startsWith("/") ? cmd.name : `/${cmd.name}`;
|
|
@@ -77,7 +79,7 @@ export default function activate({ bus, contextManager }) {
|
|
|
77
79
|
});
|
|
78
80
|
register({
|
|
79
81
|
name: "/compact",
|
|
80
|
-
description: "Compact conversation
|
|
82
|
+
description: "Compact conversation via the active compaction strategy",
|
|
81
83
|
handler: () => {
|
|
82
84
|
bus.emit("agent:compact-request", {});
|
|
83
85
|
},
|
|
@@ -88,25 +90,43 @@ export default function activate({ bus, contextManager }) {
|
|
|
88
90
|
handler: () => {
|
|
89
91
|
const stats = bus.emitPipe("context:get-stats", {
|
|
90
92
|
activeTokens: 0,
|
|
91
|
-
|
|
92
|
-
recallArchiveSize: 0,
|
|
93
|
+
totalTokens: 0,
|
|
93
94
|
budgetTokens: 0,
|
|
94
95
|
});
|
|
95
96
|
const pct = stats.budgetTokens > 0
|
|
96
97
|
? Math.round((stats.activeTokens / stats.budgetTokens) * 100)
|
|
97
98
|
: 0;
|
|
98
|
-
|
|
99
|
-
`Active context: ~${stats.activeTokens.toLocaleString()} tokens / ${stats.budgetTokens.toLocaleString()} budget (${pct}%)`,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
bus.emit("ui:info", {
|
|
100
|
+
message: `Active context: ~${stats.activeTokens.toLocaleString()} tokens / ${stats.budgetTokens.toLocaleString()} budget (${pct}%)`,
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
register({
|
|
105
|
+
name: "/reload",
|
|
106
|
+
description: "Reload user extensions from ~/.agent-sh/extensions/",
|
|
107
|
+
handler: async () => {
|
|
108
|
+
const names = await reloadExtensions(ctx);
|
|
109
|
+
if (names.length > 0) {
|
|
110
|
+
bus.emit("ui:info", { message: `Reloaded: ${names.join(", ")}` });
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
bus.emit("ui:info", { message: "No extensions to reload." });
|
|
114
|
+
}
|
|
104
115
|
},
|
|
105
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
|
+
});
|
|
106
122
|
// ── Extension registration ────────────────────────────────────
|
|
107
123
|
bus.on("command:register", (cmd) => {
|
|
108
124
|
register(cmd);
|
|
109
125
|
});
|
|
126
|
+
bus.on("command:unregister", ({ name }) => {
|
|
127
|
+
const key = name.startsWith("/") ? name : `/${name}`;
|
|
128
|
+
commands.delete(key);
|
|
129
|
+
});
|
|
110
130
|
// ── Skill commands (/skill:<name>) ────────────────────────────
|
|
111
131
|
const getSkills = () => {
|
|
112
132
|
const cwd = contextManager?.getCwd() ?? process.cwd();
|