agent-sh 0.12.27 → 0.13.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 (146) hide show
  1. package/README.md +13 -2
  2. package/dist/agent/agent-loop.d.ts +3 -5
  3. package/dist/agent/agent-loop.js +42 -98
  4. package/dist/agent/conversation-state.d.ts +9 -0
  5. package/dist/agent/conversation-state.js +16 -0
  6. package/dist/agent/history-file.d.ts +6 -0
  7. package/dist/agent/history-file.js +1 -1
  8. package/dist/agent/host-types.d.ts +125 -0
  9. package/dist/agent/index.d.ts +12 -4
  10. package/dist/agent/index.js +358 -6
  11. package/dist/agent/nuclear-form.d.ts +7 -0
  12. package/dist/{extensions → agent}/providers/deepseek.d.ts +2 -2
  13. package/dist/{extensions → agent}/providers/deepseek.js +5 -4
  14. package/dist/{extensions → agent}/providers/openai-compatible.d.ts +2 -2
  15. package/dist/{extensions → agent}/providers/openai.d.ts +2 -2
  16. package/dist/{extensions → agent}/providers/openai.js +3 -2
  17. package/dist/{extensions → agent}/providers/openrouter.d.ts +2 -2
  18. package/dist/{extensions → agent}/providers/openrouter.js +4 -3
  19. package/dist/agent/skills.js +51 -7
  20. package/dist/agent/subagent.d.ts +1 -1
  21. package/dist/agent/system-prompt.js +14 -17
  22. package/dist/agent/tool-protocol.d.ts +1 -1
  23. package/dist/agent/tool-protocol.js +5 -3
  24. package/dist/agent/tool-registry.d.ts +9 -4
  25. package/dist/agent/tool-registry.js +27 -4
  26. package/dist/agent/tools/bash.d.ts +1 -1
  27. package/dist/agent/tools/bash.js +3 -2
  28. package/dist/agent/tools/edit-file.js +0 -1
  29. package/dist/agent/tools/glob.js +1 -1
  30. package/dist/agent/tools/grep.js +1 -1
  31. package/dist/agent/tools/pwsh.d.ts +1 -1
  32. package/dist/agent/tools/pwsh.js +1 -2
  33. package/dist/agent/tools/read-file.js +7 -4
  34. package/dist/agent/tools/write-file.js +0 -1
  35. package/dist/agent/types.d.ts +17 -2
  36. package/dist/cli/auth/cli.d.ts +1 -0
  37. package/dist/cli/auth/cli.js +216 -0
  38. package/dist/cli/auth/keys.d.ts +31 -0
  39. package/dist/cli/auth/keys.js +102 -0
  40. package/dist/{index.js → cli/index.js} +29 -32
  41. package/dist/{init.js → cli/init.js} +1 -1
  42. package/dist/{install.js → cli/install.js} +31 -2
  43. package/dist/cli/subcommands.d.ts +1 -0
  44. package/dist/cli/subcommands.js +17 -0
  45. package/dist/{event-bus.d.ts → core/event-bus.d.ts} +7 -13
  46. package/dist/{extension-loader.d.ts → core/extension-loader.d.ts} +1 -1
  47. package/dist/{extension-loader.js → core/extension-loader.js} +62 -70
  48. package/dist/{core.d.ts → core/index.d.ts} +18 -15
  49. package/dist/{core.js → core/index.js} +18 -92
  50. package/dist/{settings.d.ts → core/settings.d.ts} +7 -0
  51. package/dist/{settings.js → core/settings.js} +1 -0
  52. package/dist/core/types.d.ts +49 -0
  53. package/dist/core/types.js +1 -0
  54. package/dist/extensions/file-autocomplete.d.ts +1 -1
  55. package/dist/extensions/index.d.ts +7 -14
  56. package/dist/extensions/index.js +2 -19
  57. package/dist/extensions/slash-commands.d.ts +1 -1
  58. package/dist/extensions/slash-commands.js +7 -2
  59. package/dist/shell/host-types.d.ts +114 -0
  60. package/dist/shell/host-types.js +1 -0
  61. package/dist/shell/index.d.ts +8 -7
  62. package/dist/shell/index.js +58 -9
  63. package/dist/shell/input-handler.d.ts +7 -1
  64. package/dist/shell/input-handler.js +5 -2
  65. package/dist/shell/output-parser.d.ts +1 -1
  66. package/dist/{extensions → shell}/shell-context.d.ts +1 -1
  67. package/dist/{extensions → shell}/shell-context.js +18 -12
  68. package/dist/shell/shell.d.ts +6 -4
  69. package/dist/shell/shell.js +33 -109
  70. package/dist/shell/strategies/bash.d.ts +2 -0
  71. package/dist/shell/strategies/bash.js +68 -0
  72. package/dist/shell/strategies/fish.d.ts +2 -0
  73. package/dist/shell/strategies/fish.js +65 -0
  74. package/dist/shell/strategies/index.d.ts +13 -0
  75. package/dist/shell/strategies/index.js +17 -0
  76. package/dist/shell/strategies/types.d.ts +50 -0
  77. package/dist/shell/strategies/types.js +9 -0
  78. package/dist/shell/strategies/zsh.d.ts +2 -0
  79. package/dist/shell/strategies/zsh.js +72 -0
  80. package/dist/shell/tui-input-view.js +14 -3
  81. package/dist/{extensions → shell}/tui-renderer.d.ts +1 -1
  82. package/dist/{extensions → shell}/tui-renderer.js +27 -55
  83. package/dist/utils/box-frame.d.ts +4 -0
  84. package/dist/utils/box-frame.js +17 -6
  85. package/dist/utils/compositor.d.ts +1 -1
  86. package/dist/utils/compositor.js +2 -1
  87. package/dist/{executor.js → utils/executor.js} +1 -1
  88. package/dist/utils/floating-panel.d.ts +1 -1
  89. package/dist/utils/floating-panel.js +9 -4
  90. package/dist/utils/llm-client.d.ts +16 -26
  91. package/dist/utils/llm-client.js +15 -26
  92. package/dist/utils/llm-facade.d.ts +7 -3
  93. package/dist/utils/stream-transform.d.ts +1 -1
  94. package/dist/utils/terminal-buffer.d.ts +1 -1
  95. package/dist/utils/tool-display.js +4 -0
  96. package/dist/utils/tool-interactive.d.ts +1 -1
  97. package/dist/utils/tty.d.ts +7 -0
  98. package/dist/utils/tty.js +15 -0
  99. package/examples/extensions/ash-acp-bridge/README.md +4 -1
  100. package/examples/extensions/ash-acp-bridge/src/index.ts +654 -0
  101. package/examples/extensions/ash-mcp-bridge/index.ts +1 -1
  102. package/examples/extensions/ashi/README.md +250 -0
  103. package/examples/extensions/ashi/package.json +60 -0
  104. package/examples/extensions/ashi/src/autocomplete.ts +91 -0
  105. package/examples/extensions/ashi/src/capture.ts +34 -0
  106. package/examples/extensions/ashi/src/cli.ts +176 -0
  107. package/examples/extensions/ashi/src/commands.ts +82 -0
  108. package/examples/extensions/ashi/src/compaction.ts +157 -0
  109. package/examples/extensions/ashi/src/components.ts +327 -0
  110. package/examples/extensions/ashi/src/default-renderers.ts +153 -0
  111. package/examples/extensions/ashi/src/display-config.ts +62 -0
  112. package/examples/extensions/ashi/src/frontend.ts +735 -0
  113. package/examples/extensions/ashi/src/hooks.ts +136 -0
  114. package/examples/extensions/ashi/src/multi-session-store.ts +146 -0
  115. package/examples/extensions/ashi/src/session-commands.ts +76 -0
  116. package/examples/extensions/ashi/src/session-store.ts +264 -0
  117. package/examples/extensions/ashi/src/status-footer.ts +66 -0
  118. package/examples/extensions/ashi/src/theme.ts +151 -0
  119. package/examples/extensions/ashi/tsconfig.json +14 -0
  120. package/examples/extensions/emacs-buffer.ts +1 -1
  121. package/examples/extensions/interactive-prompts.ts +114 -69
  122. package/examples/extensions/latex-images.ts +3 -3
  123. package/examples/extensions/opencode-bridge/index.ts +1 -1
  124. package/examples/extensions/overlay-agent.ts +7 -5
  125. package/examples/extensions/peer-mesh.ts +1 -1
  126. package/examples/extensions/pi-bridge/index.ts +0 -1
  127. package/examples/extensions/questionnaire.ts +2 -1
  128. package/examples/extensions/rtk-proxy.ts +3 -3
  129. package/examples/extensions/solarized-theme.ts +3 -3
  130. package/examples/extensions/subagents.ts +6 -6
  131. package/examples/extensions/terminal-buffer.ts +1 -1
  132. package/examples/extensions/tmux-pane.ts +6 -4
  133. package/examples/extensions/tunnel-vision.ts +5 -5
  134. package/examples/extensions/user-shell.ts +1 -1
  135. package/examples/extensions/web-access.ts +5 -5
  136. package/package.json +38 -22
  137. package/dist/extensions/agent-backend.d.ts +0 -14
  138. package/dist/extensions/agent-backend.js +0 -307
  139. package/dist/types.d.ts +0 -227
  140. /package/dist/{types.js → agent/host-types.js} +0 -0
  141. /package/dist/{extensions → agent}/providers/openai-compatible.js +0 -0
  142. /package/dist/{index.d.ts → cli/index.d.ts} +0 -0
  143. /package/dist/{init.d.ts → cli/init.d.ts} +0 -0
  144. /package/dist/{install.d.ts → cli/install.d.ts} +0 -0
  145. /package/dist/{event-bus.js → core/event-bus.js} +0 -0
  146. /package/dist/{executor.d.ts → utils/executor.d.ts} +0 -0
@@ -1,15 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
- import * as path from "node:path";
4
- import { activateShell, registerShellHandlers } from "./shell/index.js";
5
- import { createCore } from "./core.js";
6
- import { palette as p } from "./utils/palette.js";
7
- import { loadBuiltinExtensions } from "./extensions/index.js";
8
- import { loadExtensions } from "./extension-loader.js";
9
- import { getSettings } from "./settings.js";
10
- import { runInit } from "./init.js";
11
- import { runInstall, runUninstall, runList, suggestBridgeFor } from "./install.js";
12
- import { PACKAGE_VERSION } from "./utils/package-version.js";
3
+ import { activateShell, registerShellHandlers } from "../shell/index.js";
4
+ import { pickStrategy, FALLBACK_STRATEGY } from "../shell/strategies/index.js";
5
+ import { activateAgent } from "../agent/index.js";
6
+ import { createCore } from "../core/index.js";
7
+ import { palette as p } from "../utils/palette.js";
8
+ import { loadBuiltinExtensions } from "../extensions/index.js";
9
+ import { loadExtensions } from "../core/extension-loader.js";
10
+ import { getSettings } from "../core/settings.js";
11
+ import { dispatchSubcommand } from "./subcommands.js";
12
+ import { suggestBridgeFor } from "./install.js";
13
+ import { anyProviderConfigured } from "./auth/keys.js";
14
+ import { PACKAGE_VERSION } from "../utils/package-version.js";
15
+ import { clearOpost } from "../utils/tty.js";
13
16
  /**
14
17
  * Capture the user's full shell environment.
15
18
  * This picks up env vars exported in .zshrc/.bashrc that the
@@ -25,12 +28,9 @@ async function captureShellEnvAsync(shell) {
25
28
  resolve(result);
26
29
  };
27
30
  try {
28
- const shellName = path.basename(shell);
29
- const isZsh = shellName.includes("zsh");
30
- const sourceRc = isZsh
31
- ? 'source ~/.zshrc 2>/dev/null;'
32
- : '[ -f ~/.bashrc ] && source ~/.bashrc 2>/dev/null;';
33
- const child = spawn(shell, ["-l", "-c", `${sourceRc} env -0`], {
31
+ const strategy = pickStrategy(shell) ?? FALLBACK_STRATEGY;
32
+ const captureCmd = strategy.envCaptureCommand();
33
+ const child = spawn(shell, ["-l", "-c", captureCmd], {
34
34
  stdio: ["ignore", "pipe", "ignore"],
35
35
  timeout: 5000,
36
36
  });
@@ -119,6 +119,9 @@ Usage: agent-sh [options]
119
119
  agent-sh install <spec> [--force] Install an extension (bundled name, file:, npm:, github:)
120
120
  agent-sh uninstall <name> Remove an installed extension
121
121
  agent-sh list List installed extensions
122
+ agent-sh auth login [provider] Store an API key for a built-in provider
123
+ agent-sh auth logout <provider> Remove a stored key
124
+ agent-sh auth list Show configured providers
122
125
 
123
126
  Provider Profiles:
124
127
  --provider <name> Use a provider from ~/.agent-sh/settings.json
@@ -161,24 +164,9 @@ Inside the shell:
161
164
  return { shell, model, extensions, apiKey, baseURL, provider, backend };
162
165
  }
163
166
  async function main() {
164
- // Subcommands — handled before the shell-launch path.
165
167
  const rawArgs = process.argv.slice(2);
166
- if (rawArgs[0] === "init") {
167
- runInit({ force: rawArgs.includes("--force") });
168
+ if (await dispatchSubcommand(rawArgs))
168
169
  return;
169
- }
170
- if (rawArgs[0] === "install") {
171
- await runInstall(rawArgs[1] ?? "", { force: rawArgs.includes("--force") });
172
- return;
173
- }
174
- if (rawArgs[0] === "uninstall") {
175
- await runUninstall(rawArgs[1] ?? "");
176
- return;
177
- }
178
- if (rawArgs[0] === "list") {
179
- runList();
180
- return;
181
- }
182
170
  if (process.env.AGENT_SH) {
183
171
  console.error("agent-sh: already running inside an agent-sh session (nested sessions are not supported).");
184
172
  process.exit(1);
@@ -212,6 +200,13 @@ async function main() {
212
200
  catch {
213
201
  // Ignore errors, we already have process.env as fallback
214
202
  }
203
+ if (!config.apiKey && !config.provider && !anyProviderConfigured()) {
204
+ console.error("\nagent-sh: no LLM provider configured.\n\n" +
205
+ " Run `agent-sh auth login` to store an API key, or\n" +
206
+ " export OPENAI_API_KEY / OPENROUTER_API_KEY / DEEPSEEK_API_KEY, or\n" +
207
+ " run `agent-sh init` for a settings.json template.\n");
208
+ process.exit(1);
209
+ }
215
210
  // ── Core (frontend-agnostic) ──────────────────────────────────
216
211
  const core = createCore(config);
217
212
  const { bus } = core;
@@ -239,6 +234,7 @@ async function main() {
239
234
  const extCtx = core.extensionContext({ quit: cleanup });
240
235
  // Before loadExtensions: extensions look up shell handlers at activation.
241
236
  registerShellHandlers(extCtx);
237
+ activateAgent(extCtx);
242
238
  // Load before spawning the shell so PS1 lands below the banner.
243
239
  await loadBuiltinExtensions(extCtx, getSettings().disabledBuiltins);
244
240
  const loadExtensionsTimeoutMs = 10000;
@@ -340,6 +336,7 @@ async function main() {
340
336
  if (process.stdin.isTTY) {
341
337
  try {
342
338
  process.stdin.setRawMode(true);
339
+ clearOpost();
343
340
  }
344
341
  catch {
345
342
  // May fail if stdin is not a TTY
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { CONFIG_DIR } from "./settings.js";
3
+ import { CONFIG_DIR } from "../core/settings.js";
4
4
  const EXTENSIONS_DIR = path.join(CONFIG_DIR, "extensions");
5
5
  const SETTINGS_PATH = path.join(CONFIG_DIR, "settings.json");
6
6
  const EXAMPLE_PATH = path.join(CONFIG_DIR, "settings.example.json");
@@ -2,13 +2,13 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { spawnSync } from "node:child_process";
5
- import { CONFIG_DIR, getSettings } from "./settings.js";
5
+ import { CONFIG_DIR, getSettings } from "../core/settings.js";
6
6
  // Kept in sync with extension-loader.ts SCRIPT_EXTS.
7
7
  const SCRIPT_EXTS = [".js", ".mjs", ".ts", ".tsx", ".mts"];
8
8
  function hasIndexFile(dir) {
9
9
  return SCRIPT_EXTS.some((ext) => fs.existsSync(path.join(dir, `index${ext}`)));
10
10
  }
11
- const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../");
11
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../");
12
12
  const BUNDLED_DIR = path.join(PACKAGE_ROOT, "examples/extensions");
13
13
  const EXT_DIR = path.join(CONFIG_DIR, "extensions");
14
14
  export function listBundled() {
@@ -75,6 +75,34 @@ function readPackageJson(target) {
75
75
  return null;
76
76
  return JSON.parse(fs.readFileSync(pkgJson, "utf-8"));
77
77
  }
78
+ /** Relative `file:` deps in bundled extensions (e.g. `"agent-sh": "file:../../.."`)
79
+ * point at the wrong location after the source is copied into ~/.agent-sh/extensions/.
80
+ * Resolve them against the original source dir so npm install in the target succeeds. */
81
+ function rewriteFileDeps(target, sourcePath) {
82
+ const pkgJson = path.join(target, "package.json");
83
+ if (!fs.existsSync(pkgJson))
84
+ return;
85
+ const raw = fs.readFileSync(pkgJson, "utf-8");
86
+ const pkg = JSON.parse(raw);
87
+ const sections = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
88
+ let changed = false;
89
+ for (const section of sections) {
90
+ const deps = pkg[section];
91
+ if (!deps || typeof deps !== "object")
92
+ continue;
93
+ for (const [name, spec] of Object.entries(deps)) {
94
+ if (typeof spec !== "string" || !spec.startsWith("file:"))
95
+ continue;
96
+ const rel = spec.slice("file:".length);
97
+ if (path.isAbsolute(rel))
98
+ continue;
99
+ deps[name] = `file:${path.resolve(sourcePath, rel)}`;
100
+ changed = true;
101
+ }
102
+ }
103
+ if (changed)
104
+ fs.writeFileSync(pkgJson, `${JSON.stringify(pkg, null, 2)}\n`);
105
+ }
78
106
  function maybeNpmInstall(target, pkg) {
79
107
  const deps = { ...(pkg.dependencies ?? {}), ...(pkg.peerDependencies ?? {}) };
80
108
  if (Object.keys(deps).length === 0)
@@ -170,6 +198,7 @@ export async function runInstall(spec, opts = {}) {
170
198
  if (resolved.isDirectory) {
171
199
  fs.cpSync(resolved.sourcePath, target, { recursive: true });
172
200
  try {
201
+ rewriteFileDeps(target, resolved.sourcePath);
173
202
  const pkg = readPackageJson(target);
174
203
  if (pkg) {
175
204
  maybeNpmInstall(target, pkg);
@@ -0,0 +1 @@
1
+ export declare function dispatchSubcommand(argv: string[]): Promise<boolean>;
@@ -0,0 +1,17 @@
1
+ import { runInit } from "./init.js";
2
+ import { runInstall, runUninstall, runList } from "./install.js";
3
+ import { runAuth } from "./auth/cli.js";
4
+ const SUBCOMMANDS = {
5
+ init: (args) => runInit({ force: args.includes("--force") }),
6
+ install: (args) => runInstall(args[0] ?? "", { force: args.includes("--force") }),
7
+ uninstall: (args) => runUninstall(args[0] ?? ""),
8
+ list: () => runList(),
9
+ auth: (args) => runAuth(args),
10
+ };
11
+ export async function dispatchSubcommand(argv) {
12
+ const handler = SUBCOMMANDS[argv[0] ?? ""];
13
+ if (!handler)
14
+ return false;
15
+ await handler(argv.slice(1));
16
+ return true;
17
+ }
@@ -1,5 +1,5 @@
1
- import type { AgentMode } from "./types.js";
2
- import type { ToolResultDisplay } from "./agent/types.js";
1
+ import type { AgentMode } from "../agent/host-types.js";
2
+ import type { ToolResultDisplay } from "../agent/types.js";
3
3
  /**
4
4
  * Typed event map — every event has a known payload shape.
5
5
  */
@@ -50,7 +50,7 @@ export interface ShellEvents {
50
50
  "agent:append-user-message": {
51
51
  text: string;
52
52
  };
53
- "input-mode:register": import("./types.js").InputModeConfig;
53
+ "input-mode:register": import("../shell/host-types.js").InputModeConfig;
54
54
  "agent:query": {
55
55
  query: string;
56
56
  };
@@ -136,6 +136,8 @@ export interface ShellEvents {
136
136
  rawInput?: unknown;
137
137
  /** Pre-formatted display detail from tool's formatCall(). */
138
138
  displayDetail?: string;
139
+ /** highlight.js-style identifier for syntax-highlighting `rawInput.source`. */
140
+ sourceLanguage?: string;
139
141
  batchIndex?: number;
140
142
  batchTotal?: number;
141
143
  };
@@ -162,14 +164,6 @@ export interface ShellEvents {
162
164
  };
163
165
  "tool:interactive-start": Record<string, never>;
164
166
  "tool:interactive-end": Record<string, never>;
165
- "permission:request": {
166
- kind: string;
167
- title: string;
168
- metadata: Record<string, unknown>;
169
- /** Interactive UI capability — available when the built-in agent is active. */
170
- ui?: unknown;
171
- decision: Record<string, unknown>;
172
- };
173
167
  "command:register": {
174
168
  name: string;
175
169
  description: string;
@@ -336,14 +330,14 @@ export interface ShellEvents {
336
330
  reasoningParams?: (level: string, model?: string) => Record<string, unknown>;
337
331
  };
338
332
  "agent:register-tool": {
339
- tool: import("./agent/types.js").ToolDefinition;
333
+ tool: import("../agent/types.js").ToolDefinition;
340
334
  extensionName?: string;
341
335
  };
342
336
  "agent:unregister-tool": {
343
337
  name: string;
344
338
  };
345
339
  "agent:get-tools": {
346
- tools: import("./agent/types.js").ToolDefinition[];
340
+ tools: import("../agent/types.js").ToolDefinition[];
347
341
  };
348
342
  "agent:register-instruction": {
349
343
  name: string;
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "./types.js";
1
+ import type { ExtensionContext } from "../shell/host-types.js";
2
2
  /**
3
3
  * Load extensions from three sources (merged, deduplicated):
4
4
  *
@@ -23,7 +23,6 @@ async function ensureTsSupport(force = false) {
23
23
  if (tsRegistered && !force)
24
24
  return;
25
25
  try {
26
- // Unregister previous loader if reloading
27
26
  if (tsxUnregister) {
28
27
  try {
29
28
  await tsxUnregister();
@@ -39,70 +38,84 @@ async function ensureTsSupport(force = false) {
39
38
  }
40
39
  }
41
40
  /**
42
- * Wrap an ExtensionContext to track all registrations (bus.on, bus.onPipe,
43
- * advise, command:register). Returns the wrapped context and a dispose()
44
- * function that tears down everything registered through it.
41
+ * Wrap an ExtensionContext to track all registrations (bus.on, advise,
42
+ * command:register, plus all agent/shell surface registrars). Returns
43
+ * the wrapped context and a dispose() that tears down everything
44
+ * registered through it.
45
45
  */
46
46
  function createScopedContext(ctx, extensionName) {
47
47
  const cleanups = [];
48
48
  const bus = ctx.bus;
49
49
  const scopedBus = Object.create(bus);
50
- // Track bus.on registrations
51
50
  scopedBus.on = ((event, fn) => {
52
51
  bus.on(event, fn);
53
52
  cleanups.push(() => bus.off(event, fn));
54
53
  });
55
- // Track bus.onPipe registrations
56
54
  scopedBus.onPipe = ((event, fn) => {
57
55
  bus.onPipe(event, fn);
58
56
  cleanups.push(() => bus.offPipe(event, fn));
59
57
  });
60
- // Track advise registrations
61
- const scopedAdvise = (name, wrapper) => {
62
- const unadvise = ctx.advise(name, wrapper);
63
- cleanups.push(unadvise);
64
- return unadvise;
58
+ // Wrap any (name, fn) → unsubscribe registrar so its disposer runs on teardown.
59
+ const trackUnsub = (fn) => (a, b) => {
60
+ const unsub = fn(a, b);
61
+ cleanups.push(unsub);
62
+ return unsub;
65
63
  };
66
- // Track instruction registrations extension name captured in scope
67
- const scopedRegisterInstruction = (name, text) => {
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 }));
75
- };
76
- // Track dynamic-context producer registrations
77
- const scopedRegisterContextProducer = (name, producer) => {
78
- const dispose = ctx.registerContextProducer(name, producer);
79
- cleanups.push(dispose);
80
- return dispose;
81
- };
82
- // Track tool registrations — extension name captured in scope
83
- const scopedRegisterTool = (tool) => {
84
- bus.emit("agent:register-tool", { tool, extensionName });
85
- cleanups.push(() => bus.emit("agent:unregister-tool", { name: tool.name }));
86
- };
87
- // Track slash command registrations — without this, reloading an
88
- // extension stacks its commands (old `/status` + new `/status`) in
89
- // the slash-commands registry.
64
+ // ── substrate / sugar ──────────────────────────────────────
65
+ const scopedAdvise = trackUnsub(ctx.advise);
66
+ // Without this, reloading an extension stacks its commands (old + new)
67
+ // in the slash-commands registry.
90
68
  const scopedRegisterCommand = (name, description, handler) => {
91
69
  ctx.registerCommand(name, description, handler);
92
70
  cleanups.push(() => bus.emit("command:unregister", { name }));
93
71
  };
72
+ const scopedAdviseCommand = trackUnsub(ctx.adviseCommand);
73
+ // ── agent surface (optional — bridge backends omit it) ───
74
+ const agent = ctx.agent;
75
+ let scopedAgent;
76
+ if (agent) {
77
+ scopedAgent = {
78
+ ...agent,
79
+ registerTool: (tool) => {
80
+ bus.emit("agent:register-tool", { tool, extensionName });
81
+ cleanups.push(() => bus.emit("agent:unregister-tool", { name: tool.name }));
82
+ },
83
+ adviseTool: trackUnsub(agent.adviseTool),
84
+ adviseToolSchema: trackUnsub(agent.adviseToolSchema),
85
+ registerInstruction: (name, text) => {
86
+ bus.emit("agent:register-instruction", { name, text, extensionName });
87
+ cleanups.push(() => bus.emit("agent:remove-instruction", { name }));
88
+ },
89
+ adviseInstruction: trackUnsub(agent.adviseInstruction),
90
+ registerSkill: (name, description, filePath) => {
91
+ bus.emit("agent:register-skill", { name, description, filePath, extensionName });
92
+ cleanups.push(() => bus.emit("agent:remove-skill", { name }));
93
+ },
94
+ adviseSkill: trackUnsub(agent.adviseSkill),
95
+ registerContextProducer: (name, producer, opts) => {
96
+ const dispose = agent.registerContextProducer(name, producer, opts);
97
+ cleanups.push(dispose);
98
+ return dispose;
99
+ },
100
+ };
101
+ }
102
+ // ── shell surface (optional — headless backends omit it) ──
103
+ const shell = ctx.shell;
104
+ let scopedShell;
105
+ if (shell) {
106
+ scopedShell = {
107
+ ...shell,
108
+ adviseInputMode: trackUnsub(shell.adviseInputMode),
109
+ };
110
+ }
94
111
  const scoped = {
95
112
  ...ctx,
96
113
  bus: scopedBus,
97
114
  advise: scopedAdvise,
98
- registerInstruction: scopedRegisterInstruction,
99
- removeInstruction: ctx.removeInstruction,
100
- registerSkill: scopedRegisterSkill,
101
- removeSkill: ctx.removeSkill,
102
- registerContextProducer: scopedRegisterContextProducer,
103
- registerTool: scopedRegisterTool,
104
- unregisterTool: ctx.unregisterTool,
105
115
  registerCommand: scopedRegisterCommand,
116
+ adviseCommand: scopedAdviseCommand,
117
+ agent: scopedAgent,
118
+ shell: scopedShell,
106
119
  onDispose: (fn) => { cleanups.push(fn); },
107
120
  };
108
121
  const dispose = () => {
@@ -116,7 +129,6 @@ function createScopedContext(ctx, extensionName) {
116
129
  };
117
130
  return { scoped, dispose };
118
131
  }
119
- // Track disposers for user extensions so reload can tear them down
120
132
  const extensionDisposers = new Map();
121
133
  /**
122
134
  * Load extensions from three sources (merged, deduplicated):
@@ -134,19 +146,15 @@ const extensionDisposers = new Map();
134
146
  */
135
147
  export async function loadExtensions(ctx, cliExtensions) {
136
148
  const specifiers = [];
137
- // 1. CLI -e / --extensions
138
149
  if (cliExtensions) {
139
150
  specifiers.push(...cliExtensions);
140
151
  }
141
- // 2. settings.json
142
152
  const settings = getSettings();
143
153
  if (settings.extensions.length > 0) {
144
154
  specifiers.push(...settings.extensions);
145
155
  }
146
- // 3. ~/.agent-sh/extensions/ directory
147
156
  const userSpecifiers = await discoverUserExtensions();
148
157
  specifiers.push(...userSpecifiers);
149
- // Deduplicate
150
158
  const seen = new Set();
151
159
  const unique = specifiers.filter((s) => {
152
160
  if (seen.has(s))
@@ -154,8 +162,7 @@ export async function loadExtensions(ctx, cliExtensions) {
154
162
  seen.add(s);
155
163
  return true;
156
164
  });
157
- // Load each extension (user extensions get scoped contexts for reloadability)
158
- const loaded = await loadSpecifiers(unique, ctx, false, userSpecifiers);
165
+ const loaded = await loadSpecifiers(unique, ctx, false);
159
166
  return loaded;
160
167
  }
161
168
  async function discoverUserExtensions() {
@@ -188,8 +195,7 @@ async function discoverUserExtensions() {
188
195
  }
189
196
  return specifiers;
190
197
  }
191
- async function loadSpecifiers(specifiers, ctx, bustCache, userSpecifiers) {
192
- const userSet = new Set(userSpecifiers ?? []);
198
+ async function loadSpecifiers(specifiers, ctx, bustCache) {
193
199
  const loaded = [];
194
200
  for (const specifier of specifiers) {
195
201
  try {
@@ -197,7 +203,6 @@ async function loadSpecifiers(specifiers, ctx, bustCache, userSpecifiers) {
197
203
  if (TS_EXTS.some((ext) => importPath.endsWith(ext))) {
198
204
  await ensureTsSupport(bustCache);
199
205
  }
200
- // Append timestamp query to bust Node's module cache on reload
201
206
  if (bustCache) {
202
207
  const sep = importPath.includes("?") ? "&" : "?";
203
208
  importPath += `${sep}t=${Date.now()}`;
@@ -212,23 +217,13 @@ async function loadSpecifiers(specifiers, ctx, bustCache, userSpecifiers) {
212
217
  if (typeof activate === "function") {
213
218
  const base = path.basename(specifier).replace(/\.(ts|js|mjs|mts|tsx)$/, "");
214
219
  const name = base === "index" ? path.basename(path.dirname(specifier)) : base;
215
- // Scoped context so /reload can tear user extensions down.
216
220
  // Awaiting activate() lets extensions with async setup (e.g.
217
221
  // openrouter fetching its model catalog) finish before we move
218
222
  // on; a 10s outer timeout in index.ts guards against hangs.
219
- if (userSet.has(specifier)) {
220
- // Dispose previous load if reloading
221
- extensionDisposers.get(name)?.();
222
- const { scoped, dispose } = createScopedContext(ctx, name);
223
- await activate(scoped);
224
- extensionDisposers.set(name, dispose);
225
- }
226
- else {
227
- const { scoped, dispose } = createScopedContext(ctx, name);
228
- await activate(scoped);
229
- // Non-user extensions aren't reloadable, but track for cleanup on shutdown
230
- extensionDisposers.set(name, dispose);
231
- }
223
+ extensionDisposers.get(name)?.();
224
+ const { scoped, dispose } = createScopedContext(ctx, name);
225
+ await activate(scoped);
226
+ extensionDisposers.set(name, dispose);
232
227
  loaded.push(name);
233
228
  }
234
229
  }
@@ -246,7 +241,7 @@ async function loadSpecifiers(specifiers, ctx, bustCache, userSpecifiers) {
246
241
  */
247
242
  export async function reloadExtensions(ctx) {
248
243
  const specifiers = await discoverUserExtensions();
249
- return loadSpecifiers(specifiers, ctx, true, specifiers);
244
+ return loadSpecifiers(specifiers, ctx, true);
250
245
  }
251
246
  /**
252
247
  * Find an index file in a directory extension.
@@ -284,15 +279,12 @@ async function resolveSpecifier(specifier) {
284
279
  // Scoped packages ("@scope/pkg") contain "/" but are npm specifiers,
285
280
  // so the "@" prefix takes precedence over the "/" heuristic.
286
281
  if (specifier.includes("/") && !specifier.startsWith("@")) {
287
- // Treat as relative path from cwd
288
282
  resolved = path.resolve(process.cwd(), specifier);
289
283
  }
290
284
  else {
291
- // Bare specifier — npm package (including @scope/pkg)
292
285
  return specifier;
293
286
  }
294
287
  }
295
- // If it's a directory, find the index file
296
288
  try {
297
289
  const stat = await fs.stat(resolved);
298
290
  if (stat.isDirectory()) {
@@ -6,9 +6,9 @@
6
6
  * subscribing to bus events. Shell-specific tracking lives in the
7
7
  * shell-context built-in extension.
8
8
  *
9
- * Agent backends are loaded as extensions and register themselves via
10
- * the agent:register-backend bus event. The built-in "ash" backend is
11
- * loaded from src/extensions/agent-backend.ts.
9
+ * Agent backends register themselves via the agent:register-backend bus
10
+ * event. The built-in "ash" backend lives in src/agent/ and is activated
11
+ * by hosts via activateAgent().
12
12
  *
13
13
  * Usage:
14
14
  * import { createCore } from "agent-sh";
@@ -18,18 +18,21 @@
18
18
  * const response = await core.query("hello");
19
19
  */
20
20
  import { EventBus } from "./event-bus.js";
21
- import type { AgentShellConfig, ExtensionContext } from "./types.js";
22
- import { HandlerRegistry } from "./utils/handler-registry.js";
21
+ import type { AppConfig, ExtensionContext } from "../shell/host-types.js";
22
+ import { HandlerRegistry } from "../utils/handler-registry.js";
23
23
  export { EventBus } from "./event-bus.js";
24
- export type { ShellEvents } from "./event-bus.js";
25
- export type { AgentShellConfig, ExtensionContext, LlmInterface, LlmMessage, LlmSession } from "./types.js";
26
- export { palette, setPalette, resetPalette } from "./utils/palette.js";
27
- export type { ColorPalette } from "./utils/palette.js";
28
- export type { AgentBackend, ToolDefinition } from "./agent/types.js";
29
- export { runSubagent, type SubagentOptions } from "./agent/subagent.js";
30
- export { LlmClient } from "./utils/llm-client.js";
31
- export { HistoryFile, InMemoryHistory, NoopHistory, type HistoryAdapter } from "./agent/history-file.js";
32
- export type { NuclearEntry } from "./agent/nuclear-form.js";
24
+ export type { ShellEvents, ContentBlock } from "./event-bus.js";
25
+ export type { CoreContext, CoreConfig } from "./types.js";
26
+ export type { AgentContext, AgentConfig, AgentSurface, AgentConfigSurface, AgentMode, LlmInterface, LlmMessage, LlmSession } from "../agent/host-types.js";
27
+ export type { ShellContext, ShellConfig, ShellSurface, ShellConfigSurface, ExtensionContext, RemoteSession, RemoteSessionOptions, RenderSurface, InputModeConfig, TerminalSession, BlockTransformOptions, FencedBlockTransformOptions, AppConfig } from "../shell/host-types.js";
28
+ export { palette, setPalette, resetPalette } from "../utils/palette.js";
29
+ export type { ColorPalette } from "../utils/palette.js";
30
+ export type { AgentBackend, ToolDefinition } from "../agent/types.js";
31
+ export { runSubagent, type SubagentOptions } from "../agent/subagent.js";
32
+ export { LlmClient } from "../utils/llm-client.js";
33
+ export { HistoryFile, InMemoryHistory, NoopHistory, type HistoryAdapter } from "../agent/history-file.js";
34
+ export type { NuclearEntry } from "../agent/nuclear-form.js";
35
+ export { compileSearchRegex, matchEntry, formatNuclearLine } from "../agent/nuclear-form.js";
33
36
  export interface AgentShellCore {
34
37
  bus: EventBus;
35
38
  /** Handler registry for define/advise/call. */
@@ -51,4 +54,4 @@ export interface AgentShellCore {
51
54
  /** Tear down the agent and clean up. */
52
55
  kill(): void;
53
56
  }
54
- export declare function createCore(config: AgentShellConfig): AgentShellCore;
57
+ export declare function createCore(config: AppConfig): AgentShellCore;