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.
Files changed (88) hide show
  1. package/README.md +25 -30
  2. package/dist/agent/agent-loop.d.ts +43 -6
  3. package/dist/agent/agent-loop.js +817 -157
  4. package/dist/agent/conversation-state.d.ts +72 -21
  5. package/dist/agent/conversation-state.js +364 -151
  6. package/dist/agent/history-file.d.ts +13 -4
  7. package/dist/agent/history-file.js +110 -36
  8. package/dist/agent/nuclear-form.d.ts +28 -3
  9. package/dist/agent/nuclear-form.js +84 -3
  10. package/dist/agent/skills.d.ts +2 -4
  11. package/dist/agent/skills.js +10 -4
  12. package/dist/agent/subagent.d.ts +23 -0
  13. package/dist/agent/subagent.js +53 -11
  14. package/dist/agent/system-prompt.d.ts +34 -1
  15. package/dist/agent/system-prompt.js +96 -47
  16. package/dist/agent/token-budget.d.ts +10 -13
  17. package/dist/agent/token-budget.js +6 -46
  18. package/dist/agent/tool-protocol.d.ts +23 -1
  19. package/dist/agent/tool-protocol.js +169 -4
  20. package/dist/agent/tools/bash.js +3 -3
  21. package/dist/agent/tools/edit-file.js +9 -6
  22. package/dist/agent/tools/glob.js +4 -2
  23. package/dist/agent/tools/grep.js +27 -3
  24. package/dist/agent/tools/ls.js +5 -6
  25. package/dist/agent/types.d.ts +1 -2
  26. package/dist/context-manager.d.ts +16 -19
  27. package/dist/context-manager.js +48 -152
  28. package/dist/core.js +27 -6
  29. package/dist/event-bus.d.ts +59 -3
  30. package/dist/executor.d.ts +4 -3
  31. package/dist/executor.js +18 -15
  32. package/dist/extension-loader.js +75 -17
  33. package/dist/extensions/agent-backend.d.ts +8 -7
  34. package/dist/extensions/agent-backend.js +72 -50
  35. package/dist/extensions/index.js +0 -2
  36. package/dist/extensions/slash-commands.js +14 -9
  37. package/dist/extensions/tui-renderer.js +67 -80
  38. package/dist/index.js +25 -6
  39. package/dist/settings.d.ts +39 -16
  40. package/dist/settings.js +51 -11
  41. package/dist/shell/input-handler.d.ts +2 -1
  42. package/dist/shell/input-handler.js +84 -76
  43. package/dist/shell/shell.js +19 -2
  44. package/dist/types.d.ts +15 -0
  45. package/dist/utils/ansi.d.ts +7 -0
  46. package/dist/utils/ansi.js +69 -8
  47. package/dist/utils/box-frame.js +8 -2
  48. package/dist/utils/compositor.d.ts +5 -0
  49. package/dist/utils/compositor.js +31 -3
  50. package/dist/utils/diff-renderer.d.ts +9 -0
  51. package/dist/utils/diff-renderer.js +221 -143
  52. package/dist/utils/diff.d.ts +21 -2
  53. package/dist/utils/diff.js +165 -89
  54. package/dist/utils/handler-registry.d.ts +5 -0
  55. package/dist/utils/handler-registry.js +6 -0
  56. package/dist/utils/line-editor.d.ts +11 -1
  57. package/dist/utils/line-editor.js +44 -5
  58. package/dist/utils/markdown.js +23 -8
  59. package/dist/utils/package-version.d.ts +1 -0
  60. package/dist/utils/package-version.js +10 -0
  61. package/dist/utils/shell-output-spill.d.ts +2 -0
  62. package/dist/utils/shell-output-spill.js +81 -0
  63. package/dist/utils/tool-display.d.ts +1 -1
  64. package/dist/utils/tool-display.js +4 -4
  65. package/examples/extensions/ash-acp-bridge/src/index.ts +4 -1
  66. package/examples/extensions/ash-mcp-bridge/index.ts +13 -3
  67. package/examples/extensions/claude-code-bridge/README.md +14 -0
  68. package/examples/extensions/claude-code-bridge/index.ts +204 -145
  69. package/examples/extensions/claude-code-bridge/package.json +1 -0
  70. package/examples/extensions/interactive-prompts.ts +39 -25
  71. package/examples/extensions/overlay-agent.ts +3 -3
  72. package/examples/extensions/peer-mesh.ts +115 -0
  73. package/examples/extensions/pi-bridge/README.md +16 -0
  74. package/examples/extensions/pi-bridge/index.ts +9 -155
  75. package/examples/extensions/questionnaire.ts +16 -5
  76. package/examples/extensions/subagents.ts +19 -4
  77. package/examples/extensions/terminal-buffer.ts +163 -0
  78. package/examples/extensions/user-shell.ts +136 -0
  79. package/examples/extensions/web-access.ts +8 -0
  80. package/package.json +36 -2
  81. package/dist/agent/tools/display.d.ts +0 -13
  82. package/dist/agent/tools/display.js +0 -70
  83. package/dist/agent/tools/user-shell.d.ts +0 -13
  84. package/dist/agent/tools/user-shell.js +0 -87
  85. package/dist/extensions/shell-recall.d.ts +0 -9
  86. package/dist/extensions/shell-recall.js +0 -8
  87. package/dist/extensions/terminal-buffer.d.ts +0 -14
  88. package/dist/extensions/terminal-buffer.js +0 -134
@@ -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
- async function ensureTsSupport() {
9
- if (tsRegistered)
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
- ctx.registerInstruction(name, text);
48
- cleanups.push(() => ctx.removeInstruction(name));
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
- ctx.registerTool(tool);
53
- cleanups.push(() => ctx.unregisterTool(tool.name));
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
- // User extensions get a scoped context so /reload can tear them down
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
- activate(ctx);
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
- // Bare specifier npm package
227
- return specifier;
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
- * Owns the full LLM lifecycle:
5
- * 1. Resolves providers from settings + CLI config
6
- * 2. Creates and manages the LlmClient
7
- * 3. Builds mode list for model cycling
8
- * 4. Creates AgentLoop and registers it as the "ash" backend
9
- * 5. Handles runtime provider switching and provider registration
10
- * 6. Exposes llm:get-client handler for other extensions (e.g. command-suggest)
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
- const settings = getSettings();
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
- const effectiveApiKey = config.apiKey ?? activeProvider?.apiKey;
41
- const effectiveBaseURL = config.baseURL ?? activeProvider?.baseURL;
42
- const effectiveModel = config.model ?? activeProvider?.defaultModel;
43
- let modes = buildModes();
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
- // ── Initial modes (queryable via pipe) ─────────────────────
63
- bus.onPipe("config:get-initial-modes", () => ({
64
- modes,
65
- initialModeIndex,
66
- }));
67
- // ── Create agent loop ──────────────────────────────────────
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: "0.4",
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
- // ── Runtime provider registration ──────────────────────────
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
- const newApiKey = p.apiKey;
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: newApiKey, baseURL: p.baseURL },
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
- activeProvider = p;
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
  });
@@ -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 (move full content to nuclear summaries)",
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
- nuclearEntries: 0,
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
- const lines = [
101
- `Active context: ~${stats.activeTokens.toLocaleString()} tokens / ${stats.budgetTokens.toLocaleString()} budget (${pct}%)`,
102
- `Nuclear entries: ${stats.nuclearEntries} in-context`,
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();