agent-sh 0.13.7 → 0.14.1

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 (63) hide show
  1. package/README.md +7 -18
  2. package/dist/agent/agent-loop.d.ts +13 -17
  3. package/dist/agent/agent-loop.js +118 -224
  4. package/dist/agent/conversation-state.d.ts +1 -1
  5. package/dist/agent/events.d.ts +218 -0
  6. package/dist/agent/events.js +1 -0
  7. package/dist/agent/host-types.d.ts +20 -0
  8. package/dist/agent/index.d.ts +5 -9
  9. package/dist/agent/index.js +269 -167
  10. package/dist/{utils → agent}/llm-client.js +1 -0
  11. package/dist/agent/llm-facade.d.ts +13 -0
  12. package/dist/{utils → agent}/llm-facade.js +1 -1
  13. package/dist/agent/nuclear-form.d.ts +1 -1
  14. package/dist/agent/providers/deepseek.js +2 -5
  15. package/dist/agent/providers/openai-compatible.js +2 -2
  16. package/dist/agent/providers/openai.js +2 -5
  17. package/dist/agent/providers/openrouter.js +5 -5
  18. package/dist/agent/subagent.d.ts +1 -1
  19. package/dist/agent/tool-protocol.d.ts +1 -1
  20. package/dist/agent/tool-registry.d.ts +1 -1
  21. package/dist/agent/types.d.ts +2 -2
  22. package/dist/cli/args.js +3 -1
  23. package/dist/cli/auth/cli.js +11 -6
  24. package/dist/cli/auth/discover.d.ts +5 -0
  25. package/dist/cli/auth/discover.js +25 -0
  26. package/dist/cli/auth/keys.d.ts +5 -2
  27. package/dist/cli/auth/keys.js +22 -2
  28. package/dist/cli/index.d.ts +16 -0
  29. package/dist/cli/index.js +12 -2
  30. package/dist/cli/install.d.ts +1 -0
  31. package/dist/cli/install.js +86 -2
  32. package/dist/cli/subcommands.js +4 -1
  33. package/dist/core/event-bus.d.ts +28 -371
  34. package/dist/core/extension-loader.js +6 -6
  35. package/dist/core/index.d.ts +10 -29
  36. package/dist/core/index.js +32 -84
  37. package/dist/extensions/index.d.ts +2 -1
  38. package/dist/extensions/index.js +1 -1
  39. package/dist/extensions/slash-commands/events.d.ts +18 -0
  40. package/dist/extensions/slash-commands/events.js +1 -0
  41. package/dist/extensions/slash-commands/index.d.ts +15 -0
  42. package/dist/extensions/{slash-commands.js → slash-commands/index.js} +4 -3
  43. package/dist/shell/events.d.ts +85 -0
  44. package/dist/shell/events.js +1 -0
  45. package/dist/shell/index.d.ts +1 -0
  46. package/dist/shell/index.js +6 -0
  47. package/dist/shell/tui-renderer.js +0 -1
  48. package/dist/utils/tool-interactive.js +4 -2
  49. package/examples/extensions/ash-acp-bridge/src/index.ts +2 -2
  50. package/examples/extensions/ashi/package.json +2 -2
  51. package/examples/extensions/ashi/src/cli.ts +4 -1
  52. package/examples/extensions/claude-code-bridge/index.ts +7 -2
  53. package/examples/extensions/claude-code-bridge/package.json +1 -1
  54. package/examples/extensions/ollama.ts +47 -42
  55. package/examples/extensions/opencode-bridge/README.md +4 -0
  56. package/examples/extensions/opencode-bridge/index.ts +208 -54
  57. package/examples/extensions/opencode-bridge/package.json +1 -1
  58. package/examples/extensions/pi-bridge/index.ts +3 -4
  59. package/examples/extensions/zai-coding-plan.ts +2 -6
  60. package/package.json +1 -1
  61. package/dist/extensions/slash-commands.d.ts +0 -2
  62. package/dist/utils/llm-facade.d.ts +0 -11
  63. /package/dist/{utils → agent}/llm-client.d.ts +0 -0
@@ -10,7 +10,7 @@
10
10
  * Used by the subagent extension to delegate tasks from the main agent.
11
11
  */
12
12
  import type { EventBus } from "../core/event-bus.js";
13
- import type { LlmClient } from "../utils/llm-client.js";
13
+ import type { LlmClient } from "./llm-client.js";
14
14
  import type { ToolDefinition } from "./types.js";
15
15
  export interface SubagentOptions {
16
16
  /** LLM client to use. */
@@ -9,7 +9,7 @@
9
9
  * The agent loop uses this interface uniformly so the rest of the code
10
10
  * doesn't need to know which mode is active.
11
11
  */
12
- import type { ChatCompletionTool } from "../utils/llm-client.js";
12
+ import type { ChatCompletionTool } from "./llm-client.js";
13
13
  import type { ToolDefinition } from "./types.js";
14
14
  import type { ConversationState } from "./conversation-state.js";
15
15
  export interface PendingToolCall {
@@ -1,5 +1,5 @@
1
1
  import type { ToolDefinition, ToolResult } from "./types.js";
2
- import type { ChatCompletionTool } from "../utils/llm-client.js";
2
+ import type { ChatCompletionTool } from "./llm-client.js";
3
3
  import type { HandlerFunctions } from "../utils/handler-registry.js";
4
4
  /**
5
5
  * Registry for agent tools. Execution is routed through the named-handler
@@ -57,8 +57,8 @@ export interface InteractiveSession<T> {
57
57
  render(width: number): string[];
58
58
  /** Handle raw input. Call done(result) to finish the session. */
59
59
  handleInput(data: string, done: (result: T) => void): void;
60
- /** Called when session starts. Receives invalidate() for async re-renders. */
61
- onMount?(invalidate: () => void): void;
60
+ /** done() lets the session resolve itself from outside handleInput. */
61
+ onMount?(invalidate: () => void, done: (result: T) => void): void;
62
62
  /** Called when session ends (cleanup). */
63
63
  onUnmount?(): void;
64
64
  }
package/dist/cli/args.js CHANGED
@@ -3,7 +3,9 @@ const HELP_TEXT = `agent-sh — a shell-first terminal where AI is one keystroke
3
3
 
4
4
  Usage: agent-sh [options]
5
5
  agent-sh init [--force] Scaffold ~/.agent-sh/ (settings, examples, AGENTS.md)
6
- agent-sh install <spec> [--force] Install an extension (bundled name, file:, npm:, github:)
6
+ agent-sh install <spec> [--force] [--sync-deps]
7
+ Install an extension (bundled name, file:, npm:, github:)
8
+ --sync-deps rewrites a stale agent-sh pin to the host version
7
9
  agent-sh uninstall <name> Remove an installed extension
8
10
  agent-sh list List installed extensions
9
11
  agent-sh auth login [provider] Store an API key for a built-in provider
@@ -1,6 +1,6 @@
1
1
  import * as readline from "node:readline";
2
2
  import { palette as p } from "../../utils/palette.js";
3
- import { KNOWN_PROVIDERS, KEYS_PATH, loadKeysFile, saveKeysFile, resolveApiKey, listAllProviders, findProvider as findProviderById, } from "./keys.js";
3
+ import { KNOWN_PROVIDERS, KEYS_PATH, loadKeysFile, saveKeysFile, resolveApiKey, listAllProvidersWithDiscovery, findProvider as findProviderById, } from "./keys.js";
4
4
  export async function runAuth(args) {
5
5
  const sub = args[0];
6
6
  if (!sub || sub === "--help" || sub === "-h") {
@@ -16,7 +16,7 @@ export async function runAuth(args) {
16
16
  return;
17
17
  }
18
18
  if (sub === "list" || sub === "ls" || sub === "status") {
19
- runList();
19
+ await runList();
20
20
  return;
21
21
  }
22
22
  console.error(`agent-sh auth: unknown subcommand "${sub}"`);
@@ -90,8 +90,8 @@ function runLogout(providerArg) {
90
90
  saveKeysFile(keys);
91
91
  console.log(`${p.success}✓${p.reset} Removed ${id} key from ${KEYS_PATH}`);
92
92
  }
93
- function runList() {
94
- const providers = listAllProviders();
93
+ async function runList() {
94
+ const providers = await listAllProvidersWithDiscovery();
95
95
  console.log("Provider key status:\n");
96
96
  const idWidth = Math.max(...providers.map((p) => p.id.length));
97
97
  for (const info of providers) {
@@ -105,6 +105,9 @@ function runList() {
105
105
  if (resolved.key) {
106
106
  console.log(` ${p.success}●${p.reset} ${padded} ${p.dim}(${sourceLabel(resolved.source, info)})${p.reset}${marker}`);
107
107
  }
108
+ else if (info.noAuth) {
109
+ console.log(` ${p.success}●${p.reset} ${padded} ${p.dim}(no auth required)${p.reset}${marker}`);
110
+ }
108
111
  else {
109
112
  console.log(` ${p.muted}○${p.reset} ${padded} ${p.dim}(not configured)${p.reset}${marker}`);
110
113
  }
@@ -117,13 +120,15 @@ async function pickProvider() {
117
120
  console.error("agent-sh auth: no provider specified and stdin is not a TTY.");
118
121
  return null;
119
122
  }
120
- const providers = listAllProviders();
123
+ const providers = await listAllProvidersWithDiscovery();
121
124
  console.log("Select a provider:");
122
125
  providers.forEach((info, i) => {
123
126
  const resolved = resolveApiKey(info.id);
124
127
  const tag = resolved.key
125
128
  ? `${p.dim}(currently from ${sourceLabel(resolved.source, info)})${p.reset}`
126
- : `${p.dim}(not configured)${p.reset}`;
129
+ : info.noAuth
130
+ ? `${p.dim}(no auth required)${p.reset}`
131
+ : `${p.dim}(not configured)${p.reset}`;
127
132
  const labelStr = info.custom
128
133
  ? `${p.dim}custom${p.reset}`
129
134
  : info.unattached
@@ -0,0 +1,5 @@
1
+ export interface DiscoveredProvider {
2
+ id: string;
3
+ noAuth?: boolean;
4
+ }
5
+ export declare function discoverExtensionProviders(): Promise<DiscoveredProvider[]>;
@@ -0,0 +1,25 @@
1
+ /** Bootstrap a throwaway core to enumerate provider ids extensions
2
+ * would register, so `auth list` shows ids the user hasn't keyed yet. */
3
+ import { createCore } from "../../core/index.js";
4
+ import { activateAgent } from "../../agent/index.js";
5
+ import { loadExtensions } from "../../core/extension-loader.js";
6
+ import { loadBuiltinExtensions } from "../../extensions/index.js";
7
+ import { getSettings } from "../../core/settings.js";
8
+ let cached = null;
9
+ export async function discoverExtensionProviders() {
10
+ if (cached)
11
+ return cached;
12
+ const core = createCore({});
13
+ try {
14
+ const ctx = core.extensionContext({ quit: () => { } });
15
+ activateAgent(ctx);
16
+ await loadBuiltinExtensions(ctx, getSettings().disabledBuiltins);
17
+ await loadExtensions(ctx).catch(() => { });
18
+ const { providers } = core.bus.emitPipe("agent:providers", { providers: [] });
19
+ cached = providers.map((p) => ({ id: p.id, noAuth: p.noAuth }));
20
+ return cached;
21
+ }
22
+ finally {
23
+ core.kill();
24
+ }
25
+ }
@@ -9,11 +9,14 @@ export interface ProviderAuthInfo {
9
9
  /** True for ids only present in keys.json — likely owned by an extension
10
10
  * that registers a provider at runtime. */
11
11
  unattached?: boolean;
12
+ /** Auth UI shows "no auth required" instead of "not configured". */
13
+ noAuth?: boolean;
12
14
  }
13
15
  export declare const KNOWN_PROVIDERS: ProviderAuthInfo[];
14
- /** Built-ins merged with settings-declared providers, plus any ids that only
15
- * appear in keys.json (likely registered by an extension at runtime). */
16
+ /** Built-ins + settings + keys.json. Sync, no extension load. */
16
17
  export declare function listAllProviders(): ProviderAuthInfo[];
18
+ /** Augments listAllProviders with extension-registered ids. */
19
+ export declare function listAllProvidersWithDiscovery(): Promise<ProviderAuthInfo[]>;
17
20
  /** Resolve an id against known + settings entries only. Returns null for
18
21
  * unattached or unknown ids — callers decide whether to accept them. */
19
22
  export declare function findProvider(id: string): ProviderAuthInfo | null;
@@ -8,8 +8,7 @@ export const KNOWN_PROVIDERS = [
8
8
  { id: "openrouter", label: "OpenRouter", envVar: "OPENROUTER_API_KEY" },
9
9
  { id: "deepseek", label: "DeepSeek", envVar: "DEEPSEEK_API_KEY" },
10
10
  ];
11
- /** Built-ins merged with settings-declared providers, plus any ids that only
12
- * appear in keys.json (likely registered by an extension at runtime). */
11
+ /** Built-ins + settings + keys.json. Sync, no extension load. */
13
12
  export function listAllProviders() {
14
13
  const out = [...KNOWN_PROVIDERS];
15
14
  const seen = new Set(out.map((p) => p.id));
@@ -28,6 +27,27 @@ export function listAllProviders() {
28
27
  }
29
28
  return out;
30
29
  }
30
+ /** Augments listAllProviders with extension-registered ids. */
31
+ export async function listAllProvidersWithDiscovery() {
32
+ const out = listAllProviders();
33
+ const byId = new Map(out.map((p) => [p.id, p]));
34
+ const { discoverExtensionProviders } = await import("./discover.js");
35
+ try {
36
+ for (const d of await discoverExtensionProviders()) {
37
+ const existing = byId.get(d.id);
38
+ if (existing) {
39
+ if (d.noAuth && !existing.noAuth)
40
+ existing.noAuth = true;
41
+ continue;
42
+ }
43
+ const entry = { id: d.id, label: d.id, custom: true, noAuth: d.noAuth };
44
+ out.push(entry);
45
+ byId.set(d.id, entry);
46
+ }
47
+ }
48
+ catch { }
49
+ return out;
50
+ }
31
51
  /** Resolve an id against known + settings entries only. Returns null for
32
52
  * unattached or unknown ids — callers decide whether to accept them. */
33
53
  export function findProvider(id) {
@@ -1,2 +1,18 @@
1
1
  #!/usr/bin/env node
2
+ declare module "../core/event-bus.js" {
3
+ interface BusEvents {
4
+ /** Startup banner collection (sync pipe). Extensions contribute
5
+ * labeled item lists; the CLI renders them between the product
6
+ * name and the help hint. */
7
+ "banner:collect": {
8
+ sections: Array<{
9
+ label: string;
10
+ items: string[];
11
+ }>;
12
+ /** Name of the backend being launched. Extensions should gate
13
+ * per-backend sections on this rather than settings.defaultBackend. */
14
+ activeBackend?: string;
15
+ };
16
+ }
17
+ }
2
18
  export {};
package/dist/cli/index.js CHANGED
@@ -60,9 +60,18 @@ async function main() {
60
60
  // ── Core (frontend-agnostic) ──────────────────────────────────
61
61
  const core = createCore(config);
62
62
  const { bus } = core;
63
- // Track agent info from bus events (populated by extension backends)
64
63
  let agentInfo = null;
65
- bus.on("agent:info", (info) => { agentInfo = info; });
64
+ bus.on("agent:info", (info) => {
65
+ agentInfo = info;
66
+ // Redraw so late agent:info emits (opencode-bridge after session.create) reach the prompt.
67
+ bus.emit("config:changed", {});
68
+ });
69
+ // tui-renderer subscribes to ui:error inside activateShell, after backend
70
+ // activation — pipe to stderr until the shell is up so boot failures surface.
71
+ const bootUiError = (e) => {
72
+ process.stderr.write(`agent-sh: ${e.message}\n`);
73
+ };
74
+ bus.on("ui:error", bootUiError);
66
75
  // ── Interactive frontend ──────────────────────────────────────
67
76
  if (process.env.DEBUG) {
68
77
  console.error('[agent-sh] Setting up interactive frontend...');
@@ -157,6 +166,7 @@ async function main() {
157
166
  return { info: "" };
158
167
  },
159
168
  });
169
+ bus.off("ui:error", bootUiError);
160
170
  bus.emit("input-mode:register", {
161
171
  id: "agent",
162
172
  trigger: ">",
@@ -1,5 +1,6 @@
1
1
  interface InstallOpts {
2
2
  force?: boolean;
3
+ syncDeps?: boolean;
3
4
  }
4
5
  export declare function listBundled(): string[];
5
6
  /** Heuristic: a backend named "pi" is typically provided by an extension called "pi-bridge". */
@@ -75,6 +75,84 @@ function readPackageJson(target) {
75
75
  return null;
76
76
  return JSON.parse(fs.readFileSync(pkgJson, "utf-8"));
77
77
  }
78
+ function hostAgentShVersion() {
79
+ try {
80
+ const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf-8"));
81
+ return typeof pkg.version === "string" ? pkg.version : null;
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ function satisfies(version, spec) {
88
+ if (spec === version || spec === "*" || spec === "latest")
89
+ return true;
90
+ const [vMaj, vMin, vPatch] = version.split(/[.-]/, 3).map(Number);
91
+ if ([vMaj, vMin, vPatch].some(Number.isNaN))
92
+ return true;
93
+ const m = spec.match(/^([\^~]?)(\d+)\.(\d+)\.(\d+)/);
94
+ if (!m)
95
+ return true;
96
+ const op = m[1];
97
+ const sMaj = Number(m[2]);
98
+ const sMin = Number(m[3]);
99
+ const sPatch = Number(m[4]);
100
+ if (op === "")
101
+ return vMaj === sMaj && vMin === sMin && vPatch === sPatch;
102
+ if (op === "~")
103
+ return vMaj === sMaj && vMin === sMin && vPatch >= sPatch;
104
+ // ^x.y.z: zero-major treats minor as the breaking boundary (npm rule).
105
+ if (sMaj > 0)
106
+ return vMaj === sMaj && (vMin > sMin || (vMin === sMin && vPatch >= sPatch));
107
+ if (sMin > 0)
108
+ return vMaj === 0 && vMin === sMin && vPatch >= sPatch;
109
+ return vMaj === 0 && vMin === 0 && vPatch === sPatch;
110
+ }
111
+ /** Warn when the extension's `agent-sh` pin can't admit the host version;
112
+ * only rewrite when --sync-deps is set. */
113
+ function syncAgentShVersion(target, syncDeps) {
114
+ const hostVersion = hostAgentShVersion();
115
+ if (!hostVersion)
116
+ return;
117
+ // Prerelease hosts aren't on npm; rewriting would leave npm install unable to resolve.
118
+ if (hostVersion.includes("-"))
119
+ return;
120
+ const pkgJson = path.join(target, "package.json");
121
+ if (!fs.existsSync(pkgJson))
122
+ return;
123
+ const raw = fs.readFileSync(pkgJson, "utf-8");
124
+ const pkg = JSON.parse(raw);
125
+ const sections = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
126
+ const name = path.basename(target);
127
+ let changed = false;
128
+ let warned = false;
129
+ for (const section of sections) {
130
+ const deps = pkg[section];
131
+ if (!deps || typeof deps !== "object")
132
+ continue;
133
+ const d = deps;
134
+ const current = d["agent-sh"];
135
+ if (typeof current !== "string")
136
+ continue;
137
+ if (current.startsWith("file:"))
138
+ continue;
139
+ if (satisfies(hostVersion, current))
140
+ continue;
141
+ if (syncDeps) {
142
+ console.log(`agent-sh: rewriting ${name} agent-sh ${current} -> ${hostVersion}.`);
143
+ d["agent-sh"] = hostVersion;
144
+ changed = true;
145
+ }
146
+ else if (!warned) {
147
+ console.warn(`agent-sh: ${name} pins agent-sh ${current}, which doesn't admit host ${hostVersion}. ` +
148
+ `npm install will land an older agent-sh inside the extension and drift from the running host. ` +
149
+ `Re-run with --sync-deps to rewrite the pin to ${hostVersion}, or update the bridge's source pin.`);
150
+ warned = true;
151
+ }
152
+ }
153
+ if (changed)
154
+ fs.writeFileSync(pkgJson, `${JSON.stringify(pkg, null, 2)}\n`);
155
+ }
78
156
  /** Relative `file:` deps in bundled extensions (e.g. `"agent-sh": "file:../../.."`)
79
157
  * point at the wrong location after the source is copied into ~/.agent-sh/extensions/.
80
158
  * Resolve them against the original source dir so npm install in the target succeeds. */
@@ -165,7 +243,7 @@ function linkBins(target, pkg) {
165
243
  }
166
244
  export async function runInstall(spec, opts = {}) {
167
245
  if (!spec) {
168
- console.error("Usage: agent-sh install <name|file:|npm:|github:> [--force]\n\n" +
246
+ console.error("Usage: agent-sh install <name|file:|npm:|github:> [--force] [--sync-deps]\n\n" +
169
247
  "Bundled extensions:\n" +
170
248
  listBundled()
171
249
  .map((n) => ` ${n}`)
@@ -191,9 +269,15 @@ export async function runInstall(spec, opts = {}) {
191
269
  }
192
270
  let linkedBins = [];
193
271
  if (resolved.isDirectory) {
194
- fs.cpSync(resolved.sourcePath, target, { recursive: true });
272
+ fs.cpSync(resolved.sourcePath, target, {
273
+ recursive: true,
274
+ // Skip source node_modules: maybeNpmInstall short-circuits on
275
+ // existing node_modules, silently leaving the bridge's deps stale.
276
+ filter: (src) => path.basename(src) !== "node_modules",
277
+ });
195
278
  try {
196
279
  rewriteFileDeps(target, resolved.sourcePath);
280
+ syncAgentShVersion(target, opts.syncDeps ?? false);
197
281
  const pkg = readPackageJson(target);
198
282
  if (pkg) {
199
283
  maybeNpmInstall(target, pkg);
@@ -3,7 +3,10 @@ import { runInstall, runUninstall, runList } from "./install.js";
3
3
  import { runAuth } from "./auth/cli.js";
4
4
  const SUBCOMMANDS = {
5
5
  init: (args) => runInit({ force: args.includes("--force") }),
6
- install: (args) => runInstall(args[0] ?? "", { force: args.includes("--force") }),
6
+ install: (args) => runInstall(args[0] ?? "", {
7
+ force: args.includes("--force"),
8
+ syncDeps: args.includes("--sync-deps"),
9
+ }),
7
10
  uninstall: (args) => runUninstall(args[0] ?? ""),
8
11
  list: () => runList(),
9
12
  auth: (args) => runAuth(args),