agent-sh 0.10.0 → 0.10.2

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 (40) hide show
  1. package/README.md +12 -9
  2. package/dist/agent/agent-loop.d.ts +0 -3
  3. package/dist/agent/agent-loop.js +18 -35
  4. package/dist/agent/conversation-state.js +8 -2
  5. package/dist/agent/nuclear-form.d.ts +2 -0
  6. package/dist/agent/nuclear-form.js +11 -1
  7. package/dist/agent/system-prompt.js +1 -1
  8. package/dist/agent/token-budget.d.ts +8 -12
  9. package/dist/agent/token-budget.js +5 -40
  10. package/dist/agent/tool-registry.js +6 -0
  11. package/dist/agent/types.d.ts +3 -1
  12. package/dist/context-manager.d.ts +1 -21
  13. package/dist/context-manager.js +26 -163
  14. package/dist/event-bus.d.ts +0 -1
  15. package/dist/extension-loader.js +25 -4
  16. package/dist/extensions/agent-backend.js +3 -2
  17. package/dist/extensions/index.js +0 -1
  18. package/dist/extensions/tui-renderer.js +47 -29
  19. package/dist/settings.d.ts +3 -11
  20. package/dist/settings.js +0 -4
  21. package/dist/shell/input-handler.js +14 -9
  22. package/dist/types.d.ts +3 -0
  23. package/dist/utils/ansi.d.ts +6 -1
  24. package/dist/utils/ansi.js +114 -7
  25. package/dist/utils/box-frame.js +8 -2
  26. package/dist/utils/llm-client.d.ts +4 -0
  27. package/dist/utils/llm-client.js +8 -0
  28. package/dist/utils/markdown.d.ts +4 -0
  29. package/dist/utils/markdown.js +136 -48
  30. package/dist/utils/package-version.d.ts +1 -0
  31. package/dist/utils/package-version.js +10 -0
  32. package/dist/utils/shell-output-spill.d.ts +2 -0
  33. package/dist/utils/shell-output-spill.js +81 -0
  34. package/examples/extensions/claude-code-bridge/README.md +14 -0
  35. package/examples/extensions/claude-code-bridge/index.ts +13 -101
  36. package/examples/extensions/pi-bridge/README.md +16 -0
  37. package/examples/extensions/pi-bridge/index.ts +8 -154
  38. package/package.json +9 -1
  39. package/dist/extensions/shell-recall.d.ts +0 -9
  40. package/dist/extensions/shell-recall.js +0 -8
@@ -2,8 +2,14 @@
2
2
  * Claude Code bridge — runs Claude Code Agent SDK in-process as agent-sh's backend.
3
3
  *
4
4
  * Uses the official @anthropic-ai/claude-agent-sdk to spawn a Claude Code
5
- * session with custom MCP tools for PTY access. Claude Code
6
- * handles its own model selection, tool execution, and permissions.
5
+ * session. Claude Code handles its own model selection, tool execution, and
6
+ * permissions the bridge is a pure protocol translator between the SDK's
7
+ * event stream and agent-sh's bus events.
8
+ *
9
+ * PTY-access tools (`terminal_read`, `terminal_keys`, `user_shell`) are
10
+ * intentionally NOT bundled here. If you want Claude Code to observe or
11
+ * drive the user's live terminal, load a companion extension that
12
+ * registers those tools as MCP tools the SDK can consume.
7
13
  *
8
14
  * Setup (from repo root):
9
15
  * npm run build && npm link # register local agent-sh globally
@@ -15,103 +21,16 @@
15
21
  *
16
22
  * Requires: Claude Code CLI installed and authenticated (claude login).
17
23
  */
18
- import {
19
- query,
20
- tool,
21
- createSdkMcpServer,
22
- type Query,
23
- } from "@anthropic-ai/claude-agent-sdk";
24
- import { z } from "zod";
24
+ import { query, type Query } from "@anthropic-ai/claude-agent-sdk";
25
25
  import { readFile } from "node:fs/promises";
26
26
  import { resolve } from "node:path";
27
27
  import type { ExtensionContext } from "agent-sh/types";
28
- import type { EventBus } from "agent-sh/event-bus";
29
28
  import { computeDiff, type DiffResult } from "agent-sh/utils/diff";
30
29
 
31
- // ── Helpers ──────────────────────────────────────────────────────
32
- function interpretEscapes(str: string): string {
33
- return str.replace(/\\(x[0-9a-fA-F]{2}|r|n|t|\\|0)/g, (_, seq: string) => {
34
- if (seq === "r") return "\r";
35
- if (seq === "n") return "\n";
36
- if (seq === "t") return "\t";
37
- if (seq === "\\") return "\\";
38
- if (seq === "0") return "\0";
39
- if (seq.startsWith("x")) return String.fromCharCode(parseInt(seq.slice(1), 16));
40
- return seq;
41
- });
42
- }
43
-
44
- function settle(ms = 100): Promise<void> {
45
- return new Promise((resolve) => setTimeout(resolve, ms));
46
- }
47
-
48
- // ── terminal_read MCP tool ────────────────────────────────────────
49
- function createTerminalReadTool(ctx: ExtensionContext) {
50
- return tool(
51
- "terminal_read",
52
- "Read the current terminal screen contents. Returns clean text (ANSI stripped) " +
53
- "with cursor position and whether an alternate-screen program (vim, htop, less) is active. " +
54
- "Use this to see what the user sees before sending keystrokes with terminal_keys.",
55
- {},
56
- async () => {
57
- const tb = ctx.terminalBuffer;
58
- if (!tb) return { content: [{ type: "text" as const, text: "terminal buffer not available" }] };
59
- const { text, altScreen, cursorX, cursorY } = tb.readScreen();
60
- const info = [
61
- altScreen ? "mode: alternate screen" : "mode: normal",
62
- `cursor: row=${cursorY} col=${cursorX}`,
63
- ].join(", ");
64
- return { content: [{ type: "text" as const, text: `[${info}]\n\n${text}` }] };
65
- },
66
- );
67
- }
68
-
69
- // ── terminal_keys MCP tool ───────────────────────────────────────
70
- function createTerminalKeysTool(bus: EventBus, ctx: ExtensionContext) {
71
- return tool(
72
- "terminal_keys",
73
- "Send keystrokes to the user's live terminal. The keys are written directly to the PTY " +
74
- "as if the user typed them. Use escape sequences for special keys:\n" +
75
- " - Escape: \\x1b - Enter: \\r - Tab: \\t\n" +
76
- " - Ctrl+C: \\x03 - Arrow keys: \\x1b[A/B/C/D - Backspace: \\x7f\n" +
77
- "Example: to quit vim without saving, send keys=\"\\x1b:q!\\r\".\n" +
78
- "Always call terminal_read after sending keys to verify the result.",
79
- {
80
- keys: z.string().describe("Keystrokes to send (use \\x1b for Escape, \\r for Enter, etc.)"),
81
- settle_ms: z.number().optional().describe("Wait time in ms after sending keys (default: 150)"),
82
- },
83
- async (args) => {
84
- const keys = interpretEscapes(args.keys);
85
- const settleMs = args.settle_ms ?? 150;
86
- bus.emit("shell:stdout-show", {});
87
- process.stdout.write("\n");
88
- bus.emit("shell:pty-write", { data: keys });
89
- await settle(settleMs);
90
-
91
- const tb = ctx.terminalBuffer;
92
- if (!tb) return { content: [{ type: "text" as const, text: "Keys sent." }] };
93
- const { text, altScreen, cursorX, cursorY } = tb.readScreen();
94
- const info = [
95
- altScreen ? "mode: alternate screen" : "mode: normal",
96
- `cursor: row=${cursorY} col=${cursorX}`,
97
- ].join(", ");
98
- return { content: [{ type: "text" as const, text: `Keys sent. Screen after:\n[${info}]\n\n${text}` }] };
99
- },
100
- );
101
- }
102
-
103
30
  // ── Extension entry point ─────────────────────────────────────────
104
31
  export default function activate(ctx: ExtensionContext): void {
105
32
  const { bus } = ctx;
106
33
 
107
- const termReadTool = createTerminalReadTool(ctx);
108
- const termKeysTool = createTerminalKeysTool(bus, ctx);
109
- const shellServer = createSdkMcpServer({
110
- name: "agent-sh",
111
- version: "1.0.0",
112
- tools: [termReadTool, termKeysTool],
113
- });
114
-
115
34
  let activeQuery: Query | null = null;
116
35
  const listeners: Array<{ event: string; fn: Function }> = [];
117
36
 
@@ -119,11 +38,11 @@ export default function activate(ctx: ExtensionContext): void {
119
38
 
120
39
  /** Map Claude Code tool names to agent-sh display kinds. */
121
40
  function toolKind(name: string): string {
122
- if (name === "Read" || name.includes("terminal_read")) return "read";
41
+ if (name === "Read") return "read";
123
42
  if (name === "Edit") return "edit";
124
43
  if (name === "Write") return "write";
125
44
  if (name === "Glob" || name === "Grep") return "search";
126
- if (name === "Bash" || name.includes("terminal_keys")) return "execute";
45
+ if (name === "Bash") return "execute";
127
46
  return "execute";
128
47
  }
129
48
 
@@ -150,7 +69,6 @@ export default function activate(ctx: ExtensionContext): void {
150
69
  if (name === "Bash") return `$ ${str(input.command)}`;
151
70
  if (name === "Read" || name === "Edit" || name === "Write") return str(input.file_path ?? input.path);
152
71
  if (name === "Grep" || name === "Glob") return `${str(input.pattern)} ${str(input.path)}`.trim();
153
- if (name.includes("terminal_keys")) return str(input.keys);
154
72
  return name;
155
73
  }
156
74
 
@@ -180,15 +98,9 @@ export default function activate(ctx: ExtensionContext): void {
180
98
  preset: "claude_code",
181
99
  append:
182
100
  "You are running inside agent-sh, a terminal wrapper.\n" +
183
- "Use your standard tools (Read, Edit, Write, Bash, Glob, Grep) for investigation.\n" +
184
- "Use mcp__agent-sh__terminal_read and mcp__agent-sh__terminal_keys to observe and interact with the user's live terminal.",
101
+ "Use your standard tools (Read, Edit, Write, Bash, Glob, Grep) for investigation.",
185
102
  },
186
- mcpServers: { "agent-sh": shellServer },
187
- allowedTools: [
188
- "mcp__agent-sh__terminal_read",
189
- "mcp__agent-sh__terminal_keys",
190
- "Read", "Edit", "Write", "Bash", "Glob", "Grep",
191
- ],
103
+ allowedTools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
192
104
  permissionMode: "acceptEdits",
193
105
  includePartialMessages: true,
194
106
  },
@@ -33,3 +33,19 @@ Or switch at runtime:
33
33
 
34
34
  - pi must be configured separately (`~/.pi/settings.json`) with API keys and model preferences
35
35
  - agent-sh does not override pi's configuration — it uses whatever pi is set up with
36
+
37
+ ## What this bridge is
38
+
39
+ A pure protocol translator between pi's event stream and agent-sh's bus events. Pi's built-in tools (command execution, file ops, etc.) are used exactly as pi ships them. The bridge adds no tools of its own.
40
+
41
+ ## What this bridge intentionally does NOT bundle
42
+
43
+ Three PTY-access tools are left out on purpose:
44
+
45
+ - `terminal_read` — observe the user's live terminal screen
46
+ - `terminal_keys` — send keystrokes to the user's PTY
47
+ - `user_shell` — run commands in the user's live shell with lasting `cd`/`export`/`source` effects
48
+
49
+ These are opt-in capabilities that belong in their own extensions. If you want any of them with pi, write a small companion extension that registers the tool as a pi `ToolDefinition` (TypeBox schema, wired to the relevant bus event: `shell:pty-write`, `shell:exec-request`, or `ctx.terminalBuffer.readScreen()`) and load it alongside pi-bridge.
50
+
51
+ Keeping this split means the bridge stays narrow — only translating events — and the capability surface is composable per-backend.
@@ -5,9 +5,12 @@
5
5
  * provider settings, extensions, session management, and tool system.
6
6
  * Agent-sh provides the shell frontend and TUI rendering.
7
7
  *
8
- * In addition to pi's built-in tools, this bridge registers `user_shell`
9
- * so pi can execute commands in agent-sh's live PTY (visible to the user,
10
- * affects shell state like cd/export/source).
8
+ * The bridge is a pure protocol translator between pi's event stream and
9
+ * agent-sh's bus events. Pi brings its own tools for command execution,
10
+ * file ops, etc. PTY-access tools (`terminal_read`, `terminal_keys`,
11
+ * `user_shell`) are intentionally NOT bundled here — if you want pi to
12
+ * observe or mutate the user's live terminal, load a companion extension
13
+ * that registers those tools in pi's ToolDefinition format.
11
14
  *
12
15
  * Setup:
13
16
  * npm install @mariozechner/pi-agent-core @mariozechner/pi-ai @mariozechner/pi-coding-agent
@@ -22,157 +25,13 @@ import {
22
25
  createAgentSessionRuntime,
23
26
  SessionManager,
24
27
  } from "@mariozechner/pi-coding-agent";
25
- import { Type } from "@sinclair/typebox";
26
28
  import type { ExtensionContext } from "agent-sh/types";
27
- import type { EventBus } from "agent-sh/event-bus";
28
-
29
- // ── Helpers ──────────────────────────────────────────────────────
30
- function interpretEscapes(str: string): string {
31
- return str.replace(/\\(x[0-9a-fA-F]{2}|r|n|t|\\|0)/g, (_, seq: string) => {
32
- if (seq === "r") return "\r";
33
- if (seq === "n") return "\n";
34
- if (seq === "t") return "\t";
35
- if (seq === "\\") return "\\";
36
- if (seq === "0") return "\0";
37
- if (seq.startsWith("x")) return String.fromCharCode(parseInt(seq.slice(1), 16));
38
- return seq;
39
- });
40
- }
41
-
42
- function settle(ms = 100): Promise<void> {
43
- return new Promise((resolve) => setTimeout(resolve, ms));
44
- }
45
-
46
- // ── user_shell as a pi ToolDefinition ─────────────────────────────
47
- function createUserShellToolDef(bus: EventBus) {
48
- // Track agent-sh's live cwd so user_shell always runs in the right place
49
- let liveCwd = process.cwd();
50
- bus.on("shell:cwd-change", ({ cwd }) => { liveCwd = cwd; });
51
-
52
- const schema = Type.Object({
53
- command: Type.String({ description: "Command to execute in user's shell" }),
54
- return_output: Type.Optional(
55
- Type.Boolean({
56
- description:
57
- "Whether to return the command output. Default false — output is shown directly to the user.",
58
- }),
59
- ),
60
- });
61
-
62
- return {
63
- name: "user_shell",
64
- label: "user_shell",
65
- description:
66
- "Run a command with lasting effects in the user's live shell (cd, export, " +
67
- "install packages, start servers) or show output the user wants to see. " +
68
- "Output is shown directly to the user. Set return_output=true only " +
69
- "if you need to inspect the result.",
70
- promptSnippet: "Execute commands in the user's live terminal (PTY).",
71
- promptGuidelines: [
72
- "You are running inside agent-sh, a terminal wrapper.",
73
- "Use your standard tools (bash, file ops) for investigation — output goes to you, not the user.",
74
- "Use 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).",
75
- "Default to standard tools. Use user_shell when the user is the intended audience for the output or the command has real effects.",
76
- ],
77
- parameters: schema,
78
-
79
- async execute(_toolCallId, params) {
80
- const command = params.command;
81
- const returnOutput = params.return_output ?? false;
82
-
83
- const result = await bus.emitPipeAsync("shell:exec-request", {
84
- command,
85
- output: "",
86
- cwd: liveCwd,
87
- done: false,
88
- });
89
-
90
- const text = returnOutput
91
- ? result.output || "(no output)"
92
- : "Command executed.";
93
-
94
- return { content: [{ type: "text", text }], details: undefined };
95
- },
96
- };
97
- }
98
-
99
- // ── terminal_read as a pi ToolDefinition ─────────────────────────
100
- function createTerminalReadToolDef(ctx: ExtensionContext) {
101
- return {
102
- name: "terminal_read",
103
- label: "terminal_read",
104
- description:
105
- "Read the current terminal screen contents. Returns clean text (ANSI stripped) " +
106
- "with cursor position and whether an alternate-screen program (vim, htop, less) is active.",
107
- promptSnippet: "Read the terminal screen to see what the user sees.",
108
- promptGuidelines: [
109
- "Use terminal_read to see the current terminal screen before sending keystrokes.",
110
- "Check altScreen to know if a full-screen program (vim, htop) is running.",
111
- ],
112
- parameters: Type.Object({}),
113
- async execute() {
114
- const tb = ctx.terminalBuffer;
115
- if (!tb) return { content: [{ type: "text", text: "terminal buffer not available" }], details: undefined };
116
- const { text, altScreen, cursorX, cursorY } = tb.readScreen();
117
- const info = [
118
- altScreen ? "mode: alternate screen" : "mode: normal",
119
- `cursor: row=${cursorY} col=${cursorX}`,
120
- ].join(", ");
121
- return { content: [{ type: "text", text: `[${info}]\n\n${text}` }], details: undefined };
122
- },
123
- };
124
- }
125
-
126
- // ── terminal_keys as a pi ToolDefinition ─────────────────────────
127
- function createTerminalKeysToolDef(bus: EventBus, ctx: ExtensionContext) {
128
- return {
129
- name: "terminal_keys",
130
- label: "terminal_keys",
131
- description:
132
- "Send keystrokes to the user's live terminal as if the user typed them. " +
133
- "Use escape sequences: \\x1b for Escape, \\r for Enter, \\t for Tab, " +
134
- "\\x03 for Ctrl+C, \\x1b[A/B/C/D for arrow keys, \\x7f for Backspace. " +
135
- "Example: \\x1b:q!\\r to quit vim. Always call terminal_read after.",
136
- promptSnippet: "Send keystrokes to interactive programs in the terminal.",
137
- promptGuidelines: [
138
- "Use terminal_keys to type into interactive programs (vim, htop, less).",
139
- "Always call terminal_read after sending keys to verify the result.",
140
- ],
141
- parameters: Type.Object({
142
- keys: Type.String({ description: "Keystrokes to send (use \\x1b for Escape, \\r for Enter, etc.)" }),
143
- settle_ms: Type.Optional(
144
- Type.Number({ description: "Wait time in ms after sending keys (default: 150)" }),
145
- ),
146
- }),
147
- async execute(_toolCallId: string, params: any) {
148
- const keys = interpretEscapes(params.keys);
149
- const settleMs = params.settle_ms ?? 150;
150
- bus.emit("shell:stdout-show", {});
151
- process.stdout.write("\n");
152
- bus.emit("shell:pty-write", { data: keys });
153
- await settle(settleMs);
154
-
155
- const tb = ctx.terminalBuffer;
156
- if (!tb) return { content: [{ type: "text", text: "Keys sent." }], details: undefined };
157
- const { text, altScreen, cursorX, cursorY } = tb.readScreen();
158
- const info = [
159
- altScreen ? "mode: alternate screen" : "mode: normal",
160
- `cursor: row=${cursorY} col=${cursorX}`,
161
- ].join(", ");
162
- return { content: [{ type: "text", text: `Keys sent. Screen after:\n[${info}]\n\n${text}` }], details: undefined };
163
- },
164
- };
165
- }
166
29
 
167
30
  // ── Extension entry point ─────────────────────────────────────────
168
31
  export default function activate(ctx: ExtensionContext): void {
169
32
  const { bus } = ctx;
170
33
  const cwd = process.cwd();
171
34
 
172
- const userShellTool = createUserShellToolDef(bus);
173
- const termReadTool = createTerminalReadToolDef(ctx);
174
- const termKeysTool = createTerminalKeysToolDef(bus, ctx);
175
-
176
35
  // ── Boot pi session (async — register backend synchronously first) ──
177
36
  let session: any = null;
178
37
  let runtime: any = null;
@@ -190,7 +49,6 @@ export default function activate(ctx: ExtensionContext): void {
190
49
  const result = await createAgentSessionFromServices({
191
50
  services,
192
51
  sessionManager: opts.sessionManager ?? sessionManager,
193
- customTools: [userShellTool, termReadTool, termKeysTool],
194
52
  });
195
53
  return { ...result, services };
196
54
  };
@@ -227,9 +85,7 @@ export default function activate(ctx: ExtensionContext): void {
227
85
  bus.emit("agent:tool-started", {
228
86
  title: (event as any).toolName,
229
87
  toolCallId: (event as any).toolCallId,
230
- kind: (event as any).toolName === "user_shell" || (event as any).toolName === "bash"
231
- ? "execute"
232
- : "read",
88
+ kind: (event as any).toolName === "bash" ? "execute" : "read",
233
89
  });
234
90
  break;
235
91
 
@@ -251,9 +107,7 @@ export default function activate(ctx: ExtensionContext): void {
251
107
  bus.emit("agent:tool-completed", {
252
108
  toolCallId: (event as any).toolCallId,
253
109
  exitCode: (event as any).isError ? 1 : 0,
254
- kind: (event as any).toolName === "user_shell" || (event as any).toolName === "bash"
255
- ? "execute"
256
- : "read",
110
+ kind: (event as any).toolName === "bash" ? "execute" : "read",
257
111
  });
258
112
  break;
259
113
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-sh",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "A shell-first terminal where AI is one keystroke away",
5
5
  "type": "module",
6
6
  "main": "dist/core.js",
@@ -70,6 +70,14 @@
70
70
  "types": "./dist/agent/token-budget.d.ts",
71
71
  "default": "./dist/agent/token-budget.js"
72
72
  },
73
+ "./agent/history-file": {
74
+ "types": "./dist/agent/history-file.d.ts",
75
+ "default": "./dist/agent/history-file.js"
76
+ },
77
+ "./agent/nuclear-form": {
78
+ "types": "./dist/agent/nuclear-form.d.ts",
79
+ "default": "./dist/agent/nuclear-form.js"
80
+ },
73
81
  "./executor": {
74
82
  "types": "./dist/executor.d.ts",
75
83
  "default": "./dist/executor.js"
@@ -1,9 +0,0 @@
1
- /**
2
- * Shell recall extension.
3
- *
4
- * Intercepts __shell_recall terminal commands via the
5
- * "agent:terminal-intercept" pipe, returning virtual output from
6
- * ContextManager's recall API without spawning a subprocess.
7
- */
8
- import type { ExtensionContext } from "../types.js";
9
- export default function activate({ bus, contextManager }: ExtensionContext): void;
@@ -1,8 +0,0 @@
1
- export default function activate({ bus, contextManager }) {
2
- bus.onPipe("agent:terminal-intercept", (payload) => {
3
- if (!payload.command.trimStart().startsWith("__shell_recall"))
4
- return payload;
5
- const output = contextManager.handleRecallCommand(payload.command.trim());
6
- return { ...payload, intercepted: true, output };
7
- });
8
- }