agent-sh 0.8.0 → 0.10.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 (106) hide show
  1. package/README.md +27 -43
  2. package/dist/agent/agent-loop.d.ts +69 -6
  3. package/dist/agent/agent-loop.js +954 -153
  4. package/dist/agent/conversation-state.d.ts +74 -21
  5. package/dist/agent/conversation-state.js +361 -150
  6. package/dist/agent/history-file.d.ts +13 -4
  7. package/dist/agent/history-file.js +110 -36
  8. package/dist/agent/nuclear-form.d.ts +28 -3
  9. package/dist/agent/nuclear-form.js +88 -6
  10. package/dist/agent/skills.d.ts +2 -4
  11. package/dist/agent/skills.js +10 -4
  12. package/dist/agent/subagent.d.ts +23 -0
  13. package/dist/agent/subagent.js +53 -11
  14. package/dist/agent/system-prompt.d.ts +37 -5
  15. package/dist/agent/system-prompt.js +100 -67
  16. package/dist/{token-budget.d.ts → agent/token-budget.d.ts} +5 -4
  17. package/dist/{token-budget.js → agent/token-budget.js} +15 -20
  18. package/dist/agent/tool-protocol.d.ts +105 -0
  19. package/dist/agent/tool-protocol.js +551 -0
  20. package/dist/agent/tools/bash.js +3 -3
  21. package/dist/agent/tools/edit-file.js +9 -6
  22. package/dist/agent/tools/glob.js +4 -2
  23. package/dist/agent/tools/grep.js +27 -3
  24. package/dist/agent/tools/ls.js +5 -6
  25. package/dist/agent/types.d.ts +22 -2
  26. package/dist/context-manager.d.ts +17 -0
  27. package/dist/context-manager.js +37 -4
  28. package/dist/core.d.ts +7 -7
  29. package/dist/core.js +99 -196
  30. package/dist/event-bus.d.ts +85 -2
  31. package/dist/event-bus.js +20 -1
  32. package/dist/executor.d.ts +4 -3
  33. package/dist/executor.js +18 -15
  34. package/dist/extension-loader.d.ts +5 -0
  35. package/dist/extension-loader.js +143 -19
  36. package/dist/extensions/agent-backend.d.ts +14 -0
  37. package/dist/extensions/agent-backend.js +188 -0
  38. package/dist/extensions/command-suggest.d.ts +3 -3
  39. package/dist/extensions/command-suggest.js +4 -3
  40. package/dist/extensions/index.d.ts +19 -0
  41. package/dist/extensions/index.js +24 -0
  42. package/dist/extensions/slash-commands.d.ts +1 -1
  43. package/dist/extensions/slash-commands.js +30 -10
  44. package/dist/extensions/tui-renderer.js +117 -113
  45. package/dist/index.js +39 -26
  46. package/dist/settings.d.ts +40 -3
  47. package/dist/settings.js +57 -10
  48. package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +3 -2
  49. package/dist/{input-handler.js → shell/input-handler.js} +111 -85
  50. package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
  51. package/dist/{output-parser.js → shell/output-parser.js} +1 -1
  52. package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
  53. package/dist/{shell.js → shell/shell.js} +39 -8
  54. package/dist/types.d.ts +61 -10
  55. package/dist/utils/ansi.d.ts +5 -0
  56. package/dist/utils/ansi.js +1 -1
  57. package/dist/utils/compositor.d.ts +67 -0
  58. package/dist/utils/compositor.js +116 -0
  59. package/dist/utils/diff-renderer.d.ts +9 -0
  60. package/dist/utils/diff-renderer.js +312 -146
  61. package/dist/utils/diff.d.ts +21 -2
  62. package/dist/utils/diff.js +165 -89
  63. package/dist/utils/floating-panel.d.ts +2 -0
  64. package/dist/utils/floating-panel.js +30 -14
  65. package/dist/utils/handler-registry.d.ts +31 -10
  66. package/dist/utils/handler-registry.js +58 -16
  67. package/dist/utils/line-editor.d.ts +33 -3
  68. package/dist/utils/line-editor.js +221 -44
  69. package/dist/utils/markdown.d.ts +1 -0
  70. package/dist/utils/markdown.js +1 -1
  71. package/dist/utils/message-utils.d.ts +35 -0
  72. package/dist/utils/message-utils.js +75 -0
  73. package/dist/utils/terminal-buffer.d.ts +5 -1
  74. package/dist/utils/terminal-buffer.js +18 -2
  75. package/dist/utils/tool-display.d.ts +1 -1
  76. package/dist/utils/tool-display.js +4 -4
  77. package/dist/utils/tool-interactive.d.ts +12 -0
  78. package/dist/utils/tool-interactive.js +53 -0
  79. package/examples/extensions/ash-acp-bridge/README.md +39 -0
  80. package/examples/extensions/ash-acp-bridge/package.json +23 -0
  81. package/examples/extensions/ash-acp-bridge/src/index.ts +574 -0
  82. package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
  83. package/examples/extensions/ash-mcp-bridge/README.md +72 -0
  84. package/examples/extensions/ash-mcp-bridge/index.ts +164 -0
  85. package/examples/extensions/ash-mcp-bridge/package.json +9 -0
  86. package/examples/extensions/claude-code-bridge/index.ts +198 -51
  87. package/examples/extensions/claude-code-bridge/package.json +1 -0
  88. package/examples/extensions/interactive-prompts.ts +98 -112
  89. package/examples/extensions/overlay-agent.ts +84 -38
  90. package/examples/extensions/peer-mesh.ts +565 -0
  91. package/examples/extensions/pi-bridge/index.ts +2 -2
  92. package/examples/extensions/questionnaire.ts +260 -0
  93. package/examples/extensions/subagents.ts +19 -4
  94. package/examples/extensions/terminal-buffer.ts +32 -53
  95. package/examples/extensions/tmux-pane.ts +307 -0
  96. package/examples/extensions/user-shell.ts +136 -0
  97. package/examples/extensions/web-access.ts +335 -0
  98. package/package.json +44 -2
  99. package/dist/agent/tools/display.d.ts +0 -13
  100. package/dist/agent/tools/display.js +0 -70
  101. package/dist/agent/tools/user-shell.d.ts +0 -13
  102. package/dist/agent/tools/user-shell.js +0 -87
  103. package/dist/extensions/overlay-agent.d.ts +0 -14
  104. package/dist/extensions/overlay-agent.js +0 -147
  105. package/dist/extensions/terminal-buffer.d.ts +0 -14
  106. package/dist/extensions/terminal-buffer.js +0 -125
@@ -4,34 +4,71 @@
4
4
  * Adds permission gates for tool calls and file writes.
5
5
  * Without this extension, agent-sh runs in yolo mode (auto-approve).
6
6
  *
7
+ * Uses the interactive UI primitive for compositor-aware, themed rendering.
8
+ *
7
9
  * Usage:
8
- * # Load by short name (built-in):
9
- * agent-sh --extensions interactive-prompts
10
+ * agent-sh -e ./examples/extensions/interactive-prompts.ts
10
11
  *
11
12
  * # Or copy to ~/.agent-sh/extensions/ for permanent use:
12
13
  * cp examples/extensions/interactive-prompts.ts ~/.agent-sh/extensions/
13
- *
14
- * # Or install as an npm package and load by name:
15
- * agent-sh --extensions my-prompts-package
16
14
  */
17
15
  import { renderDiff } from "agent-sh/utils/diff-renderer.js";
18
16
  import { renderBoxFrame } from "agent-sh/utils/box-frame.js";
19
17
  import { palette as p } from "agent-sh/utils/palette.js";
20
18
  import type { ExtensionContext } from "agent-sh/types";
19
+ import type { ToolUI } from "agent-sh/agent/types.js";
21
20
 
22
- export default function activate({ bus }: ExtensionContext) {
21
+ export default function activate(ctx: ExtensionContext) {
23
22
  let autoApproveWrites = false;
24
23
 
24
+ // Advise the TUI diff renderer to add permission prompt framing.
25
+ // This replaces the default plain diff box with one that has a warning
26
+ // border and key hints, so only one diff box is shown (not two).
27
+ ctx.advise("tui:render-diff", (next, filePath: string, diff: any, width: number) => {
28
+ const boxW = Math.min(84, width);
29
+ const contentW = boxW - 4;
30
+ const MAX_DISPLAY = 25;
31
+
32
+ const stats = diff.isNewFile
33
+ ? `(+${diff.added} lines)`
34
+ : `(+${diff.added} / -${diff.removed})`;
35
+ const title = diff.isNewFile
36
+ ? `new: ${filePath} ${stats}`
37
+ : `${filePath} ${stats}`;
38
+
39
+ const diffLines = renderDiff(diff, {
40
+ width: contentW,
41
+ filePath,
42
+ maxLines: MAX_DISPLAY,
43
+ trueColor: true,
44
+ mode: "unified",
45
+ });
46
+ const content = ["", ...diffLines.slice(1), ""];
47
+
48
+ return renderBoxFrame(content, {
49
+ width: boxW,
50
+ style: "rounded",
51
+ borderColor: p.warning,
52
+ title,
53
+ footer: [` ${p.bold}[y] Apply [n] Skip [a] Don't ask again${p.reset}`],
54
+ });
55
+ });
56
+
57
+ const { bus } = ctx;
58
+
25
59
  bus.onPipeAsync("permission:request", async (payload) => {
60
+ const ui = payload.ui as ToolUI | undefined;
61
+ if (!ui) return payload;
62
+
26
63
  switch (payload.kind) {
27
64
  case "tool-call":
28
- return handleToolCallPermission(payload);
65
+ return handleToolCall(payload, ui);
29
66
  case "file-write": {
30
67
  if (autoApproveWrites) {
31
- return { ...payload, decision: { approved: true } };
68
+ return { ...payload, decision: { outcome: "approved" } };
32
69
  }
33
- const result = await handleFileWritePermission(payload);
34
- if (result.decision.autoApprove) {
70
+ const result = await handleFileWrite(payload, ui);
71
+ if ((result.decision as any).autoApprove) {
35
72
  autoApproveWrites = true;
36
73
  }
37
74
  return result;
@@ -42,120 +79,69 @@ export default function activate({ bus }: ExtensionContext) {
42
79
  });
43
80
  }
44
81
 
45
- async function handleToolCallPermission(payload) {
82
+ async function handleToolCall(payload: any, ui: ToolUI) {
46
83
  const options = payload.metadata.options;
47
- const answer = await promptPermission(payload.title);
84
+
85
+ const answer = await ui.custom<"approve" | "approve_all" | "deny">({
86
+ render(width) {
87
+ const boxW = Math.min(84, width);
88
+ return renderBoxFrame(
89
+ [`${p.bold}⚠ ${payload.title}${p.reset}`],
90
+ {
91
+ width: boxW,
92
+ style: "rounded",
93
+ borderColor: p.warning,
94
+ title: "Permission required",
95
+ footer: [` ${p.dim}[y]es / [n]o / [a]llow all${p.reset}`],
96
+ },
97
+ );
98
+ },
99
+ handleInput(data, done) {
100
+ const ch = data.toLowerCase();
101
+ if (ch === "y") done("approve");
102
+ else if (ch === "a") done("approve_all");
103
+ else if (ch === "n" || ch === "\x1b") done("deny");
104
+ },
105
+ });
48
106
 
49
107
  if (answer === "approve" || answer === "approve_all") {
50
- const option = answer === "approve_all"
51
- ? options.find((o) => o.kind === "allow_always") ?? options.find((o) => o.kind === "allow_once")
52
- : options.find((o) => o.kind === "allow_once" || o.kind === "allow_always");
108
+ const kind = answer === "approve_all" ? "allow_always" : "allow_once";
109
+ const option = options?.find((o: any) => o.kind === kind)
110
+ ?? options?.find((o: any) => o.kind === "allow_once" || o.kind === "allow_always");
53
111
  if (option) {
54
112
  return { ...payload, decision: { outcome: "selected", optionId: option.optionId } };
55
113
  }
114
+ return { ...payload, decision: { outcome: "approved" } };
56
115
  }
57
116
  return { ...payload, decision: { outcome: "cancelled" } };
58
117
  }
59
118
 
60
- async function handleFileWritePermission(payload) {
61
- const diff = payload.metadata.diff;
62
- const filePath = payload.metadata.path;
63
- const answer = await previewDiff({ path: filePath, diff });
64
- if (answer === "approve") {
65
- return { ...payload, decision: { approved: true } };
66
- }
67
- if (answer === "approve_all") {
68
- return { ...payload, decision: { approved: true, autoApprove: true } };
69
- }
70
- return { ...payload, decision: { approved: false } };
71
- }
72
-
73
- async function promptPermission(title) {
74
- const termW = process.stdout.columns || 80;
75
- const boxW = Math.min(84, termW);
76
-
77
- const framed = renderBoxFrame(
78
- [`${p.bold}⚠ ${title}${p.reset}`],
79
- {
80
- width: boxW,
81
- style: "rounded",
82
- borderColor: p.warning,
83
- title: "Permission required",
84
- footer: [` ${p.dim}[y]es / [n]o / [a]llow all${p.reset}`],
119
+ async function handleFileWrite(payload: any, ui: ToolUI) {
120
+ const answer = await ui.custom<"approve" | "approve_all" | "reject">({
121
+ render(width) {
122
+ const boxW = Math.min(84, width);
123
+ // Just show the prompt actions — the diff itself was already rendered
124
+ // by our advise on "tui:render-diff".
125
+ return renderBoxFrame([], {
126
+ width: boxW,
127
+ style: "rounded",
128
+ borderColor: p.warning,
129
+ footer: [` ${p.bold}[y] Apply [n] Skip [a] Don't ask again${p.reset}`],
130
+ });
131
+ },
132
+ handleInput(data, done) {
133
+ const ch = data.toLowerCase();
134
+ if (ch === "y") done("approve");
135
+ else if (ch === "a") done("approve_all");
136
+ else if (ch === "n" || ch === "\x1b") done("reject");
85
137
  },
86
- );
87
-
88
- process.stdout.write("\n");
89
- for (const line of framed) {
90
- process.stdout.write(line + "\n");
91
- }
92
- process.stdout.write(" ");
93
-
94
- return new Promise((resolve) => {
95
- const handler = (data) => {
96
- const ch = data.toString("utf-8").toLowerCase();
97
- process.stdin.removeListener("data", handler);
98
- process.stdout.write("\n");
99
-
100
- if (ch === "y") resolve("approve");
101
- else if (ch === "a") resolve("approve_all");
102
- else resolve(null);
103
- };
104
- process.stdin.on("data", handler);
105
- });
106
- }
107
-
108
- async function previewDiff(opts) {
109
- const termW = process.stdout.columns || 80;
110
- const boxW = Math.min(84, termW);
111
- const contentW = boxW - 4;
112
- const MAX_DISPLAY = 25;
113
-
114
- const stats = opts.diff.isNewFile
115
- ? `(+${opts.diff.added} lines)`
116
- : `(+${opts.diff.added} / -${opts.diff.removed})`;
117
- const title = opts.diff.isNewFile
118
- ? `new: ${opts.path} ${stats}`
119
- : `${opts.path} ${stats}`;
120
-
121
- const diffLines = renderDiff(opts.diff, {
122
- width: contentW,
123
- filePath: opts.path,
124
- maxLines: MAX_DISPLAY,
125
- trueColor: true,
126
- mode: "unified",
127
- });
128
- const content = ["", ...diffLines.slice(1), ""];
129
-
130
- const framed = renderBoxFrame(content, {
131
- width: boxW,
132
- style: "rounded",
133
- borderColor: p.warning,
134
- title,
135
- footer: [` ${p.bold}[y] Apply [n] Skip [a] Don't ask again${p.reset}`],
136
138
  });
137
139
 
138
- process.stdout.write("\n");
139
- for (const line of framed) {
140
- process.stdout.write(line + "\n");
140
+ if (answer === "approve") {
141
+ return { ...payload, decision: { outcome: "approved" } };
141
142
  }
142
-
143
- return new Promise((resolve) => {
144
- const handler = (data) => {
145
- const ch = data.toString("utf-8").toLowerCase();
146
- process.stdin.removeListener("data", handler);
147
-
148
- if (ch === "y") {
149
- process.stdout.write(` ${p.success}✓ Applied${p.reset}\n`);
150
- resolve("approve");
151
- } else if (ch === "a") {
152
- process.stdout.write(` ${p.success}✓ Applied (auto-approve on)${p.reset}\n`);
153
- resolve("approve_all");
154
- } else {
155
- process.stdout.write(` ${p.error}✗ Skipped${p.reset}\n`);
156
- resolve("reject");
157
- }
158
- };
159
- process.stdin.on("data", handler);
160
- });
143
+ if (answer === "approve_all") {
144
+ return { ...payload, decision: { outcome: "approved", autoApprove: true } };
145
+ }
146
+ return { ...payload, decision: { outcome: "cancelled" } };
161
147
  }
@@ -5,66 +5,112 @@
5
5
  * inside vim, htop, or ssh. Composites a floating response box on top
6
6
  * of the current terminal content.
7
7
  *
8
- * Requires: npm install @xterm/headless@5.5.0 @xterm/addon-serialize@0.13.0
8
+ * Uses createRemoteSession() to route the full tui-renderer pipeline
9
+ * (markdown, tool grouping, spinner, diffs) into the floating panel.
10
+ *
11
+ * Install:
12
+ * cp examples/extensions/overlay-agent.ts ~/.agent-sh/extensions/
9
13
  *
10
- * Usage:
14
+ * Or load directly:
11
15
  * agent-sh -e ./examples/extensions/overlay-agent.ts
12
16
  *
13
- * # Or copy to ~/.agent-sh/extensions/ for permanent use:
14
- * cp examples/extensions/overlay-agent.ts ~/.agent-sh/extensions/
17
+ * Requires: npm install @xterm/headless@5.5.0 @xterm/addon-serialize@0.13.0
15
18
  */
16
- import type { ExtensionContext } from "agent-sh/types";
17
- import { formatScreenContext } from "agent-sh/utils/terminal-buffer.js";
19
+ import type { ExtensionContext, RemoteSession } from "agent-sh/types";
20
+ import type { RenderSurface } from "agent-sh/utils/compositor";
21
+ import { FloatingPanel } from "agent-sh/utils/floating-panel";
18
22
 
19
- const BOLD = "\x1b[1m";
20
- const CYAN = "\x1b[36m";
21
- const RESET = "\x1b[0m";
23
+ /** Adapt a FloatingPanel to the RenderSurface interface. */
24
+ function createPanelSurface(panel: FloatingPanel): RenderSurface {
25
+ return {
26
+ write(text: string): void {
27
+ // Handle \r (carriage return) — overwrite the current line.
28
+ // The spinner uses "\r <content>\x1b[K" to update in-place.
29
+ if (text.startsWith("\r")) {
30
+ // Strip \r and any erase-line sequences
31
+ const cleaned = text.replace(/^\r/, "").replace(/\x1b\[\d*K/g, "");
32
+ if (cleaned.trim()) {
33
+ panel.updateLastLine(() => cleaned);
34
+ }
35
+ return;
36
+ }
37
+
38
+ // Regular text — may contain newlines
39
+ panel.appendText(text);
40
+ },
41
+ writeLine(line: string): void {
42
+ panel.appendLine(line);
43
+ },
44
+ get columns(): number {
45
+ return panel.computeGeometry().contentW;
46
+ },
47
+ };
48
+ }
22
49
 
23
- export default function activate({ bus, advise, createFloatingPanel, terminalBuffer }: ExtensionContext): void {
24
- const panel = createFloatingPanel({
50
+ export default function activate(ctx: ExtensionContext): void {
51
+ const { bus, registerInstruction, createRemoteSession, terminalBuffer } = ctx;
52
+
53
+ const panel = new FloatingPanel(bus, {
25
54
  trigger: "\x1c", // Ctrl+\
26
55
  dimBackground: true,
27
- autoDismissMs: 2000,
56
+ terminalBuffer: terminalBuffer ?? undefined,
28
57
  });
29
58
 
30
- // ── Inject terminal buffer into agent context ──────────────
31
- if (terminalBuffer) {
32
- advise("context:build-extra", (next: () => string) =>
33
- formatScreenContext(terminalBuffer.readScreen(), 80, next()),
34
- );
35
- }
59
+ const panelSurface = createPanelSurface(panel);
60
+ let session: RemoteSession | null = null;
61
+
62
+ registerInstruction("Interactive Overlay Sessions", [
63
+ "When the dynamic context includes `interactive-session: true`, the user has summoned you",
64
+ "via a hotkey overlay from inside their live terminal. They may be in the middle of using",
65
+ "a program (vim, ssh, a REPL, etc.) or at a shell prompt. In this mode:",
66
+ "- Start with terminal_read if you need to understand what's on screen.",
67
+ "- Prefer terminal_keys to interact with whatever is currently running.",
68
+ "- Use user_shell only for running new, standalone commands — not for interacting with",
69
+ " what's already on screen.",
70
+ "- Keep responses concise — the user is in the middle of a workflow.",
71
+ ].join("\n"));
72
+
73
+ // ── Panel lifecycle ────────────────────────────────────────────
36
74
 
37
- // ── Panel lifecycle ────────────────────────────────────────
38
75
  panel.handlers.advise("panel:submit", (_next, query: string) => {
76
+ if (!session) {
77
+ session = createRemoteSession({
78
+ surface: panelSurface,
79
+ suppressQueryBox: true,
80
+ interactive: true,
81
+ });
82
+ }
39
83
  panel.setActive();
40
- panel.appendLine(`${CYAN}${BOLD}❯${RESET} ${query}`);
41
- panel.appendLine("");
42
- bus.emit("agent:submit", { query });
84
+ session.submit(query);
43
85
  });
44
86
 
45
- // ── Stream agent response into panel ───────────────────────
46
- bus.on("agent:response-chunk", (e) => {
47
- if (!panel.active) return;
48
- for (const block of e.blocks) {
49
- if (block.type === "text" && block.text) {
50
- panel.appendText(block.text);
51
- }
87
+ panel.handlers.advise("panel:show", (_next) => {
88
+ // Re-establish session if panel is shown while agent is still working
89
+ if (panel.active && !session) {
90
+ session = createRemoteSession({
91
+ surface: panelSurface,
92
+ suppressQueryBox: true,
93
+ interactive: true,
94
+ });
52
95
  }
53
96
  });
54
97
 
55
- bus.on("agent:tool-started", (e) => {
56
- if (!panel.active) return;
57
- panel.appendLine(`▶ ${e.title}${e.displayDetail ? " " + e.displayDetail : ""}`);
58
- });
59
-
60
- bus.on("agent:tool-completed", (e) => {
61
- if (!panel.active) return;
62
- const mark = e.exitCode === 0 ? " ✓" : ` ✗ exit ${e.exitCode}`;
63
- panel.updateLastLine((line) => line + mark);
98
+ // On dismiss: close session only if agent is not actively processing.
99
+ // If agent is still working (phase="active"), keep session alive so
100
+ // output buffers in the panel and agent can keep executing tools.
101
+ panel.handlers.advise("panel:dismiss", (next) => {
102
+ next();
103
+ if (session && !panel.processing) {
104
+ session.close();
105
+ session = null;
106
+ }
64
107
  });
65
108
 
66
109
  bus.on("agent:processing-done", () => {
67
110
  if (!panel.active) return;
68
111
  panel.setDone();
112
+ // If panel was hidden while processing (passthrough), setDone()
113
+ // triggers dismiss() which closes the session above.
114
+ // If panel is still visible, session stays for the follow-up prompt.
69
115
  });
70
116
  }