agent-sh 0.12.27 → 0.13.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 (146) 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 +42 -98
  4. package/dist/agent/conversation-state.d.ts +9 -0
  5. package/dist/agent/conversation-state.js +16 -0
  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 +358 -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} +31 -2
  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 +1 -1
  89. package/dist/utils/floating-panel.js +9 -4
  90. package/dist/utils/llm-client.d.ts +16 -26
  91. package/dist/utils/llm-client.js +15 -26
  92. package/dist/utils/llm-facade.d.ts +7 -3
  93. package/dist/utils/stream-transform.d.ts +1 -1
  94. package/dist/utils/terminal-buffer.d.ts +1 -1
  95. package/dist/utils/tool-display.js +4 -0
  96. package/dist/utils/tool-interactive.d.ts +1 -1
  97. package/dist/utils/tty.d.ts +7 -0
  98. package/dist/utils/tty.js +15 -0
  99. package/examples/extensions/ash-acp-bridge/README.md +4 -1
  100. package/examples/extensions/ash-acp-bridge/src/index.ts +654 -0
  101. package/examples/extensions/ash-mcp-bridge/index.ts +1 -1
  102. package/examples/extensions/ashi/README.md +250 -0
  103. package/examples/extensions/ashi/package.json +60 -0
  104. package/examples/extensions/ashi/src/autocomplete.ts +91 -0
  105. package/examples/extensions/ashi/src/capture.ts +34 -0
  106. package/examples/extensions/ashi/src/cli.ts +176 -0
  107. package/examples/extensions/ashi/src/commands.ts +82 -0
  108. package/examples/extensions/ashi/src/compaction.ts +157 -0
  109. package/examples/extensions/ashi/src/components.ts +327 -0
  110. package/examples/extensions/ashi/src/default-renderers.ts +153 -0
  111. package/examples/extensions/ashi/src/display-config.ts +62 -0
  112. package/examples/extensions/ashi/src/frontend.ts +735 -0
  113. package/examples/extensions/ashi/src/hooks.ts +136 -0
  114. package/examples/extensions/ashi/src/multi-session-store.ts +146 -0
  115. package/examples/extensions/ashi/src/session-commands.ts +76 -0
  116. package/examples/extensions/ashi/src/session-store.ts +264 -0
  117. package/examples/extensions/ashi/src/status-footer.ts +66 -0
  118. package/examples/extensions/ashi/src/theme.ts +151 -0
  119. package/examples/extensions/ashi/tsconfig.json +14 -0
  120. package/examples/extensions/emacs-buffer.ts +1 -1
  121. package/examples/extensions/interactive-prompts.ts +114 -69
  122. package/examples/extensions/latex-images.ts +3 -3
  123. package/examples/extensions/opencode-bridge/index.ts +1 -1
  124. package/examples/extensions/overlay-agent.ts +7 -5
  125. package/examples/extensions/peer-mesh.ts +1 -1
  126. package/examples/extensions/pi-bridge/index.ts +0 -1
  127. package/examples/extensions/questionnaire.ts +2 -1
  128. package/examples/extensions/rtk-proxy.ts +3 -3
  129. package/examples/extensions/solarized-theme.ts +3 -3
  130. package/examples/extensions/subagents.ts +6 -6
  131. package/examples/extensions/terminal-buffer.ts +1 -1
  132. package/examples/extensions/tmux-pane.ts +6 -4
  133. package/examples/extensions/tunnel-vision.ts +5 -5
  134. package/examples/extensions/user-shell.ts +1 -1
  135. package/examples/extensions/web-access.ts +5 -5
  136. package/package.json +38 -22
  137. package/dist/extensions/agent-backend.d.ts +0 -14
  138. package/dist/extensions/agent-backend.js +0 -307
  139. package/dist/types.d.ts +0 -227
  140. /package/dist/{types.js → agent/host-types.js} +0 -0
  141. /package/dist/{extensions → agent}/providers/openai-compatible.js +0 -0
  142. /package/dist/{index.d.ts → cli/index.d.ts} +0 -0
  143. /package/dist/{init.d.ts → cli/init.d.ts} +0 -0
  144. /package/dist/{install.d.ts → cli/install.d.ts} +0 -0
  145. /package/dist/{event-bus.js → core/event-bus.js} +0 -0
  146. /package/dist/{executor.d.ts → utils/executor.d.ts} +0 -0
@@ -6,9 +6,9 @@
6
6
  * subscribing to bus events. Shell-specific tracking lives in the
7
7
  * shell-context built-in extension.
8
8
  *
9
- * Agent backends are loaded as extensions and register themselves via
10
- * the agent:register-backend bus event. The built-in "ash" backend is
11
- * loaded from src/extensions/agent-backend.ts.
9
+ * Agent backends register themselves via the agent:register-backend bus
10
+ * event. The built-in "ash" backend lives in src/agent/ and is activated
11
+ * by hosts via activateAgent().
12
12
  *
13
13
  * Usage:
14
14
  * import { createCore } from "agent-sh";
@@ -18,22 +18,18 @@
18
18
  * const response = await core.query("hello");
19
19
  */
20
20
  import { EventBus } from "./event-bus.js";
21
- import { createLlmFacade } from "./utils/llm-facade.js";
22
- import { setPalette } from "./utils/palette.js";
23
- import * as streamTransform from "./utils/stream-transform.js";
24
21
  import * as settingsMod from "./settings.js";
25
- import { HandlerRegistry } from "./utils/handler-registry.js";
22
+ import { HandlerRegistry } from "../utils/handler-registry.js";
26
23
  import crypto from "node:crypto";
27
24
  import * as fs from "node:fs";
28
25
  import * as path from "node:path";
29
- import { DefaultCompositor } from "./utils/compositor.js";
30
26
  import { CONFIG_DIR } from "./settings.js";
31
- // Re-export types that library consumers need
32
27
  export { EventBus } from "./event-bus.js";
33
- export { palette, setPalette, resetPalette } from "./utils/palette.js";
34
- export { runSubagent } from "./agent/subagent.js";
35
- export { LlmClient } from "./utils/llm-client.js";
36
- export { HistoryFile, InMemoryHistory, NoopHistory } from "./agent/history-file.js";
28
+ export { palette, setPalette, resetPalette } from "../utils/palette.js";
29
+ export { runSubagent } from "../agent/subagent.js";
30
+ export { LlmClient } from "../utils/llm-client.js";
31
+ export { HistoryFile, InMemoryHistory, NoopHistory } from "../agent/history-file.js";
32
+ export { compileSearchRegex, matchEntry, formatNuclearLine } from "../agent/nuclear-form.js";
37
33
  export function createCore(config) {
38
34
  const bus = new EventBus();
39
35
  const handlers = new HandlerRegistry();
@@ -43,9 +39,7 @@ export function createCore(config) {
43
39
  const instanceId = crypto.randomBytes(3).toString("hex");
44
40
  bus.setSource(instanceId);
45
41
  const settings = settingsMod.getSettings();
46
- // Expose raw CLI config so the agent backend extension can resolve
47
- // providers and create the LLM client.
48
- handlers.define("config:get-shell-config", () => config);
42
+ handlers.define("config:get-app-config", () => config);
49
43
  // Default; shell-context advises with the PTY-tracked cwd when loaded.
50
44
  handlers.define("cwd", () => process.cwd());
51
45
  // Empty defaults so registerContextProducer can advise regardless of
@@ -93,11 +87,6 @@ export function createCore(config) {
93
87
  const names = [...backends.keys()];
94
88
  return { names, active: activeBackendName };
95
89
  });
96
- // ── Compositor ──────────────────────────────────────────────
97
- // Generic surface-routing primitive. No defaults here — the active
98
- // frontend (src/shell/, a web bridge, headless test harness, etc.)
99
- // sets its own surfaces during activation.
100
- const compositor = new DefaultCompositor(bus);
101
90
  return {
102
91
  bus,
103
92
  handlers,
@@ -153,14 +142,12 @@ export function createCore(config) {
153
142
  const ctx = {
154
143
  bus,
155
144
  instanceId,
156
- llm: createLlmFacade(handlers),
157
- providers: {
158
- configure: (id, opts) => bus.emit("provider:configure", { id, ...opts }),
159
- },
160
145
  quit: opts.quit,
161
- setPalette,
162
- createBlockTransform: (o) => streamTransform.createBlockTransform(bus, o),
163
- createFencedBlockTransform: (o) => streamTransform.createFencedBlockTransform(bus, o),
146
+ define: (name, fn) => handlers.define(name, fn),
147
+ advise: (name, wrapper) => handlers.advise(name, wrapper),
148
+ call: (name, ...args) => handlers.call(name, ...args),
149
+ list: () => handlers.list(),
150
+ onDispose: () => { },
164
151
  getExtensionSettings: settingsMod.getExtensionSettings,
165
152
  getStoragePath: (namespace) => {
166
153
  const dir = path.join(CONFIG_DIR, namespace);
@@ -168,70 +155,9 @@ export function createCore(config) {
168
155
  return dir;
169
156
  },
170
157
  registerCommand: (name, description, handler) => bus.emit("command:register", { name, description, handler }),
171
- registerTool: (tool) => bus.emit("agent:register-tool", { tool, extensionName: "" }),
172
- unregisterTool: (name) => bus.emit("agent:unregister-tool", { name }),
173
- getTools: () => bus.emitPipe("agent:get-tools", { tools: [] }).tools,
174
- registerInstruction: (name, text) => bus.emit("agent:register-instruction", { name, text, extensionName: "" }),
175
- removeInstruction: (name) => bus.emit("agent:remove-instruction", { name }),
176
- registerSkill: (name, description, filePath) => bus.emit("agent:register-skill", { name, description, filePath, extensionName: "" }),
177
- removeSkill: (name) => bus.emit("agent:remove-skill", { name }),
178
- registerContextProducer: (_name, producer, opts) => {
179
- const handlerName = opts?.mode === "per-query"
180
- ? "query-context:build"
181
- : "dynamic-context:build";
182
- return handlers.advise(handlerName, (next) => {
183
- const base = next();
184
- const part = producer();
185
- if (!part)
186
- return base;
187
- const trimmed = part.trim();
188
- if (!trimmed)
189
- return base;
190
- return base ? `${base}\n\n${trimmed}` : trimmed;
191
- });
192
- },
193
- define: (name, fn) => handlers.define(name, fn),
194
- advise: (name, wrapper) => handlers.advise(name, wrapper),
195
- call: (name, ...args) => handlers.call(name, ...args),
196
- list: () => handlers.list(),
197
- compositor,
198
- onDispose: () => { },
199
- createRemoteSession: (opts) => {
200
- const { surface } = opts;
201
- const cleanups = [];
202
- let active = true;
203
- // Redirect all render streams
204
- cleanups.push(compositor.redirect("agent", surface));
205
- cleanups.push(compositor.redirect("query", surface));
206
- cleanups.push(compositor.redirect("status", surface));
207
- // Suppress the host shell's mute lifecycle and post-turn
208
- // redraw nudge. on-processing-done is intentionally not advised
209
- // — its scope cleanup must always run.
210
- cleanups.push(handlers.advise("shell:on-processing-start", (next) => active ? undefined : next()));
211
- cleanups.push(handlers.advise("shell:on-processing-redraw", (next) => active ? undefined : next()));
212
- // Suppress chrome
213
- if (opts.suppressBorders !== false) {
214
- cleanups.push(handlers.advise("tui:response-border", (next, ...a) => active ? null : next(...a)));
215
- }
216
- if (opts.suppressQueryBox) {
217
- cleanups.push(handlers.advise("tui:render-user-query", (next, ...a) => active ? [] : next(...a)));
218
- }
219
- if (opts.suppressUsage !== false) {
220
- cleanups.push(handlers.advise("tui:render-usage", (next, ...a) => active ? "" : next(...a)));
221
- }
222
- return {
223
- submit(query) { bus.emit("agent:submit", { query }); },
224
- get surface() { return surface; },
225
- get active() { return active; },
226
- close() {
227
- if (!active)
228
- return;
229
- active = false;
230
- for (const fn of cleanups.reverse())
231
- fn();
232
- cleanups.length = 0;
233
- },
234
- };
158
+ adviseCommand: (name, advisor) => {
159
+ const key = name.startsWith("/") ? name : `/${name}`;
160
+ return handlers.advise(`command:${key}`, advisor);
235
161
  },
236
162
  };
237
163
  return ctx;
@@ -78,6 +78,13 @@ export interface Settings {
78
78
  * "inline" — tools described as text.
79
79
  */
80
80
  toolMode?: "api" | "deferred" | "deferred-lookup" | "inline";
81
+ /**
82
+ * Extra tool names treated as "core" in deferred / deferred-lookup mode —
83
+ * always sent with full schema instead of requiring an explicit load_tool
84
+ * call. Useful when an extension registers a substrate tool that should
85
+ * have first-class footing alongside the kernel built-ins.
86
+ */
87
+ coreTools?: string[];
81
88
  /** Additional directories to scan for skills (supports ~ expansion). */
82
89
  skillPaths?: string[];
83
90
  /**
@@ -20,6 +20,7 @@ const DEFAULTS = {
20
20
  defaultProvider: undefined,
21
21
  defaultBackend: "ash",
22
22
  toolMode: "api",
23
+ coreTools: [],
23
24
  shellTruncateThreshold: 20,
24
25
  shellHeadLines: 10,
25
26
  shellTailLines: 10,
@@ -0,0 +1,49 @@
1
+ import type { EventBus } from "./event-bus.js";
2
+ export type { ContentBlock } from "./event-bus.js";
3
+ /**
4
+ * The substrate context — what every backend and every host always
5
+ * provides. Bus, handler registry, lifecycle, and per-instance storage.
6
+ *
7
+ * Hosts (agent, shell, web bridge, …) extend this with their own
8
+ * surfaces — see src/agent/host-types.ts and src/shell/host-types.ts.
9
+ * Extensions that only need the substrate should type their ctx as
10
+ * `CoreContext`; those that need host facilities should type as
11
+ * `AgentContext` or `ShellContext` to make their host dependency
12
+ * explicit (and catch misuse under bridge backends at the type level).
13
+ */
14
+ export interface CoreContext {
15
+ bus: EventBus;
16
+ /** Stable per-instance identifier (4-char hex). */
17
+ readonly instanceId: string;
18
+ quit: () => void;
19
+ /** Read extension-namespaced settings from ~/.agent-sh/settings.json. */
20
+ getExtensionSettings: <T extends Record<string, unknown>>(namespace: string, defaults: T) => T;
21
+ /**
22
+ * Get (and lazily create) a per-extension storage directory under
23
+ * ~/.agent-sh/<namespace>/. Returns the absolute path. Lets extensions
24
+ * persist state without each one re-deriving the location.
25
+ */
26
+ getStoragePath: (namespace: string) => string;
27
+ /** Register a named handler. */
28
+ define: (name: string, fn: (...args: any[]) => any) => void;
29
+ /** Wrap a named handler. Receives `next` (original) + args. Returns an unadvise function. */
30
+ advise: (name: string, wrapper: (next: (...args: any[]) => any, ...args: any[]) => any) => () => void;
31
+ /** Call a named handler. */
32
+ call: (name: string, ...args: any[]) => any;
33
+ /** Names of all registered handlers — for diagnostic / introspection use. */
34
+ list: () => string[];
35
+ /** Teardown callback fired on /reload. For resources the scoped context
36
+ * can't track: process listeners, timers, watchers, sockets. */
37
+ onDispose: (fn: () => void) => void;
38
+ }
39
+ /**
40
+ * The substrate config — kernel-level options. Hosts extend with their
41
+ * own surfaces (see AgentConfig in src/agent/host-types.ts and
42
+ * ShellConfig in src/shell/host-types.ts).
43
+ */
44
+ export interface CoreConfig {
45
+ /** Extension specifiers (paths or package names) to load on startup. */
46
+ extensions?: string[];
47
+ /** Override settings.defaultBackend for this session only (does not persist). */
48
+ backend?: string;
49
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,2 +1,2 @@
1
- import type { ExtensionContext } from "../types.js";
1
+ import type { ExtensionContext } from "../shell/host-types.js";
2
2
  export default function activate(ctx: ExtensionContext): void;
@@ -1,25 +1,18 @@
1
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
- * For order-critical frontend bootstrap (the PTY shell), see `src/shell/`.
9
- * That module exposes its own `activate(ctx, opts)` entry point, loaded
10
- * specially from `src/index.ts` rather than through this manifest.
2
+ * Cross-cutting built-ins, toggleable via `disabledBuiltins`.
3
+ * Module-owned built-ins activate inline:
4
+ * shell-context, tui-renderer registerShellHandlers (src/shell/)
5
+ * agent-backend, providers → activateAgent (src/agent/)
11
6
  */
12
- import type { ExtensionContext } from "../types.js";
7
+ import type { ExtensionContext } from "../shell/host-types.js";
13
8
  type ActivateFn = (ctx: ExtensionContext) => void;
14
9
  export declare const BUILTIN_EXTENSIONS: Array<{
15
10
  name: string;
16
- when?: () => boolean;
17
11
  load: () => Promise<ActivateFn>;
18
12
  }>;
19
13
  /**
20
- * Load built-in extensions sequentially, skipping any in the disabled list
21
- * or whose `when` predicate returns false. Returns the names of extensions
22
- * that were loaded.
14
+ * Load built-in extensions sequentially, skipping any in the disabled list.
15
+ * Returns the names of extensions that were loaded.
23
16
  */
24
17
  export declare function loadBuiltinExtensions(ctx: ExtensionContext, disabled?: string[]): Promise<string[]>;
25
18
  export {};
@@ -1,25 +1,10 @@
1
1
  export const BUILTIN_EXTENSIONS = [
2
- { name: "shell-context", load: () => import("./shell-context.js").then(m => m.default) },
3
- { name: "agent-backend", load: () => import("./agent-backend.js").then(m => m.default) },
4
- { name: "openrouter",
5
- when: () => !!process.env.OPENROUTER_API_KEY,
6
- load: () => import("./providers/openrouter.js").then(m => m.default) },
7
- { name: "openai",
8
- when: () => !!process.env.OPENAI_API_KEY && !process.env.OPENAI_BASE_URL,
9
- load: () => import("./providers/openai.js").then(m => m.default) },
10
- { name: "openai-compatible",
11
- when: () => !!process.env.OPENAI_BASE_URL,
12
- load: () => import("./providers/openai-compatible.js").then(m => m.default) },
13
- { name: "deepseek",
14
- load: () => import("./providers/deepseek.js").then(m => m.default) },
15
- { name: "tui-renderer", load: () => import("./tui-renderer.js").then(m => m.default) },
16
2
  { name: "slash-commands", load: () => import("./slash-commands.js").then(m => m.default) },
17
3
  { name: "file-autocomplete", load: () => import("./file-autocomplete.js").then(m => m.default) },
18
4
  ];
19
5
  /**
20
- * Load built-in extensions sequentially, skipping any in the disabled list
21
- * or whose `when` predicate returns false. Returns the names of extensions
22
- * that were loaded.
6
+ * Load built-in extensions sequentially, skipping any in the disabled list.
7
+ * Returns the names of extensions that were loaded.
23
8
  */
24
9
  export async function loadBuiltinExtensions(ctx, disabled = []) {
25
10
  const disabledSet = new Set(disabled);
@@ -27,8 +12,6 @@ export async function loadBuiltinExtensions(ctx, disabled = []) {
27
12
  for (const ext of BUILTIN_EXTENSIONS) {
28
13
  if (disabledSet.has(ext.name))
29
14
  continue;
30
- if (ext.when && !ext.when())
31
- continue;
32
15
  const activate = await ext.load();
33
16
  activate(ctx);
34
17
  loaded.push(ext.name);
@@ -1,2 +1,2 @@
1
- import type { ExtensionContext } from "../types.js";
1
+ import type { ExtensionContext } from "../shell/host-types.js";
2
2
  export default function activate(ctx: ExtensionContext): void;
@@ -12,13 +12,17 @@
12
12
  */
13
13
  import { palette as p } from "../utils/palette.js";
14
14
  import { discoverSkills, loadSkillContent } from "../agent/skills.js";
15
- import { reloadExtensions } from "../extension-loader.js";
15
+ import { reloadExtensions } from "../core/extension-loader.js";
16
16
  export default function activate(ctx) {
17
17
  const { bus } = ctx;
18
18
  const commands = new Map();
19
19
  const register = (cmd) => {
20
20
  const name = cmd.name.startsWith("/") ? cmd.name : `/${cmd.name}`;
21
+ if (commands.has(name)) {
22
+ throw new Error(`Command "${name}" already registered. Use ctx.adviseCommand() to wrap it.`);
23
+ }
21
24
  commands.set(name, { ...cmd, name });
25
+ ctx.define(`command:${name}`, cmd.handler);
22
26
  };
23
27
  // ── Built-in commands ─────────────────────────────────────────
24
28
  register({
@@ -101,6 +105,7 @@ export default function activate(ctx) {
101
105
  bus.on("command:unregister", ({ name }) => {
102
106
  const key = name.startsWith("/") ? name : `/${name}`;
103
107
  commands.delete(key);
108
+ // Handler entry retained so external advisors survive a reload of the owner.
104
109
  });
105
110
  // ── Skill commands (/skill:<name>) ────────────────────────────
106
111
  const getSkills = () => {
@@ -214,7 +219,7 @@ export default function activate(ctx) {
214
219
  }
215
220
  const cmd = commands.get(e.name);
216
221
  if (cmd) {
217
- const result = cmd.handler(e.args);
222
+ const result = ctx.call(`command:${e.name}`, e.args);
218
223
  if (result instanceof Promise) {
219
224
  result.catch((err) => {
220
225
  bus.emit("ui:error", {
@@ -0,0 +1,114 @@
1
+ import type { EventBus } from "../core/event-bus.js";
2
+ import type { CoreConfig, CoreContext } from "../core/types.js";
3
+ import type { AgentSurface } from "../agent/host-types.js";
4
+ import type { ColorPalette } from "../utils/palette.js";
5
+ import type { Compositor, RenderSurface } from "../utils/compositor.js";
6
+ import type { BlockTransformOptions, FencedBlockTransformOptions } from "../utils/stream-transform.js";
7
+ export type { BlockTransformOptions, FencedBlockTransformOptions } from "../utils/stream-transform.js";
8
+ export type { RenderSurface } from "../utils/compositor.js";
9
+ export interface RemoteSessionOptions {
10
+ /** The surface to render agent output to. */
11
+ surface: RenderSurface;
12
+ /** Suppress response borders (default: true). */
13
+ suppressBorders?: boolean;
14
+ /** Suppress user query box (default: false).
15
+ * True for sessions with their own input (rsplit, overlay).
16
+ * False for sessions where input comes from the main shell (split). */
17
+ suppressQueryBox?: boolean;
18
+ /** Suppress usage stats line (default: true). */
19
+ suppressUsage?: boolean;
20
+ }
21
+ export interface RemoteSession {
22
+ /** Submit a query to the agent from this session. */
23
+ submit(query: string): void;
24
+ /** The surface this session renders to. */
25
+ readonly surface: RenderSurface;
26
+ /** Whether this session is currently active. */
27
+ readonly active: boolean;
28
+ /** Tear down — restores all routing and advisors. */
29
+ close(): void;
30
+ }
31
+ /**
32
+ * Configuration for a registered input mode.
33
+ * Extensions emit "input-mode:register" with this shape to add new modes.
34
+ */
35
+ export interface InputModeConfig {
36
+ id: string;
37
+ trigger: string;
38
+ label: string;
39
+ promptIcon: string;
40
+ indicator: string;
41
+ onSubmit(query: string, bus: EventBus): void;
42
+ returnToSelf: boolean;
43
+ }
44
+ export interface TerminalSession {
45
+ id: string;
46
+ command: string;
47
+ output: string;
48
+ exitCode: number | null;
49
+ done: boolean;
50
+ resolve?: (value: void) => void;
51
+ }
52
+ /**
53
+ * Capabilities the shell host adds to the extension context, exposed
54
+ * on the nested `ctx.shell` field. Available only when the TUI shell
55
+ * frontend is loaded; under headless backends these methods are silent
56
+ * no-ops (bus emits with no listeners).
57
+ */
58
+ export interface ShellSurface {
59
+ /** Routes named render streams ("agent", "query", "status", or any
60
+ * extension-defined name) to terminal surfaces. Frontends register
61
+ * default surfaces during activation; extensions can `redirect()`
62
+ * to capture output. Shell-scoped because today only the TUI uses
63
+ * it — bus events are the wire for other frontends. */
64
+ compositor: Compositor;
65
+ /** Override color palette slots for theming. */
66
+ setPalette: (overrides: Partial<ColorPalette>) => void;
67
+ /** Register a delimiter-based content transform (e.g. $$...$$ → image). */
68
+ createBlockTransform: (opts: BlockTransformOptions) => void;
69
+ /** Register a fenced block transform (e.g. ```lang...``` → code-block). */
70
+ createFencedBlockTransform: (opts: FencedBlockTransformOptions) => void;
71
+ /** Wrap an input mode's `onSubmit`. Lets extensions transform queries
72
+ * on the way to the agent (logging, redaction, vetoing). The mode
73
+ * must already be registered via the `input-mode:register` bus event. */
74
+ adviseInputMode: (id: string, advisor: (next: (query: string, bus: EventBus) => void, query: string, bus: EventBus) => void) => () => void;
75
+ /** Create a remote session that routes agent output to a surface and
76
+ * optionally accepts queries. Handles compositor routing, shell
77
+ * lifecycle advisors, and chrome suppression. */
78
+ createRemoteSession: (opts: RemoteSessionOptions) => RemoteSession;
79
+ }
80
+ /** Substrate + shell surface. Use this when an extension only touches
81
+ * shell features (themes, palette, transforms) and doesn't need the
82
+ * agent surface. */
83
+ export type ShellContext = CoreContext & {
84
+ shell: ShellSurface;
85
+ };
86
+ /**
87
+ * What extension `activate()` functions receive. Substrate (`CoreContext`)
88
+ * + slash-command registration + host surfaces, which are **optional**
89
+ * because hosts attach them on activation: under headless backends
90
+ * `ctx.shell` is undefined; under bridge backends `ctx.agent` may be
91
+ * undefined too. Extensions guard with `ctx.shell?.foo()` /
92
+ * `if (!ctx.agent) return;`, or type their parameter as the narrower
93
+ * `AgentContext` / `ShellContext` to declare host requirements (those
94
+ * variants make the surface non-optional). When both hosts are required,
95
+ * intersect them at the use site: `ctx: AgentContext & ShellContext`.
96
+ */
97
+ export type ExtensionContext = CoreContext & {
98
+ registerCommand: (name: string, description: string, handler: (args: string) => Promise<void> | void) => void;
99
+ /** Wrap an already-registered command's handler. Name is normalized
100
+ * (leading `/` optional). */
101
+ adviseCommand: (name: string, advisor: (next: (args: string) => Promise<void> | void, args: string) => Promise<void> | void) => () => void;
102
+ agent?: AgentSurface;
103
+ shell?: ShellSurface;
104
+ };
105
+ export interface ShellConfigSurface {
106
+ /** Shell binary (e.g. /bin/zsh) launched by the PTY frontend. */
107
+ shell?: string;
108
+ }
109
+ export type ShellConfig = CoreConfig & ShellConfigSurface;
110
+ /** The full application config — substrate + agent + shell startup options.
111
+ * Prefer this in CLI/embedder code; layered names (`CoreConfig`,
112
+ * `AgentConfig`, `ShellConfig`) are for code that cares about which
113
+ * host owns which fields. */
114
+ export type AppConfig = CoreConfig & import("../agent/host-types.js").AgentConfigSurface & ShellConfigSurface;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Frontend bootstrap. Loaded directly from src/index.ts (not the built-in
3
- * extensions manifest) because PTY + stdin raw mode ownership is order-
4
- * critical. For pluggable capability extensions see `src/extensions/`.
2
+ * Frontend bootstrap. Loaded directly from src/cli/index.ts (not the
3
+ * built-in extensions manifest) because PTY + stdin raw mode ownership is
4
+ * order-critical.
5
5
  */
6
- import type { ExtensionContext } from "../types.js";
6
+ import type { ExtensionContext } from "./host-types.js";
7
7
  export interface ShellActivateOptions {
8
8
  cols: number;
9
9
  rows: number;
@@ -28,13 +28,14 @@ export interface ShellHandle {
28
28
  resize(cols: number, rows: number): void;
29
29
  }
30
30
  /**
31
- * Register shell-owned handlers extensions can `ctx.call`. Must run before
32
- * `loadExtensions`; the handlers only need the bus, not the PTY.
31
+ * Register shell-owned handlers extensions can `ctx.call`, and attach
32
+ * the shell surface to ctx. Must run before `loadExtensions` so user
33
+ * extensions see `ctx.shell` populated.
33
34
  */
34
35
  export declare function registerShellHandlers(ctx: ExtensionContext): void;
35
36
  /**
36
37
  * Construct the Shell, wire resize forwarding, and register cleanup with the
37
38
  * provided ExtensionContext. Returns a handle the caller (typically
38
- * `src/index.ts`) uses to drive lifecycle from process-level events.
39
+ * `src/cli/index.ts`) uses to drive lifecycle from process-level events.
39
40
  */
40
41
  export declare function activateShell(ctx: ExtensionContext, opts: ShellActivateOptions): ShellHandle;
@@ -1,11 +1,60 @@
1
1
  import { Shell } from "./shell.js";
2
- import { StdoutSurface } from "../utils/compositor.js";
2
+ import { DefaultCompositor, StdoutSurface } from "../utils/compositor.js";
3
3
  import { TerminalBuffer } from "../utils/terminal-buffer.js";
4
+ import { setPalette } from "../utils/palette.js";
5
+ import * as streamTransform from "../utils/stream-transform.js";
6
+ import activateShellContext from "./shell-context.js";
7
+ import activateTuiRenderer from "./tui-renderer.js";
4
8
  /**
5
- * Register shell-owned handlers extensions can `ctx.call`. Must run before
6
- * `loadExtensions`; the handlers only need the bus, not the PTY.
9
+ * Register shell-owned handlers extensions can `ctx.call`, and attach
10
+ * the shell surface to ctx. Must run before `loadExtensions` so user
11
+ * extensions see `ctx.shell` populated.
7
12
  */
8
13
  export function registerShellHandlers(ctx) {
14
+ const { bus } = ctx;
15
+ const compositor = new DefaultCompositor(bus);
16
+ const shellSurface = {
17
+ compositor,
18
+ setPalette,
19
+ createBlockTransform: (o) => streamTransform.createBlockTransform(bus, o),
20
+ createFencedBlockTransform: (o) => streamTransform.createFencedBlockTransform(bus, o),
21
+ adviseInputMode: (id, advisor) => ctx.advise(`input-mode:${id}:submit`, advisor),
22
+ createRemoteSession: (sessOpts) => {
23
+ const { surface } = sessOpts;
24
+ const cleanups = [];
25
+ let active = true;
26
+ cleanups.push(compositor.redirect("agent", surface));
27
+ cleanups.push(compositor.redirect("query", surface));
28
+ cleanups.push(compositor.redirect("status", surface));
29
+ // on-processing-done is intentionally not advised — its scope
30
+ // cleanup must always run.
31
+ cleanups.push(ctx.advise("shell:on-processing-start", (next) => active ? undefined : next()));
32
+ cleanups.push(ctx.advise("shell:on-processing-redraw", (next) => active ? undefined : next()));
33
+ if (sessOpts.suppressBorders !== false) {
34
+ cleanups.push(ctx.advise("tui:response-border", (next, ...a) => active ? null : next(...a)));
35
+ }
36
+ if (sessOpts.suppressQueryBox) {
37
+ cleanups.push(ctx.advise("tui:render-user-query", (next, ...a) => active ? [] : next(...a)));
38
+ }
39
+ if (sessOpts.suppressUsage !== false) {
40
+ cleanups.push(ctx.advise("tui:render-usage", (next, ...a) => active ? "" : next(...a)));
41
+ }
42
+ return {
43
+ submit(query) { bus.emit("agent:submit", { query }); },
44
+ get surface() { return surface; },
45
+ get active() { return active; },
46
+ close() {
47
+ if (!active)
48
+ return;
49
+ active = false;
50
+ for (const fn of cleanups.reverse())
51
+ fn();
52
+ cleanups.length = 0;
53
+ },
54
+ };
55
+ },
56
+ };
57
+ ctx.shell = shellSurface;
9
58
  let terminalBufferSingleton;
10
59
  ctx.define("terminal-buffer", () => {
11
60
  if (terminalBufferSingleton !== undefined)
@@ -13,19 +62,19 @@ export function registerShellHandlers(ctx) {
13
62
  terminalBufferSingleton = TerminalBuffer.createWired(ctx.bus);
14
63
  return terminalBufferSingleton;
15
64
  });
65
+ activateShellContext(ctx);
66
+ activateTuiRenderer(ctx);
16
67
  }
17
68
  /**
18
69
  * Construct the Shell, wire resize forwarding, and register cleanup with the
19
70
  * provided ExtensionContext. Returns a handle the caller (typically
20
- * `src/index.ts`) uses to drive lifecycle from process-level events.
71
+ * `src/cli/index.ts`) uses to drive lifecycle from process-level events.
21
72
  */
22
73
  export function activateShell(ctx, opts) {
23
- // Stdout-as-default is a frontend choice, not a kernel one — a hub or
24
- // web bridge would point these at its own surfaces.
25
74
  const stdoutSurface = new StdoutSurface();
26
- ctx.compositor.setDefault("agent", stdoutSurface);
27
- ctx.compositor.setDefault("query", stdoutSurface);
28
- ctx.compositor.setDefault("status", stdoutSurface);
75
+ ctx.shell.compositor.setDefault("agent", stdoutSurface);
76
+ ctx.shell.compositor.setDefault("query", stdoutSurface);
77
+ ctx.shell.compositor.setDefault("status", stdoutSurface);
29
78
  const shell = new Shell({
30
79
  bus: ctx.bus,
31
80
  handlers: { define: ctx.define, call: ctx.call },
@@ -1,4 +1,4 @@
1
- import type { EventBus } from "../event-bus.js";
1
+ import type { EventBus } from "../core/event-bus.js";
2
2
  import { TuiInputView } from "./tui-input-view.js";
3
3
  /** Narrow contract between InputHandler and its host (Shell). */
4
4
  export interface InputContext {
@@ -10,6 +10,10 @@ export interface InputContext {
10
10
  redrawPrompt(): void;
11
11
  freshPrompt(): void;
12
12
  }
13
+ export interface InputHandlers {
14
+ define: (name: string, fn: (...a: any[]) => any) => void;
15
+ call: (name: string, ...a: any[]) => any;
16
+ }
13
17
  /** Line editor + shell-passthrough buffer. Delegates rendering to TuiInputView. */
14
18
  export declare class InputHandler {
15
19
  private ctx;
@@ -27,11 +31,13 @@ export declare class InputHandler {
27
31
  private savedBuffer;
28
32
  private escapeTimer;
29
33
  private bus;
34
+ private handlers;
30
35
  private onShowAgentInfo;
31
36
  private view;
32
37
  constructor(opts: {
33
38
  ctx: InputContext;
34
39
  bus: EventBus;
40
+ handlers: InputHandlers;
35
41
  onShowAgentInfo: () => {
36
42
  info: string;
37
43
  model?: string;