agent-sh 0.1.0 → 0.3.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 +66 -576
- package/dist/acp-client.d.ts +24 -0
- package/dist/acp-client.js +168 -35
- package/dist/context-manager.d.ts +6 -4
- package/dist/context-manager.js +75 -44
- package/dist/event-bus.d.ts +29 -0
- package/dist/extension-loader.js +3 -14
- package/dist/extensions/shell-exec.d.ts +24 -0
- package/dist/extensions/shell-exec.js +188 -0
- package/dist/extensions/tui-renderer.d.ts +1 -1
- package/dist/extensions/tui-renderer.js +133 -28
- package/dist/index.js +195 -6
- package/dist/input-handler.d.ts +13 -3
- package/dist/input-handler.js +259 -127
- package/dist/mcp-server.d.ts +13 -0
- package/dist/mcp-server.js +234 -0
- package/dist/output-parser.d.ts +5 -26
- package/dist/output-parser.js +16 -78
- package/dist/settings.d.ts +33 -0
- package/dist/settings.js +43 -0
- package/dist/shell.d.ts +9 -4
- package/dist/shell.js +88 -10
- package/dist/types.d.ts +4 -0
- package/dist/utils/ansi.d.ts +4 -1
- package/dist/utils/ansi.js +60 -2
- package/dist/utils/line-editor.d.ts +59 -0
- package/dist/utils/line-editor.js +381 -0
- package/dist/utils/markdown.js +4 -4
- package/dist/utils/tool-display.d.ts +11 -0
- package/dist/utils/tool-display.js +92 -9
- package/examples/pi-agent-sh.ts +166 -0
- package/package.json +1 -1
package/dist/shell.js
CHANGED
|
@@ -10,7 +10,9 @@ export class Shell {
|
|
|
10
10
|
inputHandler;
|
|
11
11
|
outputParser;
|
|
12
12
|
paused = false;
|
|
13
|
+
echoSkip = false;
|
|
13
14
|
agentActive = false;
|
|
15
|
+
isZsh = false;
|
|
14
16
|
tmpDir;
|
|
15
17
|
constructor(opts) {
|
|
16
18
|
// Build environment — filter out undefined values (node-pty's native
|
|
@@ -38,6 +40,7 @@ export class Shell {
|
|
|
38
40
|
let shellArgs;
|
|
39
41
|
const osc7Cmd = 'printf "\\e]7;file://%s%s\\a" "$(hostname)" "$PWD"';
|
|
40
42
|
const promptMarker = 'printf "\\e]9999;PROMPT\\a"';
|
|
43
|
+
this.isZsh = isZsh;
|
|
41
44
|
if (isZsh) {
|
|
42
45
|
// For zsh: use ZDOTDIR to source user's real config, then append
|
|
43
46
|
// our hooks via precmd_functions (additive — doesn't clobber p10k/omz).
|
|
@@ -68,6 +71,14 @@ export class Shell {
|
|
|
68
71
|
" }",
|
|
69
72
|
"fi",
|
|
70
73
|
"zle -N zle-line-init __agent_sh_line_init",
|
|
74
|
+
"",
|
|
75
|
+
"# Hidden widget to trigger prompt redraw from Node.js side",
|
|
76
|
+
"# Bound to an unused escape sequence that no real key produces",
|
|
77
|
+
"__agent_sh_redraw() {",
|
|
78
|
+
" zle reset-prompt",
|
|
79
|
+
"}",
|
|
80
|
+
"zle -N __agent_sh_redraw",
|
|
81
|
+
"bindkey '\\e[9999~' __agent_sh_redraw",
|
|
71
82
|
].join("\n") + "\n");
|
|
72
83
|
env.ZDOTDIR = this.tmpDir;
|
|
73
84
|
shellArgs = ["--no-globalrcs"];
|
|
@@ -88,6 +99,18 @@ export class Shell {
|
|
|
88
99
|
].join("\n") + "\n");
|
|
89
100
|
shellArgs = ["--rcfile", path.join(this.tmpDir, ".bashrc")];
|
|
90
101
|
}
|
|
102
|
+
// Pause stdin before spawning PTY to avoid TTY contention on macOS.
|
|
103
|
+
// The PTY will become the controlling terminal for the child shell.
|
|
104
|
+
const wasRaw = process.stdin.isTTY && process.stdin.isRaw;
|
|
105
|
+
if (process.stdin.isTTY) {
|
|
106
|
+
try {
|
|
107
|
+
process.stdin.setRawMode(false);
|
|
108
|
+
process.stdin.pause();
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Ignore
|
|
112
|
+
}
|
|
113
|
+
}
|
|
91
114
|
this.ptyProcess = pty.spawn(shellBin, shellArgs, {
|
|
92
115
|
name: "xterm-256color",
|
|
93
116
|
cols: opts.cols,
|
|
@@ -95,6 +118,18 @@ export class Shell {
|
|
|
95
118
|
cwd: opts.cwd,
|
|
96
119
|
env,
|
|
97
120
|
});
|
|
121
|
+
// Restore stdin after PTY is created
|
|
122
|
+
if (process.stdin.isTTY) {
|
|
123
|
+
try {
|
|
124
|
+
process.stdin.resume();
|
|
125
|
+
if (wasRaw) {
|
|
126
|
+
process.stdin.setRawMode(true);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Ignore - will be set up later in index.ts
|
|
131
|
+
}
|
|
132
|
+
}
|
|
98
133
|
this.bus = opts.bus;
|
|
99
134
|
this.outputParser = new OutputParser(opts.bus, opts.cwd);
|
|
100
135
|
// Ensure temp dir cleanup on abnormal exit (SIGKILL won't fire this,
|
|
@@ -131,10 +166,11 @@ export class Shell {
|
|
|
131
166
|
this.ptyProcess.write(data);
|
|
132
167
|
}
|
|
133
168
|
/**
|
|
134
|
-
* Lightweight redraw:
|
|
135
|
-
* (
|
|
136
|
-
*
|
|
137
|
-
*
|
|
169
|
+
* Lightweight redraw: ask the shell to redraw its own prompt via a hidden
|
|
170
|
+
* ZLE widget (zsh) bound to \e[9999~. The shell knows how to draw its
|
|
171
|
+
* prompt correctly — we don't try to replay captured bytes.
|
|
172
|
+
*
|
|
173
|
+
* For bash, falls back to sending \n for a fresh prompt cycle.
|
|
138
174
|
*/
|
|
139
175
|
redrawPrompt() {
|
|
140
176
|
const result = this.bus.emitPipe("shell:redraw-prompt", {
|
|
@@ -142,12 +178,12 @@ export class Shell {
|
|
|
142
178
|
handled: false,
|
|
143
179
|
});
|
|
144
180
|
if (!result.handled) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
181
|
+
if (this.isZsh) {
|
|
182
|
+
// Trigger the hidden ZLE widget — zle reset-prompt redraws cleanly
|
|
183
|
+
this.ptyProcess.write("\x1b[9999~");
|
|
148
184
|
}
|
|
149
185
|
else {
|
|
150
|
-
//
|
|
186
|
+
// Bash: no zle reset-prompt equivalent, use fresh prompt cycle
|
|
151
187
|
this.ptyProcess.write("\n");
|
|
152
188
|
}
|
|
153
189
|
}
|
|
@@ -167,9 +203,20 @@ export class Shell {
|
|
|
167
203
|
setupOutput() {
|
|
168
204
|
this.ptyProcess.onData((data) => {
|
|
169
205
|
this.outputParser.processData(data);
|
|
170
|
-
if (
|
|
171
|
-
|
|
206
|
+
if (this.paused)
|
|
207
|
+
return;
|
|
208
|
+
// During user_shell exec, skip the command echo (first line)
|
|
209
|
+
if (this.echoSkip) {
|
|
210
|
+
const nlIdx = data.indexOf("\n");
|
|
211
|
+
if (nlIdx === -1)
|
|
212
|
+
return;
|
|
213
|
+
this.echoSkip = false;
|
|
214
|
+
const rest = data.slice(nlIdx + 1);
|
|
215
|
+
if (rest)
|
|
216
|
+
process.stdout.write(rest);
|
|
217
|
+
return;
|
|
172
218
|
}
|
|
219
|
+
process.stdout.write(data);
|
|
173
220
|
});
|
|
174
221
|
}
|
|
175
222
|
setupInput() {
|
|
@@ -191,6 +238,7 @@ export class Shell {
|
|
|
191
238
|
this.bus.on("agent:processing-done", () => {
|
|
192
239
|
this.paused = false;
|
|
193
240
|
this.agentActive = false;
|
|
241
|
+
this.echoSkip = true;
|
|
194
242
|
this.freshPrompt();
|
|
195
243
|
});
|
|
196
244
|
// Permission prompts need stdout unpaused so the interactive UI renders,
|
|
@@ -202,8 +250,38 @@ export class Shell {
|
|
|
202
250
|
this.paused = true;
|
|
203
251
|
return payload;
|
|
204
252
|
});
|
|
253
|
+
// Shell exec: write a command to the live PTY and capture its output.
|
|
254
|
+
// stdout is paused during agent processing, so PTY output flows through
|
|
255
|
+
// OutputParser (for OSC detection) but never reaches the terminal.
|
|
256
|
+
this.bus.onPipeAsync("shell:exec-request", async (payload) => {
|
|
257
|
+
this.echoSkip = true;
|
|
258
|
+
this.paused = false;
|
|
259
|
+
process.stdout.write("\n");
|
|
260
|
+
const output = await new Promise((resolve, reject) => {
|
|
261
|
+
const timeout = setTimeout(() => {
|
|
262
|
+
this.bus.off("shell:command-done", handler);
|
|
263
|
+
this.ptyProcess.write("\x03");
|
|
264
|
+
reject(new Error("Shell exec timed out after 30s"));
|
|
265
|
+
}, 30_000);
|
|
266
|
+
const handler = (e) => {
|
|
267
|
+
clearTimeout(timeout);
|
|
268
|
+
this.bus.off("shell:command-done", handler);
|
|
269
|
+
resolve({ output: e.output, cwd: e.cwd });
|
|
270
|
+
};
|
|
271
|
+
this.bus.on("shell:command-done", handler);
|
|
272
|
+
this.outputParser.onCommandEntered(payload.command, this.outputParser.getCwd());
|
|
273
|
+
this.ptyProcess.write(payload.command + "\r");
|
|
274
|
+
});
|
|
275
|
+
this.paused = true;
|
|
276
|
+
this.echoSkip = false;
|
|
277
|
+
return { ...payload, output: output.output, cwd: output.cwd, done: true };
|
|
278
|
+
});
|
|
205
279
|
}
|
|
206
280
|
// ── Public API (used by index.ts) ──
|
|
281
|
+
/** Temp directory used for shell config and sockets. */
|
|
282
|
+
getTmpDir() {
|
|
283
|
+
return this.tmpDir;
|
|
284
|
+
}
|
|
207
285
|
resize(cols, rows) {
|
|
208
286
|
this.ptyProcess.resize(cols, rows);
|
|
209
287
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface AgentShellConfig {
|
|
|
8
8
|
shell?: string;
|
|
9
9
|
model?: string;
|
|
10
10
|
extensions?: string[];
|
|
11
|
+
/** Full shell environment (from user's rc files) for agent subprocess. */
|
|
12
|
+
shellEnv?: Record<string, string>;
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
15
|
* Context passed to user/third-party extensions.
|
|
@@ -47,6 +49,8 @@ export type Exchange = {
|
|
|
47
49
|
exitCode: number | null;
|
|
48
50
|
outputLines: number;
|
|
49
51
|
outputBytes: number;
|
|
52
|
+
/** Who initiated this command: "user" (typed) or "agent" (via user_shell). */
|
|
53
|
+
source: "user" | "agent";
|
|
50
54
|
} | {
|
|
51
55
|
type: "agent_query";
|
|
52
56
|
id: number;
|
package/dist/utils/ansi.d.ts
CHANGED
|
@@ -6,7 +6,10 @@ export declare const RED = "\u001B[31m";
|
|
|
6
6
|
export declare const GRAY = "\u001B[90m";
|
|
7
7
|
export declare const BOLD = "\u001B[1m";
|
|
8
8
|
export declare const RESET = "\u001B[0m";
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Measure visible string length in terminal columns.
|
|
11
|
+
* Excludes SGR (color/style) sequences and accounts for CJK double-width chars.
|
|
12
|
+
*/
|
|
10
13
|
export declare function visibleLen(str: string): number;
|
|
11
14
|
/** Strip all ANSI escape sequences (SGR, OSC, CSI, private mode) and carriage returns. */
|
|
12
15
|
export declare function stripAnsi(str: string): string;
|
package/dist/utils/ansi.js
CHANGED
|
@@ -8,9 +8,67 @@ export const GRAY = "\x1b[90m";
|
|
|
8
8
|
export const BOLD = "\x1b[1m";
|
|
9
9
|
export const RESET = "\x1b[0m";
|
|
10
10
|
// ── ANSI utility functions ───────────────────────────────────
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Check if a Unicode code point is a wide character (CJK, fullwidth, emoji, etc.)
|
|
13
|
+
* Returns 2 for wide chars, 1 for normal chars.
|
|
14
|
+
*/
|
|
15
|
+
function charWidth(codePoint) {
|
|
16
|
+
// CJK Unified Ideographs
|
|
17
|
+
if (codePoint >= 0x4e00 && codePoint <= 0x9fff)
|
|
18
|
+
return 2;
|
|
19
|
+
// CJK Unified Ideographs Extension A
|
|
20
|
+
if (codePoint >= 0x3400 && codePoint <= 0x4dbf)
|
|
21
|
+
return 2;
|
|
22
|
+
// Hangul Syllables
|
|
23
|
+
if (codePoint >= 0xac00 && codePoint <= 0xd7af)
|
|
24
|
+
return 2;
|
|
25
|
+
// CJK Unified Ideographs Extension B-F and other CJK blocks
|
|
26
|
+
if (codePoint >= 0x20000 && codePoint <= 0x2ebef)
|
|
27
|
+
return 2;
|
|
28
|
+
// Fullwidth ASCII variants
|
|
29
|
+
if (codePoint >= 0xff01 && codePoint <= 0xff5e)
|
|
30
|
+
return 2;
|
|
31
|
+
// Halfwidth Katakana (actually narrow, skip)
|
|
32
|
+
// Fullwidth bracket forms
|
|
33
|
+
if (codePoint >= 0xff5f && codePoint <= 0xff60)
|
|
34
|
+
return 2;
|
|
35
|
+
// Fullwidth symbol variants
|
|
36
|
+
if (codePoint >= 0xffe0 && codePoint <= 0xffe6)
|
|
37
|
+
return 2;
|
|
38
|
+
// Japanese hiragana and katakana
|
|
39
|
+
if (codePoint >= 0x3040 && codePoint <= 0x309f)
|
|
40
|
+
return 2;
|
|
41
|
+
if (codePoint >= 0x30a0 && codePoint <= 0x30ff)
|
|
42
|
+
return 2;
|
|
43
|
+
// CJK symbols and punctuation
|
|
44
|
+
if (codePoint >= 0x3000 && codePoint <= 0x303f)
|
|
45
|
+
return 2;
|
|
46
|
+
// Enclosed CJK letters and months
|
|
47
|
+
if (codePoint >= 0x3200 && codePoint <= 0x32ff)
|
|
48
|
+
return 2;
|
|
49
|
+
// CJK compatibility
|
|
50
|
+
if (codePoint >= 0x3300 && codePoint <= 0x33ff)
|
|
51
|
+
return 2;
|
|
52
|
+
// Hangul Jamo
|
|
53
|
+
if (codePoint >= 0x1100 && codePoint <= 0x11ff)
|
|
54
|
+
return 2;
|
|
55
|
+
// Hangul compatibility Jamo
|
|
56
|
+
if (codePoint >= 0x3130 && codePoint <= 0x318f)
|
|
57
|
+
return 2;
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Measure visible string length in terminal columns.
|
|
62
|
+
* Excludes SGR (color/style) sequences and accounts for CJK double-width chars.
|
|
63
|
+
*/
|
|
12
64
|
export function visibleLen(str) {
|
|
13
|
-
|
|
65
|
+
// First strip ANSI escape sequences
|
|
66
|
+
const cleanStr = str.replace(/\x1b\[[^m]*m/g, "");
|
|
67
|
+
let width = 0;
|
|
68
|
+
for (const char of cleanStr) {
|
|
69
|
+
width += charWidth(char.codePointAt(0) ?? 0);
|
|
70
|
+
}
|
|
71
|
+
return width;
|
|
14
72
|
}
|
|
15
73
|
/** Strip all ANSI escape sequences (SGR, OSC, CSI, private mode) and carriage returns. */
|
|
16
74
|
export function stripAnsi(str) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal line editor with readline-style keybindings.
|
|
3
|
+
*
|
|
4
|
+
* Pure logic — no I/O, no rendering, no event bus. Consumers feed raw
|
|
5
|
+
* terminal input bytes and receive high-level actions back. Buffer and
|
|
6
|
+
* cursor state are public for rendering.
|
|
7
|
+
*/
|
|
8
|
+
export type LineEditAction = {
|
|
9
|
+
action: "changed";
|
|
10
|
+
} | {
|
|
11
|
+
action: "submit";
|
|
12
|
+
buffer: string;
|
|
13
|
+
} | {
|
|
14
|
+
action: "cancel";
|
|
15
|
+
} | {
|
|
16
|
+
action: "delete-empty";
|
|
17
|
+
} | {
|
|
18
|
+
action: "tab";
|
|
19
|
+
} | {
|
|
20
|
+
action: "shift+tab";
|
|
21
|
+
} | {
|
|
22
|
+
action: "arrow-up";
|
|
23
|
+
} | {
|
|
24
|
+
action: "arrow-down";
|
|
25
|
+
};
|
|
26
|
+
export declare class LineEditor {
|
|
27
|
+
buffer: string;
|
|
28
|
+
cursor: number;
|
|
29
|
+
private pendingSeq;
|
|
30
|
+
/** Process raw terminal input, return actions for the consumer. */
|
|
31
|
+
feed(data: string): LineEditAction[];
|
|
32
|
+
/** Check if there's a pending incomplete escape sequence. */
|
|
33
|
+
hasPendingEscape(): boolean;
|
|
34
|
+
/** Flush a pending sequence — treat bare \x1b as cancel, discard incomplete CSI. */
|
|
35
|
+
flushPendingEscape(): LineEditAction[];
|
|
36
|
+
clear(): void;
|
|
37
|
+
private readonly bindings;
|
|
38
|
+
/** Resolve a key name from the bindings table and execute it. */
|
|
39
|
+
private dispatch;
|
|
40
|
+
/** Map a legacy control character to a key name. */
|
|
41
|
+
private static readonly CTRL_MAP;
|
|
42
|
+
private handleControl;
|
|
43
|
+
/** Handle a kitty protocol CSI u sequence. Params format: "keycode;modifier". */
|
|
44
|
+
private handleKittyKey;
|
|
45
|
+
private insertAt;
|
|
46
|
+
private moveTo;
|
|
47
|
+
private deleteBackward;
|
|
48
|
+
private deleteForward;
|
|
49
|
+
private deleteRange;
|
|
50
|
+
/**
|
|
51
|
+
* Parse and handle a CSI sequence (\x1b[...) starting at `start`.
|
|
52
|
+
* Returns the number of bytes consumed and whether the sequence was incomplete.
|
|
53
|
+
*/
|
|
54
|
+
private handleCSI;
|
|
55
|
+
private wordBackward;
|
|
56
|
+
private wordForward;
|
|
57
|
+
private deleteWordBackward;
|
|
58
|
+
private deleteWordForward;
|
|
59
|
+
}
|