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/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
2
3
|
import { Shell } from "./shell.js";
|
|
3
4
|
import { createCore } from "./core.js";
|
|
4
5
|
import { palette as p } from "./utils/palette.js";
|
|
@@ -6,7 +7,67 @@ import tuiRenderer from "./extensions/tui-renderer.js";
|
|
|
6
7
|
import slashCommands from "./extensions/slash-commands.js";
|
|
7
8
|
import fileAutocomplete from "./extensions/file-autocomplete.js";
|
|
8
9
|
import shellRecall from "./extensions/shell-recall.js";
|
|
10
|
+
import shellExec from "./extensions/shell-exec.js";
|
|
9
11
|
import { loadExtensions } from "./extension-loader.js";
|
|
12
|
+
/**
|
|
13
|
+
* Capture the user's full shell environment asynchronously.
|
|
14
|
+
* This picks up env vars exported in .zshrc/.bashrc that the
|
|
15
|
+
* Node.js process doesn't have.
|
|
16
|
+
*
|
|
17
|
+
* Uses -l (login shell) instead of -i to avoid TTY blocking issues.
|
|
18
|
+
*/
|
|
19
|
+
async function captureShellEnvAsync(shell) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
try {
|
|
22
|
+
const child = spawn(shell, ["-l", "-c", "env -0"], {
|
|
23
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
24
|
+
timeout: 5000,
|
|
25
|
+
});
|
|
26
|
+
let output = "";
|
|
27
|
+
child.stdout?.on("data", (data) => {
|
|
28
|
+
output += data.toString("utf-8");
|
|
29
|
+
});
|
|
30
|
+
child.on("close", (code) => {
|
|
31
|
+
if (code !== 0 || !output) {
|
|
32
|
+
resolve({}); // Return empty to trigger fallback
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const env = {};
|
|
36
|
+
for (const entry of output.split("\0")) {
|
|
37
|
+
const eq = entry.indexOf("=");
|
|
38
|
+
if (eq > 0)
|
|
39
|
+
env[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
40
|
+
}
|
|
41
|
+
resolve(env);
|
|
42
|
+
});
|
|
43
|
+
child.on("error", () => {
|
|
44
|
+
resolve({}); // Return empty to trigger fallback
|
|
45
|
+
});
|
|
46
|
+
// Safety timeout
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
child.kill("SIGTERM");
|
|
49
|
+
resolve({});
|
|
50
|
+
}, 5000);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
resolve({});
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Merge captured shell env into base env, only adding keys that don't exist.
|
|
59
|
+
* This preserves any runtime modifications while adding missing shell vars.
|
|
60
|
+
*/
|
|
61
|
+
function mergeShellEnv(baseEnv, shellEnv) {
|
|
62
|
+
const merged = { ...baseEnv };
|
|
63
|
+
for (const [key, value] of Object.entries(shellEnv)) {
|
|
64
|
+
// Only add if key doesn't exist or is empty in base env
|
|
65
|
+
if (!(key in merged) || !merged[key]) {
|
|
66
|
+
merged[key] = value;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return merged;
|
|
70
|
+
}
|
|
10
71
|
function parseArgs(argv) {
|
|
11
72
|
// Priority: CLI args > Environment variables > Config file > Defaults
|
|
12
73
|
const defaultAgent = process.env.AGENT_SH_AGENT || "pi-acp";
|
|
@@ -78,7 +139,7 @@ Inside the shell:
|
|
|
78
139
|
}
|
|
79
140
|
return { agentCommand, agentArgs, shell, model, extensions };
|
|
80
141
|
}
|
|
81
|
-
function formatAgentInfo(agentInfo, model) {
|
|
142
|
+
function formatAgentInfo(agentInfo, model, thoughtLevel) {
|
|
82
143
|
const name = agentInfo.name.replace(/-acp$/, "").replace(/-/g, " ");
|
|
83
144
|
let infoStr = `${p.dim}${name}${p.reset}`;
|
|
84
145
|
if (model) {
|
|
@@ -88,14 +149,51 @@ function formatAgentInfo(agentInfo, model) {
|
|
|
88
149
|
.replace(/^google\//i, "");
|
|
89
150
|
infoStr += ` ${p.dim}(${cleanModel})${p.reset}`;
|
|
90
151
|
}
|
|
152
|
+
if (thoughtLevel) {
|
|
153
|
+
// Clean up verbose mode names like "Thinking: medium" → "medium"
|
|
154
|
+
const label = thoughtLevel.replace(/^Thinking:\s*/i, "");
|
|
155
|
+
infoStr += ` ${p.dim}[${label}]${p.reset}`;
|
|
156
|
+
}
|
|
91
157
|
return `${infoStr} ${p.success}●${p.reset}`;
|
|
92
158
|
}
|
|
93
159
|
async function main() {
|
|
160
|
+
// Set up signal handlers before any terminal operations.
|
|
161
|
+
// Ignore SIGTTOU to prevent suspension when modifying terminal settings.
|
|
162
|
+
process.on("SIGTTOU", () => { });
|
|
163
|
+
// Also ignore SIGTTIN which can occur when reading from terminal while backgrounded.
|
|
164
|
+
process.on("SIGTTIN", () => { });
|
|
94
165
|
const config = parseArgs(process.argv.slice(2));
|
|
166
|
+
// Start with current process environment (fast, non-blocking)
|
|
167
|
+
// We'll enrich it with shell env asynchronously in the background
|
|
168
|
+
const baseEnv = {};
|
|
169
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
170
|
+
if (v !== undefined)
|
|
171
|
+
baseEnv[k] = v;
|
|
172
|
+
}
|
|
173
|
+
config.shellEnv = baseEnv;
|
|
174
|
+
// Asynchronously capture full shell environment without blocking startup
|
|
175
|
+
const shellPath = config.shell || process.env.SHELL || "/bin/bash";
|
|
176
|
+
captureShellEnvAsync(shellPath).then((shellEnv) => {
|
|
177
|
+
if (Object.keys(shellEnv).length > 0) {
|
|
178
|
+
const merged = mergeShellEnv(config.shellEnv, shellEnv);
|
|
179
|
+
config.shellEnv = merged;
|
|
180
|
+
if (process.env.DEBUG) {
|
|
181
|
+
console.error('[agent-sh] Shell environment enriched asynchronously');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}).catch(() => {
|
|
185
|
+
// Ignore errors, we already have process.env as fallback
|
|
186
|
+
});
|
|
187
|
+
if (process.env.DEBUG) {
|
|
188
|
+
console.error('[agent-sh] Using current process environment (async enrichment pending)');
|
|
189
|
+
}
|
|
95
190
|
// ── Core (frontend-agnostic) ──────────────────────────────────
|
|
96
191
|
const core = createCore(config);
|
|
97
192
|
const { bus, client } = core;
|
|
98
193
|
// ── Interactive frontend ──────────────────────────────────────
|
|
194
|
+
if (process.env.DEBUG) {
|
|
195
|
+
console.error('[agent-sh] Setting up interactive frontend...');
|
|
196
|
+
}
|
|
99
197
|
process.stdout.write(`\x1b]0;agent-sh\x07`);
|
|
100
198
|
const cols = process.stdout.columns || 80;
|
|
101
199
|
const rows = process.stdout.rows || 24;
|
|
@@ -107,6 +205,12 @@ async function main() {
|
|
|
107
205
|
}
|
|
108
206
|
process.exit(0);
|
|
109
207
|
};
|
|
208
|
+
if (process.env.DEBUG) {
|
|
209
|
+
console.error('[agent-sh] Creating Shell...');
|
|
210
|
+
}
|
|
211
|
+
// Small delay on macOS to ensure we're fully in the foreground process group
|
|
212
|
+
// before spawning the PTY. This prevents SIGTTOU suspension.
|
|
213
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
110
214
|
const shell = new Shell({
|
|
111
215
|
bus,
|
|
112
216
|
cols,
|
|
@@ -118,26 +222,86 @@ async function main() {
|
|
|
118
222
|
const agentInfo = client.getAgentInfo();
|
|
119
223
|
const model = client.getModel();
|
|
120
224
|
if (agentInfo) {
|
|
121
|
-
|
|
225
|
+
const mode = client.getCurrentMode();
|
|
226
|
+
return { info: formatAgentInfo(agentInfo, model, mode?.name ?? null) };
|
|
122
227
|
}
|
|
123
228
|
}
|
|
124
229
|
return { info: "" };
|
|
125
230
|
},
|
|
126
231
|
});
|
|
232
|
+
if (process.env.DEBUG) {
|
|
233
|
+
console.error('[agent-sh] Shell created');
|
|
234
|
+
}
|
|
127
235
|
// ── Extensions ────────────────────────────────────────────────
|
|
236
|
+
if (process.env.DEBUG) {
|
|
237
|
+
console.error('[agent-sh] Setting up extensions...');
|
|
238
|
+
}
|
|
128
239
|
const extCtx = core.extensionContext({ quit: cleanup });
|
|
129
240
|
tuiRenderer(extCtx);
|
|
130
241
|
slashCommands(extCtx);
|
|
131
242
|
fileAutocomplete(extCtx);
|
|
132
243
|
shellRecall(extCtx);
|
|
133
|
-
|
|
244
|
+
// Shell-exec: start the Unix domain socket bridge so agent extensions
|
|
245
|
+
// and MCP servers can route tool calls to the PTY via the EventBus.
|
|
246
|
+
const tmpDir = shell.getTmpDir();
|
|
247
|
+
if (tmpDir) {
|
|
248
|
+
if (process.env.DEBUG) {
|
|
249
|
+
console.error('[agent-sh] Starting shell-exec socket server...');
|
|
250
|
+
}
|
|
251
|
+
shellExec(extCtx, { socketPath: `${tmpDir}/shell.sock` });
|
|
252
|
+
}
|
|
253
|
+
// Load extensions with timeout to prevent blocking startup
|
|
254
|
+
if (process.env.DEBUG) {
|
|
255
|
+
console.error('[agent-sh] Loading extensions...');
|
|
256
|
+
}
|
|
257
|
+
const loadExtensionsTimeoutMs = 10000; // 10 seconds
|
|
258
|
+
await Promise.race([
|
|
259
|
+
loadExtensions(extCtx, config.extensions),
|
|
260
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Extension loading timeout after ${loadExtensionsTimeoutMs}ms`)), loadExtensionsTimeoutMs)),
|
|
261
|
+
]).catch((err) => {
|
|
262
|
+
console.error(`Warning: ${err.message}`);
|
|
263
|
+
});
|
|
264
|
+
if (process.env.DEBUG) {
|
|
265
|
+
console.error('[agent-sh] Extensions loaded');
|
|
266
|
+
}
|
|
134
267
|
// ── Agent connection (async — don't block shell startup) ──────
|
|
135
|
-
|
|
268
|
+
const agentStartTimeoutMs = 35000; // 35 seconds (slightly longer than internal timeouts)
|
|
269
|
+
Promise.race([
|
|
270
|
+
core.start(),
|
|
271
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Agent connection timeout`)), agentStartTimeoutMs)),
|
|
272
|
+
]).catch((err) => {
|
|
136
273
|
console.error(`Failed to connect to ${config.agentCommand}:`, err);
|
|
137
274
|
});
|
|
138
275
|
// ── Terminal lifecycle ────────────────────────────────────────
|
|
139
276
|
process.on("SIGTERM", cleanup);
|
|
140
277
|
process.on("SIGHUP", cleanup);
|
|
278
|
+
// Handle terminal stop/resume signals properly
|
|
279
|
+
process.on("SIGTSTP", () => {
|
|
280
|
+
// Handle Ctrl+Z - suspend the entire process group
|
|
281
|
+
// Restore terminal state before suspending
|
|
282
|
+
if (process.stdin.isTTY) {
|
|
283
|
+
try {
|
|
284
|
+
process.stdin.setRawMode(false);
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// Ignore
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Re-send SIGSTOP to actually suspend
|
|
291
|
+
process.kill(process.pid, "SIGSTOP");
|
|
292
|
+
});
|
|
293
|
+
process.on("SIGCONT", () => {
|
|
294
|
+
// Re-acquire terminal when brought back to foreground
|
|
295
|
+
if (process.stdin.isTTY) {
|
|
296
|
+
try {
|
|
297
|
+
// Ensure we reacquire controlling terminal
|
|
298
|
+
process.stdin.setRawMode(true);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// May fail if stdin is not a TTY
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
});
|
|
141
305
|
process.stdout.on("resize", () => {
|
|
142
306
|
shell.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
143
307
|
});
|
|
@@ -148,10 +312,35 @@ async function main() {
|
|
|
148
312
|
}
|
|
149
313
|
process.exit(e.exitCode);
|
|
150
314
|
});
|
|
151
|
-
|
|
152
|
-
|
|
315
|
+
// Set up stdin - resume after all event listeners are in place
|
|
316
|
+
if (process.env.DEBUG) {
|
|
317
|
+
console.error('[agent-sh] Resuming stdin...');
|
|
153
318
|
}
|
|
154
319
|
process.stdin.resume();
|
|
320
|
+
// Set raw mode after resume to avoid SIGTTOU issues
|
|
321
|
+
if (process.stdin.isTTY) {
|
|
322
|
+
if (process.env.DEBUG) {
|
|
323
|
+
console.error('[agent-sh] Setting raw mode...');
|
|
324
|
+
}
|
|
325
|
+
// Use setImmediate to ensure we're in the next tick
|
|
326
|
+
setImmediate(() => {
|
|
327
|
+
try {
|
|
328
|
+
process.stdin.setRawMode(true);
|
|
329
|
+
if (process.env.DEBUG) {
|
|
330
|
+
console.error('[agent-sh] Raw mode enabled');
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
if (process.env.DEBUG) {
|
|
335
|
+
console.error(`[agent-sh] Failed to set raw mode: ${err}`);
|
|
336
|
+
}
|
|
337
|
+
// May fail if process is in background; SIGTTOU handler prevents suspension
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (process.env.DEBUG) {
|
|
342
|
+
console.error('[agent-sh] Startup complete');
|
|
343
|
+
}
|
|
155
344
|
}
|
|
156
345
|
main().catch((err) => {
|
|
157
346
|
console.error("Fatal:", err);
|
package/dist/input-handler.d.ts
CHANGED
|
@@ -17,11 +17,16 @@ export declare class InputHandler {
|
|
|
17
17
|
private ctx;
|
|
18
18
|
private lineBuffer;
|
|
19
19
|
private agentInputMode;
|
|
20
|
-
private
|
|
20
|
+
private editor;
|
|
21
21
|
private autocompleteActive;
|
|
22
22
|
private autocompleteIndex;
|
|
23
23
|
private autocompleteItems;
|
|
24
24
|
private autocompleteLines;
|
|
25
|
+
private history;
|
|
26
|
+
private historyIndex;
|
|
27
|
+
private savedBuffer;
|
|
28
|
+
private promptWrappedLines;
|
|
29
|
+
private escapeTimer;
|
|
25
30
|
private bus;
|
|
26
31
|
private onShowAgentInfo;
|
|
27
32
|
constructor(opts: {
|
|
@@ -32,17 +37,22 @@ export declare class InputHandler {
|
|
|
32
37
|
model?: string;
|
|
33
38
|
};
|
|
34
39
|
});
|
|
35
|
-
|
|
40
|
+
private loadHistory;
|
|
41
|
+
private saveHistory;
|
|
42
|
+
/** Write the agent prompt line with cursor at the correct position. */
|
|
36
43
|
private writeAgentPromptLine;
|
|
37
44
|
handleInput(data: string): void;
|
|
38
45
|
private enterAgentInputMode;
|
|
39
46
|
private exitAgentInputMode;
|
|
47
|
+
/** Move to the start of the prompt area and clear everything below. */
|
|
48
|
+
private clearPromptArea;
|
|
40
49
|
printPrompt(): void;
|
|
41
50
|
private renderAgentInput;
|
|
42
51
|
private updateAutocomplete;
|
|
43
52
|
private renderAutocomplete;
|
|
44
|
-
private clearAutocompleteLines;
|
|
45
53
|
private applyAutocomplete;
|
|
46
54
|
private dismissAutocomplete;
|
|
55
|
+
private clearAutocompleteLines;
|
|
47
56
|
private handleAgentInput;
|
|
57
|
+
private processAgentActions;
|
|
48
58
|
}
|