agent-sh 0.7.0 → 0.9.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 (86) hide show
  1. package/README.md +28 -33
  2. package/dist/agent/agent-loop.d.ts +31 -8
  3. package/dist/agent/agent-loop.js +277 -66
  4. package/dist/agent/conversation-state.d.ts +41 -9
  5. package/dist/agent/conversation-state.js +340 -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 +176 -0
  10. package/dist/agent/system-prompt.d.ts +4 -5
  11. package/dist/agent/system-prompt.js +16 -11
  12. package/dist/agent/token-budget.d.ts +13 -0
  13. package/dist/agent/token-budget.js +50 -0
  14. package/dist/agent/tool-protocol.d.ts +83 -0
  15. package/dist/agent/tool-protocol.js +386 -0
  16. package/dist/agent/tools/user-shell.js +4 -1
  17. package/dist/agent/types.d.ts +21 -1
  18. package/dist/context-manager.d.ts +0 -1
  19. package/dist/context-manager.js +5 -110
  20. package/dist/core.d.ts +7 -7
  21. package/dist/core.js +76 -180
  22. package/dist/event-bus.d.ts +40 -0
  23. package/dist/event-bus.js +20 -1
  24. package/dist/extension-loader.d.ts +5 -0
  25. package/dist/extension-loader.js +104 -17
  26. package/dist/extensions/agent-backend.d.ts +13 -0
  27. package/dist/extensions/agent-backend.js +167 -0
  28. package/dist/extensions/command-suggest.d.ts +3 -3
  29. package/dist/extensions/command-suggest.js +4 -3
  30. package/dist/extensions/index.d.ts +19 -0
  31. package/dist/extensions/index.js +25 -0
  32. package/dist/extensions/slash-commands.d.ts +1 -1
  33. package/dist/extensions/slash-commands.js +44 -1
  34. package/dist/extensions/terminal-buffer.d.ts +1 -1
  35. package/dist/extensions/terminal-buffer.js +22 -8
  36. package/dist/extensions/tui-renderer.js +177 -122
  37. package/dist/index.js +14 -20
  38. package/dist/settings.d.ts +25 -2
  39. package/dist/settings.js +25 -4
  40. package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +1 -1
  41. package/dist/{input-handler.js → shell/input-handler.js} +60 -43
  42. package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
  43. package/dist/{output-parser.js → shell/output-parser.js} +1 -1
  44. package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
  45. package/dist/{shell.js → shell/shell.js} +24 -6
  46. package/dist/types.d.ts +49 -32
  47. package/dist/utils/ansi.d.ts +10 -0
  48. package/dist/utils/ansi.js +27 -0
  49. package/dist/utils/compositor.d.ts +62 -0
  50. package/dist/utils/compositor.js +88 -0
  51. package/dist/utils/diff-renderer.js +92 -4
  52. package/dist/utils/floating-panel.d.ts +34 -3
  53. package/dist/utils/floating-panel.js +315 -82
  54. package/dist/utils/handler-registry.d.ts +26 -10
  55. package/dist/utils/handler-registry.js +52 -16
  56. package/dist/utils/line-editor.d.ts +32 -3
  57. package/dist/utils/line-editor.js +218 -36
  58. package/dist/utils/markdown.d.ts +1 -0
  59. package/dist/utils/markdown.js +4 -4
  60. package/dist/utils/message-utils.d.ts +35 -0
  61. package/dist/utils/message-utils.js +75 -0
  62. package/dist/utils/terminal-buffer.d.ts +9 -1
  63. package/dist/utils/terminal-buffer.js +31 -2
  64. package/dist/utils/tool-display.d.ts +1 -0
  65. package/dist/utils/tool-display.js +1 -1
  66. package/dist/utils/tool-interactive.d.ts +12 -0
  67. package/dist/utils/tool-interactive.js +53 -0
  68. package/examples/extensions/ash-acp-bridge/README.md +39 -0
  69. package/examples/extensions/ash-acp-bridge/package.json +23 -0
  70. package/examples/extensions/ash-acp-bridge/src/index.ts +571 -0
  71. package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
  72. package/examples/extensions/ash-mcp-bridge/README.md +72 -0
  73. package/examples/extensions/ash-mcp-bridge/index.ts +154 -0
  74. package/examples/extensions/ash-mcp-bridge/package.json +9 -0
  75. package/examples/extensions/claude-code-bridge/index.ts +77 -1
  76. package/examples/extensions/interactive-prompts.ts +82 -110
  77. package/examples/extensions/overlay-agent.ts +84 -38
  78. package/examples/extensions/peer-mesh.ts +450 -0
  79. package/examples/extensions/pi-bridge/index.ts +87 -2
  80. package/examples/extensions/questionnaire.ts +249 -0
  81. package/examples/extensions/tmux-pane.ts +307 -0
  82. package/examples/extensions/web-access.ts +327 -0
  83. package/package.json +9 -1
  84. package/dist/extensions/overlay-agent.d.ts +0 -11
  85. package/dist/extensions/overlay-agent.js +0 -43
  86. package/examples/extensions/terminal-buffer.ts +0 -184
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Utilities for manipulating OpenAI-format message arrays.
3
+ *
4
+ * Used by extensions advising `conversation:prepare` to transform
5
+ * the message array before it's sent to the LLM.
6
+ */
7
+ /* eslint-disable @typescript-eslint/no-explicit-any */
8
+ /**
9
+ * Find tool call IDs matching a tool name and optional argument filter.
10
+ *
11
+ * Scans assistant messages for tool_calls where `function.name` matches
12
+ * and parsed arguments satisfy the filter (shallow key/value match).
13
+ *
14
+ * Returns call IDs in message order (earliest first).
15
+ */
16
+ export function findToolCallIds(messages, toolName, argFilter) {
17
+ const ids = [];
18
+ for (const msg of messages) {
19
+ if (msg.role !== "assistant" || !msg.tool_calls)
20
+ continue;
21
+ for (const tc of msg.tool_calls) {
22
+ const fn = tc.function ?? tc.fn;
23
+ if (!fn || fn.name !== toolName)
24
+ continue;
25
+ if (argFilter) {
26
+ let args;
27
+ try {
28
+ args = JSON.parse(fn.arguments);
29
+ }
30
+ catch {
31
+ continue;
32
+ }
33
+ const match = Object.entries(argFilter).every(([k, v]) => args[k] === v);
34
+ if (!match)
35
+ continue;
36
+ }
37
+ ids.push(tc.id);
38
+ }
39
+ }
40
+ return ids;
41
+ }
42
+ /**
43
+ * Replace tool result content for specific call IDs.
44
+ *
45
+ * Returns a new array (shallow copy) with matching tool messages
46
+ * replaced. Non-matching messages are passed through by reference.
47
+ */
48
+ export function stubToolResults(messages, callIds, stub) {
49
+ return messages.map((msg) => {
50
+ if (msg.role === "tool" && callIds.has(msg.tool_call_id)) {
51
+ return { ...msg, content: stub };
52
+ }
53
+ return msg;
54
+ });
55
+ }
56
+ /**
57
+ * Deduplicate tool results: keep only the latest result for a given
58
+ * tool name + argument filter, replace all older results with a stub.
59
+ *
60
+ * Common use case: a file that's read repeatedly (e.g. a live transcript)
61
+ * — only the most recent read matters.
62
+ *
63
+ * Example:
64
+ * dedupeToolResults(messages, "read_file",
65
+ * { path: "/path/to/transcript.txt" },
66
+ * "[stale — superseded by later read]")
67
+ */
68
+ export function dedupeToolResults(messages, toolName, argFilter, stub = "[superseded by later call]") {
69
+ const callIds = findToolCallIds(messages, toolName, argFilter);
70
+ if (callIds.length <= 1)
71
+ return messages;
72
+ // Keep the last one, stub the rest
73
+ const staleIds = new Set(callIds.slice(0, -1));
74
+ return stubToolResults(messages, staleIds, stub);
75
+ }
@@ -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,12 +40,16 @@ 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). */
44
48
  serialize(): string;
45
49
  /** Read clean screen text with metadata. */
46
- readScreen(): ScreenSnapshot;
50
+ readScreen(opts?: {
51
+ includeScrollback?: boolean;
52
+ }): ScreenSnapshot;
47
53
  /**
48
54
  * Get terminal screen as lines, padded/trimmed to exactly `rows` lines.
49
55
  * Clean text only (ANSI stripped). Reads from the active buffer's
@@ -53,6 +59,8 @@ export declare class TerminalBuffer {
53
59
  getScreenLines(rows?: number): string[];
54
60
  /** Read visible viewport lines from a buffer. */
55
61
  private readViewportLines;
62
+ /** Read all lines including scrollback from a buffer. */
63
+ private readAllLines;
56
64
  /** Get cursor position. */
57
65
  getCursor(): {
58
66
  x: number;
@@ -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);
@@ -117,9 +130,11 @@ export class TerminalBuffer {
117
130
  return this.serializeAddon.serialize();
118
131
  }
119
132
  /** Read clean screen text with metadata. */
120
- readScreen() {
133
+ readScreen(opts) {
121
134
  const buf = this.term.buffer.active;
122
- const lines = this.readViewportLines(buf);
135
+ const lines = opts?.includeScrollback
136
+ ? this.readAllLines(buf)
137
+ : this.readViewportLines(buf);
123
138
  return {
124
139
  text: lines.join("\n"),
125
140
  altScreen: buf.type === "alternate",
@@ -148,6 +163,20 @@ export class TerminalBuffer {
148
163
  }
149
164
  return lines;
150
165
  }
166
+ /** Read all lines including scrollback from a buffer. */
167
+ readAllLines(buf) {
168
+ const total = (buf.baseY ?? 0) + buf.length;
169
+ const lines = [];
170
+ for (let y = 0; y < total; y++) {
171
+ const line = buf.getLine(y);
172
+ lines.push(line ? line.translateToString(true) : "");
173
+ }
174
+ // Trim trailing empty lines
175
+ while (lines.length > 0 && lines[lines.length - 1] === "") {
176
+ lines.pop();
177
+ }
178
+ return lines;
179
+ }
151
180
  /** Get cursor position. */
152
181
  getCursor() {
153
182
  return {
@@ -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
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Interactive UI primitive for tools.
3
+ *
4
+ * Gives a tool imperative control over rendering and input on the active
5
+ * surface. The tool provides render() + handleInput(), the primitive
6
+ * handles surface writing, input interception, shell pause/unpause,
7
+ * and cleanup.
8
+ */
9
+ import type { EventBus } from "../event-bus.js";
10
+ import type { RenderSurface } from "./compositor.js";
11
+ import type { ToolUI } from "../agent/types.js";
12
+ export declare function createToolUI(bus: EventBus, surface: RenderSurface): ToolUI;
@@ -0,0 +1,53 @@
1
+ /** Clear N lines above the cursor. */
2
+ function clearLines(surface, count) {
3
+ for (let i = 0; i < count; i++) {
4
+ surface.write("\x1b[A\x1b[2K");
5
+ }
6
+ }
7
+ export function createToolUI(bus, surface) {
8
+ return {
9
+ custom(session) {
10
+ return new Promise((resolve) => {
11
+ let prevLineCount = 0;
12
+ let finished = false;
13
+ const done = (result) => {
14
+ if (finished)
15
+ return;
16
+ finished = true;
17
+ clearLines(surface, prevLineCount);
18
+ bus.offPipe("input:intercept", interceptor);
19
+ bus.emit("shell:stdout-hide", {});
20
+ bus.emit("tool:interactive-end", {});
21
+ session.onUnmount?.();
22
+ resolve(result);
23
+ };
24
+ const render = () => {
25
+ if (finished)
26
+ return;
27
+ clearLines(surface, prevLineCount);
28
+ const lines = session.render(surface.columns);
29
+ for (const line of lines) {
30
+ surface.writeLine(line);
31
+ }
32
+ prevLineCount = lines.length;
33
+ };
34
+ const interceptor = (payload) => {
35
+ if (finished)
36
+ return payload;
37
+ // Let Ctrl+C through for agent cancellation
38
+ if (payload.data === "\x03")
39
+ return payload;
40
+ session.handleInput(payload.data, done);
41
+ render();
42
+ return { ...payload, consumed: true };
43
+ };
44
+ // Setup
45
+ bus.emit("tool:interactive-start", {});
46
+ bus.emit("shell:stdout-show", {});
47
+ bus.onPipe("input:intercept", interceptor);
48
+ session.onMount?.(() => render());
49
+ render();
50
+ });
51
+ },
52
+ };
53
+ }
@@ -0,0 +1,39 @@
1
+ # ash-acp-bridge
2
+
3
+ ACP (Agent Client Protocol) server that wraps agent-sh's headless core, allowing any ACP-compatible client to use ash as a backend.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ cd ash-acp-bridge
9
+ npm install
10
+ npm run build # or use `npx tsx src/index.ts` for dev
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```bash
16
+ ash-acp-bridge # use ~/.agent-sh/settings.json defaults
17
+ ash-acp-bridge --model gpt-4o # override model
18
+ ash-acp-bridge --provider anthropic # override provider
19
+ ```
20
+
21
+ ## How it works
22
+
23
+ ```
24
+ ACP client
25
+ ↕ JSON-RPC over stdin/stdout (ACP)
26
+ ash-acp-bridge
27
+ ↕ EventBus
28
+ agent-sh core (headless)
29
+ ↕ OpenAI-compatible API
30
+ LLM provider
31
+ ```
32
+
33
+ The adapter translates between ACP methods and agent-sh's event bus:
34
+
35
+ - `initialize` → return capabilities
36
+ - `session/new` → create core, set cwd
37
+ - `session/prompt` → `agent:submit` event
38
+ - `session/update` notifications ← `agent:response-chunk`, `agent:tool-started`, etc.
39
+ - `session/request_permission` ↔ `permission:request` async pipe
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "ash-acp-bridge",
3
+ "version": "0.1.0",
4
+ "description": "ACP server that wraps agent-sh's headless core for any ACP-compatible client",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "ash-acp-bridge": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "dev": "tsx src/index.ts",
12
+ "build": "tsc",
13
+ "start": "node dist/index.js"
14
+ },
15
+ "dependencies": {
16
+ "agent-sh": "file:../../..",
17
+ "tsx": "^4.19.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^22.0.0",
21
+ "typescript": "^5.7.0"
22
+ }
23
+ }