agent-sh 0.13.6 → 0.14.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 (57) hide show
  1. package/README.md +1 -1
  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/agent/llm-facade.d.ts +13 -0
  11. package/dist/{utils → agent}/llm-facade.js +1 -1
  12. package/dist/agent/nuclear-form.d.ts +1 -1
  13. package/dist/agent/providers/deepseek.js +2 -5
  14. package/dist/agent/providers/openai-compatible.js +2 -2
  15. package/dist/agent/providers/openai.js +2 -5
  16. package/dist/agent/providers/openrouter.js +5 -5
  17. package/dist/agent/subagent.d.ts +1 -1
  18. package/dist/agent/tool-protocol.d.ts +1 -1
  19. package/dist/agent/tool-registry.d.ts +1 -1
  20. package/dist/cli/args.d.ts +2 -0
  21. package/dist/cli/args.js +90 -0
  22. package/dist/cli/auth/cli.js +11 -6
  23. package/dist/cli/auth/discover.d.ts +5 -0
  24. package/dist/cli/auth/discover.js +25 -0
  25. package/dist/cli/auth/keys.d.ts +5 -2
  26. package/dist/cli/auth/keys.js +22 -2
  27. package/dist/cli/index.d.ts +16 -0
  28. package/dist/cli/index.js +15 -156
  29. package/dist/cli/shell-env.d.ts +2 -0
  30. package/dist/cli/shell-env.js +61 -0
  31. package/dist/core/event-bus.d.ts +28 -371
  32. package/dist/core/extension-loader.js +6 -6
  33. package/dist/core/index.d.ts +10 -29
  34. package/dist/core/index.js +31 -82
  35. package/dist/extensions/index.d.ts +2 -1
  36. package/dist/extensions/index.js +1 -1
  37. package/dist/extensions/slash-commands/events.d.ts +18 -0
  38. package/dist/extensions/slash-commands/events.js +1 -0
  39. package/dist/extensions/slash-commands/index.d.ts +15 -0
  40. package/dist/extensions/{slash-commands.js → slash-commands/index.js} +4 -3
  41. package/dist/shell/events.d.ts +85 -0
  42. package/dist/shell/events.js +1 -0
  43. package/dist/shell/index.d.ts +1 -0
  44. package/dist/shell/index.js +6 -0
  45. package/dist/shell/tui-renderer.js +0 -1
  46. package/examples/extensions/ash-acp-bridge/src/index.ts +2 -2
  47. package/examples/extensions/ashi/package.json +1 -1
  48. package/examples/extensions/ollama.ts +47 -42
  49. package/examples/extensions/opencode-bridge/README.md +4 -0
  50. package/examples/extensions/opencode-bridge/index.ts +3 -1
  51. package/examples/extensions/pi-bridge/index.ts +3 -4
  52. package/examples/extensions/zai-coding-plan.ts +2 -6
  53. package/package.json +2 -1
  54. package/dist/extensions/slash-commands.d.ts +0 -2
  55. package/dist/utils/llm-facade.d.ts +0 -11
  56. /package/dist/{utils → agent}/llm-client.d.ts +0 -0
  57. /package/dist/{utils → agent}/llm-client.js +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,117 +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
- if (activeBackendName) {
52
+ if (activeBackendName && activeBackendName !== name) {
60
53
  backends.get(activeBackendName)?.kill();
61
54
  }
62
- await backend.start?.();
63
55
  activeBackendName = name;
56
+ await backend.start?.();
64
57
  return true;
65
58
  };
66
- bus.on("agent:register-backend", (backend) => {
67
- backends.set(backend.name, backend);
68
- });
69
59
  bus.on("config:switch-backend", ({ name }) => {
70
60
  activateByName(name).then((ok) => {
71
61
  if (!ok)
72
62
  return;
73
63
  settingsMod.updateSettings({ defaultBackend: name });
74
- // Single ui:info; config:changed (which triggers prompt redraw) follows it.
75
64
  bus.emit("ui:info", { message: `Backend: ${name} (saved as default)` });
76
65
  bus.emit("config:changed", {});
77
66
  });
78
67
  });
79
68
  bus.on("config:list-backends", () => {
80
- const names = [...backends.keys()];
81
- const list = names
69
+ const list = [...backends.keys()]
82
70
  .map((n) => n === activeBackendName ? `${n} (active)` : n)
83
71
  .join(", ");
84
- bus.emit("ui:info", { message: `Backends: ${list}` });
85
- });
86
- bus.onPipe("config:get-backends", () => {
87
- const names = [...backends.keys()];
88
- return { names, active: activeBackendName };
72
+ bus.emit("ui:info", { message: `Backends: ${list || "(none registered)"}` });
89
73
  });
90
74
  return {
91
75
  bus,
92
76
  handlers,
93
77
  instanceId,
94
78
  async activateBackend(override) {
95
- if (backends.size === 0)
79
+ if (backends.size === 0) {
80
+ bus.emit("ui:info", { message: "No agent backend registered." });
96
81
  return;
97
- const preferred = override ?? settings.defaultBackend;
98
- 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;
99
87
  await activateByName(name);
100
88
  },
101
- async query(text) {
102
- return new Promise((resolve, reject) => {
103
- let response = "";
104
- let settled = false;
105
- const onChunk = (e) => {
106
- for (const b of e.blocks)
107
- if (b.type === "text")
108
- response += b.text;
109
- };
110
- const onDone = () => {
111
- if (settled)
112
- return;
113
- settled = true;
114
- cleanup();
115
- resolve(response);
116
- };
117
- const onError = (e) => {
118
- if (settled)
119
- return;
120
- settled = true;
121
- cleanup();
122
- reject(new Error(e.message));
123
- };
124
- const cleanup = () => {
125
- bus.off("agent:response-chunk", onChunk);
126
- bus.off("agent:processing-done", onDone);
127
- bus.off("agent:error", onError);
128
- };
129
- bus.on("agent:response-chunk", onChunk);
130
- bus.on("agent:processing-done", onDone);
131
- bus.on("agent:error", onError);
132
- bus.emit("agent:submit", { query: text });
133
- });
134
- },
135
- cancel() {
136
- bus.emit("agent:cancel-request", {});
137
- },
138
- appendUserMessage(text) {
139
- bus.emit("agent:append-user-message", { text });
140
- },
141
89
  extensionContext(opts) {
142
90
  const ctx = {
143
91
  bus,
@@ -165,6 +113,7 @@ export function createCore(config) {
165
113
  kill() {
166
114
  if (activeBackendName) {
167
115
  backends.get(activeBackendName)?.kill();
116
+ activeBackendName = null;
168
117
  }
169
118
  },
170
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) ──
@@ -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
  }
@@ -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
  },
@@ -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.
@@ -429,7 +429,9 @@ export default function activate(ctx: ExtensionContext): void {
429
429
  start: async () => {
430
430
  try {
431
431
  serverAbort = new AbortController();
432
- runtime = await createOpencode({ signal: serverAbort.signal });
432
+ // port: 0 dodges collision with SDK default 4096 (override via OPENCODE_SDK_PORT).
433
+ const port = process.env.OPENCODE_SDK_PORT ? Number(process.env.OPENCODE_SDK_PORT) : 0;
434
+ runtime = await createOpencode({ signal: serverAbort.signal, port });
433
435
 
434
436
  streamAbort = new AbortController();
435
437
  // Subscribe before creating the session so we don't miss early events.
@@ -272,14 +272,13 @@ export default function activate(ctx: ExtensionContext): void {
272
272
  }
273
273
  });
274
274
 
275
- const model = session.model;
275
+ booting = false;
276
+ const m = session.model;
276
277
  bus.emit("agent:info", {
277
278
  name: "pi",
278
279
  version: "0.66",
279
- model: model ? `${model.provider}/${model.id}` : undefined,
280
+ model: m ? `${m.provider}/${m.id}` : undefined,
280
281
  });
281
-
282
- booting = false;
283
282
  } catch (err) {
284
283
  booting = false;
285
284
  bus.emit("ui:error", {
@@ -25,14 +25,10 @@ function buildReasoningParams(level: string, _model?: string): Record<string, un
25
25
 
26
26
  export default function activate(ctx: AgentContext): void {
27
27
  const { key } = resolveApiKey(ID);
28
- const apiKey = key ?? process.env.ZAI_API_KEY ?? process.env.ZHIPU_API_KEY;
29
- if (!apiKey) return;
30
-
31
28
  ctx.agent.providers.configure(ID, { reasoningParams: buildReasoningParams });
32
-
33
- ctx.bus.emit("provider:register", {
29
+ ctx.agent.providers.register({
34
30
  id: ID,
35
- apiKey: apiKey,
31
+ apiKey: key ?? process.env.ZAI_API_KEY ?? process.env.ZHIPU_API_KEY,
36
32
  baseURL: BASE_URL,
37
33
  defaultModel: DEFAULT_MODELS[0].id,
38
34
  models: DEFAULT_MODELS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-sh",
3
- "version": "0.13.6",
3
+ "version": "0.14.0",
4
4
  "description": "A shell-first terminal where AI is one keystroke away",
5
5
  "type": "module",
6
6
  "main": "dist/core/index.js",
@@ -121,6 +121,7 @@
121
121
  "dev": "tsx src/cli/index.ts",
122
122
  "build": "tsc",
123
123
  "start": "node dist/cli/index.js",
124
+ "test": "npm run build && node --import tsx --test $(find tests -name '*.test.ts' -type f)",
124
125
  "prepare": "test -d dist || npm run build"
125
126
  },
126
127
  "keywords": [
@@ -1,2 +0,0 @@
1
- import type { ExtensionContext } from "../shell/host-types.js";
2
- export default function activate(ctx: ExtensionContext): void;
@@ -1,11 +0,0 @@
1
- /**
2
- * ctx.llm facade — delegates to an `llm:invoke` handler registered by the
3
- * active backend. No handler → `available` is false and calls reject.
4
- */
5
- import type { LlmInterface } from "../agent/host-types.js";
6
- interface HandlerGate {
7
- list: () => string[];
8
- call: (name: string, ...args: unknown[]) => unknown;
9
- }
10
- export declare function createLlmFacade(handlers: HandlerGate): LlmInterface;
11
- export {};
File without changes
File without changes