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
@@ -0,0 +1,188 @@
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
+ /** Read the user's persisted defaultModel for a provider, if any. */
5
+ function persistedModelFor(providerName) {
6
+ if (!providerName)
7
+ return undefined;
8
+ return getSettings().providers?.[providerName]?.defaultModel;
9
+ }
10
+ export default function agentBackend(ctx) {
11
+ const { bus } = ctx;
12
+ const config = ctx.call("config:get-shell-config") ?? {};
13
+ // Seed from settings.json; runtime provider:register events add more.
14
+ const providerRegistry = new Map();
15
+ for (const name of getProviderNames()) {
16
+ const p = resolveProvider(name);
17
+ if (p)
18
+ providerRegistry.set(name, p);
19
+ }
20
+ const buildModes = () => {
21
+ const allModes = [];
22
+ for (const [id, p] of providerRegistry) {
23
+ if (!p.apiKey)
24
+ continue;
25
+ for (const model of p.models) {
26
+ const mc = p.modelCapabilities?.get(model);
27
+ allModes.push({
28
+ model,
29
+ provider: id,
30
+ providerConfig: { apiKey: p.apiKey, baseURL: p.baseURL },
31
+ contextWindow: mc?.contextWindow ?? p.contextWindow,
32
+ reasoning: mc?.reasoning,
33
+ supportsReasoningEffort: p.supportsReasoningEffort,
34
+ });
35
+ }
36
+ }
37
+ return allModes;
38
+ };
39
+ // Placeholder client — reconfigured at core:extensions-loaded. Any
40
+ // stream() call before then fails from the OpenAI SDK; start() won't
41
+ // wire the loop until we've resolved, so users never hit that path.
42
+ const llmClient = new LlmClient({ apiKey: "not-configured", model: "not-configured" });
43
+ ctx.define("llm:get-client", () => llmClient);
44
+ let modes = [];
45
+ let initialModeIndex = 0;
46
+ let resolved = false;
47
+ bus.onPipe("config:get-initial-modes", () => ({ modes, initialModeIndex }));
48
+ // AgentLoop must be constructed *before* user extensions activate,
49
+ // because its ctor defines handlers (history:append, etc.) that
50
+ // extensions like superash call synchronously during their own
51
+ // activate. Advise-before-define works for advisers, but plain calls
52
+ // would hit a no-op stub.
53
+ const agentLoop = new AgentLoop({
54
+ bus,
55
+ contextManager: ctx.contextManager,
56
+ llmClient,
57
+ handlers: { define: ctx.define, advise: ctx.advise, call: ctx.call, list: ctx.list },
58
+ modes,
59
+ initialModeIndex,
60
+ compositor: ctx.compositor,
61
+ instanceId: ctx.instanceId,
62
+ });
63
+ bus.emit("agent:register-backend", {
64
+ name: "ash",
65
+ kill: () => agentLoop.kill(),
66
+ start: async () => {
67
+ if (!resolved) {
68
+ bus.emit("ui:error", { message: "Agent backend not started — no LLM provider available. See earlier messages." });
69
+ return;
70
+ }
71
+ agentLoop.wire();
72
+ bus.emit("agent:info", {
73
+ name: "ash",
74
+ version: "0.4",
75
+ model: llmClient.model,
76
+ provider: modes[initialModeIndex]?.provider,
77
+ contextWindow: modes[initialModeIndex]?.contextWindow,
78
+ });
79
+ },
80
+ });
81
+ bus.on("core:extensions-loaded", () => {
82
+ const settings = getSettings();
83
+ const providerName = config.provider ?? settings.defaultProvider;
84
+ const activeProvider = providerName ? providerRegistry.get(providerName) ?? null : null;
85
+ // User's persisted defaultModel wins over the provider's declared
86
+ // default. Dynamic providers (openrouter) re-register with their
87
+ // hardcoded DEFAULT_MODELS[0] each startup, which would otherwise
88
+ // clobber the user's /model selection.
89
+ const effectiveApiKey = config.apiKey ?? activeProvider?.apiKey;
90
+ const effectiveBaseURL = config.baseURL ?? activeProvider?.baseURL;
91
+ const effectiveModel = config.model ?? persistedModelFor(providerName) ?? activeProvider?.defaultModel;
92
+ if (!effectiveApiKey) {
93
+ bus.emit("ui:error", { message: "No LLM provider configured. Set --api-key, configure a provider in ~/.agent-sh/settings.json, or load a provider extension (e.g. openrouter) that sets OPENROUTER_API_KEY." });
94
+ return;
95
+ }
96
+ if (!effectiveModel) {
97
+ bus.emit("ui:error", { message: "No model specified. Use --model or configure a provider with defaultModel in ~/.agent-sh/settings.json" });
98
+ return;
99
+ }
100
+ modes = buildModes();
101
+ if (modes.length === 0)
102
+ modes = [{ model: effectiveModel }];
103
+ initialModeIndex = Math.max(0, modes.findIndex((m) => m.model === effectiveModel && (!activeProvider || m.provider === activeProvider.id)));
104
+ llmClient.reconfigure({ apiKey: effectiveApiKey, baseURL: effectiveBaseURL, model: effectiveModel });
105
+ bus.emit("config:set-modes", { modes, activeIndex: initialModeIndex });
106
+ resolved = true;
107
+ // start() emits agent:info after wiring.
108
+ });
109
+ bus.on("provider:register", (p) => {
110
+ const rawModels = p.models ?? (p.defaultModel ? [p.defaultModel] : []);
111
+ const modelIds = [];
112
+ const caps = new Map();
113
+ for (const m of rawModels) {
114
+ if (typeof m === "string") {
115
+ modelIds.push(m);
116
+ }
117
+ else {
118
+ modelIds.push(m.id);
119
+ caps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow });
120
+ }
121
+ }
122
+ providerRegistry.set(p.id, {
123
+ id: p.id,
124
+ apiKey: p.apiKey,
125
+ baseURL: p.baseURL,
126
+ defaultModel: p.defaultModel,
127
+ models: modelIds,
128
+ supportsReasoningEffort: p.supportsReasoningEffort,
129
+ modelCapabilities: caps.size > 0 ? caps : undefined,
130
+ });
131
+ const addModes = modelIds.map((m) => {
132
+ const mc = caps.get(m);
133
+ return {
134
+ model: m,
135
+ provider: p.id,
136
+ providerConfig: { apiKey: p.apiKey ?? "", baseURL: p.baseURL },
137
+ contextWindow: mc?.contextWindow,
138
+ reasoning: mc?.reasoning,
139
+ supportsReasoningEffort: p.supportsReasoningEffort,
140
+ };
141
+ });
142
+ bus.emit("config:add-modes", { modes: addModes });
143
+ // Late-registration reconcile: if this completes the user's persisted
144
+ // default (openrouter's async fetch delivers the full catalog after
145
+ // we've already fallen back to mode 0), quietly switch to it.
146
+ if (!resolved)
147
+ return;
148
+ const pendingProvider = getSettings().defaultProvider;
149
+ if (pendingProvider !== p.id)
150
+ return;
151
+ const pendingModel = persistedModelFor(pendingProvider);
152
+ if (pendingModel && modelIds.includes(pendingModel) && llmClient.model !== pendingModel) {
153
+ bus.emit("config:switch-model", { model: pendingModel });
154
+ }
155
+ });
156
+ bus.on("config:switch-provider", ({ provider: name }) => {
157
+ const p = providerRegistry.get(name);
158
+ if (!p) {
159
+ bus.emit("ui:error", { message: `Unknown provider: ${name}` });
160
+ return;
161
+ }
162
+ if (!p.apiKey) {
163
+ bus.emit("ui:error", { message: `Provider "${name}" has no API key configured` });
164
+ return;
165
+ }
166
+ const switchModel = p.defaultModel ?? p.models[0];
167
+ if (!switchModel) {
168
+ bus.emit("ui:error", { message: `Provider "${name}" has no models configured` });
169
+ return;
170
+ }
171
+ llmClient.reconfigure({ apiKey: p.apiKey, baseURL: p.baseURL, model: switchModel });
172
+ const newModes = p.models.map((m) => {
173
+ const mc = p.modelCapabilities?.get(m);
174
+ return {
175
+ model: m,
176
+ provider: name,
177
+ providerConfig: { apiKey: p.apiKey, baseURL: p.baseURL },
178
+ contextWindow: mc?.contextWindow ?? p.contextWindow,
179
+ reasoning: mc?.reasoning,
180
+ supportsReasoningEffort: p.supportsReasoningEffort,
181
+ };
182
+ });
183
+ bus.emit("config:set-modes", { modes: newModes });
184
+ bus.emit("agent:info", { name: "ash", version: "0.4", model: switchModel, provider: name, contextWindow: p.contextWindow });
185
+ bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
186
+ bus.emit("config:changed", {});
187
+ });
188
+ }
@@ -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,24 @@
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
+ ];
9
+ /**
10
+ * Load built-in extensions sequentially, skipping any in the disabled list.
11
+ * Returns the names of extensions that were loaded.
12
+ */
13
+ export async function loadBuiltinExtensions(ctx, disabled = []) {
14
+ const disabledSet = new Set(disabled);
15
+ const loaded = [];
16
+ for (const ext of BUILTIN_EXTENSIONS) {
17
+ if (disabledSet.has(ext.name))
18
+ continue;
19
+ const activate = await ext.load();
20
+ activate(ctx);
21
+ loaded.push(ext.name);
22
+ }
23
+ return loaded;
24
+ }
@@ -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}`;
@@ -77,7 +79,7 @@ export default function activate({ bus, contextManager }) {
77
79
  });
78
80
  register({
79
81
  name: "/compact",
80
- description: "Compact conversation (move full content to nuclear summaries)",
82
+ description: "Compact conversation via the active compaction strategy",
81
83
  handler: () => {
82
84
  bus.emit("agent:compact-request", {});
83
85
  },
@@ -88,25 +90,43 @@ export default function activate({ bus, contextManager }) {
88
90
  handler: () => {
89
91
  const stats = bus.emitPipe("context:get-stats", {
90
92
  activeTokens: 0,
91
- nuclearEntries: 0,
92
- recallArchiveSize: 0,
93
+ totalTokens: 0,
93
94
  budgetTokens: 0,
94
95
  });
95
96
  const pct = stats.budgetTokens > 0
96
97
  ? Math.round((stats.activeTokens / stats.budgetTokens) * 100)
97
98
  : 0;
98
- const lines = [
99
- `Active context: ~${stats.activeTokens.toLocaleString()} tokens / ${stats.budgetTokens.toLocaleString()} budget (${pct}%)`,
100
- `Nuclear entries: ${stats.nuclearEntries} in-context`,
101
- `Recall archive: ${stats.recallArchiveSize} entries`,
102
- ];
103
- bus.emit("ui:info", { message: lines.join("\n") });
99
+ bus.emit("ui:info", {
100
+ message: `Active context: ~${stats.activeTokens.toLocaleString()} tokens / ${stats.budgetTokens.toLocaleString()} budget (${pct}%)`,
101
+ });
102
+ },
103
+ });
104
+ register({
105
+ name: "/reload",
106
+ description: "Reload user extensions from ~/.agent-sh/extensions/",
107
+ handler: async () => {
108
+ const names = await reloadExtensions(ctx);
109
+ if (names.length > 0) {
110
+ bus.emit("ui:info", { message: `Reloaded: ${names.join(", ")}` });
111
+ }
112
+ else {
113
+ bus.emit("ui:info", { message: "No extensions to reload." });
114
+ }
104
115
  },
105
116
  });
117
+ // Handler form so extensions can trigger reload programmatically
118
+ // (e.g. an ash-callable reload_extensions tool in superash).
119
+ ctx.define("extensions:reload", async () => {
120
+ return await reloadExtensions(ctx);
121
+ });
106
122
  // ── Extension registration ────────────────────────────────────
107
123
  bus.on("command:register", (cmd) => {
108
124
  register(cmd);
109
125
  });
126
+ bus.on("command:unregister", ({ name }) => {
127
+ const key = name.startsWith("/") ? name : `/${name}`;
128
+ commands.delete(key);
129
+ });
110
130
  // ── Skill commands (/skill:<name>) ────────────────────────────
111
131
  const getSkills = () => {
112
132
  const cwd = contextManager?.getCwd() ?? process.cwd();