agent-sh 0.12.24 → 0.12.26
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 +61 -9
- package/dist/agent/system-prompt.js +2 -2
- package/dist/extensions/shell-context.d.ts +2 -1
- package/dist/extensions/shell-context.js +5 -4
- package/dist/index.js +30 -54
- package/dist/shell/index.d.ts +5 -0
- package/dist/shell/index.js +13 -8
- package/dist/shell/input-handler.js +75 -27
- package/dist/shell/tui-input-view.d.ts +5 -0
- package/dist/shell/tui-input-view.js +137 -96
- package/dist/utils/terminal-buffer.d.ts +6 -9
- package/dist/utils/terminal-buffer.js +21 -53
- package/examples/extensions/claude-code-bridge/README.md +14 -14
- package/examples/extensions/claude-code-bridge/index.ts +19 -25
- package/examples/extensions/opencode-bridge/README.md +59 -0
- package/examples/extensions/opencode-bridge/index.ts +601 -0
- package/examples/extensions/opencode-bridge/package.json +11 -0
- package/examples/extensions/pi-bridge/README.md +9 -2
- package/package.json +1 -1
|
@@ -11,130 +11,171 @@ export class TuiInputView {
|
|
|
11
11
|
cursorTermCol = 1;
|
|
12
12
|
autocompleteLines = 0;
|
|
13
13
|
surface;
|
|
14
|
+
frameBuf = null;
|
|
14
15
|
constructor(surface) {
|
|
15
16
|
this.surface = surface ?? new StdoutSurface();
|
|
16
17
|
}
|
|
18
|
+
// Frame buffering: coalesces all emit() calls until endFrame() into one
|
|
19
|
+
// surface.write, bracketed by cursor hide/show so intermediate redraw
|
|
20
|
+
// states never flicker through.
|
|
21
|
+
beginFrame() {
|
|
22
|
+
if (this.frameBuf === null)
|
|
23
|
+
this.frameBuf = "\x1b[?25l";
|
|
24
|
+
}
|
|
25
|
+
endFrame() {
|
|
26
|
+
if (this.frameBuf === null)
|
|
27
|
+
return;
|
|
28
|
+
const out = this.frameBuf + "\x1b[?25h";
|
|
29
|
+
this.frameBuf = null;
|
|
30
|
+
this.surface.write(out);
|
|
31
|
+
}
|
|
32
|
+
emit(s) {
|
|
33
|
+
if (this.frameBuf !== null)
|
|
34
|
+
this.frameBuf += s;
|
|
35
|
+
else
|
|
36
|
+
this.surface.write(s);
|
|
37
|
+
}
|
|
38
|
+
autoFrame(fn) {
|
|
39
|
+
const owned = this.frameBuf === null;
|
|
40
|
+
if (owned)
|
|
41
|
+
this.beginFrame();
|
|
42
|
+
try {
|
|
43
|
+
return fn();
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
if (owned)
|
|
47
|
+
this.endFrame();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
17
50
|
resetCursor() {
|
|
18
51
|
this.cursorRowsBelow = 0;
|
|
19
52
|
this.cursorTermCol = 1;
|
|
20
53
|
}
|
|
21
54
|
enableModeKeys() {
|
|
22
55
|
// Kitty progressive enhancement + bracket paste (Shift+Enter → \x1b[13;2u).
|
|
23
|
-
this.
|
|
56
|
+
this.emit("\x1b[>1u\x1b[?2004h");
|
|
24
57
|
}
|
|
25
58
|
disableModeKeys() {
|
|
26
|
-
this.
|
|
59
|
+
this.emit("\x1b[<u\x1b[?2004l");
|
|
27
60
|
}
|
|
28
61
|
clearPromptArea() {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
62
|
+
this.autoFrame(() => {
|
|
63
|
+
if (this.cursorRowsBelow > 0) {
|
|
64
|
+
this.emit(`\x1b[${this.cursorRowsBelow}A`);
|
|
65
|
+
}
|
|
66
|
+
this.emit("\r\x1b[J");
|
|
67
|
+
this.cursorRowsBelow = 0;
|
|
68
|
+
});
|
|
34
69
|
}
|
|
35
70
|
drawPrompt(vm) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
this.surface.write("\r\x1b[J");
|
|
41
|
-
const infoPrefix = vm.agentInfo.info
|
|
42
|
-
? `${vm.agentInfo.info} ${p.success}${vm.indicator}${p.reset} `
|
|
43
|
-
: `${p.success}${vm.indicator}${p.reset} `;
|
|
44
|
-
const promptPrefix = infoPrefix + p.warning + p.bold + vm.promptIcon + " " + p.reset;
|
|
45
|
-
const promptVisLen = visibleLen(infoPrefix) + visibleLen(vm.promptIcon) + 1;
|
|
46
|
-
const display = vm.showBuffer ? vm.displayText : "";
|
|
47
|
-
const dCursor = vm.showBuffer ? vm.displayCursor : 0;
|
|
48
|
-
if (!vm.showBuffer) {
|
|
49
|
-
this.surface.write(promptPrefix);
|
|
50
|
-
const N = promptVisLen;
|
|
51
|
-
this.cursorRowsBelow = N > 0 ? Math.ceil(N / termW) - 1 : 0;
|
|
52
|
-
this.cursorTermCol = N === 0 ? 1 : (N % termW === 0 ? termW : (N % termW) + 1);
|
|
53
|
-
}
|
|
54
|
-
else if (!display.includes("\n")) {
|
|
55
|
-
// DECSC/DECRC bracket the after-cursor text so the cursor lands mid-line.
|
|
56
|
-
const before = display.slice(0, dCursor);
|
|
57
|
-
const after = display.slice(dCursor);
|
|
58
|
-
this.surface.write(promptPrefix + p.accent + before + p.reset +
|
|
59
|
-
"\x1b7" +
|
|
60
|
-
p.accent + after + p.reset +
|
|
61
|
-
"\x1b8");
|
|
62
|
-
const cursorVisCol = promptVisLen + visibleLen(before);
|
|
63
|
-
this.cursorRowsBelow = cursorVisCol > 0 ? Math.ceil(cursorVisCol / termW) - 1 : 0;
|
|
64
|
-
this.cursorTermCol = cursorVisCol === 0 ? 1 : (cursorVisCol % termW === 0 ? termW : (cursorVisCol % termW) + 1);
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
const lines = display.split("\n");
|
|
68
|
-
const indent = " ".repeat(promptVisLen);
|
|
69
|
-
let charsRemaining = dCursor;
|
|
70
|
-
let cursorLine = 0;
|
|
71
|
-
for (let li = 0; li < lines.length; li++) {
|
|
72
|
-
if (charsRemaining <= lines[li].length) {
|
|
73
|
-
cursorLine = li;
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
charsRemaining -= lines[li].length + 1;
|
|
77
|
-
cursorLine = li + 1;
|
|
71
|
+
this.autoFrame(() => {
|
|
72
|
+
const termW = this.surface.columns;
|
|
73
|
+
if (this.cursorRowsBelow > 0) {
|
|
74
|
+
this.emit(`\x1b[${this.cursorRowsBelow}A`);
|
|
78
75
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
76
|
+
this.emit("\r\x1b[J");
|
|
77
|
+
const infoPrefix = vm.agentInfo.info
|
|
78
|
+
? `${vm.agentInfo.info} ${p.success}${vm.indicator}${p.reset} `
|
|
79
|
+
: `${p.success}${vm.indicator}${p.reset} `;
|
|
80
|
+
const promptPrefix = infoPrefix + p.warning + p.bold + vm.promptIcon + " " + p.reset;
|
|
81
|
+
const promptVisLen = visibleLen(infoPrefix) + visibleLen(vm.promptIcon) + 1;
|
|
82
|
+
const display = vm.showBuffer ? vm.displayText : "";
|
|
83
|
+
const dCursor = vm.showBuffer ? vm.displayCursor : 0;
|
|
84
|
+
if (!vm.showBuffer) {
|
|
85
|
+
this.emit(promptPrefix);
|
|
86
|
+
const N = promptVisLen;
|
|
87
|
+
this.cursorRowsBelow = N > 0 ? Math.ceil(N / termW) - 1 : 0;
|
|
88
|
+
this.cursorTermCol = N === 0 ? 1 : (N % termW === 0 ? termW : (N % termW) + 1);
|
|
89
|
+
}
|
|
90
|
+
else if (!display.includes("\n")) {
|
|
91
|
+
// DECSC/DECRC bracket the after-cursor text so the cursor lands mid-line.
|
|
92
|
+
const before = display.slice(0, dCursor);
|
|
93
|
+
const after = display.slice(dCursor);
|
|
94
|
+
this.emit(promptPrefix + p.accent + before + p.reset +
|
|
95
|
+
"\x1b7" +
|
|
96
|
+
p.accent + after + p.reset +
|
|
97
|
+
"\x1b8");
|
|
98
|
+
const cursorVisCol = promptVisLen + visibleLen(before);
|
|
99
|
+
this.cursorRowsBelow = cursorVisCol > 0 ? Math.ceil(cursorVisCol / termW) - 1 : 0;
|
|
100
|
+
this.cursorTermCol = cursorVisCol === 0 ? 1 : (cursorVisCol % termW === 0 ? termW : (cursorVisCol % termW) + 1);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const lines = display.split("\n");
|
|
104
|
+
const indent = " ".repeat(promptVisLen);
|
|
105
|
+
let charsRemaining = dCursor;
|
|
106
|
+
let cursorLine = 0;
|
|
107
|
+
for (let li = 0; li < lines.length; li++) {
|
|
108
|
+
if (charsRemaining <= lines[li].length) {
|
|
109
|
+
cursorLine = li;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
charsRemaining -= lines[li].length + 1;
|
|
113
|
+
cursorLine = li + 1;
|
|
96
114
|
}
|
|
97
|
-
|
|
98
|
-
|
|
115
|
+
let output = "";
|
|
116
|
+
let cursorRowFromTop = 0;
|
|
117
|
+
let rowsSoFar = 0;
|
|
118
|
+
for (let li = 0; li < lines.length; li++) {
|
|
119
|
+
const prefix = li === 0 ? promptPrefix : indent;
|
|
120
|
+
const lineText = lines[li];
|
|
121
|
+
const lineVisLen = promptVisLen + visibleLen(lineText);
|
|
122
|
+
const lineTermRows = lineVisLen > 0 ? Math.ceil(lineVisLen / termW) : 1;
|
|
123
|
+
if (li === cursorLine) {
|
|
124
|
+
const before = lineText.slice(0, charsRemaining);
|
|
125
|
+
const after = lineText.slice(charsRemaining);
|
|
126
|
+
output += prefix + p.accent + before + p.reset;
|
|
127
|
+
output += "\x1b7";
|
|
128
|
+
output += p.accent + after + p.reset;
|
|
129
|
+
const beforeVisCol = promptVisLen + visibleLen(before);
|
|
130
|
+
cursorRowFromTop = rowsSoFar + (beforeVisCol > 0 ? Math.ceil(beforeVisCol / termW) - 1 : 0);
|
|
131
|
+
this.cursorTermCol = beforeVisCol === 0 ? 1 : (beforeVisCol % termW === 0 ? termW : (beforeVisCol % termW) + 1);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
output += prefix + p.accent + lineText + p.reset;
|
|
135
|
+
}
|
|
136
|
+
if (li < lines.length - 1)
|
|
137
|
+
output += "\n";
|
|
138
|
+
rowsSoFar += lineTermRows;
|
|
99
139
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
rowsSoFar += lineTermRows;
|
|
140
|
+
this.emit(output + "\x1b8");
|
|
141
|
+
this.cursorRowsBelow = cursorRowFromTop;
|
|
103
142
|
}
|
|
104
|
-
|
|
105
|
-
this.cursorRowsBelow = cursorRowFromTop;
|
|
106
|
-
}
|
|
143
|
+
});
|
|
107
144
|
}
|
|
108
145
|
drawAutocomplete(vm) {
|
|
109
146
|
if (vm.items.length === 0)
|
|
110
147
|
return;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
148
|
+
this.autoFrame(() => {
|
|
149
|
+
const lines = [];
|
|
150
|
+
for (let i = 0; i < vm.items.length; i++) {
|
|
151
|
+
const item = vm.items[i];
|
|
152
|
+
const selected = i === vm.selected;
|
|
153
|
+
if (selected) {
|
|
154
|
+
lines.push(` \x1b[7m ${p.accent}${item.name.padEnd(12)}${p.reset}\x1b[7m ${item.description} ${p.reset}`);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
lines.push(` ${p.muted}${item.name.padEnd(12)} ${item.description}${p.reset}`);
|
|
158
|
+
}
|
|
117
159
|
}
|
|
118
|
-
|
|
119
|
-
|
|
160
|
+
this.emit("\n" + lines.join("\n"));
|
|
161
|
+
this.autocompleteLines = lines.length;
|
|
162
|
+
if (this.autocompleteLines > 0) {
|
|
163
|
+
this.emit(`\x1b[${this.autocompleteLines}A`);
|
|
120
164
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (this.autocompleteLines > 0) {
|
|
125
|
-
this.surface.write(`\x1b[${this.autocompleteLines}A`);
|
|
126
|
-
}
|
|
127
|
-
// Absolute column set — preceding \n may have scrolled, invalidating DECSC.
|
|
128
|
-
this.surface.write(`\x1b[${this.cursorTermCol}G`);
|
|
165
|
+
// Absolute column set — preceding \n may have scrolled, invalidating DECSC.
|
|
166
|
+
this.emit(`\x1b[${this.cursorTermCol}G`);
|
|
167
|
+
});
|
|
129
168
|
}
|
|
130
169
|
clearAutocomplete() {
|
|
131
170
|
if (this.autocompleteLines <= 0)
|
|
132
171
|
return;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
this.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
172
|
+
this.autoFrame(() => {
|
|
173
|
+
// CSI B (cursor down, bounded) so we don't scroll on the last row.
|
|
174
|
+
for (let i = 0; i < this.autocompleteLines; i++) {
|
|
175
|
+
this.emit("\x1b[B\x1b[2K");
|
|
176
|
+
}
|
|
177
|
+
this.emit(`\x1b[${this.autocompleteLines}A\x1b[${this.cursorTermCol}G`);
|
|
178
|
+
this.autocompleteLines = 0;
|
|
179
|
+
});
|
|
139
180
|
}
|
|
140
181
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { EventBus } from "../event-bus.js";
|
|
2
|
-
/** Check if @xterm/headless is installed without loading it. */
|
|
3
|
-
export declare function isXtermAvailable(): boolean;
|
|
4
2
|
export interface TerminalBufferConfig {
|
|
5
3
|
/** Terminal width in columns. Default: process.stdout.columns || 80. */
|
|
6
4
|
cols?: number;
|
|
@@ -31,15 +29,14 @@ export declare class TerminalBuffer {
|
|
|
31
29
|
/** Flush pending drip-feed data (set by createWired). */
|
|
32
30
|
_flushPending: (() => void) | null;
|
|
33
31
|
private constructor();
|
|
32
|
+
static create(config?: TerminalBufferConfig): TerminalBuffer;
|
|
34
33
|
/**
|
|
35
|
-
* Create a
|
|
34
|
+
* Create a TerminalBuffer wired to a bus's `shell:pty-data` event.
|
|
35
|
+
* Drip-feeds writes asynchronously: synchronous `term.write()` in the
|
|
36
|
+
* pty-data handler changes PTY read coalescing enough to introduce
|
|
37
|
+
* visual artifacts.
|
|
36
38
|
*/
|
|
37
|
-
static
|
|
38
|
-
/**
|
|
39
|
-
* Create a TerminalBuffer and wire it to a bus's shell:pty-data event.
|
|
40
|
-
* Returns null if xterm is not installed.
|
|
41
|
-
*/
|
|
42
|
-
static createWired(bus: EventBus, config?: TerminalBufferConfig): TerminalBuffer | null;
|
|
39
|
+
static createWired(bus: EventBus, config?: TerminalBufferConfig): TerminalBuffer;
|
|
43
40
|
/** Flush any pending drip-feed data into the virtual terminal. */
|
|
44
41
|
flush(): void;
|
|
45
42
|
/** Write raw data into the virtual terminal. */
|
|
@@ -9,38 +9,19 @@
|
|
|
9
9
|
* - floating-panel.ts: composited overlay rendering + screen restore
|
|
10
10
|
* - terminal-buffer extension: agent tools (terminal_read, terminal_keys)
|
|
11
11
|
* - Any extension needing a virtual terminal snapshot
|
|
12
|
-
*
|
|
13
|
-
* The xterm dependency is loaded lazily on first use. If @xterm/headless
|
|
14
|
-
* is not installed, create() returns null.
|
|
15
|
-
*
|
|
16
|
-
* Install (optional):
|
|
17
|
-
* npm install @xterm/headless@5.5.0 @xterm/addon-serialize@0.13.0
|
|
18
12
|
*/
|
|
13
|
+
// xterm is loaded lazily on first TerminalBuffer.create(). Subcommands
|
|
14
|
+
// (init/install/list) and non-shell frontends (web bridges) import this
|
|
15
|
+
// file transitively but never instantiate a buffer; they shouldn't pay
|
|
16
|
+
// the xterm parse cost at startup.
|
|
19
17
|
import { createRequire } from "module";
|
|
20
|
-
// ── Lazy xterm loader ───────────────────────────────────────────
|
|
21
18
|
const require = createRequire(import.meta.url);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return available;
|
|
29
|
-
loadAttempted = true;
|
|
30
|
-
try {
|
|
31
|
-
TerminalCtor = require("@xterm/headless").Terminal;
|
|
32
|
-
SerializeAddonCtor = require("@xterm/addon-serialize").SerializeAddon;
|
|
33
|
-
available = true;
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
available = false;
|
|
37
|
-
}
|
|
38
|
-
return available;
|
|
39
|
-
}
|
|
40
|
-
/** Check if @xterm/headless is installed without loading it. */
|
|
41
|
-
export function isXtermAvailable() {
|
|
42
|
-
return ensureXterm();
|
|
43
|
-
}
|
|
19
|
+
// Node's require cache memoizes the first hit; subsequent calls are
|
|
20
|
+
// just a hashmap lookup, so this stays lazy without our own caching.
|
|
21
|
+
const loadXterm = () => ({
|
|
22
|
+
Terminal: require("@xterm/headless").Terminal,
|
|
23
|
+
SerializeAddon: require("@xterm/addon-serialize").SerializeAddon,
|
|
24
|
+
});
|
|
44
25
|
/**
|
|
45
26
|
* Format a screen snapshot as an XML context block for agent injection.
|
|
46
27
|
* Trims, caps to `maxLines` (from the bottom), and wraps in `<terminal_buffer>`.
|
|
@@ -71,47 +52,35 @@ export class TerminalBuffer {
|
|
|
71
52
|
this.term = term;
|
|
72
53
|
this.serializeAddon = serialize;
|
|
73
54
|
}
|
|
74
|
-
/**
|
|
75
|
-
* Create a new TerminalBuffer. Returns null if xterm is not installed.
|
|
76
|
-
*/
|
|
77
55
|
static create(config) {
|
|
78
|
-
|
|
79
|
-
return null;
|
|
56
|
+
const { Terminal, SerializeAddon } = loadXterm();
|
|
80
57
|
const cols = config?.cols ?? (process.stdout.columns || 80);
|
|
81
58
|
const rows = config?.rows ?? (process.stdout.rows || 24);
|
|
82
59
|
const scrollback = config?.scrollback ?? 200;
|
|
83
|
-
const term = new
|
|
84
|
-
const serialize = new
|
|
60
|
+
const term = new Terminal({ cols, rows, allowProposedApi: true, scrollback });
|
|
61
|
+
const serialize = new SerializeAddon();
|
|
85
62
|
term.loadAddon(serialize);
|
|
86
63
|
return new TerminalBuffer(term, serialize);
|
|
87
64
|
}
|
|
88
65
|
/**
|
|
89
|
-
* Create a TerminalBuffer
|
|
90
|
-
*
|
|
66
|
+
* Create a TerminalBuffer wired to a bus's `shell:pty-data` event.
|
|
67
|
+
* Drip-feeds writes asynchronously: synchronous `term.write()` in the
|
|
68
|
+
* pty-data handler changes PTY read coalescing enough to introduce
|
|
69
|
+
* visual artifacts.
|
|
91
70
|
*/
|
|
92
71
|
static createWired(bus, config) {
|
|
93
72
|
const tb = TerminalBuffer.create(config);
|
|
94
|
-
if (!tb)
|
|
95
|
-
return null;
|
|
96
|
-
// Buffer PTY data and drip-feed to xterm in the background.
|
|
97
|
-
// Synchronous term.write() in the pty-data handler introduces enough
|
|
98
|
-
// latency to change PTY read coalescing, causing visual artifacts.
|
|
99
73
|
let pending = "";
|
|
100
|
-
|
|
101
|
-
setInterval(() => {
|
|
102
|
-
if (pending) {
|
|
103
|
-
const d = pending;
|
|
104
|
-
pending = "";
|
|
105
|
-
tb.write(d);
|
|
106
|
-
}
|
|
107
|
-
}, 50);
|
|
108
|
-
tb._flushPending = () => {
|
|
74
|
+
const drain = () => {
|
|
109
75
|
if (pending) {
|
|
110
76
|
const d = pending;
|
|
111
77
|
pending = "";
|
|
112
78
|
tb.write(d);
|
|
113
79
|
}
|
|
114
80
|
};
|
|
81
|
+
bus.on("shell:pty-data", ({ raw }) => { pending += raw; });
|
|
82
|
+
setInterval(drain, 50);
|
|
83
|
+
tb._flushPending = drain;
|
|
115
84
|
process.stdout.on("resize", () => {
|
|
116
85
|
tb.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
117
86
|
});
|
|
@@ -171,7 +140,6 @@ export class TerminalBuffer {
|
|
|
171
140
|
const line = buf.getLine(y);
|
|
172
141
|
lines.push(line ? line.translateToString(true) : "");
|
|
173
142
|
}
|
|
174
|
-
// Trim trailing empty lines
|
|
175
143
|
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
176
144
|
lines.pop();
|
|
177
145
|
}
|
|
@@ -5,12 +5,16 @@ Runs Claude Code as an agent-sh backend using the official [@anthropic-ai/claude
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
agent-sh install claude-code-bridge
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This copies the bundled extension into `~/.agent-sh/extensions/claude-code-bridge` and runs `npm install` for you. To overwrite an existing install, pass `--force`. To uninstall, run `agent-sh uninstall claude-code-bridge`.
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
Manual alternative (e.g. for a development checkout you want to symlink):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cp -r examples/extensions/claude-code-bridge ~/.agent-sh/extensions/claude-code-bridge
|
|
17
|
+
cd ~/.agent-sh/extensions/claude-code-bridge && npm install
|
|
14
18
|
```
|
|
15
19
|
|
|
16
20
|
## Configure
|
|
@@ -34,16 +38,12 @@ Or switch at runtime:
|
|
|
34
38
|
- `ANTHROPIC_API_KEY` must be set in your environment
|
|
35
39
|
- Claude Code manages its own model selection — no model configuration needed in agent-sh
|
|
36
40
|
|
|
37
|
-
## What
|
|
38
|
-
|
|
39
|
-
A pure protocol translator between the Claude Agent SDK's event stream and agent-sh's bus events. Claude Code uses its own built-in tools exactly as the SDK ships them (`Read`, `Edit`, `Write`, `Bash`, `Glob`, `Grep`). The bridge adds no tools of its own.
|
|
41
|
+
## What works under claude-code
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
agent-sh's per-query context producers (e.g. `<shell_events>` from `shell-context`) are inlined into the prompt before each query, so claude-code sees the user's recent shell activity even though the SDK doesn't subscribe to agent-sh's shell bus directly.
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
The SDK's working directory follows agent-sh's PTY-tracked cwd, so when the user `cd`s in the terminal, claude-code's tools (Bash, Read, etc.) operate in the new directory.
|
|
44
46
|
|
|
45
|
-
|
|
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
|
|
47
|
+
## What this bridge is
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
A pure protocol translator between the Claude Agent SDK's event stream and agent-sh's bus events. Claude Code uses its own built-in tools exactly as the SDK ships them (`Read`, `Edit`, `Write`, `Bash`, `Glob`, `Grep`). The bridge adds no tools of its own.
|
|
@@ -1,25 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Claude Code bridge — runs Claude Code Agent SDK in-process as agent-sh's backend.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
13
|
-
*
|
|
14
|
-
* Setup (from repo root):
|
|
15
|
-
* npm run build && npm link # register local agent-sh globally
|
|
16
|
-
* cd examples/extensions/claude-code-bridge
|
|
17
|
-
* npm install && npm link agent-sh # link local dev copy
|
|
18
|
-
*
|
|
19
|
-
* Usage:
|
|
20
|
-
* agent-sh -e examples/extensions/claude-code-bridge
|
|
21
|
-
*
|
|
22
|
-
* Requires: Claude Code CLI installed and authenticated (claude login).
|
|
4
|
+
* Pure protocol translator between the SDK's event stream and agent-sh's bus.
|
|
5
|
+
* Requires Claude Code CLI installed and authenticated (claude login).
|
|
23
6
|
*/
|
|
24
7
|
import { query, type Query } from "@anthropic-ai/claude-agent-sdk";
|
|
25
8
|
import { readFile } from "node:fs/promises";
|
|
@@ -29,7 +12,13 @@ import { computeDiff, type DiffResult } from "agent-sh/utils/diff";
|
|
|
29
12
|
|
|
30
13
|
// ── Extension entry point ─────────────────────────────────────────
|
|
31
14
|
export default function activate(ctx: ExtensionContext): void {
|
|
32
|
-
const { bus } = ctx;
|
|
15
|
+
const { bus, call } = ctx;
|
|
16
|
+
|
|
17
|
+
// PTY-tracked cwd from shell-context; falls back when no PTY frontend.
|
|
18
|
+
const cwd = (): string => {
|
|
19
|
+
const v = call("cwd");
|
|
20
|
+
return typeof v === "string" && v ? v : process.cwd();
|
|
21
|
+
};
|
|
33
22
|
|
|
34
23
|
let activeQuery: Query | null = null;
|
|
35
24
|
const listeners: Array<{ event: string; fn: Function }> = [];
|
|
@@ -88,11 +77,16 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
88
77
|
/** Pre-edit file snapshots for diff display (Edit/Write tools). */
|
|
89
78
|
const fileSnapshots = new Map<string, string | null>();
|
|
90
79
|
|
|
80
|
+
// Splice per-query context (e.g. <shell_events>) into the prompt — the
|
|
81
|
+
// SDK has no other channel for it. Mirrors pi-bridge.
|
|
82
|
+
const ctxText = String(call("query-context:build") ?? "").trim();
|
|
83
|
+
const finalPrompt = ctxText ? `${ctxText}\n\n${userQuery}` : userQuery;
|
|
84
|
+
|
|
91
85
|
try {
|
|
92
86
|
activeQuery = query({
|
|
93
|
-
prompt:
|
|
87
|
+
prompt: finalPrompt,
|
|
94
88
|
options: {
|
|
95
|
-
cwd:
|
|
89
|
+
cwd: cwd(),
|
|
96
90
|
systemPrompt: {
|
|
97
91
|
type: "preset",
|
|
98
92
|
preset: "claude_code",
|
|
@@ -155,7 +149,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
155
149
|
|
|
156
150
|
// Snapshot file content before Edit/Write modifies it
|
|
157
151
|
if ((meta.name === "Edit" || meta.name === "Write") && typeof (input as any).file_path === "string") {
|
|
158
|
-
const absPath = resolve(
|
|
152
|
+
const absPath = resolve(cwd(), (input as any).file_path);
|
|
159
153
|
readFile(absPath, "utf-8")
|
|
160
154
|
.then(content => fileSnapshots.set(meta.id, content))
|
|
161
155
|
.catch(() => fileSnapshots.set(meta.id, null)); // file doesn't exist yet
|
|
@@ -191,7 +185,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
191
185
|
|
|
192
186
|
// Snapshot file content before Edit/Write modifies it
|
|
193
187
|
if ((b.name === "Edit" || b.name === "Write") && typeof (input as any).file_path === "string") {
|
|
194
|
-
const absPath = resolve(
|
|
188
|
+
const absPath = resolve(cwd(), (input as any).file_path);
|
|
195
189
|
readFile(absPath, "utf-8")
|
|
196
190
|
.then(content => fileSnapshots.set(b.id, content))
|
|
197
191
|
.catch(() => fileSnapshots.set(b.id, null));
|
|
@@ -226,7 +220,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
226
220
|
fileSnapshots.delete(toolUseId);
|
|
227
221
|
const filePath = (pending.input as any)?.file_path as string | undefined;
|
|
228
222
|
if (filePath) {
|
|
229
|
-
const absPath = resolve(
|
|
223
|
+
const absPath = resolve(cwd(), filePath);
|
|
230
224
|
try {
|
|
231
225
|
const newContent = await readFile(absPath, "utf-8");
|
|
232
226
|
const diff = computeDiff(oldContent, newContent);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# opencode-bridge
|
|
2
|
+
|
|
3
|
+
Runs [opencode](https://opencode.ai/) as an agent-sh backend using the official [@opencode-ai/sdk](https://www.npmjs.com/package/@opencode-ai/sdk). opencode brings its own configuration, models, tools, and authentication — agent-sh just provides the terminal.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
agent-sh install opencode-bridge
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This copies the bundled extension into `~/.agent-sh/extensions/opencode-bridge` and runs `npm install` for you. To overwrite an existing install, pass `--force`. To uninstall, run `agent-sh uninstall opencode-bridge`.
|
|
12
|
+
|
|
13
|
+
Manual alternative (e.g. for a development checkout you want to symlink):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cp -r examples/extensions/opencode-bridge ~/.agent-sh/extensions/opencode-bridge
|
|
17
|
+
cd ~/.agent-sh/extensions/opencode-bridge && npm install
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Configure
|
|
21
|
+
|
|
22
|
+
Set as default backend in `~/.agent-sh/settings.json`:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"defaultBackend": "opencode"
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or switch at runtime:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
> /backend opencode
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
opencode reads its own config from `~/.local/share/opencode/` (auth credentials) and `opencode.json` / `opencode.jsonc` in your project. Configure providers and authentication by running `opencode auth login` directly — agent-sh does not override opencode's configuration.
|
|
37
|
+
|
|
38
|
+
## Requirements
|
|
39
|
+
|
|
40
|
+
- opencode authenticated locally — run `opencode auth login` once before using this bridge.
|
|
41
|
+
- Provider env vars (e.g. `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`) as required by opencode for the model you've selected.
|
|
42
|
+
|
|
43
|
+
## What works under opencode
|
|
44
|
+
|
|
45
|
+
agent-sh's per-query context producers (e.g. `<shell_events>` from `shell-context`) are inlined into opencode's prompt before each query, so opencode sees the user's recent shell activity even though the SDK doesn't subscribe to agent-sh's shell bus directly. The current cwd is part of that context, so opencode knows where the user is even when its tools are anchored elsewhere.
|
|
46
|
+
|
|
47
|
+
## cwd handling
|
|
48
|
+
|
|
49
|
+
opencode treats the `directory` query param as a project ID and routes its event stream per-project — switching project mid-session silences the SSE channel we already subscribed to (no tool events, no streaming text). Because of that, the bridge **pins the session to the directory agent-sh launched from** and does not propagate later in-shell `cd`s to opencode. opencode's tools (`Bash`, `Read`, `Edit`, etc.) operate from that pinned directory; the agent learns the user's real cwd from `<shell_events>` and can still reach other locations through absolute paths or `cd && cmd` in `Bash`.
|
|
50
|
+
|
|
51
|
+
To re-anchor the agent to your current cwd, run `/reset` — it tears down the conversation and creates a fresh session in your present directory.
|
|
52
|
+
|
|
53
|
+
## Permission prompts
|
|
54
|
+
|
|
55
|
+
opencode supports a `permission.edit = "ask"` config in `opencode.json` that gates write/edit tools behind an approval. The bridge has no UI primitive for showing that prompt, so it **auto-approves each request once** — without this, write/edit tool calls hang forever waiting for a reply that never comes. This matches claude-code-bridge's `permissionMode: "acceptEdits"` behavior. If you want to actually gate edits, set `permission.edit` to `"allow"` (skip the prompt entirely) or run opencode standalone for the interactive flow.
|
|
56
|
+
|
|
57
|
+
## What this bridge is
|
|
58
|
+
|
|
59
|
+
A pure protocol translator between opencode's SSE event stream and agent-sh's bus events. opencode runs as an in-process HTTP server (booted by `createOpencode()`); the bridge consumes its global event stream, filters by the active session's ID, and translates `message.part.updated` events (text/reasoning deltas, `ToolPart.state` transitions) into agent-sh tool/response events. opencode's built-in tools (bash, edit, read, write, grep, glob, etc.) are used exactly as opencode ships them. The bridge adds no tools of its own.
|