agent-sh 0.1.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 +659 -0
- package/dist/acp-client.d.ts +76 -0
- package/dist/acp-client.js +507 -0
- package/dist/context-manager.d.ts +45 -0
- package/dist/context-manager.js +405 -0
- package/dist/core.d.ts +41 -0
- package/dist/core.js +76 -0
- package/dist/event-bus.d.ts +140 -0
- package/dist/event-bus.js +79 -0
- package/dist/executor.d.ts +31 -0
- package/dist/executor.js +116 -0
- package/dist/extension-loader.d.ts +16 -0
- package/dist/extension-loader.js +164 -0
- package/dist/extensions/file-autocomplete.d.ts +2 -0
- package/dist/extensions/file-autocomplete.js +63 -0
- package/dist/extensions/shell-recall.d.ts +9 -0
- package/dist/extensions/shell-recall.js +8 -0
- package/dist/extensions/slash-commands.d.ts +2 -0
- package/dist/extensions/slash-commands.js +105 -0
- package/dist/extensions/tui-renderer.d.ts +2 -0
- package/dist/extensions/tui-renderer.js +354 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +159 -0
- package/dist/input-handler.d.ts +48 -0
- package/dist/input-handler.js +302 -0
- package/dist/output-parser.d.ts +55 -0
- package/dist/output-parser.js +166 -0
- package/dist/shell.d.ts +54 -0
- package/dist/shell.js +219 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.js +1 -0
- package/dist/utils/ansi.d.ts +12 -0
- package/dist/utils/ansi.js +23 -0
- package/dist/utils/box-frame.d.ts +21 -0
- package/dist/utils/box-frame.js +60 -0
- package/dist/utils/diff-renderer.d.ts +20 -0
- package/dist/utils/diff-renderer.js +506 -0
- package/dist/utils/diff.d.ts +24 -0
- package/dist/utils/diff.js +122 -0
- package/dist/utils/file-watcher.d.ts +31 -0
- package/dist/utils/file-watcher.js +101 -0
- package/dist/utils/markdown.d.ts +39 -0
- package/dist/utils/markdown.js +248 -0
- package/dist/utils/palette.d.ts +32 -0
- package/dist/utils/palette.js +36 -0
- package/dist/utils/tool-display.d.ts +33 -0
- package/dist/utils/tool-display.js +141 -0
- package/examples/extensions/interactive-prompts.ts +161 -0
- package/examples/extensions/solarized-theme.ts +27 -0
- package/package.json +72 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI renderer extension.
|
|
3
|
+
*
|
|
4
|
+
* Subscribes to EventBus events and renders agent output to the terminal:
|
|
5
|
+
* bordered markdown responses, spinner, tool call display, streaming
|
|
6
|
+
* command output, error/info messages.
|
|
7
|
+
*
|
|
8
|
+
* Without this extension loaded, agent-sh runs headlessly — PTY
|
|
9
|
+
* passthrough, agent queries, tool execution all function; output is
|
|
10
|
+
* silently dropped. Alternative renderers (web UI, logging, minimal)
|
|
11
|
+
* can subscribe to the same events.
|
|
12
|
+
*/
|
|
13
|
+
import { MarkdownRenderer } from "../utils/markdown.js";
|
|
14
|
+
import { palette as p } from "../utils/palette.js";
|
|
15
|
+
import { renderToolCall, renderToolResult, startSpinner, stopSpinner as stopToolSpinner, } from "../utils/tool-display.js";
|
|
16
|
+
import { renderDiff } from "../utils/diff-renderer.js";
|
|
17
|
+
import { renderBoxFrame } from "../utils/box-frame.js";
|
|
18
|
+
const MAX_COMMAND_OUTPUT_LINES = 30;
|
|
19
|
+
export default function activate({ bus }) {
|
|
20
|
+
let spinner = null;
|
|
21
|
+
let renderer = null;
|
|
22
|
+
let commandOutputBuffer = "";
|
|
23
|
+
let commandOutputLineCount = 0;
|
|
24
|
+
let commandOutputOverflow = 0;
|
|
25
|
+
let lastCommand = "";
|
|
26
|
+
let isThinking = false;
|
|
27
|
+
let showThinkingText = false;
|
|
28
|
+
let lastTruncatedDiff = null;
|
|
29
|
+
// ── Event subscriptions ─────────────────────────────────────
|
|
30
|
+
bus.on("agent:query", (e) => {
|
|
31
|
+
showUserQuery(e.query);
|
|
32
|
+
startAgentResponse();
|
|
33
|
+
startThinkingSpinner();
|
|
34
|
+
});
|
|
35
|
+
bus.on("agent:thinking-chunk", (e) => {
|
|
36
|
+
if (!isThinking) {
|
|
37
|
+
isThinking = true;
|
|
38
|
+
stopCurrentSpinner();
|
|
39
|
+
if (showThinkingText) {
|
|
40
|
+
if (!renderer)
|
|
41
|
+
startAgentResponse();
|
|
42
|
+
renderer.writeLine(`${p.dim}${p.bold}💭 Thinking${p.reset}`);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
startThinkingSpinner("Thinking");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (showThinkingText && e.text) {
|
|
49
|
+
if (!renderer)
|
|
50
|
+
startAgentResponse();
|
|
51
|
+
renderer.push(`${p.dim}${e.text}${p.reset}`);
|
|
52
|
+
flushOutput();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
bus.on("agent:response-chunk", (e) => writeAgentText(e.text));
|
|
56
|
+
bus.on("agent:response-done", () => {
|
|
57
|
+
isThinking = false;
|
|
58
|
+
endAgentResponse();
|
|
59
|
+
});
|
|
60
|
+
bus.on("agent:tool-call", (e) => {
|
|
61
|
+
lastCommand = e.tool;
|
|
62
|
+
});
|
|
63
|
+
bus.on("agent:tool-started", (e) => {
|
|
64
|
+
stopCurrentSpinner();
|
|
65
|
+
showToolCall(e.title, lastCommand);
|
|
66
|
+
lastCommand = "";
|
|
67
|
+
});
|
|
68
|
+
bus.on("agent:tool-completed", (e) => showToolComplete(e.exitCode));
|
|
69
|
+
bus.on("agent:tool-output-chunk", (e) => writeCommandOutput(e.chunk));
|
|
70
|
+
bus.on("agent:tool-output", () => flushCommandOutput());
|
|
71
|
+
bus.on("agent:cancelled", () => {
|
|
72
|
+
isThinking = false;
|
|
73
|
+
stopCurrentSpinner();
|
|
74
|
+
showInfo("(cancelled)");
|
|
75
|
+
endAgentResponse();
|
|
76
|
+
});
|
|
77
|
+
bus.on("agent:error", (e) => showError(e.message));
|
|
78
|
+
// Flush rendering state and show inline diff for file writes
|
|
79
|
+
bus.on("permission:request", (e) => {
|
|
80
|
+
stopCurrentSpinner();
|
|
81
|
+
flushCommandOutput();
|
|
82
|
+
renderer?.flush();
|
|
83
|
+
if (e.kind === "file-write" && e.metadata?.diff) {
|
|
84
|
+
showFileDiff(e.title, e.metadata.diff);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// Non-file permission (e.g. tool-call) — end response box
|
|
88
|
+
// so interactive extensions can render their own UI
|
|
89
|
+
endAgentResponse();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
bus.on("input:keypress", (e) => {
|
|
93
|
+
if (e.key === "\x0f")
|
|
94
|
+
expandLastDiff(); // Ctrl+O
|
|
95
|
+
if (e.key === "\x14")
|
|
96
|
+
toggleThinkingDisplay(); // Ctrl+T
|
|
97
|
+
});
|
|
98
|
+
bus.on("ui:info", (e) => showInfo(e.message));
|
|
99
|
+
bus.on("ui:error", (e) => showError(e.message));
|
|
100
|
+
// ── Rendering functions ─────────────────────────────────────
|
|
101
|
+
function flushOutput() {
|
|
102
|
+
if (process.stdout.writable) {
|
|
103
|
+
try {
|
|
104
|
+
process.stdout.write("");
|
|
105
|
+
}
|
|
106
|
+
catch { }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function startAgentResponse() {
|
|
110
|
+
renderer = new MarkdownRenderer();
|
|
111
|
+
process.stdout.write("\n");
|
|
112
|
+
renderer.printTopBorder();
|
|
113
|
+
}
|
|
114
|
+
function endAgentResponse() {
|
|
115
|
+
if (renderer) {
|
|
116
|
+
renderer.flush();
|
|
117
|
+
renderer.printBottomBorder();
|
|
118
|
+
renderer = null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function showUserQuery(query) {
|
|
122
|
+
const termW = process.stdout.columns || 80;
|
|
123
|
+
const boxW = Math.min(84, termW);
|
|
124
|
+
const contentW = boxW - 4; // inside box padding
|
|
125
|
+
// Wrap long queries to fit within box
|
|
126
|
+
const lines = [];
|
|
127
|
+
for (const raw of query.split("\n")) {
|
|
128
|
+
if (raw.length <= contentW) {
|
|
129
|
+
lines.push(`${p.accent}${raw}${p.reset}`);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Simple word wrap
|
|
133
|
+
let remaining = raw;
|
|
134
|
+
while (remaining.length > contentW) {
|
|
135
|
+
let breakAt = remaining.lastIndexOf(" ", contentW);
|
|
136
|
+
if (breakAt <= 0)
|
|
137
|
+
breakAt = contentW;
|
|
138
|
+
lines.push(`${p.accent}${remaining.slice(0, breakAt)}${p.reset}`);
|
|
139
|
+
remaining = remaining.slice(breakAt).trimStart();
|
|
140
|
+
}
|
|
141
|
+
if (remaining)
|
|
142
|
+
lines.push(`${p.accent}${remaining}${p.reset}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const framed = renderBoxFrame(lines, {
|
|
146
|
+
width: boxW,
|
|
147
|
+
style: "rounded",
|
|
148
|
+
borderColor: p.accent,
|
|
149
|
+
title: `${p.accent}${p.bold}❯${p.reset}`,
|
|
150
|
+
});
|
|
151
|
+
process.stdout.write("\n");
|
|
152
|
+
for (const line of framed) {
|
|
153
|
+
process.stdout.write(line + "\n");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function writeAgentText(text) {
|
|
157
|
+
if (isThinking) {
|
|
158
|
+
isThinking = false;
|
|
159
|
+
if (showThinkingText && renderer) {
|
|
160
|
+
renderer.flush();
|
|
161
|
+
const termW = process.stdout.columns || 80;
|
|
162
|
+
const w = Math.min(80, termW);
|
|
163
|
+
renderer.writeLine(`${p.dim}${"─".repeat(w)}${p.reset}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
stopCurrentSpinner();
|
|
167
|
+
if (!renderer)
|
|
168
|
+
startAgentResponse();
|
|
169
|
+
renderer.push(text);
|
|
170
|
+
flushOutput();
|
|
171
|
+
}
|
|
172
|
+
function showToolCall(title, command) {
|
|
173
|
+
stopCurrentSpinner();
|
|
174
|
+
if (!renderer)
|
|
175
|
+
startAgentResponse();
|
|
176
|
+
renderer.flush();
|
|
177
|
+
const termW = process.stdout.columns || 80;
|
|
178
|
+
const lines = renderToolCall({ title, command: command || undefined }, termW);
|
|
179
|
+
for (const line of lines) {
|
|
180
|
+
renderer.writeLine(line);
|
|
181
|
+
}
|
|
182
|
+
// Reset output tracking for the new tool
|
|
183
|
+
commandOutputLineCount = 0;
|
|
184
|
+
commandOutputOverflow = 0;
|
|
185
|
+
}
|
|
186
|
+
function showToolComplete(exitCode) {
|
|
187
|
+
if (!renderer)
|
|
188
|
+
return;
|
|
189
|
+
const termW = process.stdout.columns || 80;
|
|
190
|
+
const lines = renderToolResult({ exitCode }, termW);
|
|
191
|
+
for (const line of lines) {
|
|
192
|
+
renderer.writeLine(line);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function startThinkingSpinner(label = "Thinking") {
|
|
196
|
+
stopCurrentSpinner();
|
|
197
|
+
spinner = startSpinner(label);
|
|
198
|
+
}
|
|
199
|
+
function stopCurrentSpinner() {
|
|
200
|
+
if (spinner) {
|
|
201
|
+
stopToolSpinner(spinner);
|
|
202
|
+
spinner = null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function writeCommandOutput(chunk) {
|
|
206
|
+
if (!renderer)
|
|
207
|
+
return;
|
|
208
|
+
commandOutputBuffer += chunk;
|
|
209
|
+
const lines = commandOutputBuffer.split("\n");
|
|
210
|
+
commandOutputBuffer = lines.pop();
|
|
211
|
+
for (const line of lines) {
|
|
212
|
+
if (commandOutputLineCount < MAX_COMMAND_OUTPUT_LINES) {
|
|
213
|
+
renderer.writeLine(`${p.dim} ${line}${p.reset}`);
|
|
214
|
+
commandOutputLineCount++;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
commandOutputOverflow++;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function flushCommandOutput() {
|
|
222
|
+
if (!renderer)
|
|
223
|
+
return;
|
|
224
|
+
if (commandOutputBuffer) {
|
|
225
|
+
if (commandOutputLineCount < MAX_COMMAND_OUTPUT_LINES) {
|
|
226
|
+
renderer.writeLine(`${p.dim} ${commandOutputBuffer}${p.reset}`);
|
|
227
|
+
commandOutputLineCount++;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
commandOutputOverflow++;
|
|
231
|
+
}
|
|
232
|
+
commandOutputBuffer = "";
|
|
233
|
+
}
|
|
234
|
+
if (commandOutputOverflow > 0) {
|
|
235
|
+
renderer.writeLine(`${p.dim} … ${commandOutputOverflow} more lines${p.reset}`);
|
|
236
|
+
commandOutputOverflow = 0;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const DIFF_MAX_LINES = 20;
|
|
240
|
+
function diffTitle(filePath, diff) {
|
|
241
|
+
const stats = diff.isNewFile
|
|
242
|
+
? `${p.success}+${diff.added}${p.reset}`
|
|
243
|
+
: `${p.success}+${diff.added}${p.reset} ${p.error}-${diff.removed}${p.reset}`;
|
|
244
|
+
return `${p.dim}${filePath}${p.reset} ${stats}`;
|
|
245
|
+
}
|
|
246
|
+
function showFileDiff(filePath, diff) {
|
|
247
|
+
if (diff.isIdentical)
|
|
248
|
+
return;
|
|
249
|
+
const termW = process.stdout.columns || 80;
|
|
250
|
+
const boxW = Math.min(84, termW);
|
|
251
|
+
const contentW = boxW - 4;
|
|
252
|
+
const diffLines = renderDiff(diff, {
|
|
253
|
+
width: contentW,
|
|
254
|
+
filePath,
|
|
255
|
+
maxLines: DIFF_MAX_LINES,
|
|
256
|
+
trueColor: true,
|
|
257
|
+
mode: "unified",
|
|
258
|
+
});
|
|
259
|
+
const lastLine = diffLines[diffLines.length - 1] ?? "";
|
|
260
|
+
const isTruncated = lastLine.includes("… ");
|
|
261
|
+
if (isTruncated) {
|
|
262
|
+
lastTruncatedDiff = { filePath, diff, expanded: false };
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
lastTruncatedDiff = null;
|
|
266
|
+
}
|
|
267
|
+
const body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
|
|
268
|
+
const footer = isTruncated
|
|
269
|
+
? [` ${p.dim}ctrl+o to expand${p.reset}`]
|
|
270
|
+
: undefined;
|
|
271
|
+
const framed = renderBoxFrame(body, {
|
|
272
|
+
width: boxW,
|
|
273
|
+
style: "rounded",
|
|
274
|
+
borderColor: p.dim,
|
|
275
|
+
title: diffTitle(filePath, diff),
|
|
276
|
+
footer,
|
|
277
|
+
});
|
|
278
|
+
if (!renderer)
|
|
279
|
+
startAgentResponse();
|
|
280
|
+
for (const line of framed) {
|
|
281
|
+
renderer.writeLine(line);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function expandLastDiff() {
|
|
285
|
+
if (!lastTruncatedDiff)
|
|
286
|
+
return;
|
|
287
|
+
const entry = lastTruncatedDiff;
|
|
288
|
+
entry.expanded = !entry.expanded;
|
|
289
|
+
if (!entry.expanded) {
|
|
290
|
+
showFileDiffCached(entry);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (!entry.expandedLines) {
|
|
294
|
+
const { filePath, diff } = entry;
|
|
295
|
+
const termW = process.stdout.columns || 80;
|
|
296
|
+
const boxW = Math.min(120, termW);
|
|
297
|
+
const contentW = boxW - 4;
|
|
298
|
+
const diffLines = renderDiff(diff, {
|
|
299
|
+
width: contentW,
|
|
300
|
+
filePath,
|
|
301
|
+
maxLines: 500,
|
|
302
|
+
trueColor: true,
|
|
303
|
+
});
|
|
304
|
+
const body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
|
|
305
|
+
entry.expandedLines = renderBoxFrame(body, {
|
|
306
|
+
width: boxW,
|
|
307
|
+
style: "rounded",
|
|
308
|
+
borderColor: p.dim,
|
|
309
|
+
title: diffTitle(filePath, diff),
|
|
310
|
+
footer: [` ${p.dim}ctrl+o to collapse${p.reset}`],
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
process.stdout.write("\n");
|
|
314
|
+
for (const line of entry.expandedLines) {
|
|
315
|
+
process.stdout.write(line + "\n");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function showFileDiffCached(entry) {
|
|
319
|
+
const { filePath, diff } = entry;
|
|
320
|
+
const termW = process.stdout.columns || 80;
|
|
321
|
+
const boxW = Math.min(84, termW);
|
|
322
|
+
const contentW = boxW - 4;
|
|
323
|
+
const diffLines = renderDiff(diff, {
|
|
324
|
+
width: contentW,
|
|
325
|
+
filePath,
|
|
326
|
+
maxLines: DIFF_MAX_LINES,
|
|
327
|
+
trueColor: true,
|
|
328
|
+
mode: "unified",
|
|
329
|
+
});
|
|
330
|
+
const body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
|
|
331
|
+
const framed = renderBoxFrame(body, {
|
|
332
|
+
width: boxW,
|
|
333
|
+
style: "rounded",
|
|
334
|
+
borderColor: p.dim,
|
|
335
|
+
title: diffTitle(filePath, diff),
|
|
336
|
+
footer: [` ${p.dim}ctrl+o to expand${p.reset}`],
|
|
337
|
+
});
|
|
338
|
+
process.stdout.write("\n");
|
|
339
|
+
for (const line of framed) {
|
|
340
|
+
process.stdout.write(line + "\n");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function toggleThinkingDisplay() {
|
|
344
|
+
showThinkingText = !showThinkingText;
|
|
345
|
+
const state = showThinkingText ? "on" : "off";
|
|
346
|
+
process.stdout.write(`\n${p.dim}Thinking display: ${state}${p.reset}\n`);
|
|
347
|
+
}
|
|
348
|
+
function showError(message) {
|
|
349
|
+
process.stdout.write(`\n${p.error}Error: ${message}${p.reset}\n`);
|
|
350
|
+
}
|
|
351
|
+
function showInfo(message) {
|
|
352
|
+
process.stdout.write(`${p.muted}${message}${p.reset}\n`);
|
|
353
|
+
}
|
|
354
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Shell } from "./shell.js";
|
|
3
|
+
import { createCore } from "./core.js";
|
|
4
|
+
import { palette as p } from "./utils/palette.js";
|
|
5
|
+
import tuiRenderer from "./extensions/tui-renderer.js";
|
|
6
|
+
import slashCommands from "./extensions/slash-commands.js";
|
|
7
|
+
import fileAutocomplete from "./extensions/file-autocomplete.js";
|
|
8
|
+
import shellRecall from "./extensions/shell-recall.js";
|
|
9
|
+
import { loadExtensions } from "./extension-loader.js";
|
|
10
|
+
function parseArgs(argv) {
|
|
11
|
+
// Priority: CLI args > Environment variables > Config file > Defaults
|
|
12
|
+
const defaultAgent = process.env.AGENT_SH_AGENT || "pi-acp";
|
|
13
|
+
let agentCommand = defaultAgent;
|
|
14
|
+
let agentArgs = [];
|
|
15
|
+
let model;
|
|
16
|
+
let extensions;
|
|
17
|
+
const shell = process.env.SHELL || "/bin/bash";
|
|
18
|
+
for (let i = 0; i < argv.length; i++) {
|
|
19
|
+
const arg = argv[i];
|
|
20
|
+
if (arg === "--agent" && argv[i + 1]) {
|
|
21
|
+
agentCommand = argv[++i];
|
|
22
|
+
}
|
|
23
|
+
else if (arg === "--agent-args" && argv[i + 1]) {
|
|
24
|
+
const argsString = argv[++i];
|
|
25
|
+
agentArgs = argsString.split(" ");
|
|
26
|
+
// Extract model from agent args if provided
|
|
27
|
+
const modelArgIndex = agentArgs.findIndex(a => a === "--model" || a === "-m");
|
|
28
|
+
if (modelArgIndex !== -1 && agentArgs[modelArgIndex + 1]) {
|
|
29
|
+
model = agentArgs[modelArgIndex + 1];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else if (arg === "--shell" && argv[i + 1]) {
|
|
33
|
+
return { agentCommand, agentArgs, shell: argv[++i], model, extensions };
|
|
34
|
+
}
|
|
35
|
+
else if ((arg === "--extensions" || arg === "-e") && argv[i + 1]) {
|
|
36
|
+
const exts = argv[++i].split(",").map(s => s.trim());
|
|
37
|
+
extensions = extensions ? [...extensions, ...exts] : exts;
|
|
38
|
+
}
|
|
39
|
+
else if (arg === "--help" || arg === "-h") {
|
|
40
|
+
console.log(`agent-sh — a shell-first terminal with ACP agent access
|
|
41
|
+
|
|
42
|
+
Usage: agent-sh [options]
|
|
43
|
+
|
|
44
|
+
Quick Start:
|
|
45
|
+
npm start Start with default agent (pi-acp)
|
|
46
|
+
npm run pi Start with pi-acp agent
|
|
47
|
+
npm run claude Start with Claude agent
|
|
48
|
+
|
|
49
|
+
Options:
|
|
50
|
+
--agent <cmd> Agent command to launch (default: $AGENT_SH_AGENT or "pi-acp")
|
|
51
|
+
--agent-args <args> Arguments for the agent (space-separated, quoted)
|
|
52
|
+
--shell <path> Shell to use (default: $SHELL or /bin/bash)
|
|
53
|
+
-e, --extensions Extensions to load (comma-separated, repeatable)
|
|
54
|
+
-h, --help Show this help
|
|
55
|
+
|
|
56
|
+
Extensions:
|
|
57
|
+
Extensions are loaded from (in order):
|
|
58
|
+
1. -e flags: npm packages or file paths
|
|
59
|
+
2. settings: ~/.agent-sh/settings.json → "extensions": [...]
|
|
60
|
+
3. directory: ~/.agent-sh/extensions/ (files or dirs with index.ts)
|
|
61
|
+
|
|
62
|
+
Environment Variables:
|
|
63
|
+
AGENT_SH_AGENT Default agent to use (e.g., "pi-acp", "claude")
|
|
64
|
+
|
|
65
|
+
Examples:
|
|
66
|
+
npm start --agent pi-acp
|
|
67
|
+
npm start -- -e my-extension-package
|
|
68
|
+
npm start -- -e ./local-ext.ts -e another-package
|
|
69
|
+
|
|
70
|
+
Inside the shell:
|
|
71
|
+
Type normally Commands run in your real shell
|
|
72
|
+
> <query> Send query to the AI agent
|
|
73
|
+
> /help Show available slash commands
|
|
74
|
+
Ctrl-C Cancel agent response (or signal shell as usual)
|
|
75
|
+
`);
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { agentCommand, agentArgs, shell, model, extensions };
|
|
80
|
+
}
|
|
81
|
+
function formatAgentInfo(agentInfo, model) {
|
|
82
|
+
const name = agentInfo.name.replace(/-acp$/, "").replace(/-/g, " ");
|
|
83
|
+
let infoStr = `${p.dim}${name}${p.reset}`;
|
|
84
|
+
if (model) {
|
|
85
|
+
const cleanModel = model
|
|
86
|
+
.replace(/^openai\//i, "")
|
|
87
|
+
.replace(/^anthropic\//i, "")
|
|
88
|
+
.replace(/^google\//i, "");
|
|
89
|
+
infoStr += ` ${p.dim}(${cleanModel})${p.reset}`;
|
|
90
|
+
}
|
|
91
|
+
return `${infoStr} ${p.success}●${p.reset}`;
|
|
92
|
+
}
|
|
93
|
+
async function main() {
|
|
94
|
+
const config = parseArgs(process.argv.slice(2));
|
|
95
|
+
// ── Core (frontend-agnostic) ──────────────────────────────────
|
|
96
|
+
const core = createCore(config);
|
|
97
|
+
const { bus, client } = core;
|
|
98
|
+
// ── Interactive frontend ──────────────────────────────────────
|
|
99
|
+
process.stdout.write(`\x1b]0;agent-sh\x07`);
|
|
100
|
+
const cols = process.stdout.columns || 80;
|
|
101
|
+
const rows = process.stdout.rows || 24;
|
|
102
|
+
const cleanup = () => {
|
|
103
|
+
core.kill();
|
|
104
|
+
shell.kill();
|
|
105
|
+
if (process.stdin.isTTY) {
|
|
106
|
+
process.stdin.setRawMode(false);
|
|
107
|
+
}
|
|
108
|
+
process.exit(0);
|
|
109
|
+
};
|
|
110
|
+
const shell = new Shell({
|
|
111
|
+
bus,
|
|
112
|
+
cols,
|
|
113
|
+
rows,
|
|
114
|
+
shell: config.shell || process.env.SHELL || "/bin/bash",
|
|
115
|
+
cwd: process.cwd(),
|
|
116
|
+
onShowAgentInfo: () => {
|
|
117
|
+
if (client.isConnected()) {
|
|
118
|
+
const agentInfo = client.getAgentInfo();
|
|
119
|
+
const model = client.getModel();
|
|
120
|
+
if (agentInfo) {
|
|
121
|
+
return { info: formatAgentInfo(agentInfo, model) };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { info: "" };
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
// ── Extensions ────────────────────────────────────────────────
|
|
128
|
+
const extCtx = core.extensionContext({ quit: cleanup });
|
|
129
|
+
tuiRenderer(extCtx);
|
|
130
|
+
slashCommands(extCtx);
|
|
131
|
+
fileAutocomplete(extCtx);
|
|
132
|
+
shellRecall(extCtx);
|
|
133
|
+
await loadExtensions(extCtx, config.extensions);
|
|
134
|
+
// ── Agent connection (async — don't block shell startup) ──────
|
|
135
|
+
core.start().catch((err) => {
|
|
136
|
+
console.error(`Failed to connect to ${config.agentCommand}:`, err);
|
|
137
|
+
});
|
|
138
|
+
// ── Terminal lifecycle ────────────────────────────────────────
|
|
139
|
+
process.on("SIGTERM", cleanup);
|
|
140
|
+
process.on("SIGHUP", cleanup);
|
|
141
|
+
process.stdout.on("resize", () => {
|
|
142
|
+
shell.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
143
|
+
});
|
|
144
|
+
shell.onExit((e) => {
|
|
145
|
+
core.kill();
|
|
146
|
+
if (process.stdin.isTTY) {
|
|
147
|
+
process.stdin.setRawMode(false);
|
|
148
|
+
}
|
|
149
|
+
process.exit(e.exitCode);
|
|
150
|
+
});
|
|
151
|
+
if (process.stdin.isTTY) {
|
|
152
|
+
process.stdin.setRawMode(true);
|
|
153
|
+
}
|
|
154
|
+
process.stdin.resume();
|
|
155
|
+
}
|
|
156
|
+
main().catch((err) => {
|
|
157
|
+
console.error("Fatal:", err);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { EventBus } from "./event-bus.js";
|
|
2
|
+
/**
|
|
3
|
+
* Narrow contract between InputHandler and its host (Shell).
|
|
4
|
+
* InputHandler never touches the PTY or EventBus directly —
|
|
5
|
+
* it goes through this interface for all cross-cutting concerns.
|
|
6
|
+
*/
|
|
7
|
+
export interface InputContext {
|
|
8
|
+
isForegroundBusy(): boolean;
|
|
9
|
+
getCwd(): string;
|
|
10
|
+
isAgentActive(): boolean;
|
|
11
|
+
writeToPty(data: string): void;
|
|
12
|
+
onCommandEntered(command: string, cwd: string): void;
|
|
13
|
+
redrawPrompt(): void;
|
|
14
|
+
freshPrompt(): void;
|
|
15
|
+
}
|
|
16
|
+
export declare class InputHandler {
|
|
17
|
+
private ctx;
|
|
18
|
+
private lineBuffer;
|
|
19
|
+
private agentInputMode;
|
|
20
|
+
private agentInputBuffer;
|
|
21
|
+
private autocompleteActive;
|
|
22
|
+
private autocompleteIndex;
|
|
23
|
+
private autocompleteItems;
|
|
24
|
+
private autocompleteLines;
|
|
25
|
+
private bus;
|
|
26
|
+
private onShowAgentInfo;
|
|
27
|
+
constructor(opts: {
|
|
28
|
+
ctx: InputContext;
|
|
29
|
+
bus: EventBus;
|
|
30
|
+
onShowAgentInfo: () => {
|
|
31
|
+
info: string;
|
|
32
|
+
model?: string;
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
/** Write the agent prompt line (clear + info prefix + ❯ + buffer text). */
|
|
36
|
+
private writeAgentPromptLine;
|
|
37
|
+
handleInput(data: string): void;
|
|
38
|
+
private enterAgentInputMode;
|
|
39
|
+
private exitAgentInputMode;
|
|
40
|
+
printPrompt(): void;
|
|
41
|
+
private renderAgentInput;
|
|
42
|
+
private updateAutocomplete;
|
|
43
|
+
private renderAutocomplete;
|
|
44
|
+
private clearAutocompleteLines;
|
|
45
|
+
private applyAutocomplete;
|
|
46
|
+
private dismissAutocomplete;
|
|
47
|
+
private handleAgentInput;
|
|
48
|
+
}
|