agent-sh 0.7.0 → 0.9.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 (86) hide show
  1. package/README.md +28 -33
  2. package/dist/agent/agent-loop.d.ts +31 -8
  3. package/dist/agent/agent-loop.js +277 -66
  4. package/dist/agent/conversation-state.d.ts +41 -9
  5. package/dist/agent/conversation-state.js +340 -17
  6. package/dist/agent/history-file.d.ts +36 -0
  7. package/dist/agent/history-file.js +167 -0
  8. package/dist/agent/nuclear-form.d.ts +41 -0
  9. package/dist/agent/nuclear-form.js +176 -0
  10. package/dist/agent/system-prompt.d.ts +4 -5
  11. package/dist/agent/system-prompt.js +16 -11
  12. package/dist/agent/token-budget.d.ts +13 -0
  13. package/dist/agent/token-budget.js +50 -0
  14. package/dist/agent/tool-protocol.d.ts +83 -0
  15. package/dist/agent/tool-protocol.js +386 -0
  16. package/dist/agent/tools/user-shell.js +4 -1
  17. package/dist/agent/types.d.ts +21 -1
  18. package/dist/context-manager.d.ts +0 -1
  19. package/dist/context-manager.js +5 -110
  20. package/dist/core.d.ts +7 -7
  21. package/dist/core.js +76 -180
  22. package/dist/event-bus.d.ts +40 -0
  23. package/dist/event-bus.js +20 -1
  24. package/dist/extension-loader.d.ts +5 -0
  25. package/dist/extension-loader.js +104 -17
  26. package/dist/extensions/agent-backend.d.ts +13 -0
  27. package/dist/extensions/agent-backend.js +167 -0
  28. package/dist/extensions/command-suggest.d.ts +3 -3
  29. package/dist/extensions/command-suggest.js +4 -3
  30. package/dist/extensions/index.d.ts +19 -0
  31. package/dist/extensions/index.js +25 -0
  32. package/dist/extensions/slash-commands.d.ts +1 -1
  33. package/dist/extensions/slash-commands.js +44 -1
  34. package/dist/extensions/terminal-buffer.d.ts +1 -1
  35. package/dist/extensions/terminal-buffer.js +22 -8
  36. package/dist/extensions/tui-renderer.js +177 -122
  37. package/dist/index.js +14 -20
  38. package/dist/settings.d.ts +25 -2
  39. package/dist/settings.js +25 -4
  40. package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +1 -1
  41. package/dist/{input-handler.js → shell/input-handler.js} +60 -43
  42. package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
  43. package/dist/{output-parser.js → shell/output-parser.js} +1 -1
  44. package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
  45. package/dist/{shell.js → shell/shell.js} +24 -6
  46. package/dist/types.d.ts +49 -32
  47. package/dist/utils/ansi.d.ts +10 -0
  48. package/dist/utils/ansi.js +27 -0
  49. package/dist/utils/compositor.d.ts +62 -0
  50. package/dist/utils/compositor.js +88 -0
  51. package/dist/utils/diff-renderer.js +92 -4
  52. package/dist/utils/floating-panel.d.ts +34 -3
  53. package/dist/utils/floating-panel.js +315 -82
  54. package/dist/utils/handler-registry.d.ts +26 -10
  55. package/dist/utils/handler-registry.js +52 -16
  56. package/dist/utils/line-editor.d.ts +32 -3
  57. package/dist/utils/line-editor.js +218 -36
  58. package/dist/utils/markdown.d.ts +1 -0
  59. package/dist/utils/markdown.js +4 -4
  60. package/dist/utils/message-utils.d.ts +35 -0
  61. package/dist/utils/message-utils.js +75 -0
  62. package/dist/utils/terminal-buffer.d.ts +9 -1
  63. package/dist/utils/terminal-buffer.js +31 -2
  64. package/dist/utils/tool-display.d.ts +1 -0
  65. package/dist/utils/tool-display.js +1 -1
  66. package/dist/utils/tool-interactive.d.ts +12 -0
  67. package/dist/utils/tool-interactive.js +53 -0
  68. package/examples/extensions/ash-acp-bridge/README.md +39 -0
  69. package/examples/extensions/ash-acp-bridge/package.json +23 -0
  70. package/examples/extensions/ash-acp-bridge/src/index.ts +571 -0
  71. package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
  72. package/examples/extensions/ash-mcp-bridge/README.md +72 -0
  73. package/examples/extensions/ash-mcp-bridge/index.ts +154 -0
  74. package/examples/extensions/ash-mcp-bridge/package.json +9 -0
  75. package/examples/extensions/claude-code-bridge/index.ts +77 -1
  76. package/examples/extensions/interactive-prompts.ts +82 -110
  77. package/examples/extensions/overlay-agent.ts +84 -38
  78. package/examples/extensions/peer-mesh.ts +450 -0
  79. package/examples/extensions/pi-bridge/index.ts +87 -2
  80. package/examples/extensions/questionnaire.ts +249 -0
  81. package/examples/extensions/tmux-pane.ts +307 -0
  82. package/examples/extensions/web-access.ts +327 -0
  83. package/package.json +9 -1
  84. package/dist/extensions/overlay-agent.d.ts +0 -11
  85. package/dist/extensions/overlay-agent.js +0 -43
  86. package/examples/extensions/terminal-buffer.ts +0 -184
@@ -0,0 +1,167 @@
1
+ import { AgentLoop } from "../agent/agent-loop.js";
2
+ import { LlmClient } from "../utils/llm-client.js";
3
+ import { resolveProvider, getProviderNames, getSettings } from "../settings.js";
4
+ export default function agentBackend(ctx) {
5
+ const { bus } = ctx;
6
+ // ── Resolve providers ──────────────────────────────────────
7
+ const config = ctx.call("config:get-shell-config") ?? {};
8
+ const settings = getSettings();
9
+ let activeProvider = null;
10
+ const providerRegistry = new Map();
11
+ for (const name of getProviderNames()) {
12
+ const p = resolveProvider(name);
13
+ if (p)
14
+ providerRegistry.set(name, p);
15
+ }
16
+ const providerName = config.provider ?? settings.defaultProvider;
17
+ if (providerName) {
18
+ activeProvider = providerRegistry.get(providerName) ?? null;
19
+ }
20
+ // ── Build modes ────────────────────────────────────────────
21
+ const buildModes = () => {
22
+ const allModes = [];
23
+ for (const [id, p] of providerRegistry) {
24
+ if (!p.apiKey)
25
+ continue;
26
+ for (const model of p.models) {
27
+ const mc = p.modelCapabilities?.get(model);
28
+ allModes.push({
29
+ model,
30
+ provider: id,
31
+ providerConfig: { apiKey: p.apiKey, baseURL: p.baseURL },
32
+ contextWindow: mc?.contextWindow ?? p.contextWindow,
33
+ reasoning: mc?.reasoning,
34
+ supportsReasoningEffort: p.supportsReasoningEffort,
35
+ });
36
+ }
37
+ }
38
+ return allModes;
39
+ };
40
+ const effectiveApiKey = config.apiKey ?? activeProvider?.apiKey;
41
+ const effectiveBaseURL = config.baseURL ?? activeProvider?.baseURL;
42
+ const effectiveModel = config.model ?? activeProvider?.defaultModel;
43
+ let modes = buildModes();
44
+ if (modes.length === 0 && effectiveApiKey && effectiveModel) {
45
+ modes = [{ model: effectiveModel }];
46
+ }
47
+ const initialModeIndex = Math.max(0, modes.findIndex((m) => m.model === effectiveModel && (!activeProvider || m.provider === activeProvider.id)));
48
+ // ── Create LLM client ─────────────────────────────────────
49
+ if (!effectiveApiKey)
50
+ return; // No LLM provider configured — skip
51
+ if (!effectiveModel) {
52
+ bus.emit("ui:error", { message: "No model specified. Use --model or configure a provider with defaultModel in ~/.agent-sh/settings.json" });
53
+ return;
54
+ }
55
+ const llmClient = new LlmClient({
56
+ apiKey: effectiveApiKey,
57
+ baseURL: effectiveBaseURL,
58
+ model: effectiveModel,
59
+ });
60
+ // Expose LLM client for other extensions (e.g. command-suggest)
61
+ ctx.define("llm:get-client", () => llmClient);
62
+ // ── Initial modes (queryable via pipe) ─────────────────────
63
+ bus.onPipe("config:get-initial-modes", () => ({
64
+ modes,
65
+ initialModeIndex,
66
+ }));
67
+ // ── Create agent loop ──────────────────────────────────────
68
+ const agentLoop = new AgentLoop({
69
+ bus,
70
+ contextManager: ctx.contextManager,
71
+ llmClient,
72
+ handlers: { define: ctx.define, advise: ctx.advise, call: ctx.call },
73
+ modes,
74
+ initialModeIndex,
75
+ compositor: ctx.compositor,
76
+ });
77
+ // Register as backend
78
+ bus.emit("agent:register-backend", {
79
+ name: "ash",
80
+ kill: () => agentLoop.kill(),
81
+ start: async () => {
82
+ agentLoop.wire();
83
+ bus.emit("agent:info", {
84
+ name: "ash",
85
+ version: "0.4",
86
+ model: llmClient.model,
87
+ provider: modes[initialModeIndex]?.provider,
88
+ contextWindow: modes[initialModeIndex]?.contextWindow,
89
+ });
90
+ },
91
+ });
92
+ // ── Runtime provider registration ──────────────────────────
93
+ bus.on("provider:register", (p) => {
94
+ const rawModels = p.models ?? (p.defaultModel ? [p.defaultModel] : []);
95
+ const modelIds = [];
96
+ const caps = new Map();
97
+ for (const m of rawModels) {
98
+ if (typeof m === "string") {
99
+ modelIds.push(m);
100
+ }
101
+ else {
102
+ modelIds.push(m.id);
103
+ caps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow });
104
+ }
105
+ }
106
+ providerRegistry.set(p.id, {
107
+ id: p.id,
108
+ apiKey: p.apiKey,
109
+ baseURL: p.baseURL,
110
+ defaultModel: p.defaultModel,
111
+ models: modelIds,
112
+ supportsReasoningEffort: p.supportsReasoningEffort,
113
+ modelCapabilities: caps.size > 0 ? caps : undefined,
114
+ });
115
+ const addModes = modelIds.map((m) => {
116
+ const mc = caps.get(m);
117
+ return {
118
+ model: m,
119
+ provider: p.id,
120
+ providerConfig: { apiKey: p.apiKey ?? "", baseURL: p.baseURL },
121
+ contextWindow: mc?.contextWindow,
122
+ reasoning: mc?.reasoning,
123
+ supportsReasoningEffort: p.supportsReasoningEffort,
124
+ };
125
+ });
126
+ bus.emit("config:add-modes", { modes: addModes });
127
+ });
128
+ // ── Runtime provider switching ─────────────────────────────
129
+ bus.on("config:switch-provider", ({ provider: name }) => {
130
+ const p = providerRegistry.get(name);
131
+ if (!p) {
132
+ bus.emit("ui:error", { message: `Unknown provider: ${name}` });
133
+ return;
134
+ }
135
+ const newApiKey = p.apiKey;
136
+ if (!newApiKey) {
137
+ bus.emit("ui:error", { message: `Provider "${name}" has no API key configured` });
138
+ return;
139
+ }
140
+ const switchModel = p.defaultModel ?? p.models[0];
141
+ if (!switchModel) {
142
+ bus.emit("ui:error", { message: `Provider "${name}" has no models configured` });
143
+ return;
144
+ }
145
+ llmClient.reconfigure({
146
+ apiKey: newApiKey,
147
+ baseURL: p.baseURL,
148
+ model: switchModel,
149
+ });
150
+ const newModes = p.models.map((m) => {
151
+ const mc = p.modelCapabilities?.get(m);
152
+ return {
153
+ model: m,
154
+ provider: name,
155
+ providerConfig: { apiKey: newApiKey, baseURL: p.baseURL },
156
+ contextWindow: mc?.contextWindow ?? p.contextWindow,
157
+ reasoning: mc?.reasoning,
158
+ supportsReasoningEffort: p.supportsReasoningEffort,
159
+ };
160
+ });
161
+ bus.emit("config:set-modes", { modes: newModes });
162
+ activeProvider = p;
163
+ bus.emit("agent:info", { name: "ash", version: "0.4", model: switchModel, provider: name, contextWindow: p.contextWindow });
164
+ bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
165
+ bus.emit("config:changed", {});
166
+ });
167
+ }
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Command suggestion extension (fast-path LLM feature).
3
3
  *
4
- * After a shell command fails (non-zero exit), uses llmClient.complete()
4
+ * After a shell command fails (non-zero exit), uses LlmClient.complete()
5
5
  * to suggest a fix. Shows the suggestion below the prompt.
6
6
  *
7
- * Only active when llmClient is available (internal agent mode).
7
+ * Only active when an LLM client is available (registered by agent-backend).
8
8
  */
9
9
  import type { ExtensionContext } from "../types.js";
10
- export default function activate({ bus, llmClient }: ExtensionContext): void;
10
+ export default function activate({ bus, call }: ExtensionContext): void;
@@ -1,6 +1,4 @@
1
- export default function activate({ bus, llmClient }) {
2
- if (!llmClient)
3
- return;
1
+ export default function activate({ bus, call }) {
4
2
  let suggesting = false;
5
3
  bus.on("shell:command-done", ({ command, output, exitCode, cwd }) => {
6
4
  if (exitCode === null || exitCode === 0)
@@ -9,6 +7,9 @@ export default function activate({ bus, llmClient }) {
9
7
  return;
10
8
  if (suggesting)
11
9
  return; // don't stack suggestions
10
+ const llmClient = call("llm:get-client");
11
+ if (!llmClient)
12
+ return;
12
13
  suggesting = true;
13
14
  // Truncate output to avoid blowing up the prompt
14
15
  const truncated = output.length > 1000
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Built-in extension manifest.
3
+ *
4
+ * These extensions ship with agent-sh and load before user extensions.
5
+ * They receive unscoped contexts (not reloadable) and can be individually
6
+ * disabled via the `disabledBuiltins` setting in ~/.agent-sh/settings.json.
7
+ */
8
+ import type { ExtensionContext } from "../types.js";
9
+ type ActivateFn = (ctx: ExtensionContext) => void;
10
+ export declare const BUILTIN_EXTENSIONS: Array<{
11
+ name: string;
12
+ load: () => Promise<ActivateFn>;
13
+ }>;
14
+ /**
15
+ * Load built-in extensions sequentially, skipping any in the disabled list.
16
+ * Returns the names of extensions that were loaded.
17
+ */
18
+ export declare function loadBuiltinExtensions(ctx: ExtensionContext, disabled?: string[]): Promise<string[]>;
19
+ export {};
@@ -0,0 +1,25 @@
1
+ export const BUILTIN_EXTENSIONS = [
2
+ { name: "agent-backend", load: () => import("./agent-backend.js").then(m => m.default) },
3
+ { name: "tui-renderer", load: () => import("./tui-renderer.js").then(m => m.default) },
4
+ { name: "slash-commands", load: () => import("./slash-commands.js").then(m => m.default) },
5
+ { name: "file-autocomplete", load: () => import("./file-autocomplete.js").then(m => m.default) },
6
+ { name: "shell-recall", load: () => import("./shell-recall.js").then(m => m.default) },
7
+ { name: "command-suggest", load: () => import("./command-suggest.js").then(m => m.default) },
8
+ { name: "terminal-buffer", load: () => import("./terminal-buffer.js").then(m => m.default) },
9
+ ];
10
+ /**
11
+ * Load built-in extensions sequentially, skipping any in the disabled list.
12
+ * Returns the names of extensions that were loaded.
13
+ */
14
+ export async function loadBuiltinExtensions(ctx, disabled = []) {
15
+ const disabledSet = new Set(disabled);
16
+ const loaded = [];
17
+ for (const ext of BUILTIN_EXTENSIONS) {
18
+ if (disabledSet.has(ext.name))
19
+ continue;
20
+ const activate = await ext.load();
21
+ activate(ctx);
22
+ loaded.push(ext.name);
23
+ }
24
+ return loaded;
25
+ }
@@ -1,2 +1,2 @@
1
1
  import type { ExtensionContext } from "../types.js";
2
- export default function activate({ bus, contextManager }: ExtensionContext): void;
2
+ export default function activate(ctx: ExtensionContext): void;
@@ -12,7 +12,9 @@
12
12
  */
13
13
  import { palette as p } from "../utils/palette.js";
14
14
  import { discoverSkills, loadSkillContent } from "../agent/skills.js";
15
- export default function activate({ bus, contextManager }) {
15
+ import { reloadExtensions } from "../extension-loader.js";
16
+ export default function activate(ctx) {
17
+ const { bus, contextManager } = ctx;
16
18
  const commands = new Map();
17
19
  const register = (cmd) => {
18
20
  const name = cmd.name.startsWith("/") ? cmd.name : `/${cmd.name}`;
@@ -75,6 +77,47 @@ export default function activate({ bus, contextManager }) {
75
77
  }
76
78
  },
77
79
  });
80
+ register({
81
+ name: "/compact",
82
+ description: "Compact conversation (move full content to nuclear summaries)",
83
+ handler: () => {
84
+ bus.emit("agent:compact-request", {});
85
+ },
86
+ });
87
+ register({
88
+ name: "/context",
89
+ description: "Show context budget usage",
90
+ handler: () => {
91
+ const stats = bus.emitPipe("context:get-stats", {
92
+ activeTokens: 0,
93
+ nuclearEntries: 0,
94
+ recallArchiveSize: 0,
95
+ budgetTokens: 0,
96
+ });
97
+ const pct = stats.budgetTokens > 0
98
+ ? Math.round((stats.activeTokens / stats.budgetTokens) * 100)
99
+ : 0;
100
+ const lines = [
101
+ `Active context: ~${stats.activeTokens.toLocaleString()} tokens / ${stats.budgetTokens.toLocaleString()} budget (${pct}%)`,
102
+ `Nuclear entries: ${stats.nuclearEntries} in-context`,
103
+ `Recall archive: ${stats.recallArchiveSize} entries`,
104
+ ];
105
+ bus.emit("ui:info", { message: lines.join("\n") });
106
+ },
107
+ });
108
+ register({
109
+ name: "/reload",
110
+ description: "Reload user extensions from ~/.agent-sh/extensions/",
111
+ handler: async () => {
112
+ const names = await reloadExtensions(ctx);
113
+ if (names.length > 0) {
114
+ bus.emit("ui:info", { message: `Reloaded: ${names.join(", ")}` });
115
+ }
116
+ else {
117
+ bus.emit("ui:info", { message: "No extensions to reload." });
118
+ }
119
+ },
120
+ });
78
121
  // ── Extension registration ────────────────────────────────────
79
122
  bus.on("command:register", (cmd) => {
80
123
  register(cmd);
@@ -11,4 +11,4 @@
11
11
  * Requires: npm install @xterm/headless@5.5.0 @xterm/addon-serialize@0.13.0
12
12
  */
13
13
  import type { ExtensionContext } from "../types.js";
14
- export default function activate({ bus, terminalBuffer: tb, registerTool }: ExtensionContext): void;
14
+ export default function activate(ctx: ExtensionContext): void;
@@ -19,17 +19,26 @@ function interpretEscapes(str) {
19
19
  function settle(ms = 100) {
20
20
  return new Promise((resolve) => setTimeout(resolve, ms));
21
21
  }
22
- export default function activate({ bus, terminalBuffer: tb, registerTool }) {
22
+ export default function activate(ctx) {
23
+ const { bus, terminalBuffer: tb, registerTool, registerInstruction } = ctx;
23
24
  if (!tb)
24
25
  return; // @xterm/headless not installed
25
26
  registerTool({
26
27
  name: "terminal_read",
27
- description: "Read the current terminal screen contents. Returns clean text (ANSI stripped) " +
28
+ description: "Read what is currently visible on the user's terminal screen. Returns clean text (ANSI stripped) " +
28
29
  "with cursor position and whether an alternate-screen program (vim, htop, less) is active. " +
29
- "Use this to see what the user sees before sending keystrokes with terminal_keys.",
30
+ "Use this to observe what the user sees helpful for answering questions about terminal output, " +
31
+ "diagnosing errors on screen, or checking state before/after sending keystrokes with terminal_keys.",
30
32
  input_schema: {
31
33
  type: "object",
32
- properties: {},
34
+ properties: {
35
+ include_scrollback: {
36
+ type: "boolean",
37
+ description: "If true, include scrollback buffer (content that scrolled off screen) " +
38
+ "in addition to the visible viewport. Useful for capturing output from " +
39
+ "long-running or streaming commands. Default: false.",
40
+ },
41
+ },
33
42
  },
34
43
  showOutput: true,
35
44
  getDisplayInfo: () => ({
@@ -37,8 +46,9 @@ export default function activate({ bus, terminalBuffer: tb, registerTool }) {
37
46
  icon: "⊞",
38
47
  locations: [],
39
48
  }),
40
- async execute() {
41
- const { text, altScreen, cursorX, cursorY } = tb.readScreen();
49
+ async execute(args) {
50
+ const includeScrollback = args.include_scrollback ?? false;
51
+ const { text, altScreen, cursorX, cursorY } = tb.readScreen({ includeScrollback });
42
52
  const info = [
43
53
  altScreen ? "mode: alternate screen" : "mode: normal",
44
54
  `cursor: row=${cursorY} col=${cursorX}`,
@@ -52,8 +62,11 @@ export default function activate({ bus, terminalBuffer: tb, registerTool }) {
52
62
  });
53
63
  registerTool({
54
64
  name: "terminal_keys",
55
- description: "Send keystrokes to the user's live terminal. The keys are written directly to the PTY " +
56
- "as if the user typed them. Use escape sequences for special keys:\n" +
65
+ description: "Send keystrokes directly into the user's live terminal PTY, as if the user typed them. " +
66
+ "Use this to interact with programs already running in the terminal (vim, htop, less, ssh, REPLs, etc.) " +
67
+ "or to type commands at the shell prompt. Do NOT use user_shell for this — user_shell runs a new " +
68
+ "command in a subshell, while terminal_keys types into whatever is currently on screen.\n\n" +
69
+ "Escape sequences for special keys:\n" +
57
70
  " - Escape: \\x1b\n" +
58
71
  " - Enter/Return: \\r\n" +
59
72
  " - Tab: \\t\n" +
@@ -105,6 +118,7 @@ export default function activate({ bus, terminalBuffer: tb, registerTool }) {
105
118
  process.stdout.write("\n");
106
119
  bus.emit("shell:pty-write", { data: keys });
107
120
  await settle(settleMs);
121
+ bus.emit("shell:stdout-hide", {});
108
122
  const { text, altScreen, cursorX, cursorY } = tb.readScreen();
109
123
  const info = [
110
124
  altScreen ? "mode: alternate screen" : "mode: normal",