agent-sh 0.12.27 → 0.13.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.
Files changed (144) 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 +357 -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-facade.d.ts +7 -3
  91. package/dist/utils/stream-transform.d.ts +1 -1
  92. package/dist/utils/terminal-buffer.d.ts +1 -1
  93. package/dist/utils/tool-display.js +4 -0
  94. package/dist/utils/tool-interactive.d.ts +1 -1
  95. package/dist/utils/tty.d.ts +7 -0
  96. package/dist/utils/tty.js +15 -0
  97. package/examples/extensions/ash-acp-bridge/README.md +4 -1
  98. package/examples/extensions/ash-acp-bridge/src/index.ts +654 -0
  99. package/examples/extensions/ash-mcp-bridge/index.ts +1 -1
  100. package/examples/extensions/ashi/README.md +250 -0
  101. package/examples/extensions/ashi/package.json +60 -0
  102. package/examples/extensions/ashi/src/autocomplete.ts +91 -0
  103. package/examples/extensions/ashi/src/capture.ts +34 -0
  104. package/examples/extensions/ashi/src/cli.ts +126 -0
  105. package/examples/extensions/ashi/src/commands.ts +82 -0
  106. package/examples/extensions/ashi/src/compaction.ts +157 -0
  107. package/examples/extensions/ashi/src/components.ts +332 -0
  108. package/examples/extensions/ashi/src/default-renderers.ts +153 -0
  109. package/examples/extensions/ashi/src/display-config.ts +62 -0
  110. package/examples/extensions/ashi/src/frontend.ts +735 -0
  111. package/examples/extensions/ashi/src/hooks.ts +136 -0
  112. package/examples/extensions/ashi/src/multi-session-store.ts +146 -0
  113. package/examples/extensions/ashi/src/session-commands.ts +76 -0
  114. package/examples/extensions/ashi/src/session-store.ts +264 -0
  115. package/examples/extensions/ashi/src/status-footer.ts +66 -0
  116. package/examples/extensions/ashi/src/theme.ts +151 -0
  117. package/examples/extensions/ashi/tsconfig.json +14 -0
  118. package/examples/extensions/emacs-buffer.ts +1 -1
  119. package/examples/extensions/interactive-prompts.ts +114 -69
  120. package/examples/extensions/latex-images.ts +3 -3
  121. package/examples/extensions/opencode-bridge/index.ts +1 -1
  122. package/examples/extensions/overlay-agent.ts +7 -5
  123. package/examples/extensions/peer-mesh.ts +1 -1
  124. package/examples/extensions/pi-bridge/index.ts +0 -1
  125. package/examples/extensions/questionnaire.ts +2 -1
  126. package/examples/extensions/rtk-proxy.ts +3 -3
  127. package/examples/extensions/solarized-theme.ts +3 -3
  128. package/examples/extensions/subagents.ts +6 -6
  129. package/examples/extensions/terminal-buffer.ts +1 -1
  130. package/examples/extensions/tmux-pane.ts +6 -4
  131. package/examples/extensions/tunnel-vision.ts +5 -5
  132. package/examples/extensions/user-shell.ts +1 -1
  133. package/examples/extensions/web-access.ts +5 -5
  134. package/package.json +26 -22
  135. package/dist/extensions/agent-backend.d.ts +0 -14
  136. package/dist/extensions/agent-backend.js +0 -307
  137. package/dist/types.d.ts +0 -227
  138. /package/dist/{types.js → agent/host-types.js} +0 -0
  139. /package/dist/{extensions → agent}/providers/openai-compatible.js +0 -0
  140. /package/dist/{index.d.ts → cli/index.d.ts} +0 -0
  141. /package/dist/{init.d.ts → cli/init.d.ts} +0 -0
  142. /package/dist/{install.d.ts → cli/install.d.ts} +0 -0
  143. /package/dist/{event-bus.js → core/event-bus.js} +0 -0
  144. /package/dist/{executor.d.ts → utils/executor.d.ts} +0 -0
@@ -30,7 +30,7 @@
30
30
  */
31
31
  import * as fs from "node:fs";
32
32
  import * as path from "node:path";
33
- import type { ExtensionContext } from "agent-sh/types";
33
+ import type { AgentContext } from "agent-sh/types";
34
34
 
35
35
  const BIND_RE = /\x1b\]9996;vt=([^;]+);BIND;([^;]*);([^\x07]*)\x07/;
36
36
  const OSC7_RE = /\x1b\]7;file:\/\/[^/]*(\/[^\x07\x1b]*)/;
@@ -137,7 +137,7 @@ function renderInjection(): string {
137
137
  return lines.join("\n");
138
138
  }
139
139
 
140
- export default function activate(ctx: ExtensionContext): void {
140
+ export default function activate(ctx: AgentContext): void {
141
141
  bindingFile = path.join(ctx.getStoragePath("tunnel-vision"), "binding.json");
142
142
  loadBinding();
143
143
 
@@ -216,7 +216,7 @@ export default function activate(ctx: ExtensionContext): void {
216
216
  ctx.bus.emit("ui:info", { message: `Tunnel-vision binding to ${host} cleared.` });
217
217
  });
218
218
 
219
- ctx.registerInstruction("tunnel-vision",
219
+ ctx.agent.registerInstruction("tunnel-vision",
220
220
  `# Tunnel-vision — driving a remote shell through pty_send
221
221
 
222
222
  When tunnel-vision is active (you'll see a "Tunnel-vision active" block in
@@ -302,7 +302,7 @@ prompt-wait to capture output.`
302
302
  });
303
303
  });
304
304
 
305
- ctx.registerTool({
305
+ ctx.agent.registerTool({
306
306
  name: "pty_send",
307
307
  description:
308
308
  "Run a command in the user's interactive remote shell session (tunnel-vision). " +
@@ -399,7 +399,7 @@ prompt-wait to capture output.`
399
399
  },
400
400
  });
401
401
 
402
- ctx.registerContextProducer("tunnel-vision", () =>
402
+ ctx.agent.registerContextProducer("tunnel-vision", () =>
403
403
  binding ? renderInjection() : null,
404
404
  );
405
405
  }
@@ -17,7 +17,7 @@ import type { ExtensionContext } from "agent-sh/types";
17
17
  import type { ToolDefinition } from "agent-sh/agent/types";
18
18
 
19
19
  export default function activate(ctx: ExtensionContext): void {
20
- const { bus, registerTool, registerInstruction } = ctx;
20
+ const { bus } = ctx; const { registerTool, registerInstruction } = ctx.agent;
21
21
  const getCwd = () => ctx.call("cwd") as string;
22
22
 
23
23
  // ── Tool ───────────────────────────────────────────────────────
@@ -16,7 +16,7 @@
16
16
  *
17
17
  * Inspired by: https://github.com/nicobailon/pi-web-access
18
18
  */
19
- import type { ExtensionContext } from "agent-sh/types";
19
+ import type { AgentContext } from "agent-sh/types";
20
20
 
21
21
  // ── Constants ────────────────────────────────────────────────────────
22
22
 
@@ -123,7 +123,7 @@ async function directFetch(url: string, timeout: number): Promise<string> {
123
123
 
124
124
  // ── Extension entry point ────────────────────────────────────────────
125
125
 
126
- export default function activate(ctx: ExtensionContext) {
126
+ export default function activate(ctx: AgentContext) {
127
127
  const config = ctx.getExtensionSettings("web-access", {
128
128
  timeout: 30000,
129
129
  searchNumResults: 5,
@@ -134,7 +134,7 @@ export default function activate(ctx: ExtensionContext) {
134
134
 
135
135
  // ── System instruction ────────────────────────────────────────────
136
136
 
137
- ctx.registerInstruction(
137
+ ctx.agent.registerInstruction(
138
138
  "You have access to web search and fetching tools. " +
139
139
  "Use `web_search` to find information on the web, then `web_fetch` to read specific pages. " +
140
140
  "Use `web_fetch` with `raw: true` for JSON APIs or plain text files.",
@@ -142,7 +142,7 @@ export default function activate(ctx: ExtensionContext) {
142
142
 
143
143
  // ── Tool: web_search (Exa MCP, free) ────────────────────────────
144
144
 
145
- ctx.registerTool({
145
+ ctx.agent.registerTool({
146
146
  name: "web_search",
147
147
  displayName: "Web Search",
148
148
  description:
@@ -178,7 +178,7 @@ export default function activate(ctx: ExtensionContext) {
178
178
 
179
179
  // ── Tool: web_fetch ─────────────────────────────────────────────
180
180
 
181
- ctx.registerTool({
181
+ ctx.agent.registerTool({
182
182
  name: "web_fetch",
183
183
  displayName: "Web Fetch",
184
184
  description:
package/package.json CHANGED
@@ -1,34 +1,34 @@
1
1
  {
2
2
  "name": "agent-sh",
3
- "version": "0.12.27",
3
+ "version": "0.13.0",
4
4
  "description": "A shell-first terminal where AI is one keystroke away",
5
5
  "type": "module",
6
- "main": "dist/core.js",
7
- "types": "dist/core.d.ts",
6
+ "main": "dist/core/index.js",
7
+ "types": "dist/core/index.d.ts",
8
8
  "bin": {
9
- "agent-sh": "dist/index.js"
9
+ "agent-sh": "dist/cli/index.js"
10
10
  },
11
11
  "exports": {
12
12
  ".": {
13
- "types": "./dist/core.d.ts",
14
- "default": "./dist/core.js"
13
+ "types": "./dist/core/index.d.ts",
14
+ "default": "./dist/core/index.js"
15
15
  },
16
16
  "./core": {
17
- "types": "./dist/core.d.ts",
18
- "default": "./dist/core.js"
17
+ "types": "./dist/core/index.d.ts",
18
+ "default": "./dist/core/index.js"
19
19
  },
20
20
  "./utils/*": "./dist/utils/*",
21
21
  "./types": {
22
- "types": "./dist/types.d.ts",
23
- "default": "./dist/types.js"
22
+ "types": "./dist/core/index.d.ts",
23
+ "default": "./dist/core/index.js"
24
24
  },
25
25
  "./settings": {
26
- "types": "./dist/settings.d.ts",
27
- "default": "./dist/settings.js"
26
+ "types": "./dist/core/settings.d.ts",
27
+ "default": "./dist/core/settings.js"
28
28
  },
29
29
  "./extension-loader": {
30
- "types": "./dist/extension-loader.d.ts",
31
- "default": "./dist/extension-loader.js"
30
+ "types": "./dist/core/extension-loader.d.ts",
31
+ "default": "./dist/core/extension-loader.js"
32
32
  },
33
33
  "./extensions": {
34
34
  "types": "./dist/extensions/index.d.ts",
@@ -46,6 +46,10 @@
46
46
  "types": "./dist/utils/terminal-buffer.d.ts",
47
47
  "default": "./dist/utils/terminal-buffer.js"
48
48
  },
49
+ "./agent": {
50
+ "types": "./dist/agent/index.d.ts",
51
+ "default": "./dist/agent/index.js"
52
+ },
49
53
  "./agent/types": {
50
54
  "types": "./dist/agent/types.d.ts",
51
55
  "default": "./dist/agent/types.js"
@@ -59,8 +63,8 @@
59
63
  "default": "./dist/agent/agent-loop.js"
60
64
  },
61
65
  "./event-bus": {
62
- "types": "./dist/event-bus.d.ts",
63
- "default": "./dist/event-bus.js"
66
+ "types": "./dist/core/event-bus.d.ts",
67
+ "default": "./dist/core/event-bus.js"
64
68
  },
65
69
  "./utils/compositor": {
66
70
  "types": "./dist/utils/compositor.d.ts",
@@ -83,8 +87,8 @@
83
87
  "default": "./dist/agent/nuclear-form.js"
84
88
  },
85
89
  "./executor": {
86
- "types": "./dist/executor.d.ts",
87
- "default": "./dist/executor.js"
90
+ "types": "./dist/utils/executor.d.ts",
91
+ "default": "./dist/utils/executor.js"
88
92
  }
89
93
  },
90
94
  "files": [
@@ -93,15 +97,15 @@
93
97
  "examples/extensions/*/package.json",
94
98
  "examples/extensions/*/tsconfig.json",
95
99
  "examples/extensions/*/README.md",
96
- "examples/extensions/*/src",
100
+ "examples/extensions/*/src/**",
97
101
  "examples/extensions/*/index.ts",
98
102
  "examples/extensions/*/index.js"
99
103
  ],
100
104
  "scripts": {
101
- "dev": "tsx src/index.ts",
105
+ "dev": "tsx src/cli/index.ts",
102
106
  "build": "tsc",
103
- "start": "node dist/index.js",
104
- "prepare": "npm run build"
107
+ "start": "node dist/cli/index.js",
108
+ "prepare": "test -d dist || npm run build"
105
109
  },
106
110
  "keywords": [
107
111
  "terminal",
@@ -1,14 +0,0 @@
1
- /**
2
- * Built-in agent backend extension.
3
- *
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.
12
- */
13
- import type { ExtensionContext } from "../types.js";
14
- export default function agentBackend(ctx: ExtensionContext): void;
@@ -1,307 +0,0 @@
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
- import { PACKAGE_VERSION } from "../utils/package-version.js";
5
- import { discoverSkills } from "../agent/skills.js";
6
- /** Read the user's persisted defaultModel for a provider, if any. */
7
- function persistedModelFor(providerName) {
8
- if (!providerName)
9
- return undefined;
10
- return getSettings().providers?.[providerName]?.defaultModel;
11
- }
12
- function defaultReasoningBuilder(level) {
13
- return level === "off" ? {} : { reasoning_effort: level };
14
- }
15
- function mergeCaps(settingsCaps, payloadCaps, modelIds) {
16
- if (!settingsCaps)
17
- return payloadCaps.size > 0 ? payloadCaps : undefined;
18
- const out = new Map();
19
- for (const id of modelIds) {
20
- const s = settingsCaps.get(id);
21
- const p = payloadCaps.get(id);
22
- if (!s && !p)
23
- continue;
24
- out.set(id, {
25
- reasoning: s?.reasoning ?? p?.reasoning,
26
- contextWindow: s?.contextWindow ?? p?.contextWindow,
27
- maxTokens: s?.maxTokens ?? p?.maxTokens,
28
- echoReasoning: s?.echoReasoning ?? p?.echoReasoning,
29
- });
30
- }
31
- return out.size > 0 ? out : undefined;
32
- }
33
- export default function agentBackend(ctx) {
34
- const { bus } = ctx;
35
- const config = ctx.call("config:get-shell-config") ?? {};
36
- // Immutable settings snapshot; provider:register payloads merge against it.
37
- const providerRegistry = new Map();
38
- const settingsProviders = new Map();
39
- for (const name of getProviderNames()) {
40
- const p = resolveProvider(name);
41
- if (p) {
42
- providerRegistry.set(name, p);
43
- settingsProviders.set(name, p);
44
- }
45
- }
46
- const providerHooks = new Map();
47
- // Bakes model id into the hook so AgentMode.buildReasoningParams keeps
48
- // its (level) signature while the hook can branch on model.
49
- const bindReasoning = (shapeId, model) => {
50
- const hook = providerHooks.get(shapeId)?.reasoningParams;
51
- return hook ? (level) => hook(level, model) : defaultReasoningBuilder;
52
- };
53
- const buildModes = () => {
54
- const allModes = [];
55
- for (const [id, p] of providerRegistry) {
56
- if (!p.apiKey)
57
- continue;
58
- const shapeId = p.reasoningShape ?? id;
59
- for (const model of p.models) {
60
- const mc = p.modelCapabilities?.get(model);
61
- allModes.push({
62
- model,
63
- provider: id,
64
- providerConfig: { apiKey: p.apiKey, baseURL: p.baseURL },
65
- contextWindow: mc?.contextWindow ?? p.contextWindow,
66
- maxTokens: mc?.maxTokens ?? (mc?.contextWindow ? Math.min(Math.floor(mc.contextWindow * 0.4), 65536) : undefined),
67
- reasoning: mc?.reasoning,
68
- supportsReasoningEffort: p.supportsReasoningEffort,
69
- echoReasoning: mc?.echoReasoning,
70
- buildReasoningParams: bindReasoning(shapeId, model),
71
- });
72
- }
73
- }
74
- return allModes;
75
- };
76
- // Placeholder client — reconfigured at core:extensions-loaded. Any
77
- // stream() call before then fails from the OpenAI SDK; start() won't
78
- // wire the loop until we've resolved, so users never hit that path.
79
- const llmClient = new LlmClient({ apiKey: "not-configured", model: "not-configured" });
80
- ctx.define("llm:get-client", () => llmClient);
81
- ctx.define("llm:invoke", (messages, opts) => {
82
- return llmClient.complete({
83
- messages: messages,
84
- max_tokens: opts?.maxTokens,
85
- model: opts?.model,
86
- reasoning_effort: opts?.reasoningEffort,
87
- });
88
- });
89
- let modes = [];
90
- let initialModeIndex = 0;
91
- let resolved = false;
92
- // Gates late-registration reconcile so its config:switch-model emit doesn't misroute under a non-ash backend.
93
- let ashActive = false;
94
- bus.onPipe("config:get-initial-modes", () => ({ modes, initialModeIndex }));
95
- // AgentLoop must be constructed *before* user extensions activate,
96
- // because its ctor defines handlers (history:append, etc.) that
97
- // extensions like superash call synchronously during their own
98
- // activate. Advise-before-define works for advisers, but plain calls
99
- // would hit a no-op stub.
100
- const agentLoop = new AgentLoop({
101
- bus,
102
- llmClient,
103
- handlers: { define: ctx.define, advise: ctx.advise, call: ctx.call, list: ctx.list },
104
- modes,
105
- initialModeIndex,
106
- compositor: ctx.compositor,
107
- instanceId: ctx.instanceId,
108
- history: config.history,
109
- });
110
- let loadedExtensionNames = [];
111
- bus.on("core:extensions-loaded", ({ names }) => {
112
- loadedExtensionNames = names;
113
- const settings = getSettings();
114
- // If the user didn't pick a default, fall back to the first registered
115
- // provider (built-in load order biases to openrouter → openai).
116
- const providerName = config.provider ?? settings.defaultProvider
117
- ?? (providerRegistry.size > 0 ? providerRegistry.keys().next().value : undefined);
118
- const activeProvider = providerName ? providerRegistry.get(providerName) ?? null : null;
119
- // User's persisted defaultModel wins over the provider's declared
120
- // default. Dynamic providers (openrouter) re-register with their
121
- // hardcoded DEFAULT_MODELS[0] each startup, which would otherwise
122
- // clobber the user's /model selection.
123
- const effectiveApiKey = config.apiKey ?? activeProvider?.apiKey;
124
- const effectiveBaseURL = config.baseURL ?? activeProvider?.baseURL;
125
- const effectiveModel = config.model ?? persistedModelFor(providerName) ?? activeProvider?.defaultModel;
126
- // No provider → don't register ash at all, so another backend (e.g.
127
- // claude-code-bridge) can own activation. index.ts hard-fails only
128
- // when no backend ended up registered.
129
- if (!effectiveApiKey || !effectiveModel)
130
- return;
131
- modes = buildModes();
132
- if (modes.length === 0)
133
- modes = [{ model: effectiveModel }];
134
- let foundIdx = modes.findIndex((m) => m.model === effectiveModel && (!activeProvider || m.provider === activeProvider.id));
135
- // Persisted default may not be in the provider's curated list yet (e.g.
136
- // openrouter's async catalog fetch hasn't returned). Prepend a stub so
137
- // the initial config:set-modes activeIndex points at the real model —
138
- // otherwise AgentLoop reconfigures llmClient back to modes[0].
139
- if (foundIdx === -1 && activeProvider) {
140
- modes = [
141
- {
142
- model: effectiveModel,
143
- provider: activeProvider.id,
144
- providerConfig: { apiKey: effectiveApiKey, baseURL: effectiveBaseURL },
145
- supportsReasoningEffort: activeProvider.supportsReasoningEffort,
146
- },
147
- ...modes,
148
- ];
149
- foundIdx = 0;
150
- }
151
- initialModeIndex = Math.max(0, foundIdx);
152
- llmClient.reconfigure({ apiKey: effectiveApiKey, baseURL: effectiveBaseURL, model: effectiveModel });
153
- bus.emit("config:set-modes", { modes, activeIndex: initialModeIndex });
154
- resolved = true;
155
- bus.emit("agent:register-backend", {
156
- name: "ash",
157
- kill: () => {
158
- ashActive = false;
159
- bus.emit("command:unregister", { name: "/compact" });
160
- bus.emit("command:unregister", { name: "/context" });
161
- agentLoop.kill();
162
- },
163
- start: async () => {
164
- agentLoop.wire();
165
- ashActive = true;
166
- bus.emit("command:register", {
167
- name: "/compact",
168
- description: "Compact conversation via the active compaction strategy",
169
- handler: () => bus.emit("agent:compact-request", {}),
170
- });
171
- bus.emit("command:register", {
172
- name: "/context",
173
- description: "Show context budget usage",
174
- handler: () => {
175
- const stats = bus.emitPipe("context:get-stats", {
176
- activeTokens: 0,
177
- totalTokens: 0,
178
- budgetTokens: 0,
179
- });
180
- const pct = stats.budgetTokens > 0
181
- ? Math.round((stats.activeTokens / stats.budgetTokens) * 100)
182
- : 0;
183
- bus.emit("ui:info", {
184
- message: `Active context: ~${stats.activeTokens.toLocaleString()} tokens / ${stats.budgetTokens.toLocaleString()} budget (${pct}%)`,
185
- });
186
- },
187
- });
188
- bus.emit("agent:info", {
189
- name: "ash",
190
- version: PACKAGE_VERSION,
191
- model: llmClient.model,
192
- provider: modes[initialModeIndex]?.provider,
193
- contextWindow: modes[initialModeIndex]?.contextWindow,
194
- });
195
- },
196
- });
197
- });
198
- bus.on("provider:configure", ({ id, reasoningParams }) => {
199
- const prev = providerHooks.get(id) ?? {};
200
- if (reasoningParams !== undefined)
201
- prev.reasoningParams = reasoningParams;
202
- providerHooks.set(id, prev);
203
- });
204
- bus.on("provider:register", (p) => {
205
- const rawModels = p.models ?? (p.defaultModel ? [p.defaultModel] : []);
206
- const payloadModelIds = [];
207
- const payloadCaps = new Map();
208
- for (const m of rawModels) {
209
- if (typeof m === "string") {
210
- payloadModelIds.push(m);
211
- }
212
- else {
213
- payloadModelIds.push(m.id);
214
- payloadCaps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow, maxTokens: m.maxTokens, echoReasoning: m.echoReasoning });
215
- }
216
- }
217
- const settings = settingsProviders.get(p.id);
218
- const modelIds = settings?.modelsExplicit && settings.models.length > 0 ? settings.models : payloadModelIds;
219
- const mergedCaps = mergeCaps(settings?.modelCapabilities, payloadCaps, modelIds);
220
- const merged = {
221
- id: p.id,
222
- apiKey: settings?.apiKey ?? p.apiKey,
223
- baseURL: settings?.baseURL ?? p.baseURL,
224
- defaultModel: settings?.defaultModel ?? p.defaultModel,
225
- models: modelIds,
226
- modelsExplicit: settings?.modelsExplicit ?? false,
227
- contextWindow: settings?.contextWindow,
228
- supportsReasoningEffort: settings?.supportsReasoningEffort ?? p.supportsReasoningEffort,
229
- modelCapabilities: mergedCaps,
230
- reasoningShape: settings?.reasoningShape,
231
- };
232
- providerRegistry.set(p.id, merged);
233
- const addModes = modelIds.map((m) => {
234
- const mc = mergedCaps?.get(m);
235
- return {
236
- model: m,
237
- provider: p.id,
238
- providerConfig: { apiKey: merged.apiKey ?? "", baseURL: merged.baseURL },
239
- contextWindow: mc?.contextWindow,
240
- maxTokens: mc?.maxTokens,
241
- reasoning: mc?.reasoning,
242
- supportsReasoningEffort: merged.supportsReasoningEffort,
243
- echoReasoning: mc?.echoReasoning,
244
- buildReasoningParams: bindReasoning(p.id, m),
245
- };
246
- });
247
- bus.emit("config:add-modes", { modes: addModes });
248
- // Late-registration reconcile: if this completes the user's persisted
249
- // default (openrouter's async fetch delivers the full catalog after
250
- // we've already fallen back to mode 0), quietly switch to it.
251
- if (!resolved || !ashActive)
252
- return;
253
- const pendingProvider = getSettings().defaultProvider;
254
- if (pendingProvider !== p.id)
255
- return;
256
- const pendingModel = persistedModelFor(pendingProvider);
257
- if (pendingModel && modelIds.includes(pendingModel) && llmClient.model !== pendingModel) {
258
- bus.emit("config:switch-model", { model: pendingModel });
259
- }
260
- });
261
- bus.on("config:switch-provider", ({ provider: name }) => {
262
- const p = providerRegistry.get(name);
263
- if (!p) {
264
- bus.emit("ui:error", { message: `Unknown provider: ${name}` });
265
- return;
266
- }
267
- if (!p.apiKey) {
268
- bus.emit("ui:error", { message: `Provider "${name}" has no API key configured` });
269
- return;
270
- }
271
- const switchModel = p.defaultModel ?? p.models[0];
272
- if (!switchModel) {
273
- bus.emit("ui:error", { message: `Provider "${name}" has no models configured` });
274
- return;
275
- }
276
- llmClient.reconfigure({ apiKey: p.apiKey, baseURL: p.baseURL, model: switchModel });
277
- const newModes = p.models.map((m) => {
278
- const mc = p.modelCapabilities?.get(m);
279
- return {
280
- model: m,
281
- provider: name,
282
- providerConfig: { apiKey: p.apiKey, baseURL: p.baseURL },
283
- contextWindow: mc?.contextWindow ?? p.contextWindow,
284
- maxTokens: mc?.maxTokens ?? (mc?.contextWindow ? Math.min(Math.floor(mc.contextWindow * 0.4), 65536) : undefined),
285
- reasoning: mc?.reasoning,
286
- supportsReasoningEffort: p.supportsReasoningEffort,
287
- echoReasoning: mc?.echoReasoning,
288
- };
289
- });
290
- bus.emit("config:set-modes", { modes: newModes });
291
- bus.emit("agent:info", { name: "ash", version: PACKAGE_VERSION, model: switchModel, provider: name, contextWindow: p.contextWindow });
292
- bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
293
- bus.emit("config:changed", {});
294
- });
295
- bus.onPipe("banner:collect", (e) => {
296
- if (e.activeBackend && e.activeBackend !== "ash")
297
- return e;
298
- if (loadedExtensionNames.length > 0) {
299
- e.sections.push({ label: "Extensions", items: [...loadedExtensionNames] });
300
- }
301
- const skills = discoverSkills(ctx.call("cwd") ?? process.cwd());
302
- if (skills.length > 0) {
303
- e.sections.push({ label: "Skills", items: skills.map((s) => s.name) });
304
- }
305
- return e;
306
- });
307
- }