agent-sh 0.13.7 → 0.14.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 (63) hide show
  1. package/README.md +7 -18
  2. package/dist/agent/agent-loop.d.ts +13 -17
  3. package/dist/agent/agent-loop.js +118 -224
  4. package/dist/agent/conversation-state.d.ts +1 -1
  5. package/dist/agent/events.d.ts +218 -0
  6. package/dist/agent/events.js +1 -0
  7. package/dist/agent/host-types.d.ts +20 -0
  8. package/dist/agent/index.d.ts +5 -9
  9. package/dist/agent/index.js +269 -167
  10. package/dist/{utils → agent}/llm-client.js +1 -0
  11. package/dist/agent/llm-facade.d.ts +13 -0
  12. package/dist/{utils → agent}/llm-facade.js +1 -1
  13. package/dist/agent/nuclear-form.d.ts +1 -1
  14. package/dist/agent/providers/deepseek.js +2 -5
  15. package/dist/agent/providers/openai-compatible.js +2 -2
  16. package/dist/agent/providers/openai.js +2 -5
  17. package/dist/agent/providers/openrouter.js +5 -5
  18. package/dist/agent/subagent.d.ts +1 -1
  19. package/dist/agent/tool-protocol.d.ts +1 -1
  20. package/dist/agent/tool-registry.d.ts +1 -1
  21. package/dist/agent/types.d.ts +2 -2
  22. package/dist/cli/args.js +3 -1
  23. package/dist/cli/auth/cli.js +11 -6
  24. package/dist/cli/auth/discover.d.ts +5 -0
  25. package/dist/cli/auth/discover.js +25 -0
  26. package/dist/cli/auth/keys.d.ts +5 -2
  27. package/dist/cli/auth/keys.js +22 -2
  28. package/dist/cli/index.d.ts +16 -0
  29. package/dist/cli/index.js +12 -2
  30. package/dist/cli/install.d.ts +1 -0
  31. package/dist/cli/install.js +86 -2
  32. package/dist/cli/subcommands.js +4 -1
  33. package/dist/core/event-bus.d.ts +28 -371
  34. package/dist/core/extension-loader.js +6 -6
  35. package/dist/core/index.d.ts +10 -29
  36. package/dist/core/index.js +32 -84
  37. package/dist/extensions/index.d.ts +2 -1
  38. package/dist/extensions/index.js +1 -1
  39. package/dist/extensions/slash-commands/events.d.ts +18 -0
  40. package/dist/extensions/slash-commands/events.js +1 -0
  41. package/dist/extensions/slash-commands/index.d.ts +15 -0
  42. package/dist/extensions/{slash-commands.js → slash-commands/index.js} +4 -3
  43. package/dist/shell/events.d.ts +85 -0
  44. package/dist/shell/events.js +1 -0
  45. package/dist/shell/index.d.ts +1 -0
  46. package/dist/shell/index.js +6 -0
  47. package/dist/shell/tui-renderer.js +0 -1
  48. package/dist/utils/tool-interactive.js +4 -2
  49. package/examples/extensions/ash-acp-bridge/src/index.ts +2 -2
  50. package/examples/extensions/ashi/package.json +2 -2
  51. package/examples/extensions/ashi/src/cli.ts +4 -1
  52. package/examples/extensions/claude-code-bridge/index.ts +7 -2
  53. package/examples/extensions/claude-code-bridge/package.json +1 -1
  54. package/examples/extensions/ollama.ts +47 -42
  55. package/examples/extensions/opencode-bridge/README.md +4 -0
  56. package/examples/extensions/opencode-bridge/index.ts +208 -54
  57. package/examples/extensions/opencode-bridge/package.json +1 -1
  58. package/examples/extensions/pi-bridge/index.ts +3 -4
  59. package/examples/extensions/zai-coding-plan.ts +2 -6
  60. package/package.json +1 -1
  61. package/dist/extensions/slash-commands.d.ts +0 -2
  62. package/dist/utils/llm-facade.d.ts +0 -11
  63. /package/dist/{utils → agent}/llm-client.d.ts +0 -0
@@ -1,23 +1,14 @@
1
1
  /**
2
- * Core kernel — the minimum viable agent-sh.
3
- *
4
- * Wires up EventBus + HandlerRegistry without any frontend or agent backend.
5
- * Consumers attach their own I/O (Shell, WebSocket, REST, tests) by
6
- * subscribing to bus events. Shell-specific tracking lives in the
7
- * shell-context built-in extension.
8
- *
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
- *
13
- * Usage:
14
- * import { createCore } from "agent-sh";
15
- * const core = createCore({ apiKey: "...", model: "gpt-4o" });
16
- * core.bus.on("agent:response-chunk", ({ blocks }) => { ... });
17
- * core.activateBackend();
18
- * const response = await core.query("hello");
2
+ * Core kernel — EventBus + HandlerRegistry + backend registry. Knows
3
+ * nothing about LLMs, tools, or specific backends; backends (ash,
4
+ * claude-code-bridge, ...) register through `agent:register-backend`
5
+ * and core dispatches to whichever is configured as default.
19
6
  */
20
7
  import { EventBus } from "./event-bus.js";
8
+ // Side-effect imports so downstream tsc sees module-augmented BusEvents.
9
+ import "../shell/events.js";
10
+ import "../agent/events.js";
11
+ import "../extensions/slash-commands/events.js";
21
12
  import * as settingsMod from "./settings.js";
22
13
  import { HandlerRegistry } from "../utils/handler-registry.js";
23
14
  import crypto from "node:crypto";
@@ -27,118 +18,74 @@ import { CONFIG_DIR } from "./settings.js";
27
18
  export { EventBus } from "./event-bus.js";
28
19
  export { palette, setPalette, resetPalette } from "../utils/palette.js";
29
20
  export { runSubagent } from "../agent/subagent.js";
30
- export { LlmClient } from "../utils/llm-client.js";
21
+ export { LlmClient } from "../agent/llm-client.js";
31
22
  export { HistoryFile, InMemoryHistory, NoopHistory } from "../agent/history-file.js";
32
23
  export { compileSearchRegex, matchEntry, formatNuclearLine } from "../agent/nuclear-form.js";
33
24
  export function createCore(config) {
34
25
  const bus = new EventBus();
35
26
  const handlers = new HandlerRegistry();
36
- // 3 bytes = 6 hex chars, ~16M values ample for per-lineage uniqueness and
37
- // short enough to read/remember. Legacy content may have 16-char iids; any
38
- // parsers should accept ≥6 hex chars.
27
+ // 3 bytes = 6 hex chars; legacy content may have 16-char iids so parsers
28
+ // should accept ≥6 hex chars.
39
29
  const instanceId = crypto.randomBytes(3).toString("hex");
40
30
  bus.setSource(instanceId);
41
- const settings = settingsMod.getSettings();
42
31
  handlers.define("config:get-app-config", () => config);
43
- // Default; shell-context advises with the PTY-tracked cwd when loaded.
44
32
  handlers.define("cwd", () => process.cwd());
45
33
  // Empty defaults so registerContextProducer can advise regardless of
46
- // backend. Each backend chooses whether to consume the strings — ash
47
- // wraps them in <dynamic_context>/<query_context>; bridges may pull
48
- // query-context:build and splice into the target SDK however they like.
34
+ // backend. Each backend chooses how to consume the strings.
49
35
  handlers.define("dynamic-context:build", () => "");
50
36
  handlers.define("query-context:build", () => "");
51
37
  const backends = new Map();
52
38
  let activeBackendName = null;
39
+ bus.on("agent:register-backend", (backend) => {
40
+ backends.set(backend.name, backend);
41
+ });
42
+ bus.onPipe("config:get-backends", () => ({
43
+ names: [...backends.keys()],
44
+ active: activeBackendName,
45
+ }));
53
46
  const activateByName = async (name) => {
54
47
  const backend = backends.get(name);
55
48
  if (!backend) {
56
49
  bus.emit("ui:error", { message: `Unknown backend: ${name}` });
57
50
  return false;
58
51
  }
59
- for (const [otherName, otherBackend] of backends) {
60
- if (otherName !== name)
61
- otherBackend.kill();
52
+ if (activeBackendName && activeBackendName !== name) {
53
+ backends.get(activeBackendName)?.kill();
62
54
  }
63
- await backend.start?.();
64
55
  activeBackendName = name;
56
+ await backend.start?.();
65
57
  return true;
66
58
  };
67
- bus.on("agent:register-backend", (backend) => {
68
- backends.set(backend.name, backend);
69
- });
70
59
  bus.on("config:switch-backend", ({ name }) => {
71
60
  activateByName(name).then((ok) => {
72
61
  if (!ok)
73
62
  return;
74
63
  settingsMod.updateSettings({ defaultBackend: name });
75
- // Single ui:info; config:changed (which triggers prompt redraw) follows it.
76
64
  bus.emit("ui:info", { message: `Backend: ${name} (saved as default)` });
77
65
  bus.emit("config:changed", {});
78
66
  });
79
67
  });
80
68
  bus.on("config:list-backends", () => {
81
- const names = [...backends.keys()];
82
- const list = names
69
+ const list = [...backends.keys()]
83
70
  .map((n) => n === activeBackendName ? `${n} (active)` : n)
84
71
  .join(", ");
85
- bus.emit("ui:info", { message: `Backends: ${list}` });
86
- });
87
- bus.onPipe("config:get-backends", () => {
88
- const names = [...backends.keys()];
89
- return { names, active: activeBackendName };
72
+ bus.emit("ui:info", { message: `Backends: ${list || "(none registered)"}` });
90
73
  });
91
74
  return {
92
75
  bus,
93
76
  handlers,
94
77
  instanceId,
95
78
  async activateBackend(override) {
96
- if (backends.size === 0)
79
+ if (backends.size === 0) {
80
+ bus.emit("ui:info", { message: "No agent backend registered." });
97
81
  return;
98
- const preferred = override ?? settings.defaultBackend;
99
- const name = preferred && backends.has(preferred) ? preferred : backends.keys().next().value;
82
+ }
83
+ const preferred = override ?? settingsMod.getSettings().defaultBackend;
84
+ const name = preferred && backends.has(preferred)
85
+ ? preferred
86
+ : backends.keys().next().value;
100
87
  await activateByName(name);
101
88
  },
102
- async query(text) {
103
- return new Promise((resolve, reject) => {
104
- let response = "";
105
- let settled = false;
106
- const onChunk = (e) => {
107
- for (const b of e.blocks)
108
- if (b.type === "text")
109
- response += b.text;
110
- };
111
- const onDone = () => {
112
- if (settled)
113
- return;
114
- settled = true;
115
- cleanup();
116
- resolve(response);
117
- };
118
- const onError = (e) => {
119
- if (settled)
120
- return;
121
- settled = true;
122
- cleanup();
123
- reject(new Error(e.message));
124
- };
125
- const cleanup = () => {
126
- bus.off("agent:response-chunk", onChunk);
127
- bus.off("agent:processing-done", onDone);
128
- bus.off("agent:error", onError);
129
- };
130
- bus.on("agent:response-chunk", onChunk);
131
- bus.on("agent:processing-done", onDone);
132
- bus.on("agent:error", onError);
133
- bus.emit("agent:submit", { query: text });
134
- });
135
- },
136
- cancel() {
137
- bus.emit("agent:cancel-request", {});
138
- },
139
- appendUserMessage(text) {
140
- bus.emit("agent:append-user-message", { text });
141
- },
142
89
  extensionContext(opts) {
143
90
  const ctx = {
144
91
  bus,
@@ -166,6 +113,7 @@ export function createCore(config) {
166
113
  kill() {
167
114
  if (activeBackendName) {
168
115
  backends.get(activeBackendName)?.kill();
116
+ activeBackendName = null;
169
117
  }
170
118
  },
171
119
  };
@@ -2,7 +2,8 @@
2
2
  * Cross-cutting built-ins, toggleable via `disabledBuiltins`.
3
3
  * Module-owned built-ins activate inline:
4
4
  * shell-context, tui-renderer → registerShellHandlers (src/shell/)
5
- * agent-backend, providers → activateAgent (src/agent/)
5
+ * ash (a specific backend) → activateAgent (src/agent/)
6
+ * backend registry → createCore (src/core/)
6
7
  */
7
8
  import type { ExtensionContext } from "../shell/host-types.js";
8
9
  type ActivateFn = (ctx: ExtensionContext) => void;
@@ -1,5 +1,5 @@
1
1
  export const BUILTIN_EXTENSIONS = [
2
- { name: "slash-commands", load: () => import("./slash-commands.js").then(m => m.default) },
2
+ { name: "slash-commands", load: () => import("./slash-commands/index.js").then(m => m.default) },
3
3
  { name: "file-autocomplete", load: () => import("./file-autocomplete.js").then(m => m.default) },
4
4
  ];
5
5
  /**
@@ -0,0 +1,18 @@
1
+ /** Events slash-commands owns. */
2
+ declare module "../../core/event-bus.js" {
3
+ interface BusEvents {
4
+ "command:register": {
5
+ name: string;
6
+ description: string;
7
+ handler: (args: string) => Promise<void> | void;
8
+ };
9
+ "command:unregister": {
10
+ name: string;
11
+ };
12
+ "command:execute": {
13
+ name: string;
14
+ args: string;
15
+ };
16
+ }
17
+ }
18
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Slash commands extension.
3
+ *
4
+ * Registers built-in slash commands on the event bus:
5
+ * - Listens for "command:register" to accept commands from extensions
6
+ * - Responds to "autocomplete:request" pipe for /-prefixed completions
7
+ * - Handles "command:execute" events and dispatches to matching handler
8
+ * - Uses "ui:info"/"ui:error" for user feedback (no direct TUI dependency)
9
+ *
10
+ * Argument completion is composable: any extension can onPipe("autocomplete:request")
11
+ * and check payload.command / payload.commandArgs to add completions for any command.
12
+ */
13
+ import "./events.js";
14
+ import type { ExtensionContext } from "../../shell/host-types.js";
15
+ export default function activate(ctx: ExtensionContext): void;
@@ -10,9 +10,10 @@
10
10
  * Argument completion is composable: any extension can onPipe("autocomplete:request")
11
11
  * and check payload.command / payload.commandArgs to add completions for any command.
12
12
  */
13
- import { palette as p } from "../utils/palette.js";
14
- import { discoverSkills, loadSkillContent } from "../agent/skills.js";
15
- import { reloadExtensions } from "../core/extension-loader.js";
13
+ import "./events.js";
14
+ import { palette as p } from "../../utils/palette.js";
15
+ import { discoverSkills, loadSkillContent } from "../../agent/skills.js";
16
+ import { reloadExtensions } from "../../core/extension-loader.js";
16
17
  export default function activate(ctx) {
17
18
  const { bus } = ctx;
18
19
  const commands = new Map();
@@ -0,0 +1,85 @@
1
+ /** Events owned by the shell subsystem. */
2
+ declare module "../core/event-bus.js" {
3
+ interface BusEvents {
4
+ "shell:command-start": {
5
+ command: string;
6
+ cwd: string;
7
+ };
8
+ "shell:command-done": {
9
+ command: string;
10
+ output: string;
11
+ cwd: string;
12
+ exitCode: number | null;
13
+ };
14
+ "shell:cwd-change": {
15
+ cwd: string;
16
+ };
17
+ "shell:foreground-busy": {
18
+ busy: boolean;
19
+ };
20
+ "shell:agent-exec-start": Record<string, never>;
21
+ "shell:agent-exec-done": Record<string, never>;
22
+ "shell:pty-data": {
23
+ raw: string;
24
+ };
25
+ "shell:pty-write": {
26
+ data: string;
27
+ };
28
+ "shell:pty-resize": {
29
+ cols: number;
30
+ rows: number;
31
+ };
32
+ "shell:buffer-request": Record<string, never>;
33
+ "shell:buffer-snapshot": {
34
+ text: string;
35
+ altScreen: boolean;
36
+ cursor: {
37
+ x: number;
38
+ y: number;
39
+ };
40
+ };
41
+ "shell:stdout-hold": Record<string, never>;
42
+ "shell:stdout-release": Record<string, never>;
43
+ "shell:stdout-show": Record<string, never>;
44
+ "shell:stdout-hide": Record<string, never>;
45
+ /** Sync pipe: handled=true suppresses default redraw. */
46
+ "shell:redraw-prompt": {
47
+ cwd: string;
48
+ kind: "fresh" | "redraw";
49
+ handled: boolean;
50
+ };
51
+ /** Async pipe: extension → user's PTY. */
52
+ "shell:exec-request": {
53
+ command: string;
54
+ output: string;
55
+ cwd: string;
56
+ exitCode: number | null;
57
+ done: boolean;
58
+ };
59
+ "input-mode:register": import("./host-types.js").InputModeConfig;
60
+ "input:keypress": {
61
+ key: string;
62
+ };
63
+ "input:intercept": {
64
+ data: string;
65
+ consumed: boolean;
66
+ };
67
+ "compositor:write": {
68
+ stream: string;
69
+ text: string;
70
+ };
71
+ /** Sync pipe: extensions append items. */
72
+ "autocomplete:request": {
73
+ buffer: string;
74
+ /** "/backend" or null if not a command. */
75
+ command: string | null;
76
+ /** Text after the command name, or null. */
77
+ commandArgs: string | null;
78
+ items: {
79
+ name: string;
80
+ description: string;
81
+ }[];
82
+ };
83
+ }
84
+ }
85
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -3,6 +3,7 @@
3
3
  * built-in extensions manifest) because PTY + stdin raw mode ownership is
4
4
  * order-critical.
5
5
  */
6
+ import "./events.js";
6
7
  import type { ExtensionContext } from "./host-types.js";
7
8
  export interface ShellActivateOptions {
8
9
  cols: number;
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Frontend bootstrap. Loaded directly from src/cli/index.ts (not the
3
+ * built-in extensions manifest) because PTY + stdin raw mode ownership is
4
+ * order-critical.
5
+ */
6
+ import "./events.js"; // augments BusEvents with shell-owned events
1
7
  import { Shell } from "./shell.js";
2
8
  import { DefaultCompositor, StdoutSurface } from "../utils/compositor.js";
3
9
  import { TerminalBuffer } from "../utils/terminal-buffer.js";
@@ -149,7 +149,6 @@ export default function activate(ctx) {
149
149
  titleRight: modelLabel,
150
150
  });
151
151
  });
152
- // Track backend/model info for display on response border
153
152
  let backendInfo = null;
154
153
  bus.on("agent:info", (info) => { backendInfo = info; });
155
154
  // ── Register fenced block transform (code blocks → ContentBlock) ──
@@ -14,7 +14,6 @@ export function createToolUI(bus, surface) {
14
14
  if (finished)
15
15
  return;
16
16
  finished = true;
17
- clearLines(surface, prevLineCount);
18
17
  bus.offPipe("input:intercept", interceptor);
19
18
  bus.emit("shell:stdout-hide", {});
20
19
  bus.emit("tool:interactive-end", {});
@@ -45,7 +44,10 @@ export function createToolUI(bus, surface) {
45
44
  bus.emit("tool:interactive-start", {});
46
45
  bus.emit("shell:stdout-show", {});
47
46
  bus.onPipe("input:intercept", interceptor);
48
- session.onMount?.(() => render());
47
+ // Drop to a fresh row in case the cursor was mid-line; uncounted
48
+ // so clearLines on dismiss stops at the gap, not above it.
49
+ surface.write("\n");
50
+ session.onMount?.(() => render(), done);
49
51
  render();
50
52
  });
51
53
  },
@@ -434,10 +434,10 @@ function waitForModelsToSettle(
434
434
  timer = setTimeout(done, Math.max(0, Math.min(quietMs, remaining)));
435
435
  };
436
436
  const done = () => {
437
- core.bus.off("config:add-modes", arm);
437
+ core.bus.off("agent:modes-changed", arm);
438
438
  resolve();
439
439
  };
440
- core.bus.on("config:add-modes", arm);
440
+ core.bus.on("agent:modes-changed", arm);
441
441
  arm();
442
442
  });
443
443
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanyilun/ashi",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Ash in an interactive TUI — agent-sh's built-in agent without the shell underneath",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "@earendil-works/pi-tui": "^0.74.0",
51
- "agent-sh": "^0.13.3",
51
+ "agent-sh": "^0.14.0",
52
52
  "chalk": "^5.5.0",
53
53
  "cli-highlight": "^2.1.11"
54
54
  },
@@ -70,7 +70,10 @@ async function main(): Promise<void> {
70
70
 
71
71
  if (sub === "install" || sub === "uninstall" || sub === "list") {
72
72
  const { runInstall, runUninstall, runList } = await import("agent-sh/cli/install");
73
- if (sub === "install") await runInstall(rest[0] ?? "", { force: rest.includes("--force") });
73
+ if (sub === "install") await runInstall(rest[0] ?? "", {
74
+ force: rest.includes("--force"),
75
+ syncDeps: rest.includes("--sync-deps"),
76
+ });
74
77
  else if (sub === "uninstall") await runUninstall(rest[0] ?? "");
75
78
  else runList();
76
79
  process.exit(0);
@@ -21,6 +21,7 @@ export default function activate(ctx: ExtensionContext): void {
21
21
  };
22
22
 
23
23
  let activeQuery: Query | null = null;
24
+ let sessionId: string | null = null;
24
25
  const listeners: Array<{ event: string; fn: Function }> = [];
25
26
 
26
27
  // ── Tool display helpers ────────────────────────────────────────
@@ -97,6 +98,7 @@ export default function activate(ctx: ExtensionContext): void {
97
98
  allowedTools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
98
99
  permissionMode: "acceptEdits",
99
100
  includePartialMessages: true,
101
+ ...(sessionId ? { resume: sessionId } : {}),
100
102
  },
101
103
  });
102
104
 
@@ -262,8 +264,11 @@ export default function activate(ctx: ExtensionContext): void {
262
264
  // Tool still running — nothing to do, TUI spinner already active
263
265
  break;
264
266
 
265
- case "result":
267
+ case "result": {
268
+ const sid = (message as any).session_id;
269
+ if (typeof sid === "string" && sid) sessionId = sid;
266
270
  break;
271
+ }
267
272
  }
268
273
  }
269
274
 
@@ -291,7 +296,7 @@ export default function activate(ctx: ExtensionContext): void {
291
296
  };
292
297
 
293
298
  const onCancel = () => { activeQuery?.interrupt(); };
294
- const onReset = () => { /* each query() is a new session */ };
299
+ const onReset = () => { sessionId = null; };
295
300
 
296
301
  bus.on("agent:submit", onSubmit);
297
302
  bus.on("agent:cancel-request", onCancel);
@@ -6,7 +6,7 @@
6
6
  "main": "index.ts",
7
7
  "dependencies": {
8
8
  "@anthropic-ai/claude-agent-sdk": "^0.2.0",
9
- "agent-sh": "^0.9.0",
9
+ "agent-sh": "^0.14.0",
10
10
  "zod": "^4.0.0"
11
11
  }
12
12
  }
@@ -1,60 +1,65 @@
1
1
  /**
2
- * Ollama provider extension local daemon or Ollama Cloud.
3
- *
4
- * Cloud auth (any of):
5
- * agent-sh auth login ollama-cloud # preferred
6
- * OLLAMA_API_KEY=... # env fallback
7
- *
8
- * Local host:
9
- * OLLAMA_HOST (default http://localhost:11434)
10
- *
11
- * Catalog comes from /api/tags; per-model context length is fetched
12
- * from /api/show (model_info["${arch}.context_length"]). Chat goes
13
- * through the OpenAI-compatible /v1/chat/completions shim.
14
- *
15
- * Usage:
16
- * agent-sh -e ./examples/extensions/ollama.ts
17
- *
18
- * # Or add to settings.json:
19
- * { "extensions": ["./examples/extensions/ollama.ts"] }
2
+ * Registers `ollama` (local, no auth) and `ollama-cloud` (login via
3
+ * `agent-sh auth login ollama-cloud` or OLLAMA_API_KEY). Local host
4
+ * overridable via OLLAMA_HOST.
20
5
  */
21
6
  import { resolveApiKey } from "agent-sh/auth";
22
7
  import type { AgentContext } from "agent-sh/types";
23
8
 
24
9
  const ECHO_REASONING_PATTERNS: RegExp[] = [/deepseek/i];
25
10
 
11
+ function reasoningParams(level: string): Record<string, unknown> {
12
+ if (level === "off") return { reasoning_effort: "none" };
13
+ return { reasoning_effort: level === "xhigh" ? "high" : level };
14
+ }
15
+
26
16
  export default function activate(ctx: AgentContext): void {
27
17
  const cloudKey = resolveApiKey("ollama-cloud").key ?? process.env.OLLAMA_API_KEY;
28
- const host = cloudKey
29
- ? "https://ollama.com"
30
- : (process.env.OLLAMA_HOST ?? "http://localhost:11434").replace(/\/$/, "");
31
- const id = cloudKey ? "ollama-cloud" : "ollama";
32
-
33
- // OpenAI SDK rejects an empty apiKey; the local daemon ignores the value.
34
- const sdkKey = cloudKey || "no-key";
35
- const baseURL = `${host}/v1`;
36
- const headers: Record<string, string> = {};
37
- if (cloudKey) headers.Authorization = `Bearer ${cloudKey}`;
38
-
39
- ctx.agent.providers.configure(id, {
40
- reasoningParams: (level) => {
41
- if (level === "off") return { reasoning_effort: "none" };
42
- return { reasoning_effort: level === "xhigh" ? "high" : level };
43
- },
18
+ const cloudHost = "https://ollama.com";
19
+ const cloudBaseURL = `${cloudHost}/v1`;
20
+ ctx.agent.providers.configure("ollama-cloud", { reasoningParams });
21
+ ctx.agent.providers.register({
22
+ id: "ollama-cloud",
23
+ apiKey: cloudKey ?? undefined,
24
+ baseURL: cloudBaseURL,
25
+ models: [],
44
26
  });
27
+ if (cloudKey) {
28
+ const headers = { Authorization: `Bearer ${cloudKey}` };
29
+ fetchCatalog(cloudHost, headers).then((models) => {
30
+ if (models.length === 0) return;
31
+ ctx.agent.providers.register({
32
+ id: "ollama-cloud",
33
+ apiKey: cloudKey,
34
+ baseURL: cloudBaseURL,
35
+ defaultModel: models[0]!.id,
36
+ models,
37
+ });
38
+ }).catch(() => {});
39
+ }
45
40
 
46
- ctx.bus.emit("provider:register", { id, apiKey: sdkKey, baseURL, models: [] });
47
-
48
- fetchCatalog(host, headers).then((models) => {
41
+ const localHost = (process.env.OLLAMA_HOST ?? "http://localhost:11434").replace(/\/$/, "");
42
+ const localBaseURL = `${localHost}/v1`;
43
+ ctx.agent.providers.configure("ollama", { reasoningParams });
44
+ // OpenAI SDK rejects an empty apiKey; the local daemon ignores it.
45
+ ctx.agent.providers.register({
46
+ id: "ollama",
47
+ apiKey: "no-key",
48
+ baseURL: localBaseURL,
49
+ models: [],
50
+ noAuth: true,
51
+ });
52
+ fetchCatalog(localHost, {}).then((models) => {
49
53
  if (models.length === 0) return;
50
- ctx.bus.emit("provider:register", {
51
- id,
52
- apiKey: sdkKey,
53
- baseURL,
54
+ ctx.agent.providers.register({
55
+ id: "ollama",
56
+ apiKey: "no-key",
57
+ baseURL: localBaseURL,
54
58
  defaultModel: models[0]!.id,
55
59
  models,
60
+ noAuth: true,
56
61
  });
57
- }).catch(() => { /* leave empty — user supplies via --model */ });
62
+ }).catch(() => {});
58
63
  }
59
64
 
60
65
  async function fetchCatalog(
@@ -40,6 +40,10 @@ opencode reads its own config from `~/.local/share/opencode/` (auth credentials)
40
40
  - opencode authenticated locally — run `opencode auth login` once before using this bridge.
41
41
  - Provider env vars (e.g. `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`) as required by opencode for the model you've selected.
42
42
 
43
+ ## Environment
44
+
45
+ - `OPENCODE_SDK_PORT` — port for the in-process opencode HTTP server. Defaults to `0` (OS-assigned free port) so a stale standalone opencode on the SDK's default port 4096 can't collide with the bridge. Set explicitly only if you need a deterministic port.
46
+
43
47
  ## What works under opencode
44
48
 
45
49
  agent-sh's per-query context producers (e.g. `<shell_events>` from `shell-context`) are inlined into opencode's prompt before each query, so opencode sees the user's recent shell activity even though the SDK doesn't subscribe to agent-sh's shell bus directly. The current cwd is part of that context, so opencode knows where the user is even when its tools are anchored elsewhere.