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.
- package/README.md +28 -33
- package/dist/agent/agent-loop.d.ts +31 -8
- package/dist/agent/agent-loop.js +277 -66
- package/dist/agent/conversation-state.d.ts +41 -9
- package/dist/agent/conversation-state.js +340 -17
- package/dist/agent/history-file.d.ts +36 -0
- package/dist/agent/history-file.js +167 -0
- package/dist/agent/nuclear-form.d.ts +41 -0
- package/dist/agent/nuclear-form.js +176 -0
- package/dist/agent/system-prompt.d.ts +4 -5
- package/dist/agent/system-prompt.js +16 -11
- package/dist/agent/token-budget.d.ts +13 -0
- package/dist/agent/token-budget.js +50 -0
- package/dist/agent/tool-protocol.d.ts +83 -0
- package/dist/agent/tool-protocol.js +386 -0
- package/dist/agent/tools/user-shell.js +4 -1
- package/dist/agent/types.d.ts +21 -1
- package/dist/context-manager.d.ts +0 -1
- package/dist/context-manager.js +5 -110
- package/dist/core.d.ts +7 -7
- package/dist/core.js +76 -180
- package/dist/event-bus.d.ts +40 -0
- package/dist/event-bus.js +20 -1
- package/dist/extension-loader.d.ts +5 -0
- package/dist/extension-loader.js +104 -17
- package/dist/extensions/agent-backend.d.ts +13 -0
- package/dist/extensions/agent-backend.js +167 -0
- package/dist/extensions/command-suggest.d.ts +3 -3
- package/dist/extensions/command-suggest.js +4 -3
- package/dist/extensions/index.d.ts +19 -0
- package/dist/extensions/index.js +25 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +44 -1
- package/dist/extensions/terminal-buffer.d.ts +1 -1
- package/dist/extensions/terminal-buffer.js +22 -8
- package/dist/extensions/tui-renderer.js +177 -122
- package/dist/index.js +14 -20
- package/dist/settings.d.ts +25 -2
- package/dist/settings.js +25 -4
- package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +1 -1
- package/dist/{input-handler.js → shell/input-handler.js} +60 -43
- package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
- package/dist/{output-parser.js → shell/output-parser.js} +1 -1
- package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
- package/dist/{shell.js → shell/shell.js} +24 -6
- package/dist/types.d.ts +49 -32
- package/dist/utils/ansi.d.ts +10 -0
- package/dist/utils/ansi.js +27 -0
- package/dist/utils/compositor.d.ts +62 -0
- package/dist/utils/compositor.js +88 -0
- package/dist/utils/diff-renderer.js +92 -4
- package/dist/utils/floating-panel.d.ts +34 -3
- package/dist/utils/floating-panel.js +315 -82
- package/dist/utils/handler-registry.d.ts +26 -10
- package/dist/utils/handler-registry.js +52 -16
- package/dist/utils/line-editor.d.ts +32 -3
- package/dist/utils/line-editor.js +218 -36
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +4 -4
- package/dist/utils/message-utils.d.ts +35 -0
- package/dist/utils/message-utils.js +75 -0
- package/dist/utils/terminal-buffer.d.ts +9 -1
- package/dist/utils/terminal-buffer.js +31 -2
- package/dist/utils/tool-display.d.ts +1 -0
- package/dist/utils/tool-display.js +1 -1
- package/dist/utils/tool-interactive.d.ts +12 -0
- package/dist/utils/tool-interactive.js +53 -0
- package/examples/extensions/ash-acp-bridge/README.md +39 -0
- package/examples/extensions/ash-acp-bridge/package.json +23 -0
- package/examples/extensions/ash-acp-bridge/src/index.ts +571 -0
- package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
- package/examples/extensions/ash-mcp-bridge/README.md +72 -0
- package/examples/extensions/ash-mcp-bridge/index.ts +154 -0
- package/examples/extensions/ash-mcp-bridge/package.json +9 -0
- package/examples/extensions/claude-code-bridge/index.ts +77 -1
- package/examples/extensions/interactive-prompts.ts +82 -110
- package/examples/extensions/overlay-agent.ts +84 -38
- package/examples/extensions/peer-mesh.ts +450 -0
- package/examples/extensions/pi-bridge/index.ts +87 -2
- package/examples/extensions/questionnaire.ts +249 -0
- package/examples/extensions/tmux-pane.ts +307 -0
- package/examples/extensions/web-access.ts +327 -0
- package/package.json +9 -1
- package/dist/extensions/overlay-agent.d.ts +0 -11
- package/dist/extensions/overlay-agent.js +0 -43
- 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(
|
|
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 =
|
|
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
|
+
}
|