agent-sh 0.7.0 → 0.8.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 (41) hide show
  1. package/README.md +5 -1
  2. package/dist/agent/agent-loop.d.ts +2 -2
  3. package/dist/agent/agent-loop.js +106 -13
  4. package/dist/agent/conversation-state.d.ts +39 -9
  5. package/dist/agent/conversation-state.js +336 -17
  6. package/dist/agent/history-file.d.ts +36 -0
  7. package/dist/agent/history-file.js +167 -0
  8. package/dist/agent/nuclear-form.d.ts +41 -0
  9. package/dist/agent/nuclear-form.js +175 -0
  10. package/dist/agent/system-prompt.d.ts +2 -2
  11. package/dist/agent/system-prompt.js +25 -4
  12. package/dist/agent/tools/user-shell.js +4 -1
  13. package/dist/context-manager.d.ts +0 -1
  14. package/dist/context-manager.js +5 -110
  15. package/dist/core.js +14 -0
  16. package/dist/event-bus.d.ts +14 -0
  17. package/dist/extensions/overlay-agent.d.ts +4 -1
  18. package/dist/extensions/overlay-agent.js +115 -11
  19. package/dist/extensions/slash-commands.js +28 -0
  20. package/dist/extensions/terminal-buffer.js +9 -4
  21. package/dist/extensions/tui-renderer.js +119 -84
  22. package/dist/settings.d.ts +19 -2
  23. package/dist/settings.js +21 -3
  24. package/dist/shell.js +4 -0
  25. package/dist/token-budget.d.ts +13 -0
  26. package/dist/token-budget.js +50 -0
  27. package/dist/types.d.ts +0 -22
  28. package/dist/utils/ansi.d.ts +10 -0
  29. package/dist/utils/ansi.js +27 -0
  30. package/dist/utils/floating-panel.d.ts +32 -3
  31. package/dist/utils/floating-panel.js +296 -79
  32. package/dist/utils/line-editor.d.ts +9 -0
  33. package/dist/utils/line-editor.js +44 -0
  34. package/dist/utils/markdown.js +3 -3
  35. package/dist/utils/terminal-buffer.d.ts +4 -0
  36. package/dist/utils/terminal-buffer.js +13 -0
  37. package/dist/utils/tool-display.d.ts +1 -0
  38. package/dist/utils/tool-display.js +1 -1
  39. package/examples/extensions/claude-code-bridge/index.ts +77 -1
  40. package/examples/extensions/pi-bridge/index.ts +87 -2
  41. package/package.json +1 -1
@@ -28,6 +28,8 @@ export declare function formatScreenContext(screen: ScreenSnapshot, maxLines?: n
28
28
  export declare class TerminalBuffer {
29
29
  private readonly term;
30
30
  private readonly serializeAddon;
31
+ /** Flush pending drip-feed data (set by createWired). */
32
+ _flushPending: (() => void) | null;
31
33
  private constructor();
32
34
  /**
33
35
  * Create a new TerminalBuffer. Returns null if xterm is not installed.
@@ -38,6 +40,8 @@ export declare class TerminalBuffer {
38
40
  * Returns null if xterm is not installed.
39
41
  */
40
42
  static createWired(bus: EventBus, config?: TerminalBufferConfig): TerminalBuffer | null;
43
+ /** Flush any pending drip-feed data into the virtual terminal. */
44
+ flush(): void;
41
45
  /** Write raw data into the virtual terminal. */
42
46
  write(data: string): void;
43
47
  /** Get the raw serialized terminal output (includes ANSI sequences). */
@@ -65,6 +65,8 @@ export function formatScreenContext(screen, maxLines = 80, baseContext) {
65
65
  export class TerminalBuffer {
66
66
  term;
67
67
  serializeAddon;
68
+ /** Flush pending drip-feed data (set by createWired). */
69
+ _flushPending = null;
68
70
  constructor(term, serialize) {
69
71
  this.term = term;
70
72
  this.serializeAddon = serialize;
@@ -103,11 +105,22 @@ export class TerminalBuffer {
103
105
  tb.write(d);
104
106
  }
105
107
  }, 50);
108
+ tb._flushPending = () => {
109
+ if (pending) {
110
+ const d = pending;
111
+ pending = "";
112
+ tb.write(d);
113
+ }
114
+ };
106
115
  process.stdout.on("resize", () => {
107
116
  tb.resize(process.stdout.columns || 80, process.stdout.rows || 24);
108
117
  });
109
118
  return tb;
110
119
  }
120
+ /** Flush any pending drip-feed data into the virtual terminal. */
121
+ flush() {
122
+ this._flushPending?.();
123
+ }
111
124
  /** Write raw data into the virtual terminal. */
112
125
  write(data) {
113
126
  this.term.write(data);
@@ -30,6 +30,7 @@ export declare function selectToolDisplayMode(width: number): ToolDisplayMode;
30
30
  export declare function renderToolCall(tool: ToolCallRender, width: number): string[];
31
31
  export declare function renderToolResult(result: ToolResultRender, width: number): string[];
32
32
  export declare function formatElapsed(ms: number): string;
33
+ export declare const SPINNER_FRAMES: string[];
33
34
  export interface SpinnerState {
34
35
  frame: number;
35
36
  startTime: number;
@@ -205,7 +205,7 @@ export function formatElapsed(ms) {
205
205
  return rm > 0 ? `${h}h ${rm}m` : `${h}h`;
206
206
  }
207
207
  // ── Spinner with elapsed timer ───────────────────────────────────
208
- const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
208
+ export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
209
209
  export function createSpinner(opts) {
210
210
  return { frame: 0, startTime: opts?.startTime || Date.now() };
211
211
  }
@@ -23,6 +23,23 @@ import { z } from "zod";
23
23
  import type { ExtensionContext } from "../../src/types.js";
24
24
  import type { EventBus } from "../../src/event-bus.js";
25
25
 
26
+ // ── Helpers ──────────────────────────────────────────────────────
27
+ function interpretEscapes(str: string): string {
28
+ return str.replace(/\\(x[0-9a-fA-F]{2}|r|n|t|\\|0)/g, (_, seq: string) => {
29
+ if (seq === "r") return "\r";
30
+ if (seq === "n") return "\n";
31
+ if (seq === "t") return "\t";
32
+ if (seq === "\\") return "\\";
33
+ if (seq === "0") return "\0";
34
+ if (seq.startsWith("x")) return String.fromCharCode(parseInt(seq.slice(1), 16));
35
+ return seq;
36
+ });
37
+ }
38
+
39
+ function settle(ms = 100): Promise<void> {
40
+ return new Promise((resolve) => setTimeout(resolve, ms));
41
+ }
42
+
26
43
  // ── user_shell MCP tool ───────────────────────────────────────────
27
44
  function createUserShellTool(bus: EventBus) {
28
45
  let liveCwd = process.cwd();
@@ -56,15 +73,72 @@ function createUserShellTool(bus: EventBus) {
56
73
  );
57
74
  }
58
75
 
76
+ // ── terminal_read MCP tool ────────────────────────────────────────
77
+ function createTerminalReadTool(ctx: ExtensionContext) {
78
+ return tool(
79
+ "terminal_read",
80
+ "Read the current terminal screen contents. Returns clean text (ANSI stripped) " +
81
+ "with cursor position and whether an alternate-screen program (vim, htop, less) is active. " +
82
+ "Use this to see what the user sees before sending keystrokes with terminal_keys.",
83
+ {},
84
+ async () => {
85
+ const tb = ctx.terminalBuffer;
86
+ if (!tb) return { content: [{ type: "text" as const, text: "terminal buffer not available" }] };
87
+ const { text, altScreen, cursorX, cursorY } = tb.readScreen();
88
+ const info = [
89
+ altScreen ? "mode: alternate screen" : "mode: normal",
90
+ `cursor: row=${cursorY} col=${cursorX}`,
91
+ ].join(", ");
92
+ return { content: [{ type: "text" as const, text: `[${info}]\n\n${text}` }] };
93
+ },
94
+ );
95
+ }
96
+
97
+ // ── terminal_keys MCP tool ───────────────────────────────────────
98
+ function createTerminalKeysTool(bus: EventBus, ctx: ExtensionContext) {
99
+ return tool(
100
+ "terminal_keys",
101
+ "Send keystrokes to the user's live terminal. The keys are written directly to the PTY " +
102
+ "as if the user typed them. Use escape sequences for special keys:\n" +
103
+ " - Escape: \\x1b - Enter: \\r - Tab: \\t\n" +
104
+ " - Ctrl+C: \\x03 - Arrow keys: \\x1b[A/B/C/D - Backspace: \\x7f\n" +
105
+ "Example: to quit vim without saving, send keys=\"\\x1b:q!\\r\".\n" +
106
+ "Always call terminal_read after sending keys to verify the result.",
107
+ {
108
+ keys: z.string().describe("Keystrokes to send (use \\x1b for Escape, \\r for Enter, etc.)"),
109
+ settle_ms: z.number().optional().describe("Wait time in ms after sending keys (default: 150)"),
110
+ },
111
+ async (args) => {
112
+ const keys = interpretEscapes(args.keys);
113
+ const settleMs = args.settle_ms ?? 150;
114
+ bus.emit("shell:stdout-show", {});
115
+ process.stdout.write("\n");
116
+ bus.emit("shell:pty-write", { data: keys });
117
+ await settle(settleMs);
118
+
119
+ const tb = ctx.terminalBuffer;
120
+ if (!tb) return { content: [{ type: "text" as const, text: "Keys sent." }] };
121
+ const { text, altScreen, cursorX, cursorY } = tb.readScreen();
122
+ const info = [
123
+ altScreen ? "mode: alternate screen" : "mode: normal",
124
+ `cursor: row=${cursorY} col=${cursorX}`,
125
+ ].join(", ");
126
+ return { content: [{ type: "text" as const, text: `Keys sent. Screen after:\n[${info}]\n\n${text}` }] };
127
+ },
128
+ );
129
+ }
130
+
59
131
  // ── Extension entry point ─────────────────────────────────────────
60
132
  export default function activate(ctx: ExtensionContext): void {
61
133
  const { bus } = ctx;
62
134
 
63
135
  const shellTool = createUserShellTool(bus);
136
+ const termReadTool = createTerminalReadTool(ctx);
137
+ const termKeysTool = createTerminalKeysTool(bus, ctx);
64
138
  const shellServer = createSdkMcpServer({
65
139
  name: "agent-sh",
66
140
  version: "1.0.0",
67
- tools: [shellTool],
141
+ tools: [shellTool, termReadTool, termKeysTool],
68
142
  });
69
143
 
70
144
  let activeQuery: Query | null = null;
@@ -95,6 +169,8 @@ export default function activate(ctx: ExtensionContext): void {
95
169
  mcpServers: { "agent-sh": shellServer },
96
170
  allowedTools: [
97
171
  "mcp__agent-sh__user_shell",
172
+ "mcp__agent-sh__terminal_read",
173
+ "mcp__agent-sh__terminal_keys",
98
174
  "Read", "Edit", "Write", "Bash", "Glob", "Grep",
99
175
  ],
100
176
  permissionMode: "acceptEdits",
@@ -26,7 +26,22 @@ import { Type } from "@sinclair/typebox";
26
26
  import type { ExtensionContext } from "../../src/types.js";
27
27
  import type { EventBus } from "../../src/event-bus.js";
28
28
 
29
- // ── agent-sh context injected via tool promptGuidelines + promptSnippet ──
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
+ }
30
45
 
31
46
  // ── user_shell as a pi ToolDefinition ─────────────────────────────
32
47
  function createUserShellToolDef(bus: EventBus) {
@@ -81,12 +96,82 @@ function createUserShellToolDef(bus: EventBus) {
81
96
  };
82
97
  }
83
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
+
84
167
  // ── Extension entry point ─────────────────────────────────────────
85
168
  export default function activate(ctx: ExtensionContext): void {
86
169
  const { bus } = ctx;
87
170
  const cwd = process.cwd();
88
171
 
89
172
  const userShellTool = createUserShellToolDef(bus);
173
+ const termReadTool = createTerminalReadToolDef(ctx);
174
+ const termKeysTool = createTerminalKeysToolDef(bus, ctx);
90
175
 
91
176
  // ── Boot pi session (async — register backend synchronously first) ──
92
177
  let session: any = null;
@@ -105,7 +190,7 @@ export default function activate(ctx: ExtensionContext): void {
105
190
  const result = await createAgentSessionFromServices({
106
191
  services,
107
192
  sessionManager: opts.sessionManager ?? sessionManager,
108
- customTools: [userShellTool],
193
+ customTools: [userShellTool, termReadTool, termKeysTool],
109
194
  });
110
195
  return { ...result, services };
111
196
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-sh",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "A shell-first terminal where AI is one keystroke away",
5
5
  "type": "module",
6
6
  "main": "dist/core.js",