agent-sh 0.9.0 → 0.10.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 (88) hide show
  1. package/README.md +25 -30
  2. package/dist/agent/agent-loop.d.ts +43 -6
  3. package/dist/agent/agent-loop.js +817 -157
  4. package/dist/agent/conversation-state.d.ts +72 -21
  5. package/dist/agent/conversation-state.js +364 -151
  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 +84 -3
  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 +34 -1
  15. package/dist/agent/system-prompt.js +96 -47
  16. package/dist/agent/token-budget.d.ts +10 -13
  17. package/dist/agent/token-budget.js +6 -46
  18. package/dist/agent/tool-protocol.d.ts +23 -1
  19. package/dist/agent/tool-protocol.js +169 -4
  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 +1 -2
  26. package/dist/context-manager.d.ts +16 -19
  27. package/dist/context-manager.js +48 -152
  28. package/dist/core.js +27 -6
  29. package/dist/event-bus.d.ts +59 -3
  30. package/dist/executor.d.ts +4 -3
  31. package/dist/executor.js +18 -15
  32. package/dist/extension-loader.js +75 -17
  33. package/dist/extensions/agent-backend.d.ts +8 -7
  34. package/dist/extensions/agent-backend.js +72 -50
  35. package/dist/extensions/index.js +0 -2
  36. package/dist/extensions/slash-commands.js +14 -9
  37. package/dist/extensions/tui-renderer.js +67 -80
  38. package/dist/index.js +25 -6
  39. package/dist/settings.d.ts +39 -16
  40. package/dist/settings.js +51 -11
  41. package/dist/shell/input-handler.d.ts +2 -1
  42. package/dist/shell/input-handler.js +84 -76
  43. package/dist/shell/shell.js +19 -2
  44. package/dist/types.d.ts +15 -0
  45. package/dist/utils/ansi.d.ts +7 -0
  46. package/dist/utils/ansi.js +69 -8
  47. package/dist/utils/box-frame.js +8 -2
  48. package/dist/utils/compositor.d.ts +5 -0
  49. package/dist/utils/compositor.js +31 -3
  50. package/dist/utils/diff-renderer.d.ts +9 -0
  51. package/dist/utils/diff-renderer.js +221 -143
  52. package/dist/utils/diff.d.ts +21 -2
  53. package/dist/utils/diff.js +165 -89
  54. package/dist/utils/handler-registry.d.ts +5 -0
  55. package/dist/utils/handler-registry.js +6 -0
  56. package/dist/utils/line-editor.d.ts +11 -1
  57. package/dist/utils/line-editor.js +44 -5
  58. package/dist/utils/markdown.js +23 -8
  59. package/dist/utils/package-version.d.ts +1 -0
  60. package/dist/utils/package-version.js +10 -0
  61. package/dist/utils/shell-output-spill.d.ts +2 -0
  62. package/dist/utils/shell-output-spill.js +81 -0
  63. package/dist/utils/tool-display.d.ts +1 -1
  64. package/dist/utils/tool-display.js +4 -4
  65. package/examples/extensions/ash-acp-bridge/src/index.ts +4 -1
  66. package/examples/extensions/ash-mcp-bridge/index.ts +13 -3
  67. package/examples/extensions/claude-code-bridge/README.md +14 -0
  68. package/examples/extensions/claude-code-bridge/index.ts +204 -145
  69. package/examples/extensions/claude-code-bridge/package.json +1 -0
  70. package/examples/extensions/interactive-prompts.ts +39 -25
  71. package/examples/extensions/overlay-agent.ts +3 -3
  72. package/examples/extensions/peer-mesh.ts +115 -0
  73. package/examples/extensions/pi-bridge/README.md +16 -0
  74. package/examples/extensions/pi-bridge/index.ts +9 -155
  75. package/examples/extensions/questionnaire.ts +16 -5
  76. package/examples/extensions/subagents.ts +19 -4
  77. package/examples/extensions/terminal-buffer.ts +163 -0
  78. package/examples/extensions/user-shell.ts +136 -0
  79. package/examples/extensions/web-access.ts +8 -0
  80. package/package.json +36 -2
  81. package/dist/agent/tools/display.d.ts +0 -13
  82. package/dist/agent/tools/display.js +0 -70
  83. package/dist/agent/tools/user-shell.d.ts +0 -13
  84. package/dist/agent/tools/user-shell.js +0 -87
  85. package/dist/extensions/shell-recall.d.ts +0 -9
  86. package/dist/extensions/shell-recall.js +0 -8
  87. package/dist/extensions/terminal-buffer.d.ts +0 -14
  88. package/dist/extensions/terminal-buffer.js +0 -134
@@ -44,21 +44,20 @@ export function createLsTool(getCwd) {
44
44
  const entries = await fs.readdir(absPath, {
45
45
  withFileTypes: true,
46
46
  });
47
- const lines = [];
48
- for (const e of entries) {
47
+ const items = await Promise.all(entries.map(async (e) => {
49
48
  const fullPath = path.join(absPath, e.name);
50
49
  try {
51
50
  const stat = await fs.stat(fullPath);
52
51
  const size = e.isDirectory() ? "-" : formatSize(stat.size);
53
52
  const mtime = stat.mtime.toISOString().slice(0, 16).replace("T", " ");
54
- lines.push(`${mtime} ${size.padStart(8)} ${e.isDirectory() ? e.name + "/" : e.name}`);
53
+ return `${mtime} ${size.padStart(8)} ${e.isDirectory() ? e.name + "/" : e.name}`;
55
54
  }
56
55
  catch {
57
- lines.push(`${"?".padStart(16)} ${"?".padStart(8)} ${e.isDirectory() ? e.name + "/" : e.name}`);
56
+ return `${"?".padStart(16)} ${"?".padStart(8)} ${e.isDirectory() ? e.name + "/" : e.name}`;
58
57
  }
59
- }
58
+ }));
60
59
  return {
61
- content: lines.join("\n") || "(empty directory)",
60
+ content: items.join("\n") || "(empty directory)",
62
61
  exitCode: 0,
63
62
  isError: false,
64
63
  };
@@ -4,7 +4,6 @@
4
4
  * Backends self-wire to bus events in their constructor:
5
5
  * - agent:submit → handle queries
6
6
  * - agent:cancel-request → handle cancellation
7
- * - config:cycle → handle mode switching
8
7
  *
9
8
  * They emit bus events for results:
10
9
  * - agent:response-chunk, agent:tool-started, agent:tool-completed, etc.
@@ -38,7 +37,7 @@ export type ToolResultBody = {
38
37
  maxLines?: number;
39
38
  };
40
39
  export interface ToolDisplayInfo {
41
- kind: "read" | "write" | "execute" | "search" | "display";
40
+ kind: "read" | "write" | "execute" | "search";
42
41
  locations?: {
43
42
  path: string;
44
43
  line?: number | null;
@@ -4,41 +4,38 @@ export declare class ContextManager {
4
4
  private exchanges;
5
5
  private nextId;
6
6
  private currentCwd;
7
- private sessionStart;
8
- private firstPrompt;
9
7
  private agentShellActive;
10
- private handlers;
11
- constructor(bus: EventBus, handlers?: HandlerRegistry);
8
+ constructor(bus: EventBus, _handlers?: HandlerRegistry);
12
9
  getCwd(): string;
13
- /**
14
- * Build the <shell_context> block for the agent prompt.
15
- * Pipeline: window → truncate → format
16
- */
17
- getContext(budget?: number): string;
18
10
  /**
19
11
  * Regex/keyword search across all exchanges. Returns formatted results.
20
12
  */
21
13
  search(query: string): string;
22
14
  /**
23
- * Return content for specific exchange IDs.
24
- * Optional start/end restrict to a line range (1-indexed).
15
+ * Return shell events with id > afterId, formatted as an incremental
16
+ * delta suitable for injection into conversation history. Skips
17
+ * agent-source commands (already visible in tool results). Returns
18
+ * null when nothing new exists.
19
+ *
20
+ * The motivation: resending the full <shell_context> every turn wastes
21
+ * tokens — N turns × full history = O(N²) cost for O(N) information.
22
+ * Instead we inject only new events as regular conversation messages,
23
+ * so the provider's prefix cache amortizes them to O(N).
25
24
  */
26
- expand(ids: number[], start?: number, end?: number): string;
25
+ getEventsSince(afterId: number): {
26
+ text: string;
27
+ lastSeq: number;
28
+ } | null;
29
+ /** Highest exchange id seen so far (0 if none). */
30
+ lastSeq(): number;
27
31
  /**
28
32
  * One-line summaries of last N exchanges.
29
33
  */
30
34
  getRecentSummary(n?: number): string;
31
- /**
32
- * Parse and handle shell_recall commands.
33
- */
34
- handleRecallCommand(command: string): string;
35
35
  /**
36
36
  * Clear exchange history (used by /clear command).
37
37
  */
38
38
  clear(): void;
39
- private applyWindow;
40
- private applyTruncation;
41
- private formatContext;
42
39
  private addExchange;
43
40
  private formatExchangeTruncated;
44
41
  private formatExchangeFull;
@@ -1,32 +1,43 @@
1
1
  import { getSettings } from "./settings.js";
2
+ import { spillOutput } from "./utils/shell-output-spill.js";
2
3
  export class ContextManager {
3
4
  exchanges = [];
4
5
  nextId = 1;
5
6
  currentCwd;
6
- sessionStart;
7
- firstPrompt = true;
8
7
  agentShellActive = false; // true while user_shell command is executing
9
- handlers = null;
10
- constructor(bus, handlers) {
11
- if (handlers) {
12
- this.handlers = handlers;
13
- // Extensions can advise this to inject extra context (e.g. terminal buffer)
14
- handlers.define("context:build-extra", () => "");
15
- }
8
+ constructor(bus, _handlers) {
16
9
  this.currentCwd = process.cwd();
17
- this.sessionStart = Date.now();
18
10
  // ── Subscribe to shell events ──
19
11
  bus.on("shell:command-done", (e) => {
20
12
  const lines = e.output.split("\n");
13
+ const s = getSettings();
14
+ // Spill long outputs to a tempfile so the agent can `read_file` them
15
+ // on demand instead of carrying the full text in LLM context.
16
+ let output = e.output;
17
+ let spillPath;
18
+ if (lines.length > s.shellTruncateThreshold) {
19
+ // Reserve the id we're about to assign so the tempfile name matches.
20
+ const id = this.nextId;
21
+ try {
22
+ spillPath = spillOutput(id, e.output);
23
+ output = buildSpillStub(lines, s.shellHeadLines, s.shellTailLines, spillPath);
24
+ }
25
+ catch {
26
+ // If spill fails (e.g. disk full), fall back to keeping output in memory.
27
+ output = e.output;
28
+ spillPath = undefined;
29
+ }
30
+ }
21
31
  this.addExchange({
22
32
  type: "shell_command",
23
33
  command: e.command,
24
- output: e.output,
34
+ output,
25
35
  cwd: e.cwd,
26
36
  exitCode: e.exitCode,
27
37
  outputLines: lines.length,
28
38
  outputBytes: e.output.length,
29
39
  source: this.agentShellActive ? "agent" : "user",
40
+ spillPath,
30
41
  });
31
42
  });
32
43
  bus.on("shell:cwd-change", (e) => {
@@ -46,16 +57,6 @@ export class ContextManager {
46
57
  getCwd() {
47
58
  return this.currentCwd;
48
59
  }
49
- /**
50
- * Build the <shell_context> block for the agent prompt.
51
- * Pipeline: window → truncate → format
52
- */
53
- getContext(budget) {
54
- budget ??= getSettings().contextBudget;
55
- let exchanges = this.applyWindow(this.exchanges);
56
- exchanges = this.applyTruncation(exchanges, budget);
57
- return this.formatContext(exchanges);
58
- }
59
60
  /**
60
61
  * Regex/keyword search across all exchanges. Returns formatted results.
61
62
  */
@@ -107,38 +108,31 @@ export class ContextManager {
107
108
  return parts.join("\n");
108
109
  }
109
110
  /**
110
- * Return content for specific exchange IDs.
111
- * Optional start/end restrict to a line range (1-indexed).
111
+ * Return shell events with id > afterId, formatted as an incremental
112
+ * delta suitable for injection into conversation history. Skips
113
+ * agent-source commands (already visible in tool results). Returns
114
+ * null when nothing new exists.
115
+ *
116
+ * The motivation: resending the full <shell_context> every turn wastes
117
+ * tokens — N turns × full history = O(N²) cost for O(N) information.
118
+ * Instead we inject only new events as regular conversation messages,
119
+ * so the provider's prefix cache amortizes them to O(N).
112
120
  */
113
- expand(ids, start, end) {
114
- const results = [];
115
- for (const id of ids) {
116
- const ex = this.exchanges.find((e) => e.id === id);
117
- if (!ex) {
118
- results.push(`#${id}: not found`);
119
- continue;
120
- }
121
- const text = this.formatExchangeFull(ex);
122
- const lines = text.split("\n");
123
- const total = lines.length;
124
- if (start != null || end != null) {
125
- // Line range requested
126
- const s = Math.max(0, (start ?? 1) - 1);
127
- const e = end ?? total;
128
- results.push(lines.slice(s, e).join("\n") +
129
- `\n[showing lines ${s + 1}-${Math.min(e, total)} of ${total}]`);
130
- }
131
- else if (total > getSettings().recallExpandMaxLines) {
132
- // Too large — tell the agent to narrow down
133
- results.push(`#${ex.id}: output is ${total} lines, too large to expand fully. ` +
134
- `Use start/end params to select a line range (e.g. start=1, end=50), ` +
135
- `or use search with a regex to find specific content.`);
136
- }
137
- else {
138
- results.push(text);
139
- }
140
- }
141
- return results.join("\n\n");
121
+ getEventsSince(afterId) {
122
+ const fresh = this.exchanges.filter((e) => e.id > afterId && !(e.type === "shell_command" && e.source === "agent"));
123
+ if (fresh.length === 0)
124
+ return null;
125
+ const lastSeq = this.exchanges[this.exchanges.length - 1].id;
126
+ // Outputs already carry head+tail+spillPath stubs from capture time.
127
+ const body = fresh.map((ex) => this.formatExchangeTruncated(ex)).join("\n");
128
+ return {
129
+ text: `<shell-events>\n${body}</shell-events>`,
130
+ lastSeq,
131
+ };
132
+ }
133
+ /** Highest exchange id seen so far (0 if none). */
134
+ lastSeq() {
135
+ return this.exchanges.length === 0 ? 0 : this.exchanges[this.exchanges.length - 1].id;
142
136
  }
143
137
  /**
144
138
  * One-line summaries of last N exchanges.
@@ -149,108 +143,13 @@ export class ContextManager {
149
143
  return "No exchanges yet.";
150
144
  return recent.map((ex) => this.exchangeOneLiner(ex)).join("\n");
151
145
  }
152
- /**
153
- * Parse and handle shell_recall commands.
154
- */
155
- handleRecallCommand(command) {
156
- const args = command.replace(/^_*shell_recall\s*/, "").trim();
157
- if (!args || args === "--help") {
158
- return [
159
- "Usage:",
160
- " shell_recall Browse recent exchanges",
161
- " shell_recall --search <query> Search all exchanges",
162
- " shell_recall --expand <id,...> Show full content of exchanges",
163
- "",
164
- "Examples:",
165
- ' shell_recall --search "test fail"',
166
- " shell_recall --expand 41",
167
- " shell_recall --expand 41,42,43",
168
- ].join("\n");
169
- }
170
- const searchMatch = args.match(/^--search\s+(?:"([^"]+)"|(\S+))/);
171
- if (searchMatch) {
172
- return this.search(searchMatch[1] ?? searchMatch[2] ?? "");
173
- }
174
- const expandMatch = args.match(/^--expand\s+([\d,\s]+)/);
175
- if (expandMatch) {
176
- const ids = expandMatch[1]
177
- .split(/[,\s]+/)
178
- .map(Number)
179
- .filter((n) => !isNaN(n));
180
- if (ids.length === 0)
181
- return "No valid IDs provided.";
182
- return this.expand(ids);
183
- }
184
- // Default: browse
185
- return this.getRecentSummary();
186
- }
187
146
  /**
188
147
  * Clear exchange history (used by /clear command).
189
148
  */
190
149
  clear() {
191
150
  this.exchanges = [];
192
- this.firstPrompt = true;
193
151
  // Don't reset nextId — IDs should be globally unique within a session
194
152
  }
195
- // ── Pipeline stages ───────────────────────────────────────────
196
- applyWindow(exchanges, windowSize) {
197
- windowSize ??= getSettings().contextWindowSize;
198
- return exchanges.slice(-windowSize);
199
- }
200
- applyTruncation(exchanges, budget) {
201
- // Deep clone so we don't mutate the source
202
- const result = exchanges.map((e) => ({ ...e }));
203
- // Pass 1: per-type truncation
204
- for (const ex of result) {
205
- if (ex.type === "shell_command") {
206
- const s = getSettings();
207
- ex.output = truncateOutput(ex.output, s.shellTruncateThreshold, s.shellHeadLines, s.shellTailLines, ex.id);
208
- }
209
- // agent_query has no output to truncate
210
- }
211
- // Pass 2: budget enforcement — strip output from oldest if over budget
212
- let totalSize = result.reduce((sum, ex) => sum + this.exchangeSize(ex), 0);
213
- for (let i = 0; i < result.length - 1 && totalSize > budget; i++) {
214
- const ex = result[i];
215
- const before = this.exchangeSize(ex);
216
- if (ex.type === "shell_command") {
217
- ex.output = `[output omitted, use shell_recall tool to expand id ${ex.id}]`;
218
- }
219
- totalSize -= before - this.exchangeSize(ex);
220
- }
221
- return result;
222
- }
223
- formatContext(exchanges) {
224
- const elapsed = Math.round((Date.now() - this.sessionStart) / 60000);
225
- const totalCount = this.exchanges.length;
226
- let out = "<shell_context>\n";
227
- if (this.firstPrompt) {
228
- out += `You are an AI assistant living inside agent-sh, a shell-first terminal.\n`;
229
- out += `The user interacts with a real shell (PTY) and sends you queries inline. You are there to help them with their tasks.\n`;
230
- out += `\n`;
231
- out += `IMPORTANT tool usage rules:\n`;
232
- out += `- user_shell runs commands in the user's live shell (PTY). The user sees output directly — no summary needed.\n`;
233
- out += `- Your internal tools (bash, read, write, ls, etc.) run in an isolated subprocess. The user CANNOT see their output.\n`;
234
- out += `- When the user asks to see, list, view, or display anything, ALWAYS use user_shell. NEVER use internal tools like ls/read/bash for display — the user won't see it.\n`;
235
- out += `- Only use internal tools when YOU need to reason about content silently (e.g. reading a file to answer a question about it).\n`;
236
- out += `- After a user_shell command, the user already saw the output. Do NOT repeat or summarize it.\n`;
237
- out += `- You can browse or search shell history with shell_recall.\n`;
238
- out += `- You can browse or search evicted conversation turns with conversation_recall.\n`;
239
- out += `\n`;
240
- this.firstPrompt = false;
241
- }
242
- out += `cwd: ${this.currentCwd}\n`;
243
- out += `session: ${totalCount} exchanges, ${elapsed}m elapsed\n`;
244
- for (const ex of exchanges) {
245
- out += "\n" + this.formatExchangeTruncated(ex);
246
- }
247
- // Allow extensions to inject extra context (e.g. terminal buffer snapshot)
248
- const extra = this.handlers?.call("context:build-extra");
249
- if (extra)
250
- out += "\n" + extra + "\n";
251
- out += "\n</shell_context>\n";
252
- return out;
253
- }
254
153
  // ── Internal helpers ──────────────────────────────────────────
255
154
  addExchange(partial) {
256
155
  const exchange = {
@@ -319,14 +218,11 @@ export class ContextManager {
319
218
  }
320
219
  }
321
220
  // ── Utility functions ─────────────────────────────────────────
322
- function truncateOutput(text, threshold, headLines, tailLines, id) {
323
- const lines = text.split("\n");
324
- if (lines.length <= threshold)
325
- return text;
221
+ function buildSpillStub(lines, headLines, tailLines, spillPath) {
326
222
  const omitted = lines.length - headLines - tailLines;
327
223
  return [
328
224
  ...lines.slice(0, headLines),
329
- `[... ${omitted} lines truncated, use shell_recall tool with expand and id ${id} to see full output ...]`,
225
+ `[... ${omitted} lines truncated full output at ${spillPath}; use read_file to expand ...]`,
330
226
  ...lines.slice(-tailLines),
331
227
  ].join("\n");
332
228
  }
package/dist/core.js CHANGED
@@ -24,7 +24,11 @@ import * as settingsMod from "./settings.js";
24
24
  import { HandlerRegistry } from "./utils/handler-registry.js";
25
25
  import { TerminalBuffer } from "./utils/terminal-buffer.js";
26
26
  import crypto from "node:crypto";
27
+ import * as fs from "node:fs";
28
+ import * as path from "node:path";
29
+ import * as os from "node:os";
27
30
  import { DefaultCompositor, StdoutSurface } from "./utils/compositor.js";
31
+ const STORAGE_ROOT = path.join(os.homedir(), ".agent-sh");
28
32
  // Re-export types that library consumers need
29
33
  export { EventBus } from "./event-bus.js";
30
34
  export { palette, setPalette, resetPalette } from "./utils/palette.js";
@@ -34,7 +38,10 @@ export function createCore(config) {
34
38
  const bus = new EventBus();
35
39
  const handlers = new HandlerRegistry();
36
40
  const contextManager = new ContextManager(bus, handlers);
37
- const instanceId = crypto.randomBytes(2).toString("hex");
41
+ // 3 bytes = 6 hex chars, ~16M values — ample for per-lineage uniqueness and
42
+ // short enough to read/remember. Legacy content may have 16-char iids; any
43
+ // parsers should accept ≥6 hex chars.
44
+ const instanceId = crypto.randomBytes(3).toString("hex");
38
45
  const settings = settingsMod.getSettings();
39
46
  // Expose raw CLI config so the agent backend extension can resolve
40
47
  // providers and create the LLM client.
@@ -63,7 +70,12 @@ export function createCore(config) {
63
70
  backends.set(backend.name, backend);
64
71
  });
65
72
  bus.on("config:switch-backend", ({ name }) => {
66
- activateByName(name);
73
+ activateByName(name).then(() => {
74
+ if (activeBackendName === name) {
75
+ settingsMod.updateSettings({ defaultBackend: name });
76
+ bus.emit("ui:info", { message: `Saved '${name}' as default backend.` });
77
+ }
78
+ });
67
79
  });
68
80
  bus.on("config:list-backends", () => {
69
81
  const names = [...backends.keys()];
@@ -77,7 +89,7 @@ export function createCore(config) {
77
89
  return { names, active: activeBackendName };
78
90
  });
79
91
  // ── Compositor ──────────────────────────────────────────────
80
- const compositor = new DefaultCompositor();
92
+ const compositor = new DefaultCompositor(bus);
81
93
  const stdoutSurface = new StdoutSurface();
82
94
  compositor.setDefault("agent", stdoutSurface);
83
95
  compositor.setDefault("query", stdoutSurface);
@@ -145,7 +157,7 @@ export function createCore(config) {
145
157
  bus.emit("agent:cancel-request", {});
146
158
  },
147
159
  extensionContext(opts) {
148
- return {
160
+ const ctx = {
149
161
  bus,
150
162
  contextManager,
151
163
  instanceId,
@@ -154,15 +166,23 @@ export function createCore(config) {
154
166
  createBlockTransform: (o) => streamTransform.createBlockTransform(bus, o),
155
167
  createFencedBlockTransform: (o) => streamTransform.createFencedBlockTransform(bus, o),
156
168
  getExtensionSettings: settingsMod.getExtensionSettings,
169
+ getStoragePath: (namespace) => {
170
+ const dir = path.join(STORAGE_ROOT, namespace);
171
+ fs.mkdirSync(dir, { recursive: true });
172
+ return dir;
173
+ },
157
174
  registerCommand: (name, description, handler) => bus.emit("command:register", { name, description, handler }),
158
- registerTool: (tool) => bus.emit("agent:register-tool", { tool }),
175
+ registerTool: (tool) => bus.emit("agent:register-tool", { tool, extensionName: "" }),
159
176
  unregisterTool: (name) => bus.emit("agent:unregister-tool", { name }),
160
177
  getTools: () => bus.emitPipe("agent:get-tools", { tools: [] }).tools,
161
- registerInstruction: (name, text) => bus.emit("agent:register-instruction", { name, text }),
178
+ registerInstruction: (name, text) => bus.emit("agent:register-instruction", { name, text, extensionName: "" }),
162
179
  removeInstruction: (name) => bus.emit("agent:remove-instruction", { name }),
180
+ registerSkill: (name, description, filePath) => bus.emit("agent:register-skill", { name, description, filePath, extensionName: "" }),
181
+ removeSkill: (name) => bus.emit("agent:remove-skill", { name }),
163
182
  define: (name, fn) => handlers.define(name, fn),
164
183
  advise: (name, wrapper) => handlers.advise(name, wrapper),
165
184
  call: (name, ...args) => handlers.call(name, ...args),
185
+ list: () => handlers.list(),
166
186
  get terminalBuffer() { return getTerminalBuffer(); },
167
187
  compositor,
168
188
  createRemoteSession: (opts) => {
@@ -207,6 +227,7 @@ export function createCore(config) {
207
227
  };
208
228
  },
209
229
  };
230
+ return ctx;
210
231
  },
211
232
  kill() {
212
233
  if (activeBackendName) {
@@ -89,6 +89,28 @@ export interface ShellEvents {
89
89
  }>;
90
90
  }>;
91
91
  };
92
+ "agent:tool-batch-complete": {
93
+ results: Array<{
94
+ name: string;
95
+ isError: boolean;
96
+ errorSummary?: string;
97
+ }>;
98
+ };
99
+ "conversation:message-appended": {
100
+ role: "user" | "assistant" | "tool" | "system";
101
+ content: string;
102
+ /** For role="tool": name of the tool whose result this is. */
103
+ toolName?: string;
104
+ /** For role="tool": parsed arguments passed to the tool. */
105
+ toolArgs?: Record<string, unknown>;
106
+ /** For role="tool": whether the tool errored. */
107
+ isError?: boolean;
108
+ };
109
+ "conversation:after-compact": {
110
+ beforeTokens: number;
111
+ afterTokens: number;
112
+ evictedCount: number;
113
+ };
92
114
  "agent:tool-started": {
93
115
  title: string;
94
116
  toolCallId?: string;
@@ -115,6 +137,16 @@ export interface ShellEvents {
115
137
  "agent:tool-output-chunk": {
116
138
  chunk: string;
117
139
  };
140
+ "agent:subagent-started": {
141
+ taskId: string;
142
+ task: string;
143
+ };
144
+ "agent:subagent-completed": {
145
+ taskId: string;
146
+ task: string;
147
+ result: string;
148
+ isError: boolean;
149
+ };
118
150
  "tool:interactive-start": Record<string, never>;
119
151
  "tool:interactive-end": Record<string, never>;
120
152
  "permission:request": {
@@ -130,6 +162,9 @@ export interface ShellEvents {
130
162
  description: string;
131
163
  handler: (args: string) => Promise<void> | void;
132
164
  };
165
+ "command:unregister": {
166
+ name: string;
167
+ };
133
168
  "command:execute": {
134
169
  name: string;
135
170
  args: string;
@@ -143,6 +178,10 @@ export interface ShellEvents {
143
178
  "ui:suggestion": {
144
179
  text: string;
145
180
  };
181
+ "compositor:write": {
182
+ stream: string;
183
+ text: string;
184
+ };
146
185
  "input:keypress": {
147
186
  key: string;
148
187
  };
@@ -182,8 +221,7 @@ export interface ShellEvents {
182
221
  "agent:compact-request": Record<string, never>;
183
222
  "context:get-stats": {
184
223
  activeTokens: number;
185
- nuclearEntries: number;
186
- recallArchiveSize: number;
224
+ totalTokens: number;
187
225
  budgetTokens: number;
188
226
  };
189
227
  "agent:register-backend": {
@@ -200,7 +238,6 @@ export interface ShellEvents {
200
238
  active: string | null;
201
239
  };
202
240
  "config:changed": Record<string, never>;
203
- "config:cycle": Record<string, never>;
204
241
  "config:switch-model": {
205
242
  model: string;
206
243
  };
@@ -228,10 +265,12 @@ export interface ShellEvents {
228
265
  };
229
266
  "config:set-modes": {
230
267
  modes: AgentMode[];
268
+ activeIndex?: number;
231
269
  };
232
270
  "config:add-modes": {
233
271
  modes: AgentMode[];
234
272
  };
273
+ "core:extensions-loaded": Record<string, never>;
235
274
  "provider:register": {
236
275
  id: string;
237
276
  apiKey?: string;
@@ -247,6 +286,7 @@ export interface ShellEvents {
247
286
  };
248
287
  "agent:register-tool": {
249
288
  tool: import("./agent/types.js").ToolDefinition;
289
+ extensionName?: string;
250
290
  };
251
291
  "agent:unregister-tool": {
252
292
  name: string;
@@ -257,10 +297,26 @@ export interface ShellEvents {
257
297
  "agent:register-instruction": {
258
298
  name: string;
259
299
  text: string;
300
+ extensionName: string;
260
301
  };
261
302
  "agent:remove-instruction": {
262
303
  name: string;
263
304
  };
305
+ "agent:register-skill": {
306
+ name: string;
307
+ description: string;
308
+ filePath: string;
309
+ extensionName: string;
310
+ };
311
+ "agent:remove-skill": {
312
+ name: string;
313
+ };
314
+ "banner:collect": {
315
+ sections: Array<{
316
+ label: string;
317
+ items: string[];
318
+ }>;
319
+ };
264
320
  "autocomplete:request": {
265
321
  buffer: string;
266
322
  /** Parsed slash command name (e.g. "/backend"), or null if not a command. */
@@ -25,7 +25,8 @@ export declare function executeCommand(opts: {
25
25
  done: Promise<void>;
26
26
  };
27
27
  /**
28
- * Kill a running session's process group.
29
- * Sends SIGTERM first, then SIGKILL after 5 seconds.
28
+ * Kill a running session's process group: SIGTERM, then SIGKILL after 5s.
29
+ * Returns a cleanup that cancels the pending SIGKILL callers should invoke
30
+ * it once the process has exited.
30
31
  */
31
- export declare function killSession(session: ExecutorSession): void;
32
+ export declare function killSession(session: ExecutorSession): () => void;
package/dist/executor.js CHANGED
@@ -60,14 +60,15 @@ export function executeCommand(opts) {
60
60
  };
61
61
  child.stdout?.on("data", handleData);
62
62
  child.stderr?.on("data", handleData);
63
- // Timeout handler
63
+ let cancelKill;
64
64
  const timer = setTimeout(() => {
65
65
  if (!session.done) {
66
- killSession(session);
66
+ cancelKill = killSession(session);
67
67
  }
68
68
  }, timeout);
69
69
  child.on("exit", (code, signal) => {
70
70
  clearTimeout(timer);
71
+ cancelKill?.();
71
72
  session.exitCode = code ?? (signal ? -1 : null);
72
73
  session.done = true;
73
74
  session.process = null;
@@ -75,6 +76,7 @@ export function executeCommand(opts) {
75
76
  });
76
77
  child.on("error", (err) => {
77
78
  clearTimeout(timer);
79
+ cancelKill?.();
78
80
  if (!session.done) {
79
81
  session.exitCode = -1;
80
82
  session.output += `\nProcess error: ${err.message}`;
@@ -86,31 +88,32 @@ export function executeCommand(opts) {
86
88
  return { session, done };
87
89
  }
88
90
  /**
89
- * Kill a running session's process group.
90
- * Sends SIGTERM first, then SIGKILL after 5 seconds.
91
+ * Kill a running session's process group: SIGTERM, then SIGKILL after 5s.
92
+ * Returns a cleanup that cancels the pending SIGKILL callers should invoke
93
+ * it once the process has exited.
91
94
  */
92
95
  export function killSession(session) {
93
96
  const proc = session.process;
94
97
  if (!proc || !proc.pid)
95
- return;
98
+ return () => { };
96
99
  try {
97
- // Kill the entire process group
98
100
  process.kill(-proc.pid, "SIGTERM");
99
101
  }
100
- catch {
101
- // Process may already be dead
102
- }
103
- // Fallback: SIGKILL after 5 seconds
102
+ catch { }
103
+ let settled = false;
104
104
  const fallback = setTimeout(() => {
105
- if (!session.done && proc.pid) {
105
+ if (!settled && !session.done && proc.pid) {
106
106
  try {
107
107
  process.kill(-proc.pid, "SIGKILL");
108
108
  }
109
- catch {
110
- // Ignore
111
- }
109
+ catch { }
112
110
  }
113
111
  }, 5000);
114
- // Don't let the timer keep the process alive
115
112
  fallback.unref();
113
+ return () => {
114
+ if (!settled) {
115
+ settled = true;
116
+ clearTimeout(fallback);
117
+ }
118
+ };
116
119
  }