agent-sh 0.1.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 (50) hide show
  1. package/README.md +659 -0
  2. package/dist/acp-client.d.ts +76 -0
  3. package/dist/acp-client.js +507 -0
  4. package/dist/context-manager.d.ts +45 -0
  5. package/dist/context-manager.js +405 -0
  6. package/dist/core.d.ts +41 -0
  7. package/dist/core.js +76 -0
  8. package/dist/event-bus.d.ts +140 -0
  9. package/dist/event-bus.js +79 -0
  10. package/dist/executor.d.ts +31 -0
  11. package/dist/executor.js +116 -0
  12. package/dist/extension-loader.d.ts +16 -0
  13. package/dist/extension-loader.js +164 -0
  14. package/dist/extensions/file-autocomplete.d.ts +2 -0
  15. package/dist/extensions/file-autocomplete.js +63 -0
  16. package/dist/extensions/shell-recall.d.ts +9 -0
  17. package/dist/extensions/shell-recall.js +8 -0
  18. package/dist/extensions/slash-commands.d.ts +2 -0
  19. package/dist/extensions/slash-commands.js +105 -0
  20. package/dist/extensions/tui-renderer.d.ts +2 -0
  21. package/dist/extensions/tui-renderer.js +354 -0
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +159 -0
  24. package/dist/input-handler.d.ts +48 -0
  25. package/dist/input-handler.js +302 -0
  26. package/dist/output-parser.d.ts +55 -0
  27. package/dist/output-parser.js +166 -0
  28. package/dist/shell.d.ts +54 -0
  29. package/dist/shell.js +219 -0
  30. package/dist/types.d.ts +71 -0
  31. package/dist/types.js +1 -0
  32. package/dist/utils/ansi.d.ts +12 -0
  33. package/dist/utils/ansi.js +23 -0
  34. package/dist/utils/box-frame.d.ts +21 -0
  35. package/dist/utils/box-frame.js +60 -0
  36. package/dist/utils/diff-renderer.d.ts +20 -0
  37. package/dist/utils/diff-renderer.js +506 -0
  38. package/dist/utils/diff.d.ts +24 -0
  39. package/dist/utils/diff.js +122 -0
  40. package/dist/utils/file-watcher.d.ts +31 -0
  41. package/dist/utils/file-watcher.js +101 -0
  42. package/dist/utils/markdown.d.ts +39 -0
  43. package/dist/utils/markdown.js +248 -0
  44. package/dist/utils/palette.d.ts +32 -0
  45. package/dist/utils/palette.js +36 -0
  46. package/dist/utils/tool-display.d.ts +33 -0
  47. package/dist/utils/tool-display.js +141 -0
  48. package/examples/extensions/interactive-prompts.ts +161 -0
  49. package/examples/extensions/solarized-theme.ts +27 -0
  50. package/package.json +72 -0
@@ -0,0 +1,76 @@
1
+ import type { EventBus } from "./event-bus.js";
2
+ import type { ContextManager } from "./context-manager.js";
3
+ import type { AgentShellConfig } from "./types.js";
4
+ export declare class AcpClient {
5
+ private agentProcess;
6
+ private connection;
7
+ private sessionId;
8
+ private bus;
9
+ private contextManager;
10
+ private config;
11
+ private promptInProgress;
12
+ private currentResponseText;
13
+ private lastResponseText;
14
+ private terminalSessions;
15
+ private terminalDonePromises;
16
+ private terminalCounter;
17
+ private fileWatcher;
18
+ private pendingToolCalls;
19
+ private pendingToolCounter;
20
+ private agentInfo;
21
+ constructor(opts: {
22
+ bus: EventBus;
23
+ contextManager: ContextManager;
24
+ config: AgentShellConfig;
25
+ });
26
+ start(): Promise<void>;
27
+ /**
28
+ * Send a user query to the agent.
29
+ */
30
+ sendPrompt(query: string): Promise<void>;
31
+ /**
32
+ * Cancel the current prompt and force-recover shell mode.
33
+ */
34
+ cancel(): Promise<void>;
35
+ /**
36
+ * Start a new ACP session, clearing agent-side conversation history.
37
+ */
38
+ resetSession(): Promise<void>;
39
+ /**
40
+ * Get the text of the last agent response (for /copy).
41
+ */
42
+ getLastResponseText(): string;
43
+ /**
44
+ * Get agent information for display.
45
+ */
46
+ getAgentInfo(): {
47
+ name: string;
48
+ version: string;
49
+ } | null;
50
+ getModel(): string | undefined;
51
+ /**
52
+ * Check if agent is connected.
53
+ */
54
+ isConnected(): boolean;
55
+ private log;
56
+ /**
57
+ * Create the Client handler that responds to agent requests.
58
+ */
59
+ private createClientHandler;
60
+ private handleSessionUpdate;
61
+ private handleRequestPermission;
62
+ private handleCreateTerminal;
63
+ private handleTerminalOutput;
64
+ private handleWaitForTerminalExit;
65
+ private handleKillTerminal;
66
+ private handleReleaseTerminal;
67
+ private handleReadTextFile;
68
+ private handleWriteTextFile;
69
+ /**
70
+ * After the agent finishes, check all tracked files for changes
71
+ * made via the agent's own tools (not through fs/writeTextFile)
72
+ * and show interactive diff previews.
73
+ */
74
+ private showPendingFileChanges;
75
+ kill(): void;
76
+ }
@@ -0,0 +1,507 @@
1
+ import { spawn } from "node:child_process";
2
+ import { Readable, Writable } from "node:stream";
3
+ import * as fs from "node:fs/promises";
4
+ import * as acp from "@agentclientprotocol/sdk";
5
+ import { executeCommand, killSession } from "./executor.js";
6
+ import { computeDiff } from "./utils/diff.js";
7
+ import { FileWatcher } from "./utils/file-watcher.js";
8
+ import * as path from "node:path";
9
+ import { stripAnsi } from "./utils/ansi.js";
10
+ export class AcpClient {
11
+ agentProcess = null;
12
+ connection = null;
13
+ sessionId = null;
14
+ bus;
15
+ contextManager;
16
+ config;
17
+ promptInProgress = false;
18
+ currentResponseText = "";
19
+ lastResponseText = "";
20
+ terminalSessions = new Map();
21
+ terminalDonePromises = new Map();
22
+ terminalCounter = 0;
23
+ fileWatcher;
24
+ pendingToolCalls = new Map();
25
+ pendingToolCounter = 0;
26
+ agentInfo = null;
27
+ constructor(opts) {
28
+ this.bus = opts.bus;
29
+ this.contextManager = opts.contextManager;
30
+ this.config = opts.config;
31
+ this.fileWatcher = new FileWatcher(process.cwd());
32
+ }
33
+ async start() {
34
+ this.log(`Starting agent: ${this.config.agentCommand} ${this.config.agentArgs.join(" ")}`);
35
+ // Spawn the agent subprocess
36
+ // Spawn the agent — wait briefly to catch ENOENT and other spawn errors
37
+ this.agentProcess = spawn(this.config.agentCommand, this.config.agentArgs, {
38
+ stdio: ["pipe", "pipe", process.env.DEBUG ? "inherit" : "ignore"],
39
+ });
40
+ // Catch spawn errors (ENOENT, EACCES, etc.) before proceeding
41
+ await new Promise((resolve, reject) => {
42
+ const onError = (err) => {
43
+ reject(new Error(`Failed to start agent "${this.config.agentCommand}": ${err.message}`));
44
+ };
45
+ this.agentProcess.on("error", onError);
46
+ // spawn errors fire on next tick — wait for that, then detach the listener
47
+ setTimeout(() => {
48
+ this.agentProcess.removeListener("error", onError);
49
+ resolve();
50
+ }, 100);
51
+ });
52
+ this.log("Agent process spawned");
53
+ this.agentProcess.on("exit", (code) => {
54
+ this.bus.emit("agent:error", { message: `Agent process exited with code ${code}` });
55
+ this.connection = null;
56
+ this.sessionId = null;
57
+ });
58
+ if (!this.agentProcess.stdin || !this.agentProcess.stdout) {
59
+ throw new Error("Failed to get agent process stdio");
60
+ }
61
+ // Create ACP stream from the agent's stdio
62
+ const output = Writable.toWeb(this.agentProcess.stdin);
63
+ const input = Readable.toWeb(this.agentProcess.stdout);
64
+ const stream = acp.ndJsonStream(output, input);
65
+ this.log("Creating ACP connection");
66
+ // Create the client-side connection, providing our Client handler
67
+ this.connection = new acp.ClientSideConnection((_agent) => this.createClientHandler(), stream);
68
+ // Initialize the connection
69
+ this.log("Sending initialize request");
70
+ const initResponse = await this.connection.initialize({
71
+ protocolVersion: acp.PROTOCOL_VERSION,
72
+ clientInfo: { name: "agent-sh", version: "0.1.0" },
73
+ clientCapabilities: {
74
+ terminal: true,
75
+ fs: {
76
+ readTextFile: true,
77
+ writeTextFile: true,
78
+ },
79
+ },
80
+ });
81
+ this.log("Initialize successful");
82
+ // Store agent info for display
83
+ if (initResponse.agentInfo) {
84
+ this.agentInfo = {
85
+ name: initResponse.agentInfo.name || this.config.agentCommand,
86
+ version: initResponse.agentInfo.version || "unknown"
87
+ };
88
+ this.log(`Agent info: ${this.agentInfo.name} v${this.agentInfo.version}`);
89
+ }
90
+ // Create a session
91
+ const cwd = this.contextManager.getCwd();
92
+ this.log(`Creating new session with cwd: ${cwd}`);
93
+ const sessionResponse = await this.connection.newSession({
94
+ cwd,
95
+ mcpServers: [],
96
+ });
97
+ this.sessionId = sessionResponse.sessionId;
98
+ this.log(`Session created: ${this.sessionId}`);
99
+ }
100
+ /**
101
+ * Send a user query to the agent.
102
+ */
103
+ async sendPrompt(query) {
104
+ if (!this.connection || !this.sessionId) {
105
+ this.bus.emit("agent:error", { message: "Not connected to agent" });
106
+ return;
107
+ }
108
+ this.promptInProgress = true;
109
+ this.bus.emit("agent:processing-start", {});
110
+ await this.fileWatcher.snapshot();
111
+ this.currentResponseText = "";
112
+ let cancelled = false;
113
+ // Emit agent query event (TUI renders echo+spinner, ContextManager records it)
114
+ this.bus.emit("agent:query", { query });
115
+ // Build structured context from ContextManager
116
+ const contextBlock = this.contextManager.getContext();
117
+ try {
118
+ this.log("sending prompt...");
119
+ const response = await this.connection.prompt({
120
+ sessionId: this.sessionId,
121
+ prompt: [
122
+ {
123
+ type: "text",
124
+ text: contextBlock + "\n" + query,
125
+ },
126
+ ],
127
+ });
128
+ this.log(`prompt resolved: stopReason=${response.stopReason}`);
129
+ if (response.stopReason === "cancelled") {
130
+ cancelled = true;
131
+ this.bus.emit("agent:cancelled", {});
132
+ }
133
+ }
134
+ catch (err) {
135
+ this.log(`prompt error: ${err}`);
136
+ this.bus.emit("agent:error", {
137
+ message: err instanceof Error ? err.message : String(err),
138
+ });
139
+ }
140
+ finally {
141
+ this.log("restoring shell mode");
142
+ if (!cancelled) {
143
+ this.bus.emit("agent:response-done", {
144
+ response: this.currentResponseText,
145
+ });
146
+ }
147
+ this.lastResponseText = this.currentResponseText;
148
+ // Show diff previews for files the agent modified via its own tools
149
+ // (modifications via fs/writeTextFile are already handled inline)
150
+ if (this.promptInProgress) {
151
+ await this.showPendingFileChanges();
152
+ }
153
+ this.bus.emit("agent:processing-done", {});
154
+ this.promptInProgress = false;
155
+ }
156
+ }
157
+ /**
158
+ * Cancel the current prompt and force-recover shell mode.
159
+ */
160
+ async cancel() {
161
+ this.log("cancel requested");
162
+ // Kill all running terminal sessions
163
+ for (const session of this.terminalSessions.values()) {
164
+ if (!session.done)
165
+ killSession(session);
166
+ }
167
+ if (this.connection && this.sessionId && this.promptInProgress) {
168
+ try {
169
+ await this.connection.cancel({ sessionId: this.sessionId });
170
+ }
171
+ catch {
172
+ // Cancellation is best-effort
173
+ }
174
+ }
175
+ // Force-recover shell regardless of prompt state
176
+ if (this.promptInProgress) {
177
+ this.bus.emit("agent:cancelled", {});
178
+ }
179
+ this.bus.emit("agent:processing-done", {});
180
+ this.promptInProgress = false;
181
+ }
182
+ /**
183
+ * Start a new ACP session, clearing agent-side conversation history.
184
+ */
185
+ async resetSession() {
186
+ if (!this.connection)
187
+ return;
188
+ const sessionResponse = await this.connection.newSession({
189
+ cwd: this.contextManager.getCwd(),
190
+ mcpServers: [],
191
+ });
192
+ this.sessionId = sessionResponse.sessionId;
193
+ this.lastResponseText = "";
194
+ this.currentResponseText = "";
195
+ }
196
+ /**
197
+ * Get the text of the last agent response (for /copy).
198
+ */
199
+ getLastResponseText() {
200
+ return this.lastResponseText;
201
+ }
202
+ /**
203
+ * Get agent information for display.
204
+ */
205
+ getAgentInfo() {
206
+ return this.agentInfo;
207
+ }
208
+ getModel() {
209
+ return this.config.model;
210
+ }
211
+ /**
212
+ * Check if agent is connected.
213
+ */
214
+ isConnected() {
215
+ // Consider connected if we have a connection and agent info
216
+ // Session ID may not be set yet if we're still initializing
217
+ return this.connection !== null && this.agentInfo !== null;
218
+ }
219
+ log(msg) {
220
+ if (process.env.DEBUG) {
221
+ process.stderr.write(`[agent-sh] ${msg}\n`);
222
+ }
223
+ }
224
+ /**
225
+ * Create the Client handler that responds to agent requests.
226
+ */
227
+ createClientHandler() {
228
+ return {
229
+ // Required: handle session update notifications (streaming)
230
+ sessionUpdate: async (params) => {
231
+ this.handleSessionUpdate(params);
232
+ },
233
+ // Required: handle permission requests
234
+ requestPermission: async (params) => {
235
+ return this.handleRequestPermission(params);
236
+ },
237
+ // Optional: terminal operations
238
+ createTerminal: async (params) => {
239
+ return this.handleCreateTerminal(params);
240
+ },
241
+ terminalOutput: async (params) => {
242
+ return this.handleTerminalOutput(params);
243
+ },
244
+ waitForTerminalExit: async (params) => {
245
+ return this.handleWaitForTerminalExit(params);
246
+ },
247
+ killTerminal: async (params) => {
248
+ return this.handleKillTerminal(params);
249
+ },
250
+ releaseTerminal: async (params) => {
251
+ return this.handleReleaseTerminal(params);
252
+ },
253
+ // Optional: filesystem operations
254
+ readTextFile: async (params) => {
255
+ return this.handleReadTextFile(params);
256
+ },
257
+ writeTextFile: async (params) => {
258
+ return this.handleWriteTextFile(params);
259
+ },
260
+ };
261
+ }
262
+ // ── Session update handler ─────────────────────────────────────
263
+ handleSessionUpdate(params) {
264
+ // Suppress rendering during initialization / between prompts
265
+ if (!this.promptInProgress)
266
+ return;
267
+ const update = params.update;
268
+ switch (update.sessionUpdate) {
269
+ case "agent_message_chunk": {
270
+ const content = update.content;
271
+ if (content.type === "text") {
272
+ this.currentResponseText += content.text;
273
+ this.bus.emit("agent:response-chunk", { text: content.text });
274
+ }
275
+ break;
276
+ }
277
+ case "agent_thought_chunk": {
278
+ const thought = update.content;
279
+ if (thought.type === "text" && thought.text) {
280
+ this.bus.emit("agent:thinking-chunk", { text: thought.text });
281
+ }
282
+ break;
283
+ }
284
+ case "tool_call": {
285
+ // Use toolCallId if available, otherwise generate a simple ID
286
+ const toolId = update.toolCallId || `tool-${this.pendingToolCounter++}`;
287
+ this.pendingToolCalls.set(toolId, true);
288
+ this.bus.emit("agent:tool-started", { title: update.title, toolCallId: toolId });
289
+ break;
290
+ }
291
+ case "tool_call_update": {
292
+ // Only show result when the tool completes, don't show tool call again
293
+ if (update.status === "completed" || update.status === "failed") {
294
+ const toolId = update.toolCallId;
295
+ const exitCode = update.status === "completed" ? 0 : 1;
296
+ if (toolId && this.pendingToolCalls.has(toolId)) {
297
+ this.pendingToolCalls.delete(toolId);
298
+ this.bus.emit("agent:tool-completed", { toolCallId: toolId, exitCode });
299
+ }
300
+ else if (!toolId) {
301
+ this.bus.emit("agent:tool-completed", { exitCode });
302
+ }
303
+ }
304
+ break;
305
+ }
306
+ default:
307
+ // Ignore other update types for now
308
+ break;
309
+ }
310
+ }
311
+ // ── Permission handler ─────────────────────────────────────────
312
+ async handleRequestPermission(params) {
313
+ const title = params.toolCall.title ?? "Unknown action";
314
+ const result = await this.bus.emitPipeAsync("permission:request", {
315
+ kind: "tool-call",
316
+ title,
317
+ metadata: {
318
+ options: params.options.map((o) => ({
319
+ optionId: o.optionId,
320
+ kind: o.kind,
321
+ })),
322
+ },
323
+ decision: {
324
+ outcome: "selected",
325
+ optionId: (params.options.find((o) => o.kind === "allow_once") ?? params.options[0])?.optionId,
326
+ },
327
+ });
328
+ return { outcome: result.decision };
329
+ }
330
+ // ── Terminal handlers (isolated execution via child_process) ────
331
+ async handleCreateTerminal(params) {
332
+ const fullCommand = params.args?.length
333
+ ? `${params.command} ${params.args.join(" ")}`
334
+ : params.command;
335
+ const cwd = params.cwd ?? this.contextManager.getCwd();
336
+ // Let extensions intercept before spawning a real process
337
+ const intercept = this.bus.emitPipe("agent:terminal-intercept", {
338
+ command: fullCommand,
339
+ cwd,
340
+ intercepted: false,
341
+ output: "",
342
+ });
343
+ if (intercept.intercepted) {
344
+ const id = `t${++this.terminalCounter}`;
345
+ const session = {
346
+ id,
347
+ command: fullCommand,
348
+ output: intercept.output,
349
+ exitCode: 0,
350
+ done: true,
351
+ truncated: false,
352
+ process: null,
353
+ };
354
+ this.terminalSessions.set(id, session);
355
+ this.terminalDonePromises.set(id, Promise.resolve());
356
+ return { terminalId: id };
357
+ }
358
+ this.bus.emit("agent:tool-call", {
359
+ tool: fullCommand,
360
+ args: { command: params.command, args: params.args, cwd },
361
+ });
362
+ const id = `t${++this.terminalCounter}`;
363
+ const { session, done } = executeCommand({
364
+ command: fullCommand,
365
+ cwd,
366
+ timeout: 60_000,
367
+ maxOutputBytes: 256 * 1024,
368
+ onOutput: (chunk) => {
369
+ // Stream output into the box in real-time (strip ANSI for display)
370
+ this.bus.emit("agent:tool-output-chunk", { chunk: stripAnsi(chunk) });
371
+ },
372
+ });
373
+ session.id = id;
374
+ this.terminalSessions.set(id, session);
375
+ this.terminalDonePromises.set(id, done);
376
+ return { terminalId: id };
377
+ }
378
+ async handleTerminalOutput(params) {
379
+ const session = this.terminalSessions.get(params.terminalId);
380
+ if (!session) {
381
+ return { output: "", truncated: false };
382
+ }
383
+ return {
384
+ output: session.output,
385
+ truncated: session.truncated,
386
+ ...(session.done && {
387
+ exitStatus: { exitCode: session.exitCode },
388
+ }),
389
+ };
390
+ }
391
+ async handleWaitForTerminalExit(params) {
392
+ const session = this.terminalSessions.get(params.terminalId);
393
+ if (!session) {
394
+ return { exitCode: -1 };
395
+ }
396
+ if (!session.done) {
397
+ const done = this.terminalDonePromises.get(params.terminalId);
398
+ if (done)
399
+ await done;
400
+ }
401
+ this.bus.emit("agent:tool-output", {
402
+ tool: session.command ?? "",
403
+ output: session.output,
404
+ exitCode: session.exitCode,
405
+ });
406
+ return { exitCode: session.exitCode ?? -1 };
407
+ }
408
+ async handleKillTerminal(params) {
409
+ const session = this.terminalSessions.get(params.terminalId);
410
+ if (session && !session.done) {
411
+ killSession(session);
412
+ }
413
+ return {};
414
+ }
415
+ async handleReleaseTerminal(params) {
416
+ this.terminalSessions.delete(params.terminalId);
417
+ this.terminalDonePromises.delete(params.terminalId);
418
+ return {};
419
+ }
420
+ // ── Filesystem handlers ────────────────────────────────────────
421
+ async handleReadTextFile(params) {
422
+ try {
423
+ let content = await fs.readFile(params.path, "utf-8");
424
+ if (params.line != null || params.limit != null) {
425
+ const lines = content.split("\n");
426
+ const start = (params.line ?? 1) - 1;
427
+ const end = params.limit != null ? start + params.limit : lines.length;
428
+ content = lines.slice(start, end).join("\n");
429
+ }
430
+ return { content };
431
+ }
432
+ catch (err) {
433
+ throw new Error(`Failed to read ${params.path}: ${err instanceof Error ? err.message : String(err)}`);
434
+ }
435
+ }
436
+ async handleWriteTextFile(params) {
437
+ // Read original content for diff preview
438
+ let original = null;
439
+ try {
440
+ original = await fs.readFile(params.path, "utf-8");
441
+ }
442
+ catch {
443
+ // File doesn't exist yet — will show as "new file"
444
+ }
445
+ const diff = computeDiff(original, params.content);
446
+ // Identical content — nothing to do
447
+ if (diff.isIdentical)
448
+ return {};
449
+ // Extensions can gate this — default is auto-approve (yolo mode)
450
+ const result = await this.bus.emitPipeAsync("permission:request", {
451
+ kind: "file-write",
452
+ title: params.path,
453
+ metadata: { path: params.path, diff, content: params.content },
454
+ decision: { approved: true },
455
+ });
456
+ if (!result.decision.approved) {
457
+ throw new Error(`User rejected modification: ${params.path}`);
458
+ }
459
+ // Write the file
460
+ try {
461
+ await fs.writeFile(params.path, params.content, "utf-8");
462
+ this.fileWatcher.approve(path.resolve(params.path), params.content);
463
+ return {};
464
+ }
465
+ catch (err) {
466
+ throw new Error(`Failed to write ${params.path}: ${err instanceof Error ? err.message : String(err)}`);
467
+ }
468
+ }
469
+ // ── Diff preview for non-ACP file changes ────────────────────
470
+ /**
471
+ * After the agent finishes, check all tracked files for changes
472
+ * made via the agent's own tools (not through fs/writeTextFile)
473
+ * and show interactive diff previews.
474
+ */
475
+ async showPendingFileChanges() {
476
+ const changes = await this.fileWatcher.detectChanges();
477
+ if (changes.length === 0)
478
+ return;
479
+ for (const change of changes) {
480
+ const diff = computeDiff(change.before, change.after);
481
+ if (diff.isIdentical)
482
+ continue;
483
+ const result = await this.bus.emitPipeAsync("permission:request", {
484
+ kind: "file-write",
485
+ title: change.relPath,
486
+ metadata: { path: change.relPath, diff, content: change.after },
487
+ decision: { approved: true },
488
+ });
489
+ if (result.decision.approved) {
490
+ this.fileWatcher.approve(change.path, change.after);
491
+ }
492
+ else {
493
+ await this.fileWatcher.revert(change.path);
494
+ }
495
+ }
496
+ }
497
+ // ── Cleanup ────────────────────────────────────────────────────
498
+ kill() {
499
+ for (const session of this.terminalSessions.values()) {
500
+ if (!session.done)
501
+ killSession(session);
502
+ }
503
+ if (this.agentProcess && !this.agentProcess.killed) {
504
+ this.agentProcess.kill("SIGTERM");
505
+ }
506
+ }
507
+ }
@@ -0,0 +1,45 @@
1
+ import type { EventBus } from "./event-bus.js";
2
+ export declare class ContextManager {
3
+ private exchanges;
4
+ private nextId;
5
+ private currentCwd;
6
+ private sessionStart;
7
+ private pendingToolCalls;
8
+ constructor(bus: EventBus);
9
+ getCwd(): string;
10
+ /**
11
+ * Build the <shell_context> block for the agent prompt.
12
+ * Pipeline: window → truncate → format
13
+ */
14
+ getContext(budget?: number): string;
15
+ /**
16
+ * Regex/keyword search across all exchanges. Returns formatted results.
17
+ */
18
+ search(query: string): string;
19
+ /**
20
+ * Return full untruncated content for specific exchange IDs.
21
+ */
22
+ expand(ids: number[]): string;
23
+ /**
24
+ * One-line summaries of last N exchanges.
25
+ */
26
+ getRecentSummary(n?: number): string;
27
+ /**
28
+ * Parse and handle __shell_recall commands.
29
+ */
30
+ handleRecallCommand(command: string): string;
31
+ /**
32
+ * Clear exchange history (used by /clear command).
33
+ */
34
+ clear(): void;
35
+ private applyWindow;
36
+ private applyTruncation;
37
+ private formatContext;
38
+ private addExchange;
39
+ private formatExchangeTruncated;
40
+ private truncateForRecall;
41
+ private formatExchangeFull;
42
+ private exchangeOneLiner;
43
+ private exchangeSearchText;
44
+ private exchangeSize;
45
+ }