agent-sh 0.3.1 → 0.5.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 -96
- package/dist/agent/agent-loop.d.ts +85 -0
- package/dist/agent/agent-loop.js +611 -0
- package/dist/agent/conversation-state.d.ts +27 -0
- package/dist/agent/conversation-state.js +59 -0
- package/dist/agent/index.d.ts +11 -0
- package/dist/agent/index.js +9 -0
- package/dist/agent/skills.d.ts +25 -0
- package/dist/agent/skills.js +186 -0
- package/dist/agent/subagent.d.ts +37 -0
- package/dist/agent/subagent.js +117 -0
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +98 -0
- package/dist/agent/tool-registry.d.ts +15 -0
- package/dist/agent/tool-registry.js +30 -0
- package/dist/agent/tools/bash.d.ts +7 -0
- package/dist/agent/tools/bash.js +62 -0
- package/dist/agent/tools/edit-file.d.ts +2 -0
- package/dist/agent/tools/edit-file.js +95 -0
- package/dist/agent/tools/glob.d.ts +2 -0
- package/dist/agent/tools/glob.js +55 -0
- package/dist/agent/tools/grep.d.ts +2 -0
- package/dist/agent/tools/grep.js +77 -0
- package/dist/agent/tools/list-skills.d.ts +2 -0
- package/dist/agent/tools/list-skills.js +28 -0
- package/dist/agent/tools/ls.d.ts +2 -0
- package/dist/agent/tools/ls.js +43 -0
- package/dist/agent/tools/read-file.d.ts +2 -0
- package/dist/agent/tools/read-file.js +55 -0
- package/dist/agent/tools/user-shell.d.ts +13 -0
- package/dist/agent/tools/user-shell.js +57 -0
- package/dist/agent/tools/write-file.d.ts +2 -0
- package/dist/agent/tools/write-file.js +74 -0
- package/dist/agent/types.d.ts +44 -0
- package/dist/agent/types.js +1 -0
- package/dist/core.d.ts +24 -14
- package/dist/core.js +260 -36
- package/dist/event-bus.d.ts +84 -14
- package/dist/event-bus.js +10 -1
- package/dist/extension-loader.js +12 -1
- package/dist/extensions/command-suggest.d.ts +10 -0
- package/dist/extensions/command-suggest.js +41 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +161 -64
- package/dist/extensions/tui-renderer.js +111 -53
- package/dist/index.js +124 -120
- package/dist/input-handler.d.ts +17 -8
- package/dist/input-handler.js +152 -45
- package/dist/output-parser.d.ts +7 -0
- package/dist/output-parser.js +27 -0
- package/dist/settings.d.ts +53 -2
- package/dist/settings.js +45 -2
- package/dist/shell.js +36 -27
- package/dist/types.d.ts +46 -6
- package/dist/utils/box-frame.d.ts +3 -1
- package/dist/utils/box-frame.js +12 -5
- package/dist/utils/line-editor.js +4 -0
- package/dist/utils/llm-client.d.ts +45 -0
- package/dist/utils/llm-client.js +60 -0
- package/dist/utils/markdown.js +2 -2
- package/dist/utils/stream-transform.js +20 -47
- package/dist/utils/tool-display.js +15 -5
- package/examples/extensions/claude-code-bridge/README.md +35 -0
- package/examples/extensions/claude-code-bridge/index.ts +198 -0
- package/examples/extensions/claude-code-bridge/package.json +11 -0
- package/examples/extensions/openrouter.ts +87 -0
- package/examples/extensions/pi-bridge/README.md +35 -0
- package/examples/extensions/pi-bridge/index.ts +265 -0
- package/examples/extensions/pi-bridge/package.json +13 -0
- package/examples/extensions/subagents.ts +87 -0
- package/package.json +3 -5
- package/dist/acp-client.d.ts +0 -100
- package/dist/acp-client.js +0 -656
- package/dist/extensions/shell-exec.d.ts +0 -24
- package/dist/extensions/shell-exec.js +0 -188
- package/dist/mcp-server.d.ts +0 -13
- package/dist/mcp-server.js +0 -234
- package/examples/pi-agent-sh.ts +0 -166
package/dist/input-handler.js
CHANGED
|
@@ -8,7 +8,10 @@ const HISTORY_FILE = path.join(CONFIG_DIR, "history");
|
|
|
8
8
|
export class InputHandler {
|
|
9
9
|
ctx;
|
|
10
10
|
lineBuffer = "";
|
|
11
|
-
|
|
11
|
+
activeMode = null;
|
|
12
|
+
pendingReturnMode = null; // mode id to return to after processing
|
|
13
|
+
modes = new Map(); // keyed by trigger char
|
|
14
|
+
modesById = new Map(); // keyed by id
|
|
12
15
|
editor = new LineEditor();
|
|
13
16
|
autocompleteActive = false;
|
|
14
17
|
autocompleteIndex = 0;
|
|
@@ -28,9 +31,23 @@ export class InputHandler {
|
|
|
28
31
|
this.loadHistory();
|
|
29
32
|
// Re-render prompt when config changes (e.g. thinking level cycled)
|
|
30
33
|
this.bus.on("config:changed", () => {
|
|
31
|
-
if (this.
|
|
32
|
-
this.
|
|
34
|
+
if (this.activeMode)
|
|
35
|
+
this.writeModePromptLine();
|
|
33
36
|
});
|
|
37
|
+
// Listen for mode registrations from extensions
|
|
38
|
+
this.bus.on("input-mode:register", (config) => {
|
|
39
|
+
this.registerMode(config);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
registerMode(config) {
|
|
43
|
+
if (this.modes.has(config.trigger)) {
|
|
44
|
+
this.bus.emit("ui:error", {
|
|
45
|
+
message: `Input mode "${config.id}" cannot register trigger "${config.trigger}" — already taken by "${this.modes.get(config.trigger).id}"`,
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.modes.set(config.trigger, config);
|
|
50
|
+
this.modesById.set(config.id, config);
|
|
34
51
|
}
|
|
35
52
|
loadHistory() {
|
|
36
53
|
try {
|
|
@@ -52,8 +69,8 @@ export class InputHandler {
|
|
|
52
69
|
// Non-critical — ignore write failures
|
|
53
70
|
}
|
|
54
71
|
}
|
|
55
|
-
/** Write the
|
|
56
|
-
|
|
72
|
+
/** Write the mode prompt line with cursor at the correct position. */
|
|
73
|
+
writeModePromptLine(showBuffer = true) {
|
|
57
74
|
const termW = process.stdout.columns || 80;
|
|
58
75
|
// Move cursor to the start of the prompt area (first line of wrapped content)
|
|
59
76
|
if (this.promptWrappedLines > 0) {
|
|
@@ -62,9 +79,13 @@ export class InputHandler {
|
|
|
62
79
|
// Clear from here to end of screen — removes current + all wrapped lines below
|
|
63
80
|
process.stdout.write("\r\x1b[J");
|
|
64
81
|
const agentInfo = this.onShowAgentInfo();
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
82
|
+
const indicator = this.activeMode?.indicator ?? "●";
|
|
83
|
+
const infoPrefix = agentInfo.info
|
|
84
|
+
? `${agentInfo.info} ${p.success}${indicator}${p.reset} `
|
|
85
|
+
: `${p.success}${indicator}${p.reset} `;
|
|
86
|
+
const icon = this.activeMode?.promptIcon ?? "❯";
|
|
87
|
+
const promptPrefix = infoPrefix + p.warning + p.bold + icon + " " + p.reset;
|
|
88
|
+
const promptVisLen = visibleLen(infoPrefix) + visibleLen(icon) + 1; // icon + space
|
|
68
89
|
if (!showBuffer || !this.editor.buffer.includes("\n")) {
|
|
69
90
|
// Single-line: simple rendering
|
|
70
91
|
const bufferText = showBuffer ? p.accent + this.editor.buffer + p.reset : "";
|
|
@@ -127,7 +148,7 @@ export class InputHandler {
|
|
|
127
148
|
return;
|
|
128
149
|
}
|
|
129
150
|
// Intercept control chars for TUI (Ctrl+T, Ctrl+O) — don't pass to PTY
|
|
130
|
-
if (data.length === 1 && data.charCodeAt(0) < 32 && !this.
|
|
151
|
+
if (data.length === 1 && data.charCodeAt(0) < 32 && !this.activeMode) {
|
|
131
152
|
const code = data.charCodeAt(0);
|
|
132
153
|
// Keys consumed by TUI extensions
|
|
133
154
|
if (code === 0x14 || code === 0x0f) { // Ctrl+T, Ctrl+O
|
|
@@ -139,9 +160,9 @@ export class InputHandler {
|
|
|
139
160
|
this.bus.emit("input:keypress", { key: data });
|
|
140
161
|
}
|
|
141
162
|
}
|
|
142
|
-
// If in
|
|
143
|
-
if (this.
|
|
144
|
-
this.
|
|
163
|
+
// If in an input mode (typing a query)
|
|
164
|
+
if (this.activeMode) {
|
|
165
|
+
this.handleModeInput(data);
|
|
145
166
|
return;
|
|
146
167
|
}
|
|
147
168
|
for (let i = 0; i < data.length; i++) {
|
|
@@ -166,15 +187,52 @@ export class InputHandler {
|
|
|
166
187
|
this.lineBuffer = "";
|
|
167
188
|
this.ctx.writeToPty(ch);
|
|
168
189
|
}
|
|
190
|
+
else if (ch === "\x1b") {
|
|
191
|
+
// Escape sequence — forward the entire sequence to the PTY but
|
|
192
|
+
// don't let it corrupt lineBuffer. Skip CSI (ESC [ ... final)
|
|
193
|
+
// and SS3 (ESC O <char>) sequences; anything else: just ESC.
|
|
194
|
+
let seq = ch;
|
|
195
|
+
if (i + 1 < data.length) {
|
|
196
|
+
const next = data[i + 1];
|
|
197
|
+
if (next === "[") {
|
|
198
|
+
// CSI: ESC [ (params) (intermediates) final_byte
|
|
199
|
+
seq += next;
|
|
200
|
+
i++;
|
|
201
|
+
while (i + 1 < data.length && data[i + 1].charCodeAt(0) < 0x40) {
|
|
202
|
+
i++;
|
|
203
|
+
seq += data[i];
|
|
204
|
+
}
|
|
205
|
+
if (i + 1 < data.length) {
|
|
206
|
+
i++;
|
|
207
|
+
seq += data[i];
|
|
208
|
+
} // final byte
|
|
209
|
+
}
|
|
210
|
+
else if (next === "O") {
|
|
211
|
+
// SS3: ESC O <char>
|
|
212
|
+
seq += next;
|
|
213
|
+
i++;
|
|
214
|
+
if (i + 1 < data.length) {
|
|
215
|
+
i++;
|
|
216
|
+
seq += data[i];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
// ESC + single char (alt-key, etc.)
|
|
221
|
+
seq += next;
|
|
222
|
+
i++;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
this.ctx.writeToPty(seq);
|
|
226
|
+
}
|
|
169
227
|
else if (ch.charCodeAt(0) < 32 && ch !== "\t") {
|
|
170
|
-
this.lineBuffer = "";
|
|
171
228
|
this.ctx.writeToPty(ch);
|
|
172
229
|
}
|
|
173
230
|
else {
|
|
174
|
-
// Check if
|
|
231
|
+
// Check if trigger char at start of empty line → enter that mode
|
|
175
232
|
// But not if a foreground process (ssh, vim, etc.) is running
|
|
176
|
-
|
|
177
|
-
|
|
233
|
+
const mode = this.modes.get(ch);
|
|
234
|
+
if (this.lineBuffer === "" && mode && !this.ctx.isForegroundBusy()) {
|
|
235
|
+
this.enterMode(mode);
|
|
178
236
|
return; // don't process remaining chars
|
|
179
237
|
}
|
|
180
238
|
this.lineBuffer += ch;
|
|
@@ -182,17 +240,17 @@ export class InputHandler {
|
|
|
182
240
|
}
|
|
183
241
|
}
|
|
184
242
|
}
|
|
185
|
-
|
|
186
|
-
this.
|
|
243
|
+
enterMode(mode) {
|
|
244
|
+
this.activeMode = mode;
|
|
187
245
|
this.editor.clear();
|
|
188
246
|
// Enable kitty keyboard protocol (progressive enhancement flag 1)
|
|
189
247
|
// so Shift+Enter sends \x1b[13;2u instead of plain \r
|
|
190
248
|
process.stdout.write("\x1b[>1u");
|
|
191
|
-
this.
|
|
249
|
+
this.writeModePromptLine(false);
|
|
192
250
|
}
|
|
193
|
-
|
|
251
|
+
exitMode() {
|
|
194
252
|
this.dismissAutocomplete();
|
|
195
|
-
this.
|
|
253
|
+
this.activeMode = null;
|
|
196
254
|
this.editor.clear();
|
|
197
255
|
// Disable kitty keyboard protocol
|
|
198
256
|
process.stdout.write("\x1b[<u");
|
|
@@ -210,14 +268,41 @@ export class InputHandler {
|
|
|
210
268
|
printPrompt() {
|
|
211
269
|
this.ctx.redrawPrompt();
|
|
212
270
|
}
|
|
213
|
-
|
|
271
|
+
/**
|
|
272
|
+
* Called when agent processing completes. Returns true if the input
|
|
273
|
+
* handler re-entered a mode (so caller should skip shell prompt).
|
|
274
|
+
*/
|
|
275
|
+
handleProcessingDone() {
|
|
276
|
+
if (this.pendingReturnMode) {
|
|
277
|
+
const mode = this.modesById.get(this.pendingReturnMode);
|
|
278
|
+
this.pendingReturnMode = null;
|
|
279
|
+
if (mode) {
|
|
280
|
+
this.enterMode(mode);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
renderModeInput() {
|
|
214
287
|
this.clearAutocompleteLines();
|
|
215
|
-
this.
|
|
288
|
+
this.writeModePromptLine();
|
|
216
289
|
this.updateAutocomplete();
|
|
217
290
|
}
|
|
218
291
|
updateAutocomplete() {
|
|
292
|
+
const buf = this.editor.buffer;
|
|
293
|
+
let command = null;
|
|
294
|
+
let commandArgs = null;
|
|
295
|
+
if (buf.startsWith("/")) {
|
|
296
|
+
const spaceIdx = buf.indexOf(" ");
|
|
297
|
+
if (spaceIdx !== -1) {
|
|
298
|
+
command = buf.slice(0, spaceIdx);
|
|
299
|
+
commandArgs = buf.slice(spaceIdx + 1);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
219
302
|
const { items } = this.bus.emitPipe("autocomplete:request", {
|
|
220
|
-
buffer:
|
|
303
|
+
buffer: buf,
|
|
304
|
+
command,
|
|
305
|
+
commandArgs,
|
|
221
306
|
items: [],
|
|
222
307
|
});
|
|
223
308
|
if (items.length > 0) {
|
|
@@ -252,9 +337,15 @@ export class InputHandler {
|
|
|
252
337
|
if (this.autocompleteLines > 0) {
|
|
253
338
|
process.stdout.write(`\x1b[${this.autocompleteLines}A`);
|
|
254
339
|
}
|
|
340
|
+
// Reposition cursor: must match the layout in writeModePromptLine()
|
|
255
341
|
const agentInfo = this.onShowAgentInfo();
|
|
256
|
-
const
|
|
257
|
-
const
|
|
342
|
+
const indicator = this.activeMode?.indicator ?? "●";
|
|
343
|
+
const infoPrefix = agentInfo.info
|
|
344
|
+
? `${agentInfo.info} ${indicator} `
|
|
345
|
+
: `${indicator} `;
|
|
346
|
+
const icon = this.activeMode?.promptIcon ?? "❯";
|
|
347
|
+
const promptVisLen = visibleLen(infoPrefix) + visibleLen(icon) + 1;
|
|
348
|
+
const col = promptVisLen + this.editor.cursor;
|
|
258
349
|
process.stdout.write(`\r\x1b[${col}C`);
|
|
259
350
|
}
|
|
260
351
|
applyAutocomplete() {
|
|
@@ -279,7 +370,7 @@ export class InputHandler {
|
|
|
279
370
|
this.autocompleteActive = false;
|
|
280
371
|
this.autocompleteItems = [];
|
|
281
372
|
this.autocompleteIndex = 0;
|
|
282
|
-
this.
|
|
373
|
+
this.writeModePromptLine();
|
|
283
374
|
if (isFileAc)
|
|
284
375
|
this.updateAutocomplete();
|
|
285
376
|
}
|
|
@@ -299,7 +390,7 @@ export class InputHandler {
|
|
|
299
390
|
process.stdout.write("\x1b8"); // restore cursor
|
|
300
391
|
this.autocompleteLines = 0;
|
|
301
392
|
}
|
|
302
|
-
|
|
393
|
+
handleModeInput(data) {
|
|
303
394
|
// Clear any pending escape timer — new data arrived
|
|
304
395
|
if (this.escapeTimer) {
|
|
305
396
|
clearTimeout(this.escapeTimer);
|
|
@@ -313,24 +404,38 @@ export class InputHandler {
|
|
|
313
404
|
this.escapeTimer = null;
|
|
314
405
|
const flushed = this.editor.flushPendingEscape();
|
|
315
406
|
if (flushed.length > 0)
|
|
316
|
-
this.
|
|
407
|
+
this.processModeActions(flushed);
|
|
317
408
|
}, 50);
|
|
318
409
|
}
|
|
319
|
-
this.
|
|
410
|
+
this.processModeActions(actions);
|
|
320
411
|
}
|
|
321
|
-
|
|
412
|
+
processModeActions(actions) {
|
|
322
413
|
for (const act of actions) {
|
|
323
414
|
switch (act.action) {
|
|
324
|
-
case "changed":
|
|
415
|
+
case "changed": {
|
|
416
|
+
// If the buffer is exactly a trigger char for a different mode, switch to it
|
|
417
|
+
const switchMode = this.modes.get(this.editor.buffer);
|
|
418
|
+
if (this.editor.buffer.length === 1 && switchMode && switchMode !== this.activeMode) {
|
|
419
|
+
this.dismissAutocomplete();
|
|
420
|
+
this.clearPromptArea();
|
|
421
|
+
this.activeMode = switchMode;
|
|
422
|
+
this.editor.clear();
|
|
423
|
+
this.writeModePromptLine(false);
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
325
426
|
this.historyIndex = -1;
|
|
326
427
|
this.autocompleteIndex = 0;
|
|
327
|
-
this.
|
|
428
|
+
this.renderModeInput();
|
|
328
429
|
break;
|
|
430
|
+
}
|
|
329
431
|
case "submit": {
|
|
330
432
|
if (this.autocompleteActive) {
|
|
331
433
|
this.applyAutocomplete();
|
|
332
434
|
}
|
|
333
|
-
|
|
435
|
+
// Use editor.buffer (not act.buffer) so autocomplete selections
|
|
436
|
+
// take effect — act.buffer is a stale snapshot from before
|
|
437
|
+
// applyAutocomplete() updated the buffer.
|
|
438
|
+
const query = this.editor.buffer.trim();
|
|
334
439
|
if (query) {
|
|
335
440
|
// Add to history (avoid consecutive duplicates)
|
|
336
441
|
if (this.history.length === 0 || this.history[this.history.length - 1] !== query) {
|
|
@@ -343,7 +448,8 @@ export class InputHandler {
|
|
|
343
448
|
this.clearAutocompleteLines();
|
|
344
449
|
this.clearPromptArea();
|
|
345
450
|
process.stdout.write("\x1b[<u"); // disable kitty keyboard protocol
|
|
346
|
-
|
|
451
|
+
const currentMode = this.activeMode;
|
|
452
|
+
this.activeMode = null;
|
|
347
453
|
this.editor.clear();
|
|
348
454
|
this.dismissAutocomplete();
|
|
349
455
|
if (query && query.startsWith("/")) {
|
|
@@ -351,28 +457,29 @@ export class InputHandler {
|
|
|
351
457
|
const name = spaceIdx === -1 ? query : query.slice(0, spaceIdx);
|
|
352
458
|
const args = spaceIdx === -1 ? "" : query.slice(spaceIdx + 1).trim();
|
|
353
459
|
this.bus.emit("command:execute", { name, args });
|
|
354
|
-
this.ctx.
|
|
460
|
+
this.ctx.freshPrompt();
|
|
355
461
|
}
|
|
356
462
|
else if (query) {
|
|
357
|
-
this.
|
|
463
|
+
this.pendingReturnMode = currentMode.returnToSelf ? currentMode.id : null;
|
|
464
|
+
currentMode.onSubmit(query, this.bus);
|
|
358
465
|
}
|
|
359
466
|
else {
|
|
360
|
-
this.
|
|
467
|
+
this.exitMode();
|
|
361
468
|
}
|
|
362
469
|
return;
|
|
363
470
|
}
|
|
364
471
|
case "cancel":
|
|
365
472
|
if (this.autocompleteActive) {
|
|
366
473
|
this.dismissAutocomplete();
|
|
367
|
-
this.
|
|
474
|
+
this.writeModePromptLine();
|
|
368
475
|
}
|
|
369
476
|
else {
|
|
370
|
-
this.
|
|
477
|
+
this.exitMode();
|
|
371
478
|
}
|
|
372
479
|
return;
|
|
373
480
|
case "delete-empty":
|
|
374
481
|
this.dismissAutocomplete();
|
|
375
|
-
this.
|
|
482
|
+
this.exitMode();
|
|
376
483
|
return;
|
|
377
484
|
case "tab":
|
|
378
485
|
if (this.autocompleteActive) {
|
|
@@ -389,7 +496,7 @@ export class InputHandler {
|
|
|
389
496
|
? this.autocompleteItems.length - 1
|
|
390
497
|
: this.autocompleteIndex - 1;
|
|
391
498
|
this.clearAutocompleteLines();
|
|
392
|
-
this.
|
|
499
|
+
this.writeModePromptLine();
|
|
393
500
|
this.renderAutocomplete();
|
|
394
501
|
}
|
|
395
502
|
else if (this.history.length > 0) {
|
|
@@ -402,7 +509,7 @@ export class InputHandler {
|
|
|
402
509
|
}
|
|
403
510
|
this.editor.buffer = this.history[this.historyIndex];
|
|
404
511
|
this.editor.cursor = this.editor.buffer.length;
|
|
405
|
-
this.
|
|
512
|
+
this.renderModeInput();
|
|
406
513
|
}
|
|
407
514
|
break;
|
|
408
515
|
case "arrow-down":
|
|
@@ -412,7 +519,7 @@ export class InputHandler {
|
|
|
412
519
|
? 0
|
|
413
520
|
: this.autocompleteIndex + 1;
|
|
414
521
|
this.clearAutocompleteLines();
|
|
415
|
-
this.
|
|
522
|
+
this.writeModePromptLine();
|
|
416
523
|
this.renderAutocomplete();
|
|
417
524
|
}
|
|
418
525
|
else if (this.historyIndex !== -1) {
|
|
@@ -425,7 +532,7 @@ export class InputHandler {
|
|
|
425
532
|
this.editor.buffer = this.savedBuffer;
|
|
426
533
|
}
|
|
427
534
|
this.editor.cursor = this.editor.buffer.length;
|
|
428
|
-
this.
|
|
535
|
+
this.renderModeInput();
|
|
429
536
|
}
|
|
430
537
|
break;
|
|
431
538
|
}
|
package/dist/output-parser.d.ts
CHANGED
|
@@ -19,6 +19,13 @@ export declare class OutputParser {
|
|
|
19
19
|
isPromptReady(): boolean;
|
|
20
20
|
isForegroundBusy(): boolean;
|
|
21
21
|
getCwd(): string;
|
|
22
|
+
/**
|
|
23
|
+
* Detect preexec marker (OSC 9997) emitted by the shell's preexec hook.
|
|
24
|
+
* This carries the actual command text from the shell — more reliable than
|
|
25
|
+
* the InputHandler's lineBuffer which can't track history recall or tab
|
|
26
|
+
* completion. Returns data with the OSC stripped out.
|
|
27
|
+
*/
|
|
28
|
+
private handlePreexec;
|
|
22
29
|
private parseOSC7;
|
|
23
30
|
/**
|
|
24
31
|
* Detect our custom prompt marker (OSC 9999) in the PTY stream.
|
package/dist/output-parser.js
CHANGED
|
@@ -17,6 +17,7 @@ export class OutputParser {
|
|
|
17
17
|
/** Process a chunk of PTY output data. */
|
|
18
18
|
processData(data) {
|
|
19
19
|
this.parseOSC7(data);
|
|
20
|
+
data = this.handlePreexec(data);
|
|
20
21
|
this.parsePromptMarker(data);
|
|
21
22
|
this.parsePromptEnd(data);
|
|
22
23
|
}
|
|
@@ -41,6 +42,32 @@ export class OutputParser {
|
|
|
41
42
|
return this.cwd;
|
|
42
43
|
}
|
|
43
44
|
// ── Parsing ─────────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* Detect preexec marker (OSC 9997) emitted by the shell's preexec hook.
|
|
47
|
+
* This carries the actual command text from the shell — more reliable than
|
|
48
|
+
* the InputHandler's lineBuffer which can't track history recall or tab
|
|
49
|
+
* completion. Returns data with the OSC stripped out.
|
|
50
|
+
*/
|
|
51
|
+
handlePreexec(data) {
|
|
52
|
+
const marker = "\x1b]9997;";
|
|
53
|
+
const idx = data.indexOf(marker);
|
|
54
|
+
if (idx === -1)
|
|
55
|
+
return data;
|
|
56
|
+
const endIdx = data.indexOf("\x07", idx + marker.length);
|
|
57
|
+
if (endIdx === -1)
|
|
58
|
+
return data; // incomplete OSC, wait for next chunk
|
|
59
|
+
const command = data.slice(idx + marker.length, endIdx);
|
|
60
|
+
// Authoritative command from the shell — override any lineBuffer guess
|
|
61
|
+
this.lastCommand = command;
|
|
62
|
+
this.currentOutputCapture = ""; // discard echoed text accumulated before preexec
|
|
63
|
+
if (!this.foregroundBusy) {
|
|
64
|
+
this.foregroundBusy = true;
|
|
65
|
+
this.bus.emit("shell:foreground-busy", { busy: true });
|
|
66
|
+
}
|
|
67
|
+
this.bus.emit("shell:command-start", { command, cwd: this.cwd });
|
|
68
|
+
// Return only data after the OSC — everything before was the echo
|
|
69
|
+
return data.slice(endIdx + 1);
|
|
70
|
+
}
|
|
44
71
|
parseOSC7(data) {
|
|
45
72
|
const match = data.match(/\x1b\]7;file:\/\/[^/]*(\/[^\x07\x1b]*)/);
|
|
46
73
|
if (match?.[1]) {
|
package/dist/settings.d.ts
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
export declare const CONFIG_DIR: string;
|
|
2
|
+
/** Provider profile — a named LLM configuration. */
|
|
3
|
+
export interface ProviderConfig {
|
|
4
|
+
/** API key (supports $ENV_VAR syntax for runtime expansion). */
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
/** Base URL for OpenAI-compatible API. */
|
|
7
|
+
baseURL?: string;
|
|
8
|
+
/** Default model to use. Falls back to first entry in models list. */
|
|
9
|
+
defaultModel?: string;
|
|
10
|
+
/** Models available for cycling. */
|
|
11
|
+
models?: string[];
|
|
12
|
+
/** Context window size in tokens (e.g. 128000). Used for usage display. */
|
|
13
|
+
contextWindow?: number;
|
|
14
|
+
}
|
|
2
15
|
export interface Settings {
|
|
3
16
|
/** Extensions to load (npm packages or file paths). */
|
|
4
17
|
extensions?: string[];
|
|
5
18
|
/** Max agent query history entries to keep. */
|
|
6
19
|
historySize?: number;
|
|
20
|
+
/** Named provider configurations. */
|
|
21
|
+
providers?: Record<string, ProviderConfig>;
|
|
22
|
+
/** Which provider to use by default. */
|
|
23
|
+
defaultProvider?: string;
|
|
24
|
+
/** Preferred agent backend (extension name, e.g. "pi", "claude-code"). */
|
|
25
|
+
defaultBackend?: string;
|
|
7
26
|
/** Recent exchanges included in agent context window. */
|
|
8
27
|
contextWindowSize?: number;
|
|
9
28
|
/** Context budget in bytes (~4 chars per token). */
|
|
@@ -22,8 +41,12 @@ export interface Settings {
|
|
|
22
41
|
readOutputMaxLines?: number;
|
|
23
42
|
/** Max diff lines shown before "ctrl+o to expand". */
|
|
24
43
|
diffMaxLines?: number;
|
|
25
|
-
/**
|
|
26
|
-
|
|
44
|
+
/** Additional directories to scan for skills (supports ~ expansion). */
|
|
45
|
+
skillPaths?: string[];
|
|
46
|
+
/** Show a startup banner when agent-sh launches. */
|
|
47
|
+
startupBanner?: boolean;
|
|
48
|
+
/** Show a subtle agent-sh indicator in the shell prompt. */
|
|
49
|
+
promptIndicator?: boolean;
|
|
27
50
|
}
|
|
28
51
|
declare const DEFAULTS: Required<Settings>;
|
|
29
52
|
/** Load settings from disk (cached after first call). */
|
|
@@ -41,4 +64,32 @@ export declare function getSettings(): Settings & typeof DEFAULTS;
|
|
|
41
64
|
export declare function getExtensionSettings<T extends Record<string, unknown>>(namespace: string, defaults: T): T;
|
|
42
65
|
/** Reset cached settings (for testing or after external edit). */
|
|
43
66
|
export declare function reloadSettings(): void;
|
|
67
|
+
/**
|
|
68
|
+
* Expand $ENV_VAR references in a string.
|
|
69
|
+
* Supports $VAR and ${VAR} syntax.
|
|
70
|
+
*/
|
|
71
|
+
export declare function expandEnvVars(value: string): string;
|
|
72
|
+
/** Resolved provider ready for use (env vars expanded, defaults applied). */
|
|
73
|
+
export interface ResolvedProvider {
|
|
74
|
+
id: string;
|
|
75
|
+
apiKey?: string;
|
|
76
|
+
baseURL?: string;
|
|
77
|
+
defaultModel?: string;
|
|
78
|
+
models: string[];
|
|
79
|
+
contextWindow?: number;
|
|
80
|
+
/** Provider supports the reasoning_effort parameter. Default: true. */
|
|
81
|
+
supportsReasoningEffort?: boolean;
|
|
82
|
+
/** Per-model capabilities, keyed by model id. */
|
|
83
|
+
modelCapabilities?: Map<string, {
|
|
84
|
+
reasoning?: boolean;
|
|
85
|
+
contextWindow?: number;
|
|
86
|
+
}>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve a provider config by name from settings.
|
|
90
|
+
* Returns null if provider not found.
|
|
91
|
+
*/
|
|
92
|
+
export declare function resolveProvider(name: string): ResolvedProvider | null;
|
|
93
|
+
/** Get all configured provider names. */
|
|
94
|
+
export declare function getProviderNames(): string[];
|
|
44
95
|
export {};
|
package/dist/settings.js
CHANGED
|
@@ -12,6 +12,9 @@ const SETTINGS_PATH = path.join(CONFIG_DIR, "settings.json");
|
|
|
12
12
|
const DEFAULTS = {
|
|
13
13
|
extensions: [],
|
|
14
14
|
historySize: 500,
|
|
15
|
+
providers: {},
|
|
16
|
+
defaultProvider: undefined,
|
|
17
|
+
defaultBackend: "agent-sh",
|
|
15
18
|
contextWindowSize: 20,
|
|
16
19
|
contextBudget: 16384,
|
|
17
20
|
shellTruncateThreshold: 10,
|
|
@@ -21,7 +24,9 @@ const DEFAULTS = {
|
|
|
21
24
|
maxCommandOutputLines: 3,
|
|
22
25
|
readOutputMaxLines: 0,
|
|
23
26
|
diffMaxLines: 20,
|
|
24
|
-
|
|
27
|
+
skillPaths: [],
|
|
28
|
+
startupBanner: true,
|
|
29
|
+
promptIndicator: true,
|
|
25
30
|
};
|
|
26
31
|
let cached = null;
|
|
27
32
|
/** Load settings from disk (cached after first call). */
|
|
@@ -31,7 +36,10 @@ export function getSettings() {
|
|
|
31
36
|
const raw = fs.readFileSync(SETTINGS_PATH, "utf-8");
|
|
32
37
|
cached = JSON.parse(raw);
|
|
33
38
|
}
|
|
34
|
-
catch {
|
|
39
|
+
catch (err) {
|
|
40
|
+
if (err instanceof SyntaxError) {
|
|
41
|
+
console.error(`[agent-sh] Warning: invalid JSON in ${SETTINGS_PATH}: ${err.message}`);
|
|
42
|
+
}
|
|
35
43
|
cached = {};
|
|
36
44
|
}
|
|
37
45
|
}
|
|
@@ -59,3 +67,38 @@ export function getExtensionSettings(namespace, defaults) {
|
|
|
59
67
|
export function reloadSettings() {
|
|
60
68
|
cached = null;
|
|
61
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Expand $ENV_VAR references in a string.
|
|
72
|
+
* Supports $VAR and ${VAR} syntax.
|
|
73
|
+
*/
|
|
74
|
+
export function expandEnvVars(value) {
|
|
75
|
+
return value.replace(/\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, braced, plain) => {
|
|
76
|
+
const name = braced || plain;
|
|
77
|
+
return process.env[name] ?? "";
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolve a provider config by name from settings.
|
|
82
|
+
* Returns null if provider not found.
|
|
83
|
+
*/
|
|
84
|
+
export function resolveProvider(name) {
|
|
85
|
+
const settings = getSettings();
|
|
86
|
+
const provider = settings.providers?.[name];
|
|
87
|
+
if (!provider)
|
|
88
|
+
return null;
|
|
89
|
+
const models = provider.models ?? (provider.defaultModel ? [provider.defaultModel] : []);
|
|
90
|
+
const defaultModel = provider.defaultModel ?? models[0];
|
|
91
|
+
return {
|
|
92
|
+
id: name,
|
|
93
|
+
apiKey: provider.apiKey ? expandEnvVars(provider.apiKey) : undefined,
|
|
94
|
+
baseURL: provider.baseURL,
|
|
95
|
+
defaultModel,
|
|
96
|
+
models: models.length ? models : (defaultModel ? [defaultModel] : []),
|
|
97
|
+
contextWindow: provider.contextWindow,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Get all configured provider names. */
|
|
101
|
+
export function getProviderNames() {
|
|
102
|
+
const settings = getSettings();
|
|
103
|
+
return Object.keys(settings.providers ?? {});
|
|
104
|
+
}
|