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,405 @@
|
|
|
1
|
+
const DEFAULT_WINDOW_SIZE = 20;
|
|
2
|
+
const DEFAULT_BUDGET = 16384; // ~4K tokens at ~4 chars/token
|
|
3
|
+
// Truncation thresholds (in lines)
|
|
4
|
+
const SHELL_TRUNCATE_THRESHOLD = 30;
|
|
5
|
+
const SHELL_HEAD_LINES = 10;
|
|
6
|
+
const SHELL_TAIL_LINES = 10;
|
|
7
|
+
const AGENT_RESPONSE_TRUNCATE_THRESHOLD = 20;
|
|
8
|
+
const AGENT_RESPONSE_HEAD_LINES = 15;
|
|
9
|
+
const TOOL_TRUNCATE_THRESHOLD = 20;
|
|
10
|
+
const TOOL_HEAD_LINES = 5;
|
|
11
|
+
const TOOL_TAIL_LINES = 5;
|
|
12
|
+
const RECALL_EXPAND_MAX_LINES = 500;
|
|
13
|
+
export class ContextManager {
|
|
14
|
+
exchanges = [];
|
|
15
|
+
nextId = 1;
|
|
16
|
+
currentCwd;
|
|
17
|
+
sessionStart;
|
|
18
|
+
pendingToolCalls = [];
|
|
19
|
+
constructor(bus) {
|
|
20
|
+
this.currentCwd = process.cwd();
|
|
21
|
+
this.sessionStart = Date.now();
|
|
22
|
+
// ── Subscribe to shell events ──
|
|
23
|
+
bus.on("shell:command-done", (e) => {
|
|
24
|
+
const lines = e.output.split("\n");
|
|
25
|
+
this.addExchange({
|
|
26
|
+
type: "shell_command",
|
|
27
|
+
command: e.command,
|
|
28
|
+
output: e.output,
|
|
29
|
+
cwd: e.cwd,
|
|
30
|
+
exitCode: e.exitCode,
|
|
31
|
+
outputLines: lines.length,
|
|
32
|
+
outputBytes: e.output.length,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
bus.on("shell:cwd-change", (e) => {
|
|
36
|
+
this.currentCwd = e.cwd;
|
|
37
|
+
});
|
|
38
|
+
// ── Subscribe to agent events ──
|
|
39
|
+
bus.on("agent:query", (e) => {
|
|
40
|
+
this.pendingToolCalls = [];
|
|
41
|
+
this.addExchange({ type: "agent_query", query: e.query });
|
|
42
|
+
});
|
|
43
|
+
bus.on("agent:response-done", (e) => {
|
|
44
|
+
this.addExchange({
|
|
45
|
+
type: "agent_response",
|
|
46
|
+
response: e.response,
|
|
47
|
+
toolCalls: this.pendingToolCalls,
|
|
48
|
+
});
|
|
49
|
+
this.pendingToolCalls = [];
|
|
50
|
+
});
|
|
51
|
+
bus.on("agent:tool-call", (e) => {
|
|
52
|
+
// Accumulate tool calls for the agent_response summary
|
|
53
|
+
this.pendingToolCalls.push({
|
|
54
|
+
tool: e.tool,
|
|
55
|
+
args: e.args,
|
|
56
|
+
output: "",
|
|
57
|
+
exitCode: null,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
bus.on("agent:tool-output", (e) => {
|
|
61
|
+
// Update the last pending tool call with output
|
|
62
|
+
const last = this.pendingToolCalls[this.pendingToolCalls.length - 1];
|
|
63
|
+
if (last) {
|
|
64
|
+
last.output = e.output;
|
|
65
|
+
last.exitCode = e.exitCode;
|
|
66
|
+
}
|
|
67
|
+
// Also store as a separate exchange for chronological log
|
|
68
|
+
const lines = e.output.split("\n");
|
|
69
|
+
this.addExchange({
|
|
70
|
+
type: "tool_execution",
|
|
71
|
+
tool: e.tool,
|
|
72
|
+
args: {},
|
|
73
|
+
output: e.output,
|
|
74
|
+
exitCode: e.exitCode,
|
|
75
|
+
outputLines: lines.length,
|
|
76
|
+
outputBytes: e.output.length,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// ── Public query API ──────────────────────────────────────────
|
|
81
|
+
getCwd() {
|
|
82
|
+
return this.currentCwd;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build the <shell_context> block for the agent prompt.
|
|
86
|
+
* Pipeline: window → truncate → format
|
|
87
|
+
*/
|
|
88
|
+
getContext(budget = DEFAULT_BUDGET) {
|
|
89
|
+
let exchanges = this.applyWindow(this.exchanges);
|
|
90
|
+
exchanges = this.applyTruncation(exchanges, budget);
|
|
91
|
+
return this.formatContext(exchanges);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Regex/keyword search across all exchanges. Returns formatted results.
|
|
95
|
+
*/
|
|
96
|
+
search(query) {
|
|
97
|
+
if (!query.trim())
|
|
98
|
+
return "No query provided.";
|
|
99
|
+
let regex;
|
|
100
|
+
try {
|
|
101
|
+
regex = new RegExp(query, "i");
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Fallback: treat as literal keywords with OR logic
|
|
105
|
+
const words = query.split(/\s+/).filter((w) => w.length > 0);
|
|
106
|
+
const pattern = words.map((w) => escapeRegex(w)).join("|");
|
|
107
|
+
regex = new RegExp(pattern, "i");
|
|
108
|
+
}
|
|
109
|
+
const matches = [];
|
|
110
|
+
for (const ex of this.exchanges) {
|
|
111
|
+
const text = this.exchangeSearchText(ex);
|
|
112
|
+
const lines = text.split("\n");
|
|
113
|
+
const matchingLineIndices = [];
|
|
114
|
+
for (let i = 0; i < lines.length; i++) {
|
|
115
|
+
if (regex.test(lines[i])) {
|
|
116
|
+
matchingLineIndices.push(i);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (matchingLineIndices.length > 0) {
|
|
120
|
+
// Extract excerpts with 2 lines of context around each match
|
|
121
|
+
const excerpts = [];
|
|
122
|
+
for (const idx of matchingLineIndices.slice(0, 5)) {
|
|
123
|
+
const start = Math.max(0, idx - 2);
|
|
124
|
+
const end = Math.min(lines.length, idx + 3);
|
|
125
|
+
excerpts.push(lines.slice(start, end).join("\n"));
|
|
126
|
+
}
|
|
127
|
+
matches.push({ exchange: ex, excerpts });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (matches.length === 0) {
|
|
131
|
+
return `No results found for "${query}".`;
|
|
132
|
+
}
|
|
133
|
+
const parts = [`Search results for "${query}" (${matches.length} exchanges):\n`];
|
|
134
|
+
for (const m of matches.slice(0, 20)) {
|
|
135
|
+
parts.push(`#${m.exchange.id} [${m.exchange.type}]`);
|
|
136
|
+
for (const excerpt of m.excerpts) {
|
|
137
|
+
parts.push(indent(excerpt, " "));
|
|
138
|
+
}
|
|
139
|
+
parts.push("");
|
|
140
|
+
}
|
|
141
|
+
return parts.join("\n");
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Return full untruncated content for specific exchange IDs.
|
|
145
|
+
*/
|
|
146
|
+
expand(ids) {
|
|
147
|
+
const results = [];
|
|
148
|
+
for (const id of ids) {
|
|
149
|
+
const ex = this.exchanges.find((e) => e.id === id);
|
|
150
|
+
if (!ex) {
|
|
151
|
+
results.push(`#${id}: not found`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
results.push(this.formatExchangeFull(ex));
|
|
155
|
+
}
|
|
156
|
+
return results.join("\n\n");
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* One-line summaries of last N exchanges.
|
|
160
|
+
*/
|
|
161
|
+
getRecentSummary(n = 25) {
|
|
162
|
+
const recent = this.exchanges.slice(-n);
|
|
163
|
+
if (recent.length === 0)
|
|
164
|
+
return "No exchanges yet.";
|
|
165
|
+
return recent.map((ex) => this.exchangeOneLiner(ex)).join("\n");
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Parse and handle __shell_recall commands.
|
|
169
|
+
*/
|
|
170
|
+
handleRecallCommand(command) {
|
|
171
|
+
const args = command.replace(/^__shell_recall\s*/, "").trim();
|
|
172
|
+
if (!args || args === "--help") {
|
|
173
|
+
return [
|
|
174
|
+
"Usage:",
|
|
175
|
+
" __shell_recall Browse recent exchanges",
|
|
176
|
+
" __shell_recall --search <query> Search all exchanges",
|
|
177
|
+
" __shell_recall --expand <id,...> Show full content of exchanges",
|
|
178
|
+
"",
|
|
179
|
+
"Examples:",
|
|
180
|
+
' __shell_recall --search "test fail"',
|
|
181
|
+
" __shell_recall --expand 41",
|
|
182
|
+
" __shell_recall --expand 41,42,43",
|
|
183
|
+
].join("\n");
|
|
184
|
+
}
|
|
185
|
+
const searchMatch = args.match(/^--search\s+(?:"([^"]+)"|(\S+))/);
|
|
186
|
+
if (searchMatch) {
|
|
187
|
+
return this.search(searchMatch[1] ?? searchMatch[2] ?? "");
|
|
188
|
+
}
|
|
189
|
+
const expandMatch = args.match(/^--expand\s+([\d,\s]+)/);
|
|
190
|
+
if (expandMatch) {
|
|
191
|
+
const ids = expandMatch[1]
|
|
192
|
+
.split(/[,\s]+/)
|
|
193
|
+
.map(Number)
|
|
194
|
+
.filter((n) => !isNaN(n));
|
|
195
|
+
if (ids.length === 0)
|
|
196
|
+
return "No valid IDs provided.";
|
|
197
|
+
return this.expand(ids);
|
|
198
|
+
}
|
|
199
|
+
// Default: browse
|
|
200
|
+
return this.getRecentSummary();
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Clear exchange history (used by /clear command).
|
|
204
|
+
*/
|
|
205
|
+
clear() {
|
|
206
|
+
this.exchanges = [];
|
|
207
|
+
this.pendingToolCalls = [];
|
|
208
|
+
// Don't reset nextId — IDs should be globally unique within a session
|
|
209
|
+
}
|
|
210
|
+
// ── Pipeline stages ───────────────────────────────────────────
|
|
211
|
+
applyWindow(exchanges, windowSize = DEFAULT_WINDOW_SIZE) {
|
|
212
|
+
return exchanges.slice(-windowSize);
|
|
213
|
+
}
|
|
214
|
+
applyTruncation(exchanges, budget) {
|
|
215
|
+
// Deep clone so we don't mutate the source
|
|
216
|
+
const result = exchanges.map((e) => ({ ...e }));
|
|
217
|
+
// Pass 1: per-type truncation
|
|
218
|
+
for (const ex of result) {
|
|
219
|
+
if (ex.type === "shell_command") {
|
|
220
|
+
ex.output = truncateOutput(ex.output, SHELL_TRUNCATE_THRESHOLD, SHELL_HEAD_LINES, SHELL_TAIL_LINES, ex.id);
|
|
221
|
+
}
|
|
222
|
+
else if (ex.type === "agent_response") {
|
|
223
|
+
ex.response = truncateHead(ex.response, AGENT_RESPONSE_TRUNCATE_THRESHOLD, AGENT_RESPONSE_HEAD_LINES, ex.id);
|
|
224
|
+
}
|
|
225
|
+
else if (ex.type === "tool_execution") {
|
|
226
|
+
ex.output = truncateOutput(ex.output, TOOL_TRUNCATE_THRESHOLD, TOOL_HEAD_LINES, TOOL_TAIL_LINES, ex.id);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Pass 2: budget enforcement — strip output from oldest if over budget
|
|
230
|
+
let totalSize = result.reduce((sum, ex) => sum + this.exchangeSize(ex), 0);
|
|
231
|
+
for (let i = 0; i < result.length - 1 && totalSize > budget; i++) {
|
|
232
|
+
const ex = result[i];
|
|
233
|
+
const before = this.exchangeSize(ex);
|
|
234
|
+
if (ex.type === "shell_command") {
|
|
235
|
+
ex.output = `[output omitted, use __shell_recall --expand ${ex.id}]`;
|
|
236
|
+
}
|
|
237
|
+
else if (ex.type === "tool_execution") {
|
|
238
|
+
ex.output = `[output omitted, use __shell_recall --expand ${ex.id}]`;
|
|
239
|
+
}
|
|
240
|
+
else if (ex.type === "agent_response") {
|
|
241
|
+
ex.response = `[response omitted, use __shell_recall --expand ${ex.id}]`;
|
|
242
|
+
}
|
|
243
|
+
totalSize -= before - this.exchangeSize(ex);
|
|
244
|
+
}
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
formatContext(exchanges) {
|
|
248
|
+
const elapsed = Math.round((Date.now() - this.sessionStart) / 60000);
|
|
249
|
+
const totalCount = this.exchanges.length;
|
|
250
|
+
let out = "<shell_context>\n";
|
|
251
|
+
out += `cwd: ${this.currentCwd}\n`;
|
|
252
|
+
out += `session: ${totalCount} exchanges, ${elapsed}m elapsed\n`;
|
|
253
|
+
out += `[hint: run \`__shell_recall --search "query"\` or \`__shell_recall --expand ID\` to retrieve truncated content]\n`;
|
|
254
|
+
for (const ex of exchanges) {
|
|
255
|
+
out += "\n" + this.formatExchangeTruncated(ex);
|
|
256
|
+
}
|
|
257
|
+
out += "\n</shell_context>\n";
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
// ── Internal helpers ──────────────────────────────────────────
|
|
261
|
+
addExchange(partial) {
|
|
262
|
+
const exchange = {
|
|
263
|
+
...partial,
|
|
264
|
+
id: this.nextId++,
|
|
265
|
+
timestamp: Date.now(),
|
|
266
|
+
};
|
|
267
|
+
this.exchanges.push(exchange);
|
|
268
|
+
}
|
|
269
|
+
formatExchangeTruncated(ex) {
|
|
270
|
+
switch (ex.type) {
|
|
271
|
+
case "shell_command": {
|
|
272
|
+
let s = `#${ex.id} [shell] $ ${ex.command}\n`;
|
|
273
|
+
if (ex.output)
|
|
274
|
+
s += indent(ex.output, " ") + "\n";
|
|
275
|
+
if (ex.exitCode !== null)
|
|
276
|
+
s += ` exit ${ex.exitCode}\n`;
|
|
277
|
+
return s;
|
|
278
|
+
}
|
|
279
|
+
case "agent_query":
|
|
280
|
+
return `#${ex.id} [you] > ${ex.query}\n`;
|
|
281
|
+
case "agent_response": {
|
|
282
|
+
let s = `#${ex.id} [agent] `;
|
|
283
|
+
if (ex.response)
|
|
284
|
+
s += ex.response.split("\n")[0] + "\n";
|
|
285
|
+
if (ex.response.includes("\n")) {
|
|
286
|
+
const rest = ex.response.slice(ex.response.indexOf("\n") + 1);
|
|
287
|
+
if (rest.trim())
|
|
288
|
+
s += indent(rest, " ") + "\n";
|
|
289
|
+
}
|
|
290
|
+
return s;
|
|
291
|
+
}
|
|
292
|
+
case "tool_execution": {
|
|
293
|
+
let s = `#${ex.id} [tool] ${ex.tool}\n`;
|
|
294
|
+
if (ex.output)
|
|
295
|
+
s += indent(ex.output, " ") + "\n";
|
|
296
|
+
if (ex.exitCode !== null)
|
|
297
|
+
s += ` exit ${ex.exitCode}\n`;
|
|
298
|
+
return s;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
truncateForRecall(text) {
|
|
303
|
+
const lines = text.split("\n");
|
|
304
|
+
if (lines.length <= RECALL_EXPAND_MAX_LINES)
|
|
305
|
+
return text;
|
|
306
|
+
const half = RECALL_EXPAND_MAX_LINES / 2;
|
|
307
|
+
return (lines.slice(0, half).join("\n") +
|
|
308
|
+
`\n[... ${lines.length - RECALL_EXPAND_MAX_LINES} more lines ...]\n` +
|
|
309
|
+
lines.slice(-half).join("\n"));
|
|
310
|
+
}
|
|
311
|
+
formatExchangeFull(ex) {
|
|
312
|
+
switch (ex.type) {
|
|
313
|
+
case "shell_command": {
|
|
314
|
+
const output = this.truncateForRecall(ex.output);
|
|
315
|
+
let s = `#${ex.id} [shell] $ ${ex.command} (${ex.outputLines} lines, ${ex.outputBytes} bytes)\n`;
|
|
316
|
+
if (output)
|
|
317
|
+
s += output + "\n";
|
|
318
|
+
if (ex.exitCode !== null)
|
|
319
|
+
s += `exit ${ex.exitCode}\n`;
|
|
320
|
+
return s;
|
|
321
|
+
}
|
|
322
|
+
case "agent_query":
|
|
323
|
+
return `#${ex.id} [you] > ${ex.query}`;
|
|
324
|
+
case "agent_response":
|
|
325
|
+
return `#${ex.id} [agent]\n${ex.response}`;
|
|
326
|
+
case "tool_execution": {
|
|
327
|
+
const output = this.truncateForRecall(ex.output);
|
|
328
|
+
let s = `#${ex.id} [tool] ${ex.tool} (${ex.outputLines} lines, ${ex.outputBytes} bytes)\n`;
|
|
329
|
+
if (output)
|
|
330
|
+
s += output + "\n";
|
|
331
|
+
if (ex.exitCode !== null)
|
|
332
|
+
s += `exit ${ex.exitCode}\n`;
|
|
333
|
+
return s;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
exchangeOneLiner(ex) {
|
|
338
|
+
switch (ex.type) {
|
|
339
|
+
case "shell_command":
|
|
340
|
+
return `#${ex.id} shell: ${ex.command} (${ex.outputLines} lines, exit ${ex.exitCode ?? "?"})`;
|
|
341
|
+
case "agent_query":
|
|
342
|
+
return `#${ex.id} query: ${ex.query}`;
|
|
343
|
+
case "agent_response": {
|
|
344
|
+
const preview = ex.response.split("\n")[0]?.slice(0, 80) ?? "";
|
|
345
|
+
return `#${ex.id} agent: ${preview}${ex.response.length > 80 ? "..." : ""}`;
|
|
346
|
+
}
|
|
347
|
+
case "tool_execution":
|
|
348
|
+
return `#${ex.id} tool: ${ex.tool} (${ex.outputLines} lines, exit ${ex.exitCode ?? "?"})`;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
exchangeSearchText(ex) {
|
|
352
|
+
switch (ex.type) {
|
|
353
|
+
case "shell_command":
|
|
354
|
+
return `${ex.command}\n${ex.output}`;
|
|
355
|
+
case "agent_query":
|
|
356
|
+
return ex.query;
|
|
357
|
+
case "agent_response":
|
|
358
|
+
return ex.response;
|
|
359
|
+
case "tool_execution":
|
|
360
|
+
return `${ex.tool}\n${ex.output}`;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
exchangeSize(ex) {
|
|
364
|
+
switch (ex.type) {
|
|
365
|
+
case "shell_command":
|
|
366
|
+
return ex.command.length + ex.output.length;
|
|
367
|
+
case "agent_query":
|
|
368
|
+
return ex.query.length;
|
|
369
|
+
case "agent_response":
|
|
370
|
+
return ex.response.length;
|
|
371
|
+
case "tool_execution":
|
|
372
|
+
return ex.tool.length + ex.output.length;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// ── Utility functions ─────────────────────────────────────────
|
|
377
|
+
function truncateOutput(text, threshold, headLines, tailLines, id) {
|
|
378
|
+
const lines = text.split("\n");
|
|
379
|
+
if (lines.length <= threshold)
|
|
380
|
+
return text;
|
|
381
|
+
const omitted = lines.length - headLines - tailLines;
|
|
382
|
+
return [
|
|
383
|
+
...lines.slice(0, headLines),
|
|
384
|
+
`[... ${omitted} lines truncated, use __shell_recall --expand ${id} to see full output ...]`,
|
|
385
|
+
...lines.slice(-tailLines),
|
|
386
|
+
].join("\n");
|
|
387
|
+
}
|
|
388
|
+
function truncateHead(text, threshold, headLines, id) {
|
|
389
|
+
const lines = text.split("\n");
|
|
390
|
+
if (lines.length <= threshold)
|
|
391
|
+
return text;
|
|
392
|
+
return [
|
|
393
|
+
...lines.slice(0, headLines),
|
|
394
|
+
`[... truncated, use __shell_recall --expand ${id} for full response ...]`,
|
|
395
|
+
].join("\n");
|
|
396
|
+
}
|
|
397
|
+
function indent(text, prefix) {
|
|
398
|
+
return text
|
|
399
|
+
.split("\n")
|
|
400
|
+
.map((line) => prefix + line)
|
|
401
|
+
.join("\n");
|
|
402
|
+
}
|
|
403
|
+
function escapeRegex(str) {
|
|
404
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
405
|
+
}
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core kernel — the minimum viable agent-sh.
|
|
3
|
+
*
|
|
4
|
+
* Wires up EventBus + ContextManager + AcpClient without any frontend.
|
|
5
|
+
* Consumers attach their own I/O (Shell, WebSocket, REST, tests) by
|
|
6
|
+
* subscribing to bus events and calling client methods.
|
|
7
|
+
*
|
|
8
|
+
* The core listens for `agent:submit` and `agent:cancel-request` events
|
|
9
|
+
* from any frontend, routing them to the AcpClient. This means frontends
|
|
10
|
+
* never need a direct reference to AcpClient — they just emit events.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* import { createCore } from "agent-sh";
|
|
14
|
+
* const core = createCore({ agentCommand: "pi-acp" });
|
|
15
|
+
* core.bus.on("agent:response-chunk", ({ text }) => ws.send(text));
|
|
16
|
+
* await core.start();
|
|
17
|
+
* core.bus.emit("agent:submit", { query: "hello" });
|
|
18
|
+
*/
|
|
19
|
+
import { EventBus } from "./event-bus.js";
|
|
20
|
+
import { ContextManager } from "./context-manager.js";
|
|
21
|
+
import { AcpClient } from "./acp-client.js";
|
|
22
|
+
import type { AgentShellConfig, ExtensionContext } from "./types.js";
|
|
23
|
+
export { EventBus } from "./event-bus.js";
|
|
24
|
+
export type { ShellEvents } from "./event-bus.js";
|
|
25
|
+
export type { AgentShellConfig, ExtensionContext } from "./types.js";
|
|
26
|
+
export { palette, setPalette, resetPalette } from "./utils/palette.js";
|
|
27
|
+
export type { ColorPalette } from "./utils/palette.js";
|
|
28
|
+
export interface AgentShellCore {
|
|
29
|
+
bus: EventBus;
|
|
30
|
+
contextManager: ContextManager;
|
|
31
|
+
client: AcpClient;
|
|
32
|
+
/** Connect to the agent subprocess. Call after wiring up bus listeners. */
|
|
33
|
+
start(): Promise<void>;
|
|
34
|
+
/** Build an ExtensionContext for loading extensions against this core. */
|
|
35
|
+
extensionContext(opts: {
|
|
36
|
+
quit: () => void;
|
|
37
|
+
}): ExtensionContext;
|
|
38
|
+
/** Tear down the agent process and clean up. */
|
|
39
|
+
kill(): void;
|
|
40
|
+
}
|
|
41
|
+
export declare function createCore(config: AgentShellConfig): AgentShellCore;
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core kernel — the minimum viable agent-sh.
|
|
3
|
+
*
|
|
4
|
+
* Wires up EventBus + ContextManager + AcpClient without any frontend.
|
|
5
|
+
* Consumers attach their own I/O (Shell, WebSocket, REST, tests) by
|
|
6
|
+
* subscribing to bus events and calling client methods.
|
|
7
|
+
*
|
|
8
|
+
* The core listens for `agent:submit` and `agent:cancel-request` events
|
|
9
|
+
* from any frontend, routing them to the AcpClient. This means frontends
|
|
10
|
+
* never need a direct reference to AcpClient — they just emit events.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* import { createCore } from "agent-sh";
|
|
14
|
+
* const core = createCore({ agentCommand: "pi-acp" });
|
|
15
|
+
* core.bus.on("agent:response-chunk", ({ text }) => ws.send(text));
|
|
16
|
+
* await core.start();
|
|
17
|
+
* core.bus.emit("agent:submit", { query: "hello" });
|
|
18
|
+
*/
|
|
19
|
+
import { EventBus } from "./event-bus.js";
|
|
20
|
+
import { ContextManager } from "./context-manager.js";
|
|
21
|
+
import { AcpClient } from "./acp-client.js";
|
|
22
|
+
import { setPalette } from "./utils/palette.js";
|
|
23
|
+
// Re-export types that library consumers need
|
|
24
|
+
export { EventBus } from "./event-bus.js";
|
|
25
|
+
export { palette, setPalette, resetPalette } from "./utils/palette.js";
|
|
26
|
+
export function createCore(config) {
|
|
27
|
+
const bus = new EventBus();
|
|
28
|
+
const contextManager = new ContextManager(bus);
|
|
29
|
+
const client = new AcpClient({ bus, contextManager, config });
|
|
30
|
+
let connected = false;
|
|
31
|
+
// Route frontend events to the agent — any frontend (Shell, WebSocket,
|
|
32
|
+
// REST handler, test harness) can emit these without knowing about AcpClient.
|
|
33
|
+
bus.on("agent:submit", ({ query }) => {
|
|
34
|
+
(async () => {
|
|
35
|
+
// Wait briefly for agent connection if start() is still in progress
|
|
36
|
+
if (!connected) {
|
|
37
|
+
for (let i = 0; i < 30 && !connected; i++) {
|
|
38
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!connected) {
|
|
42
|
+
bus.emit("ui:error", { message: "Agent not connected. Please wait a moment and try again." });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
await client.sendPrompt(query);
|
|
46
|
+
})().catch((err) => {
|
|
47
|
+
bus.emit("agent:error", {
|
|
48
|
+
message: err instanceof Error ? err.message : String(err),
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
bus.on("agent:cancel-request", () => {
|
|
53
|
+
client.cancel().catch(() => { });
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
bus,
|
|
57
|
+
contextManager,
|
|
58
|
+
client,
|
|
59
|
+
async start() {
|
|
60
|
+
await client.start();
|
|
61
|
+
connected = true;
|
|
62
|
+
},
|
|
63
|
+
extensionContext(opts) {
|
|
64
|
+
return {
|
|
65
|
+
bus,
|
|
66
|
+
contextManager,
|
|
67
|
+
getAcpClient: () => client,
|
|
68
|
+
quit: opts.quit,
|
|
69
|
+
setPalette,
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
kill() {
|
|
73
|
+
client.kill();
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed event map — every event has a known payload shape.
|
|
3
|
+
*/
|
|
4
|
+
export interface ShellEvents {
|
|
5
|
+
"shell:command-start": {
|
|
6
|
+
command: string;
|
|
7
|
+
cwd: string;
|
|
8
|
+
};
|
|
9
|
+
"shell:command-done": {
|
|
10
|
+
command: string;
|
|
11
|
+
output: string;
|
|
12
|
+
cwd: string;
|
|
13
|
+
exitCode: number | null;
|
|
14
|
+
};
|
|
15
|
+
"shell:cwd-change": {
|
|
16
|
+
cwd: string;
|
|
17
|
+
};
|
|
18
|
+
"shell:foreground-busy": {
|
|
19
|
+
busy: boolean;
|
|
20
|
+
};
|
|
21
|
+
"agent:submit": {
|
|
22
|
+
query: string;
|
|
23
|
+
};
|
|
24
|
+
"agent:cancel-request": Record<string, never>;
|
|
25
|
+
"agent:query": {
|
|
26
|
+
query: string;
|
|
27
|
+
};
|
|
28
|
+
"agent:thinking-chunk": {
|
|
29
|
+
text: string;
|
|
30
|
+
};
|
|
31
|
+
"agent:response-chunk": {
|
|
32
|
+
text: string;
|
|
33
|
+
};
|
|
34
|
+
"agent:response-done": {
|
|
35
|
+
response: string;
|
|
36
|
+
};
|
|
37
|
+
"agent:processing-start": Record<string, never>;
|
|
38
|
+
"agent:processing-done": Record<string, never>;
|
|
39
|
+
"agent:cancelled": Record<string, never>;
|
|
40
|
+
"agent:error": {
|
|
41
|
+
message: string;
|
|
42
|
+
};
|
|
43
|
+
"agent:tool-call": {
|
|
44
|
+
tool: string;
|
|
45
|
+
args: Record<string, unknown>;
|
|
46
|
+
};
|
|
47
|
+
"agent:tool-output": {
|
|
48
|
+
tool: string;
|
|
49
|
+
output: string;
|
|
50
|
+
exitCode: number | null;
|
|
51
|
+
};
|
|
52
|
+
"agent:tool-started": {
|
|
53
|
+
title: string;
|
|
54
|
+
toolCallId?: string;
|
|
55
|
+
};
|
|
56
|
+
"agent:tool-completed": {
|
|
57
|
+
toolCallId?: string;
|
|
58
|
+
exitCode: number | null;
|
|
59
|
+
};
|
|
60
|
+
"agent:tool-output-chunk": {
|
|
61
|
+
chunk: string;
|
|
62
|
+
};
|
|
63
|
+
"permission:request": {
|
|
64
|
+
kind: string;
|
|
65
|
+
title: string;
|
|
66
|
+
metadata: Record<string, unknown>;
|
|
67
|
+
decision: Record<string, unknown>;
|
|
68
|
+
};
|
|
69
|
+
"command:execute": {
|
|
70
|
+
name: string;
|
|
71
|
+
args: string;
|
|
72
|
+
};
|
|
73
|
+
"ui:info": {
|
|
74
|
+
message: string;
|
|
75
|
+
};
|
|
76
|
+
"ui:error": {
|
|
77
|
+
message: string;
|
|
78
|
+
};
|
|
79
|
+
"input:keypress": {
|
|
80
|
+
key: string;
|
|
81
|
+
};
|
|
82
|
+
"agent:terminal-intercept": {
|
|
83
|
+
command: string;
|
|
84
|
+
cwd: string;
|
|
85
|
+
intercepted: boolean;
|
|
86
|
+
output: string;
|
|
87
|
+
};
|
|
88
|
+
"shell:redraw-prompt": {
|
|
89
|
+
cwd: string;
|
|
90
|
+
handled: boolean;
|
|
91
|
+
};
|
|
92
|
+
"autocomplete:request": {
|
|
93
|
+
buffer: string;
|
|
94
|
+
items: {
|
|
95
|
+
name: string;
|
|
96
|
+
description: string;
|
|
97
|
+
}[];
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
type Listener<T> = (payload: T) => void;
|
|
101
|
+
type PipeListener<T> = (payload: T) => T;
|
|
102
|
+
type AsyncPipeListener<T> = (payload: T) => T | Promise<T>;
|
|
103
|
+
/**
|
|
104
|
+
* Typed event bus with two modes:
|
|
105
|
+
* - emit/on/off: fire-and-forget notifications
|
|
106
|
+
* - emitPipe/onPipe: synchronous transform chain where each listener
|
|
107
|
+
* can modify the payload before passing to the next
|
|
108
|
+
*/
|
|
109
|
+
export declare class EventBus {
|
|
110
|
+
private emitter;
|
|
111
|
+
private pipeListeners;
|
|
112
|
+
private asyncPipeListeners;
|
|
113
|
+
/** Subscribe to a fire-and-forget event. */
|
|
114
|
+
on<K extends keyof ShellEvents>(event: K, fn: Listener<ShellEvents[K]>): void;
|
|
115
|
+
/** Unsubscribe from a fire-and-forget event. */
|
|
116
|
+
off<K extends keyof ShellEvents>(event: K, fn: Listener<ShellEvents[K]>): void;
|
|
117
|
+
/** Emit a fire-and-forget event. */
|
|
118
|
+
emit<K extends keyof ShellEvents>(event: K, payload: ShellEvents[K]): void;
|
|
119
|
+
/** Register a transform listener for a pipeline event. */
|
|
120
|
+
onPipe<K extends keyof ShellEvents>(event: K, fn: PipeListener<ShellEvents[K]>): void;
|
|
121
|
+
/**
|
|
122
|
+
* Emit a pipeline event — each registered pipe listener receives the
|
|
123
|
+
* output of the previous one. Returns the final transformed payload.
|
|
124
|
+
* If no listeners are registered, returns the original payload unchanged.
|
|
125
|
+
*/
|
|
126
|
+
emitPipe<K extends keyof ShellEvents>(event: K, payload: ShellEvents[K]): ShellEvents[K];
|
|
127
|
+
/** Register an async transform listener for a pipeline event. */
|
|
128
|
+
onPipeAsync<K extends keyof ShellEvents>(event: K, fn: AsyncPipeListener<ShellEvents[K]>): void;
|
|
129
|
+
/**
|
|
130
|
+
* Emit an async pipeline event. Two phases:
|
|
131
|
+
* 1. Notify — fire regular `on` listeners synchronously (e.g., TUI flushes state)
|
|
132
|
+
* 2. Transform — run async pipe listeners in series, each receiving the
|
|
133
|
+
* output of the previous (e.g., extension provides a permission decision)
|
|
134
|
+
*
|
|
135
|
+
* Returns the final transformed payload. If no pipe listeners are registered,
|
|
136
|
+
* returns the original payload unchanged (with safe defaults).
|
|
137
|
+
*/
|
|
138
|
+
emitPipeAsync<K extends keyof ShellEvents>(event: K, payload: ShellEvents[K]): Promise<ShellEvents[K]>;
|
|
139
|
+
}
|
|
140
|
+
export {};
|