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.
- package/README.md +37 -115
- package/dist/agent/agent-loop.d.ts +86 -0
- package/dist/agent/agent-loop.js +704 -0
- package/dist/agent/conversation-state.d.ts +27 -0
- package/dist/agent/conversation-state.js +59 -0
- package/dist/agent/index.d.ts +11 -0
- package/dist/agent/index.js +9 -0
- package/dist/agent/skills.d.ts +25 -0
- package/dist/agent/skills.js +186 -0
- package/dist/agent/subagent.d.ts +37 -0
- package/dist/agent/subagent.js +119 -0
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +103 -0
- package/dist/agent/tool-registry.d.ts +15 -0
- package/dist/agent/tool-registry.js +30 -0
- package/dist/agent/tools/bash.d.ts +7 -0
- package/dist/agent/tools/bash.js +71 -0
- package/dist/agent/tools/display.d.ts +13 -0
- package/dist/agent/tools/display.js +70 -0
- package/dist/agent/tools/edit-file.d.ts +2 -0
- package/dist/agent/tools/edit-file.js +148 -0
- package/dist/agent/tools/glob.d.ts +2 -0
- package/dist/agent/tools/glob.js +87 -0
- package/dist/agent/tools/grep.d.ts +2 -0
- package/dist/agent/tools/grep.js +168 -0
- package/dist/agent/tools/list-skills.d.ts +2 -0
- package/dist/agent/tools/list-skills.js +28 -0
- package/dist/agent/tools/ls.d.ts +2 -0
- package/dist/agent/tools/ls.js +72 -0
- package/dist/agent/tools/read-file.d.ts +10 -0
- package/dist/agent/tools/read-file.js +101 -0
- package/dist/agent/tools/user-shell.d.ts +13 -0
- package/dist/agent/tools/user-shell.js +84 -0
- package/dist/agent/tools/write-file.d.ts +2 -0
- package/dist/agent/tools/write-file.js +82 -0
- package/dist/agent/types.d.ts +78 -0
- package/dist/agent/types.js +1 -0
- package/dist/core.d.ts +22 -14
- package/dist/core.js +256 -36
- package/dist/event-bus.d.ts +98 -17
- package/dist/event-bus.js +10 -1
- package/dist/extension-loader.d.ts +1 -1
- package/dist/extension-loader.js +10 -1
- package/dist/extensions/command-suggest.d.ts +10 -0
- package/dist/extensions/command-suggest.js +41 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +161 -64
- package/dist/extensions/tui-renderer.js +426 -126
- package/dist/index.js +110 -129
- package/dist/input-handler.js +78 -9
- package/dist/output-parser.d.ts +7 -0
- package/dist/output-parser.js +27 -0
- package/dist/settings.d.ts +53 -2
- package/dist/settings.js +46 -3
- package/dist/shell.js +35 -28
- package/dist/types.d.ts +33 -6
- package/dist/utils/box-frame.d.ts +3 -1
- package/dist/utils/box-frame.js +12 -5
- package/dist/utils/diff.js +10 -0
- package/dist/utils/llm-client.d.ts +45 -0
- package/dist/utils/llm-client.js +60 -0
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +25 -3
- package/dist/utils/stream-transform.js +20 -47
- package/dist/utils/tool-display.d.ts +4 -0
- package/dist/utils/tool-display.js +35 -8
- package/examples/extensions/claude-code-bridge/README.md +35 -0
- package/examples/extensions/claude-code-bridge/index.ts +194 -0
- package/examples/extensions/claude-code-bridge/package.json +11 -0
- package/examples/extensions/openrouter.ts +87 -0
- package/examples/extensions/pi-bridge/README.md +35 -0
- package/examples/extensions/pi-bridge/index.ts +263 -0
- package/examples/extensions/pi-bridge/package.json +13 -0
- package/examples/extensions/secret-guard.ts +100 -0
- package/examples/extensions/subagents.ts +87 -0
- package/package.json +3 -5
- package/dist/acp-client.d.ts +0 -105
- package/dist/acp-client.js +0 -684
- package/dist/extensions/shell-exec.d.ts +0 -24
- package/dist/extensions/shell-exec.js +0 -188
- package/dist/mcp-server.d.ts +0 -13
- package/dist/mcp-server.js +0 -234
- 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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
55
|
+
// Separate text blocks (to buffer) from non-text blocks (pass through)
|
|
70
56
|
let incoming = "";
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 -
|
|
102
|
+
detail = formatRawInput(tool.rawInput, width - 4);
|
|
87
103
|
}
|
|
88
104
|
}
|
|
89
105
|
}
|
|
90
106
|
}
|
|
91
|
-
// Render as single line:
|
|
92
|
-
const maxDetailW = Math.max(1, width -
|
|
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}${
|
|
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}${
|
|
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
|