agent-sh 0.12.26 → 0.13.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 (144) hide show
  1. package/README.md +13 -2
  2. package/dist/agent/agent-loop.d.ts +3 -5
  3. package/dist/agent/agent-loop.js +44 -100
  4. package/dist/agent/conversation-state.d.ts +9 -0
  5. package/dist/agent/conversation-state.js +38 -1
  6. package/dist/agent/history-file.d.ts +6 -0
  7. package/dist/agent/history-file.js +1 -1
  8. package/dist/agent/host-types.d.ts +125 -0
  9. package/dist/agent/index.d.ts +12 -4
  10. package/dist/agent/index.js +357 -6
  11. package/dist/agent/nuclear-form.d.ts +7 -0
  12. package/dist/{extensions → agent}/providers/deepseek.d.ts +2 -2
  13. package/dist/{extensions → agent}/providers/deepseek.js +5 -4
  14. package/dist/{extensions → agent}/providers/openai-compatible.d.ts +2 -2
  15. package/dist/{extensions → agent}/providers/openai.d.ts +2 -2
  16. package/dist/{extensions → agent}/providers/openai.js +3 -2
  17. package/dist/{extensions → agent}/providers/openrouter.d.ts +2 -2
  18. package/dist/{extensions → agent}/providers/openrouter.js +4 -3
  19. package/dist/agent/skills.js +51 -7
  20. package/dist/agent/subagent.d.ts +1 -1
  21. package/dist/agent/system-prompt.js +14 -17
  22. package/dist/agent/tool-protocol.d.ts +1 -1
  23. package/dist/agent/tool-protocol.js +5 -3
  24. package/dist/agent/tool-registry.d.ts +9 -4
  25. package/dist/agent/tool-registry.js +27 -4
  26. package/dist/agent/tools/bash.d.ts +1 -1
  27. package/dist/agent/tools/bash.js +3 -2
  28. package/dist/agent/tools/edit-file.js +0 -1
  29. package/dist/agent/tools/glob.js +1 -1
  30. package/dist/agent/tools/grep.js +1 -1
  31. package/dist/agent/tools/pwsh.d.ts +1 -1
  32. package/dist/agent/tools/pwsh.js +1 -2
  33. package/dist/agent/tools/read-file.js +7 -4
  34. package/dist/agent/tools/write-file.js +0 -1
  35. package/dist/agent/types.d.ts +17 -2
  36. package/dist/cli/auth/cli.d.ts +1 -0
  37. package/dist/cli/auth/cli.js +216 -0
  38. package/dist/cli/auth/keys.d.ts +31 -0
  39. package/dist/cli/auth/keys.js +102 -0
  40. package/dist/{index.js → cli/index.js} +29 -32
  41. package/dist/{init.js → cli/init.js} +1 -1
  42. package/dist/{install.js → cli/install.js} +114 -5
  43. package/dist/cli/subcommands.d.ts +1 -0
  44. package/dist/cli/subcommands.js +17 -0
  45. package/dist/{event-bus.d.ts → core/event-bus.d.ts} +7 -13
  46. package/dist/{extension-loader.d.ts → core/extension-loader.d.ts} +1 -1
  47. package/dist/{extension-loader.js → core/extension-loader.js} +62 -70
  48. package/dist/{core.d.ts → core/index.d.ts} +18 -15
  49. package/dist/{core.js → core/index.js} +18 -92
  50. package/dist/{settings.d.ts → core/settings.d.ts} +7 -0
  51. package/dist/{settings.js → core/settings.js} +1 -0
  52. package/dist/core/types.d.ts +49 -0
  53. package/dist/core/types.js +1 -0
  54. package/dist/extensions/file-autocomplete.d.ts +1 -1
  55. package/dist/extensions/index.d.ts +7 -14
  56. package/dist/extensions/index.js +2 -19
  57. package/dist/extensions/slash-commands.d.ts +1 -1
  58. package/dist/extensions/slash-commands.js +7 -2
  59. package/dist/shell/host-types.d.ts +114 -0
  60. package/dist/shell/host-types.js +1 -0
  61. package/dist/shell/index.d.ts +8 -7
  62. package/dist/shell/index.js +58 -9
  63. package/dist/shell/input-handler.d.ts +7 -1
  64. package/dist/shell/input-handler.js +5 -2
  65. package/dist/shell/output-parser.d.ts +1 -1
  66. package/dist/{extensions → shell}/shell-context.d.ts +1 -1
  67. package/dist/{extensions → shell}/shell-context.js +18 -12
  68. package/dist/shell/shell.d.ts +6 -4
  69. package/dist/shell/shell.js +33 -109
  70. package/dist/shell/strategies/bash.d.ts +2 -0
  71. package/dist/shell/strategies/bash.js +68 -0
  72. package/dist/shell/strategies/fish.d.ts +2 -0
  73. package/dist/shell/strategies/fish.js +65 -0
  74. package/dist/shell/strategies/index.d.ts +13 -0
  75. package/dist/shell/strategies/index.js +17 -0
  76. package/dist/shell/strategies/types.d.ts +50 -0
  77. package/dist/shell/strategies/types.js +9 -0
  78. package/dist/shell/strategies/zsh.d.ts +2 -0
  79. package/dist/shell/strategies/zsh.js +72 -0
  80. package/dist/shell/tui-input-view.js +14 -3
  81. package/dist/{extensions → shell}/tui-renderer.d.ts +1 -1
  82. package/dist/{extensions → shell}/tui-renderer.js +27 -55
  83. package/dist/utils/box-frame.d.ts +4 -0
  84. package/dist/utils/box-frame.js +17 -6
  85. package/dist/utils/compositor.d.ts +1 -1
  86. package/dist/utils/compositor.js +2 -1
  87. package/dist/{executor.js → utils/executor.js} +1 -1
  88. package/dist/utils/floating-panel.d.ts +17 -5
  89. package/dist/utils/floating-panel.js +218 -70
  90. package/dist/utils/llm-facade.d.ts +7 -3
  91. package/dist/utils/stream-transform.d.ts +1 -1
  92. package/dist/utils/terminal-buffer.d.ts +1 -1
  93. package/dist/utils/tool-display.js +4 -0
  94. package/dist/utils/tool-interactive.d.ts +1 -1
  95. package/dist/utils/tty.d.ts +7 -0
  96. package/dist/utils/tty.js +15 -0
  97. package/examples/extensions/ash-acp-bridge/README.md +4 -1
  98. package/examples/extensions/ash-acp-bridge/src/index.ts +654 -0
  99. package/examples/extensions/ash-mcp-bridge/index.ts +1 -1
  100. package/examples/extensions/ashi/README.md +250 -0
  101. package/examples/extensions/ashi/package.json +60 -0
  102. package/examples/extensions/ashi/src/autocomplete.ts +91 -0
  103. package/examples/extensions/ashi/src/capture.ts +34 -0
  104. package/examples/extensions/ashi/src/cli.ts +126 -0
  105. package/examples/extensions/ashi/src/commands.ts +82 -0
  106. package/examples/extensions/ashi/src/compaction.ts +157 -0
  107. package/examples/extensions/ashi/src/components.ts +332 -0
  108. package/examples/extensions/ashi/src/default-renderers.ts +153 -0
  109. package/examples/extensions/ashi/src/display-config.ts +62 -0
  110. package/examples/extensions/ashi/src/frontend.ts +735 -0
  111. package/examples/extensions/ashi/src/hooks.ts +136 -0
  112. package/examples/extensions/ashi/src/multi-session-store.ts +146 -0
  113. package/examples/extensions/ashi/src/session-commands.ts +76 -0
  114. package/examples/extensions/ashi/src/session-store.ts +264 -0
  115. package/examples/extensions/ashi/src/status-footer.ts +66 -0
  116. package/examples/extensions/ashi/src/theme.ts +151 -0
  117. package/examples/extensions/ashi/tsconfig.json +14 -0
  118. package/examples/extensions/emacs-buffer.ts +364 -0
  119. package/examples/extensions/interactive-prompts.ts +114 -69
  120. package/examples/extensions/latex-images.ts +3 -3
  121. package/examples/extensions/opencode-bridge/index.ts +1 -1
  122. package/examples/extensions/overlay-agent.ts +35 -10
  123. package/examples/extensions/peer-mesh.ts +1 -1
  124. package/examples/extensions/pi-bridge/index.ts +0 -1
  125. package/examples/extensions/questionnaire.ts +2 -1
  126. package/examples/extensions/rtk-proxy.ts +3 -3
  127. package/examples/extensions/solarized-theme.ts +3 -3
  128. package/examples/extensions/subagents.ts +6 -6
  129. package/examples/extensions/terminal-buffer.ts +174 -33
  130. package/examples/extensions/tmux-pane.ts +6 -4
  131. package/examples/extensions/tunnel-vision.ts +405 -0
  132. package/examples/extensions/user-shell.ts +1 -1
  133. package/examples/extensions/web-access.ts +8 -113
  134. package/package.json +26 -22
  135. package/dist/extensions/agent-backend.d.ts +0 -14
  136. package/dist/extensions/agent-backend.js +0 -307
  137. package/dist/types.d.ts +0 -227
  138. /package/dist/{types.js → agent/host-types.js} +0 -0
  139. /package/dist/{extensions → agent}/providers/openai-compatible.js +0 -0
  140. /package/dist/{index.d.ts → cli/index.d.ts} +0 -0
  141. /package/dist/{init.d.ts → cli/init.d.ts} +0 -0
  142. /package/dist/{install.d.ts → cli/install.d.ts} +0 -0
  143. /package/dist/{event-bus.js → core/event-bus.js} +0 -0
  144. /package/dist/{executor.d.ts → utils/executor.d.ts} +0 -0
@@ -16,25 +16,129 @@
16
16
  */
17
17
  import type { ExtensionContext } from "agent-sh/types";
18
18
 
19
- /** Interpret C-style escape sequences (e.g. \r → CR, \x1b ESC). */
20
- function interpretEscapes(str: string): string {
21
- return str.replace(/\\(x[0-9a-fA-F]{2}|r|n|t|\\|0)/g, (_, seq: string) => {
22
- if (seq === "r") return "\r";
23
- if (seq === "n") return "\n";
24
- if (seq === "t") return "\t";
25
- if (seq === "\\") return "\\";
26
- if (seq === "0") return "\0";
27
- if (seq.startsWith("x")) return String.fromCharCode(parseInt(seq.slice(1), 16));
28
- return seq;
29
- });
19
+ const NAMED_KEYS: Record<string, string> = {
20
+ RET: "\r", ENTER: "\r", CR: "\r",
21
+ ESC: "\x1b",
22
+ TAB: "\t",
23
+ BS: "\x7f", BACKSPACE: "\x7f",
24
+ DEL: "\x1b[3~", DELETE: "\x1b[3~",
25
+ SPC: " ", SPACE: " ",
26
+ UP: "\x1b[A", DOWN: "\x1b[B", RIGHT: "\x1b[C", LEFT: "\x1b[D",
27
+ HOME: "\x1b[H", END: "\x1b[F",
28
+ PGUP: "\x1b[5~", PGDN: "\x1b[6~",
29
+ };
30
+
31
+ function ctrlByte(ch: string): string {
32
+ if (ch === " ") return "\x00";
33
+ const code = ch.charCodeAt(0);
34
+ if (code >= 0x40 && code <= 0x7e) return String.fromCharCode(code & 0x1f);
35
+ throw new Error(`Cannot apply Ctrl modifier to ${JSON.stringify(ch)}`);
36
+ }
37
+
38
+ function parseToken(body: string): string {
39
+ if (!body) throw new Error("Empty key token <>");
40
+ const upper = body.toUpperCase();
41
+ if (upper in NAMED_KEYS) return NAMED_KEYS[upper];
42
+
43
+ const fn = /^F(\d{1,2})$/i.exec(body);
44
+ if (fn) {
45
+ const n = parseInt(fn[1], 10);
46
+ const map: Record<number, string> = {
47
+ 1: "\x1bOP", 2: "\x1bOQ", 3: "\x1bOR", 4: "\x1bOS",
48
+ 5: "\x1b[15~", 6: "\x1b[17~", 7: "\x1b[18~", 8: "\x1b[19~",
49
+ 9: "\x1b[20~", 10: "\x1b[21~", 11: "\x1b[23~", 12: "\x1b[24~",
50
+ };
51
+ if (n in map) return map[n];
52
+ throw new Error(`Unknown function key <${body}>`);
53
+ }
54
+
55
+ let rest = body;
56
+ let ctrl = false, meta = false;
57
+ while (true) {
58
+ if (/^C-/i.test(rest)) { ctrl = true; rest = rest.slice(2); }
59
+ else if (/^M-/i.test(rest) || /^A-/i.test(rest)) { meta = true; rest = rest.slice(2); }
60
+ else break;
61
+ }
62
+
63
+ let core: string;
64
+ const restUpper = rest.toUpperCase();
65
+ if (restUpper in NAMED_KEYS) core = NAMED_KEYS[restUpper];
66
+ else if (rest.length === 1) core = rest;
67
+ else throw new Error(`Unparseable key token <${body}>`);
68
+
69
+ if (ctrl) {
70
+ if (core.length !== 1) {
71
+ throw new Error(`Ctrl modifier on multi-byte key <${body}> not supported`);
72
+ }
73
+ core = ctrlByte(core);
74
+ }
75
+ if (meta) core = "\x1b" + core;
76
+ return core;
77
+ }
78
+
79
+ /**
80
+ * Tokenize a `keys` string into chords — atomic units that get written
81
+ * separately when inter_key_ms > 0, so multi-key leaders resolve under
82
+ * the leader timer.
83
+ */
84
+ export function tokenizeKeys(input: string): string[] {
85
+ const chords: string[] = [];
86
+ let i = 0;
87
+ while (i < input.length) {
88
+ const ch = input[i];
89
+ if (ch === "<") {
90
+ const end = input.indexOf(">", i + 1);
91
+ if (end === -1) throw new Error(`Unterminated key token starting at index ${i}`);
92
+ chords.push(parseToken(input.slice(i + 1, end)));
93
+ i = end + 1;
94
+ continue;
95
+ }
96
+ if (ch === "\\" && i + 1 < input.length) {
97
+ const next = input[i + 1];
98
+ if (next === "r") { chords.push("\r"); i += 2; continue; }
99
+ if (next === "n") { chords.push("\n"); i += 2; continue; }
100
+ if (next === "t") { chords.push("\t"); i += 2; continue; }
101
+ if (next === "\\") { chords.push("\\"); i += 2; continue; }
102
+ if (next === "0") { chords.push("\0"); i += 2; continue; }
103
+ if (next === "x" && i + 3 < input.length) {
104
+ const hex = input.slice(i + 2, i + 4);
105
+ if (/^[0-9a-fA-F]{2}$/.test(hex)) {
106
+ chords.push(String.fromCharCode(parseInt(hex, 16)));
107
+ i += 4;
108
+ continue;
109
+ }
110
+ }
111
+ chords.push(ch);
112
+ i += 1;
113
+ continue;
114
+ }
115
+ chords.push(ch);
116
+ i += 1;
117
+ }
118
+ return chords;
30
119
  }
31
120
 
32
121
  function settle(ms = 100): Promise<void> {
33
122
  return new Promise((resolve) => setTimeout(resolve, ms));
34
123
  }
35
124
 
125
+ function diffScreens(before: string, after: string): string {
126
+ const beforeLines = before.split("\n");
127
+ const afterLines = after.split("\n");
128
+ const max = Math.max(beforeLines.length, afterLines.length);
129
+ const changes: string[] = [];
130
+ for (let i = 0; i < max; i++) {
131
+ const a = beforeLines[i] ?? "";
132
+ const b = afterLines[i] ?? "";
133
+ if (a !== b) changes.push(`row ${i}: ${JSON.stringify(a)} → ${JSON.stringify(b)}`);
134
+ }
135
+ if (changes.length === 0) return "(no visible change)";
136
+ if (changes.length > 12) return `${changes.length} rows changed (see full screen below)`;
137
+ return changes.join("\n");
138
+ }
139
+
36
140
  export default function activate(ctx: ExtensionContext): void {
37
- const { bus, registerTool, registerInstruction } = ctx;
141
+ const { bus } = ctx; const { registerTool } = ctx.agent;
38
142
  const tb = ctx.call("terminal-buffer");
39
143
  if (!tb) return; // @xterm/headless not installed, or shell frontend not loaded
40
144
 
@@ -86,17 +190,22 @@ export default function activate(ctx: ExtensionContext): void {
86
190
  description:
87
191
  "Send keystrokes directly into the user's live terminal PTY, as if the user typed them. " +
88
192
  "Use this to interact with programs already running in the terminal (vim, htop, less, ssh, REPLs, etc.) " +
89
- "or to type commands at the shell prompt. This types directly into whatever is currently on screen.\n\n" +
90
- "Escape sequences for special keys:\n" +
91
- " - Escape: \\x1b\n" +
92
- " - Enter/Return: \\r\n" +
93
- " - Tab: \\t\n" +
94
- " - Ctrl+C: \\x03\n" +
95
- " - Ctrl+D: \\x04\n" +
96
- " - Ctrl+Z: \\x1a\n" +
97
- " - Arrow keys: \\x1b[A (up), \\x1b[B (down), \\x1b[C (right), \\x1b[D (left)\n" +
98
- " - Backspace: \\x7f\n\n" +
99
- "Example: to quit vim without saving, send keys=\"\\x1b:q!\\r\" (Escape, :q!, Enter).\n" +
193
+ "or to type commands at the shell prompt.\n\n" +
194
+ "Preferred input: named-key tokens in angle brackets. They are unambiguous and let inter_key_ms " +
195
+ "delay the right boundaries (one chord per token):\n" +
196
+ " <RET> <ESC> <TAB> <BS> <DEL> <SPC>\n" +
197
+ " <UP> <DOWN> <LEFT> <RIGHT> <HOME> <END> <PGUP> <PGDN>\n" +
198
+ " <F1>..<F12>\n" +
199
+ " <C-x> = Ctrl+x, <M-x> = Meta/Alt+x, <C-M-x> = Ctrl+Meta+x\n\n" +
200
+ "Backslash escapes are also accepted for raw bytes: \\r \\n \\t \\xNN.\n\n" +
201
+ "Example: quit vim without saving keys=\"<ESC>:q!<RET>\" (or the older \"\\x1b:q!\\r\").\n\n" +
202
+ "Emacs pitfalls (read before sending keys to a running Emacs):\n" +
203
+ " - Abort is <C-g>, NOT <C-c>. <C-c> is a prefix key in Emacs and will queue garbage.\n" +
204
+ " - Failed multi-key chords get inserted into the buffer as literal text. Send small, " +
205
+ " well-tested sequences and call terminal_read between them to verify.\n" +
206
+ " - Doom/Spacemacs leader sequences (e.g. <SPC> f f) need timing — set inter_key_ms=50 " +
207
+ " or higher so the leader timer can resolve each chord.\n" +
208
+ " - For complex Emacs operations, prefer `emacsclient -e '(...)'` over typing keys.\n\n" +
100
209
  "Always call terminal_read after sending keys to verify the result.",
101
210
  input_schema: {
102
211
  type: "object",
@@ -104,14 +213,20 @@ export default function activate(ctx: ExtensionContext): void {
104
213
  keys: {
105
214
  type: "string",
106
215
  description:
107
- "The keystrokes to send. Use \\x1b for Escape, \\r for Enter, \\t for Tab, " +
108
- "\\x03 for Ctrl+C, etc. Regular characters are sent as-is.",
216
+ "The keystrokes to send. Prefer named tokens like <C-g>, <RET>, <ESC>, <SPC>. " +
217
+ "Backslash escapes (\\r, \\t, \\x1b) and raw characters are also accepted.",
109
218
  },
110
219
  settle_ms: {
111
220
  type: "number",
112
221
  description:
113
- "Milliseconds to wait after sending keys for the terminal to settle before " +
114
- "returning (default: 150). Increase for slow programs.",
222
+ "Milliseconds to wait after the last chord for the terminal to settle before " +
223
+ "snapshotting the screen (default: 150). Increase for slow programs.",
224
+ },
225
+ inter_key_ms: {
226
+ type: "number",
227
+ description:
228
+ "Milliseconds to wait between chords. Default 0 (send all at once). Set ~50 for " +
229
+ "Doom/Spacemacs leader sequences or any binding that depends on key-chord timeouts.",
115
230
  },
116
231
  },
117
232
  required: ["keys"],
@@ -133,29 +248,55 @@ export default function activate(ctx: ExtensionContext): void {
133
248
  .replace(/\\t|\t/g, "TAB")
134
249
  .replace(/\\x03|\x03/g, "^C")
135
250
  .replace(/\\x04|\x04/g, "^D")
251
+ .replace(/\\x07|\x07/g, "^G")
136
252
  .replace(/\\x7f|\x7f/g, "BS");
137
253
  },
138
254
 
139
255
  async execute(args) {
140
256
  const raw = args.keys as string;
141
- const keys = interpretEscapes(raw);
142
257
  const settleMs = (args.settle_ms as number) ?? 150;
258
+ const interKeyMs = (args.inter_key_ms as number) ?? 0;
259
+
260
+ let chords: string[];
261
+ try {
262
+ chords = tokenizeKeys(raw);
263
+ } catch (e) {
264
+ return {
265
+ content: `Invalid keys argument: ${(e as Error).message}`,
266
+ exitCode: 1,
267
+ isError: true,
268
+ };
269
+ }
270
+
271
+ tb.flush();
272
+ const before = tb.readScreen();
143
273
 
144
274
  bus.emit("shell:stdout-show", {});
145
275
  process.stdout.write("\n");
146
- bus.emit("shell:pty-write", { data: keys });
276
+
277
+ for (let i = 0; i < chords.length; i++) {
278
+ bus.emit("shell:pty-write", { data: chords[i] });
279
+ if (interKeyMs > 0 && i < chords.length - 1) {
280
+ await settle(interKeyMs);
281
+ }
282
+ }
147
283
 
148
284
  await settle(settleMs);
149
285
  bus.emit("shell:stdout-hide", {});
150
286
 
151
- const { text, altScreen, cursorX, cursorY } = tb.readScreen();
287
+ tb.flush();
288
+ const after = tb.readScreen();
152
289
  const info = [
153
- altScreen ? "mode: alternate screen" : "mode: normal",
154
- `cursor: row=${cursorY} col=${cursorX}`,
290
+ after.altScreen ? "mode: alternate screen" : "mode: normal",
291
+ `cursor: row=${after.cursorY} col=${after.cursorX}`,
155
292
  ].join(", ");
293
+ const diff = diffScreens(before.text, after.text);
156
294
 
157
295
  return {
158
- content: `Keys sent. Screen after:\n[${info}]\n\n${text}`,
296
+ content:
297
+ `Keys sent (${chords.length} chord${chords.length === 1 ? "" : "s"}).\n` +
298
+ `Diff:\n${diff}\n\n` +
299
+ `Screen after:\n[${info}]\n\n${after.text}`,
159
300
  exitCode: 0,
160
301
  isError: false,
161
302
  };
@@ -22,7 +22,7 @@ import * as net from "node:net";
22
22
  import * as os from "node:os";
23
23
  import * as path from "node:path";
24
24
  import { execSync, spawn } from "node:child_process";
25
- import type { ExtensionContext, RenderSurface, RemoteSession } from "agent-sh/types";
25
+ import type { AgentContext, ShellContext, RenderSurface, RemoteSession } from "agent-sh/types";
26
26
 
27
27
  // ── Helpers ─────────────────────────────────────────────────────
28
28
 
@@ -142,15 +142,17 @@ interface PaneState {
142
142
 
143
143
  // ── Extension ───────────────────────────────────────────────────
144
144
 
145
- export default function activate(ctx: ExtensionContext): void {
146
- const { bus, registerCommand, registerInstruction, createRemoteSession } = ctx;
145
+ export default function activate(ctx: AgentContext & ShellContext): void {
146
+ const { bus, registerCommand } = ctx;
147
+ const { registerInstruction } = ctx.agent;
148
+ const { createRemoteSession } = ctx.shell;
147
149
 
148
150
  if (!inTmux()) return;
149
151
 
150
152
  let state: PaneState | null = null;
151
153
 
152
154
  // Tell the LLM it's running inside an interactive pane session.
153
- ctx.registerContextProducer("interactive-session", () =>
155
+ ctx.agent.registerContextProducer("interactive-session", () =>
154
156
  state?.mode === "rsplit" ? "interactive-session: true" : null,
155
157
  );
156
158