agent-sh 0.4.0 → 0.6.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 (83) hide show
  1. package/README.md +37 -115
  2. package/dist/agent/agent-loop.d.ts +86 -0
  3. package/dist/agent/agent-loop.js +704 -0
  4. package/dist/agent/conversation-state.d.ts +27 -0
  5. package/dist/agent/conversation-state.js +59 -0
  6. package/dist/agent/index.d.ts +11 -0
  7. package/dist/agent/index.js +9 -0
  8. package/dist/agent/skills.d.ts +25 -0
  9. package/dist/agent/skills.js +186 -0
  10. package/dist/agent/subagent.d.ts +37 -0
  11. package/dist/agent/subagent.js +119 -0
  12. package/dist/agent/system-prompt.d.ts +14 -0
  13. package/dist/agent/system-prompt.js +103 -0
  14. package/dist/agent/tool-registry.d.ts +15 -0
  15. package/dist/agent/tool-registry.js +30 -0
  16. package/dist/agent/tools/bash.d.ts +7 -0
  17. package/dist/agent/tools/bash.js +71 -0
  18. package/dist/agent/tools/display.d.ts +13 -0
  19. package/dist/agent/tools/display.js +70 -0
  20. package/dist/agent/tools/edit-file.d.ts +2 -0
  21. package/dist/agent/tools/edit-file.js +148 -0
  22. package/dist/agent/tools/glob.d.ts +2 -0
  23. package/dist/agent/tools/glob.js +87 -0
  24. package/dist/agent/tools/grep.d.ts +2 -0
  25. package/dist/agent/tools/grep.js +168 -0
  26. package/dist/agent/tools/list-skills.d.ts +2 -0
  27. package/dist/agent/tools/list-skills.js +28 -0
  28. package/dist/agent/tools/ls.d.ts +2 -0
  29. package/dist/agent/tools/ls.js +72 -0
  30. package/dist/agent/tools/read-file.d.ts +10 -0
  31. package/dist/agent/tools/read-file.js +101 -0
  32. package/dist/agent/tools/user-shell.d.ts +13 -0
  33. package/dist/agent/tools/user-shell.js +84 -0
  34. package/dist/agent/tools/write-file.d.ts +2 -0
  35. package/dist/agent/tools/write-file.js +82 -0
  36. package/dist/agent/types.d.ts +78 -0
  37. package/dist/agent/types.js +1 -0
  38. package/dist/core.d.ts +22 -14
  39. package/dist/core.js +256 -36
  40. package/dist/event-bus.d.ts +98 -17
  41. package/dist/event-bus.js +10 -1
  42. package/dist/extension-loader.d.ts +1 -1
  43. package/dist/extension-loader.js +10 -1
  44. package/dist/extensions/command-suggest.d.ts +10 -0
  45. package/dist/extensions/command-suggest.js +41 -0
  46. package/dist/extensions/slash-commands.d.ts +1 -1
  47. package/dist/extensions/slash-commands.js +161 -64
  48. package/dist/extensions/tui-renderer.js +426 -126
  49. package/dist/index.js +110 -129
  50. package/dist/input-handler.js +78 -9
  51. package/dist/output-parser.d.ts +7 -0
  52. package/dist/output-parser.js +27 -0
  53. package/dist/settings.d.ts +53 -2
  54. package/dist/settings.js +46 -3
  55. package/dist/shell.js +35 -28
  56. package/dist/types.d.ts +33 -6
  57. package/dist/utils/box-frame.d.ts +3 -1
  58. package/dist/utils/box-frame.js +12 -5
  59. package/dist/utils/diff.js +10 -0
  60. package/dist/utils/llm-client.d.ts +45 -0
  61. package/dist/utils/llm-client.js +60 -0
  62. package/dist/utils/markdown.d.ts +1 -0
  63. package/dist/utils/markdown.js +25 -3
  64. package/dist/utils/stream-transform.js +20 -47
  65. package/dist/utils/tool-display.d.ts +4 -0
  66. package/dist/utils/tool-display.js +35 -8
  67. package/examples/extensions/claude-code-bridge/README.md +35 -0
  68. package/examples/extensions/claude-code-bridge/index.ts +194 -0
  69. package/examples/extensions/claude-code-bridge/package.json +11 -0
  70. package/examples/extensions/openrouter.ts +87 -0
  71. package/examples/extensions/pi-bridge/README.md +35 -0
  72. package/examples/extensions/pi-bridge/index.ts +263 -0
  73. package/examples/extensions/pi-bridge/package.json +13 -0
  74. package/examples/extensions/secret-guard.ts +100 -0
  75. package/examples/extensions/subagents.ts +87 -0
  76. package/package.json +3 -5
  77. package/dist/acp-client.d.ts +0 -105
  78. package/dist/acp-client.js +0 -684
  79. package/dist/extensions/shell-exec.d.ts +0 -24
  80. package/dist/extensions/shell-exec.js +0 -188
  81. package/dist/mcp-server.d.ts +0 -13
  82. package/dist/mcp-server.js +0 -234
  83. package/examples/pi-agent-sh.ts +0 -166
@@ -19,37 +19,23 @@
19
19
  export function createBlockTransform(bus, opts) {
20
20
  let buffer = "";
21
21
  bus.onPipe("agent:response-chunk", (e) => {
22
- // Process text from e.text and from text blocks in e.blocks
23
22
  const outBlocks = [];
24
- if (e.blocks) {
25
- for (const block of e.blocks) {
26
- if (block.type === "text") {
27
- // Run delimiter detection on text blocks
28
- buffer += block.text;
29
- const { blocks: parsed, pending } = processBuffer(buffer, opts);
30
- buffer = pending;
31
- outBlocks.push(...parsed);
32
- }
33
- else {
34
- // Pass through non-text blocks unchanged
35
- outBlocks.push(block);
36
- }
23
+ for (const block of e.blocks) {
24
+ if (block.type === "text") {
25
+ buffer += block.text;
26
+ const { blocks: parsed, pending } = processBuffer(buffer, opts);
27
+ buffer = pending;
28
+ outBlocks.push(...parsed);
29
+ }
30
+ else {
31
+ outBlocks.push(block);
37
32
  }
38
33
  }
39
- // Also process any raw text not yet in blocks
40
- if (e.text) {
41
- buffer += e.text;
42
- const { blocks: parsed, pending } = processBuffer(buffer, opts);
43
- buffer = pending;
44
- outBlocks.push(...parsed);
45
- }
46
- return { ...e, text: "", blocks: outBlocks };
34
+ return { blocks: outBlocks };
47
35
  });
48
36
  bus.onPipe("agent:response-done", (e) => {
49
37
  if (buffer) {
50
- // Unclosed pattern — flush as text
51
38
  bus.emitTransform("agent:response-chunk", {
52
- text: buffer,
53
39
  blocks: [{ type: "text", text: buffer }],
54
40
  });
55
41
  buffer = "";
@@ -66,35 +52,23 @@ export function createFencedBlockTransform(bus, opts) {
66
52
  bus.onPipe("agent:response-chunk", (e) => {
67
53
  if (flushing)
68
54
  return e; // pass through during flush to avoid re-buffering
69
- // Collect text from blocks or raw text
55
+ // Separate text blocks (to buffer) from non-text blocks (pass through)
70
56
  let incoming = "";
71
- if (e.blocks) {
72
- // Process text blocks, pass through non-text blocks
73
- const passthrough = [];
74
- for (const block of e.blocks) {
75
- if (block.type === "text") {
76
- incoming += block.text;
77
- }
78
- else {
79
- passthrough.push(block);
80
- }
57
+ const passthrough = [];
58
+ for (const block of e.blocks) {
59
+ if (block.type === "text") {
60
+ incoming += block.text;
61
+ }
62
+ else {
63
+ passthrough.push(block);
81
64
  }
82
- const { blocks, pending } = processFencedBuffer(buffer + incoming, opts, inFence, fenceMatch, fenceLines);
83
- buffer = pending.text;
84
- inFence = pending.inFence;
85
- fenceMatch = pending.fenceMatch;
86
- fenceLines = pending.fenceLines;
87
- return { ...e, text: "", blocks: [...passthrough, ...blocks] };
88
65
  }
89
- // No blocks yet work with raw text
90
- incoming = buffer + e.text;
91
- const { blocks, pending } = processFencedBuffer(incoming, opts, inFence, fenceMatch, fenceLines);
66
+ const { blocks, pending } = processFencedBuffer(buffer + incoming, opts, inFence, fenceMatch, fenceLines);
92
67
  buffer = pending.text;
93
68
  inFence = pending.inFence;
94
69
  fenceMatch = pending.fenceMatch;
95
70
  fenceLines = pending.fenceLines;
96
- const existing = e.blocks ?? [];
97
- return { ...e, text: "", blocks: [...existing, ...blocks] };
71
+ return { blocks: [...passthrough, ...blocks] };
98
72
  });
99
73
  function flushBuffer() {
100
74
  if (!buffer && !inFence)
@@ -110,7 +84,6 @@ export function createFencedBlockTransform(bus, opts) {
110
84
  if (remaining) {
111
85
  flushing = true;
112
86
  bus.emitTransform("agent:response-chunk", {
113
- text: "",
114
87
  blocks: [{ type: "text", text: remaining }],
115
88
  });
116
89
  flushing = false;
@@ -6,6 +6,8 @@ export interface ToolCallRender {
6
6
  command?: string;
7
7
  /** Tool kind from ACP (read, edit, execute, search, etc.). */
8
8
  kind?: string;
9
+ /** Custom icon character — when set, tool name is omitted (icon implies tool). */
10
+ icon?: string;
9
11
  /** File locations affected by the tool call. */
10
12
  locations?: {
11
13
  path: string;
@@ -13,6 +15,8 @@ export interface ToolCallRender {
13
15
  }[];
14
16
  /** Raw input parameters sent to the tool. */
15
17
  rawInput?: unknown;
18
+ /** Pre-formatted display detail from tool's formatCall(). Takes precedence over rawInput extraction. */
19
+ displayDetail?: string;
16
20
  }
17
21
  export interface ToolResultRender {
18
22
  exitCode: number | null;
@@ -39,6 +39,7 @@ const KIND_ICONS = {
39
39
  move: "↗",
40
40
  search: "⌕",
41
41
  execute: "▶",
42
+ display: "◇",
42
43
  think: "◇",
43
44
  fetch: "↓",
44
45
  switch_mode: "⇄",
@@ -49,7 +50,10 @@ function kindIcon(kind) {
49
50
  // ── Tool call rendering ──────────────────────────────────────────
50
51
  export function renderToolCall(tool, width) {
51
52
  const mode = selectToolDisplayMode(width);
52
- const icon = kindIcon(tool.kind);
53
+ const icon = tool.icon ?? kindIcon(tool.kind);
54
+ // If the tool registered a custom icon, it's self-describing — omit the name.
55
+ // Otherwise, include the tool name so the user knows what ran.
56
+ const hasCustomIcon = !!tool.icon;
53
57
  if (mode === "summary") {
54
58
  const text = truncateVisible(`${icon} ${tool.title}`, width);
55
59
  return [`${p.warning}${text}${p.reset}`];
@@ -58,7 +62,10 @@ export function renderToolCall(tool, width) {
58
62
  // Build a compact detail string to append after the title
59
63
  let detail = "";
60
64
  const cwd = process.cwd();
61
- if (mode === "full") {
65
+ if (mode === "full" && tool.displayDetail) {
66
+ detail = tool.displayDetail;
67
+ }
68
+ else if (mode === "full") {
62
69
  if (tool.command) {
63
70
  detail = `$ ${tool.command}`;
64
71
  }
@@ -73,6 +80,15 @@ export function renderToolCall(tool, width) {
73
80
  if (typeof raw.command === "string") {
74
81
  detail = `$ ${raw.command}`;
75
82
  }
83
+ else if (typeof raw.pattern === "string") {
84
+ // grep/glob — show the search pattern
85
+ const target = typeof raw.path === "string" ? ` ${shortenPath(raw.path, cwd)}` : "";
86
+ detail = `${raw.pattern}${target}`;
87
+ }
88
+ else if (typeof raw.path === "string") {
89
+ // read_file, write_file, etc.
90
+ detail = shortenPath(raw.path, cwd);
91
+ }
76
92
  else if (typeof raw.operation === "string") {
77
93
  detail = raw.operation;
78
94
  if (raw.ids && Array.isArray(raw.ids)) {
@@ -83,20 +99,31 @@ export function renderToolCall(tool, width) {
83
99
  }
84
100
  }
85
101
  else {
86
- detail = formatRawInput(tool.rawInput, width - tool.title.length - 6);
102
+ detail = formatRawInput(tool.rawInput, width - 4);
87
103
  }
88
104
  }
89
105
  }
90
106
  }
91
- // Render as single line: title: detail
92
- const maxDetailW = Math.max(1, width - tool.title.length - 6);
93
- if (detail) {
107
+ // Render as single line: icon + kind + detail
108
+ const maxDetailW = Math.max(1, width - 4);
109
+ if (detail && hasCustomIcon && tool.kind) {
110
+ const combined = `${tool.kind} ${detail}`;
111
+ const truncated = combined.length > maxDetailW ? combined.slice(0, maxDetailW - 1) + "…" : combined;
112
+ lines.push(`${p.warning}${icon}${p.reset} ${p.dim}${truncated}${p.reset}`);
113
+ }
114
+ else if (detail && hasCustomIcon) {
94
115
  if (detail.length > maxDetailW)
95
116
  detail = detail.slice(0, maxDetailW - 1) + "…";
96
- lines.push(`${p.warning}${p.bold}${icon} ${tool.title}${p.reset}${p.dim}: ${detail}${p.reset}`);
117
+ lines.push(`${p.warning}${icon}${p.reset} ${p.dim}${detail}${p.reset}`);
118
+ }
119
+ else if (detail) {
120
+ const prefix = `${tool.title}: `;
121
+ const combined = prefix + detail;
122
+ const truncated = combined.length > maxDetailW ? combined.slice(0, maxDetailW - 1) + "…" : combined;
123
+ lines.push(`${p.warning}${icon}${p.reset} ${p.dim}${truncated}${p.reset}`);
97
124
  }
98
125
  else {
99
- lines.push(`${p.warning}${p.bold}${icon} ${tool.title}${p.reset}`);
126
+ lines.push(`${p.warning}${icon} ${tool.title}${p.reset}`);
100
127
  }
101
128
  // Show additional file locations on separate lines (if more than one)
102
129
  if (mode === "full" && tool.locations && tool.locations.length > 1) {
@@ -0,0 +1,35 @@
1
+ # claude-code-bridge
2
+
3
+ Runs Claude Code as an agent-sh backend using the official [@anthropic-ai/claude-agent-sdk](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ # Copy or symlink into your extensions directory
9
+ cp -r examples/extensions/claude-code-bridge ~/.agent-sh/extensions/claude-code-bridge
10
+
11
+ # Install dependencies
12
+ cd ~/.agent-sh/extensions/claude-code-bridge
13
+ npm install
14
+ ```
15
+
16
+ ## Configure
17
+
18
+ Set as default backend in `~/.agent-sh/settings.json`:
19
+
20
+ ```json
21
+ {
22
+ "defaultBackend": "claude-code"
23
+ }
24
+ ```
25
+
26
+ Or switch at runtime:
27
+
28
+ ```
29
+ ? /backend claude-code
30
+ ```
31
+
32
+ ## Requirements
33
+
34
+ - `ANTHROPIC_API_KEY` must be set in your environment
35
+ - Claude Code manages its own model selection — no model configuration needed in agent-sh
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Claude Code bridge — runs Claude Code Agent SDK in-process as agent-sh's backend.
3
+ *
4
+ * Uses the official @anthropic-ai/claude-agent-sdk to spawn a Claude Code
5
+ * session with a custom user_shell MCP tool for PTY access. Claude Code
6
+ * handles its own model selection, tool execution, and permissions.
7
+ *
8
+ * Setup:
9
+ * npm install @anthropic-ai/claude-agent-sdk
10
+ *
11
+ * Usage:
12
+ * agent-sh -e examples/extensions/claude-code-bridge
13
+ *
14
+ * Requires: Claude Code CLI installed and authenticated (claude login).
15
+ */
16
+ import {
17
+ query,
18
+ tool,
19
+ createSdkMcpServer,
20
+ type Query,
21
+ } from "@anthropic-ai/claude-agent-sdk";
22
+ import { z } from "zod";
23
+ import type { ExtensionContext } from "../../src/types.js";
24
+ import type { EventBus } from "../../src/event-bus.js";
25
+
26
+ // ── user_shell MCP tool ───────────────────────────────────────────
27
+ function createUserShellTool(bus: EventBus) {
28
+ let liveCwd = process.cwd();
29
+ bus.on("shell:cwd-change", ({ cwd }) => { liveCwd = cwd; });
30
+
31
+ return tool(
32
+ "user_shell",
33
+ "Run a command with lasting effects in the user's live shell (cd, export, " +
34
+ "install packages, start servers) or show output the user wants to see. " +
35
+ "Set return_output=true only if you need to inspect the result.",
36
+ {
37
+ command: z.string().describe("Command to execute in user's shell"),
38
+ return_output: z.boolean().optional().describe(
39
+ "Whether to return the command output. Default false.",
40
+ ),
41
+ },
42
+ async (args) => {
43
+ const result = await bus.emitPipeAsync("shell:exec-request", {
44
+ command: args.command,
45
+ output: "",
46
+ cwd: liveCwd,
47
+ done: false,
48
+ });
49
+
50
+ const text = args.return_output
51
+ ? result.output || "(no output)"
52
+ : "Command executed.";
53
+
54
+ return { content: [{ type: "text" as const, text }] };
55
+ },
56
+ );
57
+ }
58
+
59
+ // ── Extension entry point ─────────────────────────────────────────
60
+ export default function activate(ctx: ExtensionContext): void {
61
+ const { bus } = ctx;
62
+
63
+ const shellTool = createUserShellTool(bus);
64
+ const shellServer = createSdkMcpServer({
65
+ name: "agent-sh",
66
+ version: "1.0.0",
67
+ tools: [shellTool],
68
+ });
69
+
70
+ let activeQuery: Query | null = null;
71
+ const listeners: Array<{ event: string; fn: Function }> = [];
72
+
73
+ const wireListeners = () => {
74
+ const onSubmit = async ({ query: userQuery }: any) => {
75
+ bus.emit("agent:query", { query: userQuery });
76
+ bus.emit("agent:processing-start", {});
77
+
78
+ let fullResponseText = "";
79
+ let streamed = false;
80
+
81
+ try {
82
+ activeQuery = query({
83
+ prompt: userQuery,
84
+ options: {
85
+ cwd: process.cwd(),
86
+ systemPrompt: {
87
+ type: "preset",
88
+ preset: "claude_code",
89
+ append:
90
+ "You are running inside agent-sh, a terminal wrapper.\n" +
91
+ "Use your standard tools (Read, Edit, Write, Bash, Glob, Grep) for investigation.\n" +
92
+ "Use mcp__agent-sh__user_shell to run commands in the user's live shell when they ask to see output or need lasting effects (cd, install, start servers).\n" +
93
+ "Default to standard tools. Use user_shell when the user is the intended audience for the output or the command has real effects.",
94
+ },
95
+ mcpServers: { "agent-sh": shellServer },
96
+ allowedTools: [
97
+ "mcp__agent-sh__user_shell",
98
+ "Read", "Edit", "Write", "Bash", "Glob", "Grep",
99
+ ],
100
+ permissionMode: "acceptEdits",
101
+ includePartialMessages: true,
102
+ },
103
+ });
104
+
105
+ for await (const message of activeQuery) {
106
+ switch (message.type) {
107
+ case "stream_event": {
108
+ streamed = true;
109
+ const event = message.event;
110
+ if (event.type === "content_block_delta") {
111
+ const delta = event.delta as any;
112
+ if (delta.type === "text_delta" && delta.text) {
113
+ bus.emitTransform("agent:response-chunk", {
114
+ blocks: [{ type: "text" as const, text: delta.text }],
115
+ });
116
+ fullResponseText += delta.text;
117
+ } else if (delta.type === "thinking_delta" && delta.thinking) {
118
+ bus.emit("agent:thinking-chunk", { text: delta.thinking });
119
+ }
120
+ }
121
+ break;
122
+ }
123
+
124
+ case "assistant": {
125
+ const msg = message.message;
126
+ for (const block of msg.content) {
127
+ const b = block as any;
128
+ if (b.type === "text" && b.text && !streamed) {
129
+ bus.emitTransform("agent:response-chunk", {
130
+ blocks: [{ type: "text" as const, text: b.text }],
131
+ });
132
+ fullResponseText += b.text;
133
+ } else if (b.type === "tool_use") {
134
+ bus.emit("agent:tool-started", {
135
+ title: b.name,
136
+ toolCallId: b.id,
137
+ kind: b.name.includes("shell") || b.name === "Bash"
138
+ ? "execute"
139
+ : "read",
140
+ });
141
+ }
142
+ }
143
+ break;
144
+ }
145
+
146
+ case "result":
147
+ break;
148
+ }
149
+ }
150
+
151
+ bus.emitTransform("agent:response-done", {
152
+ response: fullResponseText,
153
+ });
154
+ } catch (err) {
155
+ bus.emit("agent:error", {
156
+ message: err instanceof Error ? err.message : String(err),
157
+ });
158
+ } finally {
159
+ activeQuery = null;
160
+ bus.emit("agent:processing-done", {});
161
+ }
162
+ };
163
+
164
+ const onCancel = () => { activeQuery?.interrupt(); };
165
+ const onReset = () => { /* each query() is a new session */ };
166
+
167
+ bus.on("agent:submit", onSubmit);
168
+ bus.on("agent:cancel-request", onCancel);
169
+ bus.on("agent:reset-session", onReset);
170
+ listeners.push(
171
+ { event: "agent:submit", fn: onSubmit },
172
+ { event: "agent:cancel-request", fn: onCancel },
173
+ { event: "agent:reset-session", fn: onReset },
174
+ );
175
+ };
176
+
177
+ const unwireListeners = () => {
178
+ for (const { event, fn } of listeners) bus.off(event as any, fn as any);
179
+ listeners.length = 0;
180
+ };
181
+
182
+ // ── Register as backend ───────────────────────────────────────
183
+ bus.emit("agent:register-backend", {
184
+ name: "claude-code",
185
+ start: async () => {
186
+ wireListeners();
187
+ bus.emit("agent:info", { name: "claude-code", version: "1.0" });
188
+ },
189
+ kill: () => {
190
+ activeQuery?.interrupt();
191
+ unwireListeners();
192
+ },
193
+ });
194
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "agent-sh-claude-code-bridge",
3
+ "version": "0.1.0",
4
+ "description": "Claude Code agent backend for agent-sh",
5
+ "type": "module",
6
+ "main": "index.ts",
7
+ "dependencies": {
8
+ "@anthropic-ai/claude-agent-sdk": "^0.2.0",
9
+ "zod": "^4.0.0"
10
+ }
11
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * OpenRouter provider extension.
3
+ *
4
+ * Registers OpenRouter as a provider and fetches its full model catalog
5
+ * at startup. Models appear in /model autocomplete as "model [openrouter]"
6
+ * and are available for cycling with Shift+Tab.
7
+ *
8
+ * Model capabilities (reasoning, context window) are read from the
9
+ * OpenRouter API response — no hardcoded model lists.
10
+ *
11
+ * Setup:
12
+ * export OPENROUTER_API_KEY="your-key"
13
+ *
14
+ * Usage:
15
+ * agent-sh -e ./examples/extensions/openrouter.ts
16
+ *
17
+ * # Or add to settings.json:
18
+ * { "extensions": ["./examples/extensions/openrouter.ts"] }
19
+ */
20
+ import type { ExtensionContext } from "agent-sh/types";
21
+
22
+ const BASE_URL = "https://openrouter.ai/api/v1";
23
+ const API_KEY = process.env.OPENROUTER_API_KEY ?? "";
24
+
25
+ /** Curated default models — used immediately while the full catalog loads. */
26
+ const DEFAULT_MODELS = [
27
+ "anthropic/claude-sonnet-4",
28
+ "google/gemini-2.5-pro-preview",
29
+ "openai/gpt-4.1",
30
+ "deepseek/deepseek-r1",
31
+ "meta-llama/llama-4-maverick",
32
+ ];
33
+
34
+ interface OpenRouterModel {
35
+ id: string;
36
+ name: string;
37
+ context_length?: number;
38
+ supported_parameters?: string[];
39
+ pricing?: { prompt: string; completion: string };
40
+ }
41
+
42
+ export default function activate({ bus }: ExtensionContext): void {
43
+ if (!API_KEY) {
44
+ bus.emit("ui:error", {
45
+ message: "OpenRouter extension: OPENROUTER_API_KEY not set. Skipping.",
46
+ });
47
+ return;
48
+ }
49
+
50
+ // Register provider immediately with curated defaults
51
+ bus.emit("provider:register", {
52
+ id: "openrouter",
53
+ apiKey: API_KEY,
54
+ baseURL: BASE_URL,
55
+ defaultModel: DEFAULT_MODELS[0],
56
+ models: DEFAULT_MODELS,
57
+ });
58
+
59
+ // Fetch full model catalog in background, re-register with capabilities
60
+ fetchModels().then((models) => {
61
+ if (models.length > 0) {
62
+ bus.emit("provider:register", {
63
+ id: "openrouter",
64
+ apiKey: API_KEY,
65
+ baseURL: BASE_URL,
66
+ defaultModel: DEFAULT_MODELS[0],
67
+ supportsReasoningEffort: true,
68
+ models: models.map((m) => ({
69
+ id: m.id,
70
+ reasoning: m.supported_parameters?.includes("reasoning") ?? false,
71
+ contextWindow: m.context_length,
72
+ })),
73
+ });
74
+ }
75
+ }).catch(() => {
76
+ // Silently fall back to curated defaults
77
+ });
78
+ }
79
+
80
+ async function fetchModels(): Promise<OpenRouterModel[]> {
81
+ const res = await fetch(`${BASE_URL}/models`, {
82
+ headers: { Authorization: `Bearer ${API_KEY}` },
83
+ });
84
+ if (!res.ok) return [];
85
+ const data = await res.json();
86
+ return (data.data ?? []) as OpenRouterModel[];
87
+ }
@@ -0,0 +1,35 @@
1
+ # pi-bridge
2
+
3
+ Runs [pi](https://github.com/nickarora/pi)'s full coding agent as an agent-sh backend. Uses pi's own configuration, models, tools, and extensions — agent-sh just provides the terminal.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ # Copy or symlink into your extensions directory
9
+ cp -r examples/extensions/pi-bridge ~/.agent-sh/extensions/pi-bridge
10
+
11
+ # Install dependencies
12
+ cd ~/.agent-sh/extensions/pi-bridge
13
+ npm install
14
+ ```
15
+
16
+ ## Configure
17
+
18
+ Set as default backend in `~/.agent-sh/settings.json`:
19
+
20
+ ```json
21
+ {
22
+ "defaultBackend": "pi"
23
+ }
24
+ ```
25
+
26
+ Or switch at runtime:
27
+
28
+ ```
29
+ ? /backend pi
30
+ ```
31
+
32
+ ## Requirements
33
+
34
+ - pi must be configured separately (`~/.pi/settings.json`) with API keys and model preferences
35
+ - agent-sh does not override pi's configuration — it uses whatever pi is set up with