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.
Files changed (78) hide show
  1. package/README.md +66 -96
  2. package/dist/agent/agent-loop.d.ts +85 -0
  3. package/dist/agent/agent-loop.js +611 -0
  4. package/dist/agent/conversation-state.d.ts +27 -0
  5. package/dist/agent/conversation-state.js +59 -0
  6. package/dist/agent/index.d.ts +11 -0
  7. package/dist/agent/index.js +9 -0
  8. package/dist/agent/skills.d.ts +25 -0
  9. package/dist/agent/skills.js +186 -0
  10. package/dist/agent/subagent.d.ts +37 -0
  11. package/dist/agent/subagent.js +117 -0
  12. package/dist/agent/system-prompt.d.ts +14 -0
  13. package/dist/agent/system-prompt.js +98 -0
  14. package/dist/agent/tool-registry.d.ts +15 -0
  15. package/dist/agent/tool-registry.js +30 -0
  16. package/dist/agent/tools/bash.d.ts +7 -0
  17. package/dist/agent/tools/bash.js +62 -0
  18. package/dist/agent/tools/edit-file.d.ts +2 -0
  19. package/dist/agent/tools/edit-file.js +95 -0
  20. package/dist/agent/tools/glob.d.ts +2 -0
  21. package/dist/agent/tools/glob.js +55 -0
  22. package/dist/agent/tools/grep.d.ts +2 -0
  23. package/dist/agent/tools/grep.js +77 -0
  24. package/dist/agent/tools/list-skills.d.ts +2 -0
  25. package/dist/agent/tools/list-skills.js +28 -0
  26. package/dist/agent/tools/ls.d.ts +2 -0
  27. package/dist/agent/tools/ls.js +43 -0
  28. package/dist/agent/tools/read-file.d.ts +2 -0
  29. package/dist/agent/tools/read-file.js +55 -0
  30. package/dist/agent/tools/user-shell.d.ts +13 -0
  31. package/dist/agent/tools/user-shell.js +57 -0
  32. package/dist/agent/tools/write-file.d.ts +2 -0
  33. package/dist/agent/tools/write-file.js +74 -0
  34. package/dist/agent/types.d.ts +44 -0
  35. package/dist/agent/types.js +1 -0
  36. package/dist/core.d.ts +24 -14
  37. package/dist/core.js +260 -36
  38. package/dist/event-bus.d.ts +84 -14
  39. package/dist/event-bus.js +10 -1
  40. package/dist/extension-loader.js +12 -1
  41. package/dist/extensions/command-suggest.d.ts +10 -0
  42. package/dist/extensions/command-suggest.js +41 -0
  43. package/dist/extensions/slash-commands.d.ts +1 -1
  44. package/dist/extensions/slash-commands.js +161 -64
  45. package/dist/extensions/tui-renderer.js +111 -53
  46. package/dist/index.js +124 -120
  47. package/dist/input-handler.d.ts +17 -8
  48. package/dist/input-handler.js +152 -45
  49. package/dist/output-parser.d.ts +7 -0
  50. package/dist/output-parser.js +27 -0
  51. package/dist/settings.d.ts +53 -2
  52. package/dist/settings.js +45 -2
  53. package/dist/shell.js +36 -27
  54. package/dist/types.d.ts +46 -6
  55. package/dist/utils/box-frame.d.ts +3 -1
  56. package/dist/utils/box-frame.js +12 -5
  57. package/dist/utils/line-editor.js +4 -0
  58. package/dist/utils/llm-client.d.ts +45 -0
  59. package/dist/utils/llm-client.js +60 -0
  60. package/dist/utils/markdown.js +2 -2
  61. package/dist/utils/stream-transform.js +20 -47
  62. package/dist/utils/tool-display.js +15 -5
  63. package/examples/extensions/claude-code-bridge/README.md +35 -0
  64. package/examples/extensions/claude-code-bridge/index.ts +198 -0
  65. package/examples/extensions/claude-code-bridge/package.json +11 -0
  66. package/examples/extensions/openrouter.ts +87 -0
  67. package/examples/extensions/pi-bridge/README.md +35 -0
  68. package/examples/extensions/pi-bridge/index.ts +265 -0
  69. package/examples/extensions/pi-bridge/package.json +13 -0
  70. package/examples/extensions/subagents.ts +87 -0
  71. package/package.json +3 -5
  72. package/dist/acp-client.d.ts +0 -100
  73. package/dist/acp-client.js +0 -656
  74. package/dist/extensions/shell-exec.d.ts +0 -24
  75. package/dist/extensions/shell-exec.js +0 -188
  76. package/dist/mcp-server.d.ts +0 -13
  77. package/dist/mcp-server.js +0 -234
  78. package/examples/pi-agent-sh.ts +0 -166
@@ -8,7 +8,10 @@ const HISTORY_FILE = path.join(CONFIG_DIR, "history");
8
8
  export class InputHandler {
9
9
  ctx;
10
10
  lineBuffer = "";
11
- agentInputMode = false;
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.agentInputMode)
32
- this.writeAgentPromptLine();
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 agent prompt line with cursor at the correct position. */
56
- writeAgentPromptLine(showBuffer = true) {
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 infoPrefix = agentInfo.info ? `${agentInfo.info} ` : "";
66
- const promptPrefix = infoPrefix + p.warning + p.bold + "❯ " + p.reset;
67
- const promptVisLen = visibleLen(infoPrefix) + 2; // "❯ "
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.agentInputMode) {
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 agent input mode (typing a query after ">")
143
- if (this.agentInputMode) {
144
- this.handleAgentInput(data);
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 ">" at start of empty line → enter agent input mode
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
- if (this.lineBuffer === "" && ch === ">" && !this.ctx.isForegroundBusy()) {
177
- this.enterAgentInputMode();
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
- enterAgentInputMode() {
186
- this.agentInputMode = true;
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.writeAgentPromptLine(false);
249
+ this.writeModePromptLine(false);
192
250
  }
193
- exitAgentInputMode() {
251
+ exitMode() {
194
252
  this.dismissAutocomplete();
195
- this.agentInputMode = false;
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
- renderAgentInput() {
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.writeAgentPromptLine();
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: this.editor.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 infoLength = visibleLen(agentInfo.info);
257
- const col = infoLength + 2 + this.editor.cursor;
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.writeAgentPromptLine();
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
- handleAgentInput(data) {
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.processAgentActions(flushed);
407
+ this.processModeActions(flushed);
317
408
  }, 50);
318
409
  }
319
- this.processAgentActions(actions);
410
+ this.processModeActions(actions);
320
411
  }
321
- processAgentActions(actions) {
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.renderAgentInput();
428
+ this.renderModeInput();
328
429
  break;
430
+ }
329
431
  case "submit": {
330
432
  if (this.autocompleteActive) {
331
433
  this.applyAutocomplete();
332
434
  }
333
- const query = act.buffer.trim();
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
- this.agentInputMode = false;
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.redrawPrompt();
460
+ this.ctx.freshPrompt();
355
461
  }
356
462
  else if (query) {
357
- this.bus.emit("agent:submit", { query });
463
+ this.pendingReturnMode = currentMode.returnToSelf ? currentMode.id : null;
464
+ currentMode.onSubmit(query, this.bus);
358
465
  }
359
466
  else {
360
- this.exitAgentInputMode();
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.writeAgentPromptLine();
474
+ this.writeModePromptLine();
368
475
  }
369
476
  else {
370
- this.exitAgentInputMode();
477
+ this.exitMode();
371
478
  }
372
479
  return;
373
480
  case "delete-empty":
374
481
  this.dismissAutocomplete();
375
- this.exitAgentInputMode();
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.writeAgentPromptLine();
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.renderAgentInput();
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.writeAgentPromptLine();
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.renderAgentInput();
535
+ this.renderModeInput();
429
536
  }
430
537
  break;
431
538
  }
@@ -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.
@@ -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]) {
@@ -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
- /** Register MCP server for bridge tools (shell_cwd, user_shell, shell_recall). Default true. */
26
- enableMcp?: boolean;
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
- enableMcp: true,
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
+ }