agent-sh 0.5.0 → 0.7.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.
- package/README.md +12 -43
- package/dist/agent/agent-loop.d.ts +1 -0
- package/dist/agent/agent-loop.js +119 -26
- package/dist/agent/subagent.js +3 -1
- package/dist/agent/system-prompt.d.ts +1 -1
- package/dist/agent/system-prompt.js +21 -16
- package/dist/agent/tools/bash.js +10 -1
- package/dist/agent/tools/display.d.ts +13 -0
- package/dist/agent/tools/display.js +70 -0
- package/dist/agent/tools/edit-file.js +60 -7
- package/dist/agent/tools/glob.js +39 -7
- package/dist/agent/tools/grep.js +111 -20
- package/dist/agent/tools/ls.js +31 -2
- package/dist/agent/tools/read-file.d.ts +9 -1
- package/dist/agent/tools/read-file.js +50 -4
- package/dist/agent/tools/user-shell.js +40 -13
- package/dist/agent/tools/write-file.js +9 -1
- package/dist/agent/types.d.ts +35 -1
- package/dist/context-manager.d.ts +3 -1
- package/dist/context-manager.js +11 -1
- package/dist/core.d.ts +1 -3
- package/dist/core.js +23 -12
- package/dist/event-bus.d.ts +41 -3
- package/dist/extension-loader.d.ts +1 -1
- package/dist/extension-loader.js +1 -3
- package/dist/extensions/overlay-agent.d.ts +11 -0
- package/dist/extensions/overlay-agent.js +43 -0
- package/dist/extensions/terminal-buffer.d.ts +14 -0
- package/dist/extensions/terminal-buffer.js +120 -0
- package/dist/extensions/tui-renderer.js +344 -83
- package/dist/index.js +45 -36
- package/dist/input-handler.js +10 -3
- package/dist/output-parser.js +8 -0
- package/dist/settings.js +1 -1
- package/dist/shell.d.ts +5 -0
- package/dist/shell.js +29 -4
- package/dist/types.d.ts +13 -0
- package/dist/utils/diff.js +10 -0
- package/dist/utils/floating-panel.d.ts +198 -0
- package/dist/utils/floating-panel.js +590 -0
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +23 -1
- package/dist/utils/output-writer.d.ts +14 -0
- package/dist/utils/output-writer.js +16 -0
- package/dist/utils/terminal-buffer.d.ts +65 -0
- package/dist/utils/terminal-buffer.js +166 -0
- package/dist/utils/tool-display.d.ts +4 -0
- package/dist/utils/tool-display.js +22 -5
- package/examples/extensions/claude-code-bridge/index.ts +8 -12
- package/examples/extensions/overlay-agent.ts +70 -0
- package/examples/extensions/pi-bridge/index.ts +10 -12
- package/examples/extensions/secret-guard.ts +100 -0
- package/examples/extensions/terminal-buffer.ts +184 -0
- package/package.json +5 -1
package/dist/agent/types.d.ts
CHANGED
|
@@ -21,15 +21,36 @@ export interface ToolResult {
|
|
|
21
21
|
exitCode: number | null;
|
|
22
22
|
isError: boolean;
|
|
23
23
|
}
|
|
24
|
+
/** Structured result display — returned by formatResult or computed by defaults. */
|
|
25
|
+
export interface ToolResultDisplay {
|
|
26
|
+
/** One-line summary shown next to ✓/✗ (e.g. "42 papers found", "+3/-1"). */
|
|
27
|
+
summary?: string;
|
|
28
|
+
/** Structured content to render below the status line. */
|
|
29
|
+
body?: ToolResultBody;
|
|
30
|
+
}
|
|
31
|
+
export type ToolResultBody = {
|
|
32
|
+
kind: "diff";
|
|
33
|
+
diff: unknown;
|
|
34
|
+
filePath: string;
|
|
35
|
+
} | {
|
|
36
|
+
kind: "lines";
|
|
37
|
+
lines: string[];
|
|
38
|
+
maxLines?: number;
|
|
39
|
+
};
|
|
24
40
|
export interface ToolDisplayInfo {
|
|
25
|
-
kind: "read" | "write" | "execute" | "search";
|
|
41
|
+
kind: "read" | "write" | "execute" | "search" | "display";
|
|
26
42
|
locations?: {
|
|
27
43
|
path: string;
|
|
28
44
|
line?: number | null;
|
|
29
45
|
}[];
|
|
46
|
+
/** Custom icon character for TUI display (e.g., "◆", "⌕"). When set, the TUI shows
|
|
47
|
+
* icon + detail only. When absent, the tool name is shown alongside the detail. */
|
|
48
|
+
icon?: string;
|
|
30
49
|
}
|
|
31
50
|
export interface ToolDefinition {
|
|
32
51
|
name: string;
|
|
52
|
+
/** Short label for TUI display (e.g. "search" instead of "ads_search"). Defaults to name. */
|
|
53
|
+
displayName?: string;
|
|
33
54
|
description: string;
|
|
34
55
|
input_schema: Record<string, unknown>;
|
|
35
56
|
execute(args: Record<string, unknown>, onChunk?: (chunk: string) => void): Promise<ToolResult>;
|
|
@@ -41,4 +62,17 @@ export interface ToolDefinition {
|
|
|
41
62
|
requiresPermission?: boolean;
|
|
42
63
|
/** Derive display metadata (icon kind, file paths) for the TUI. */
|
|
43
64
|
getDisplayInfo?: (args: Record<string, unknown>) => ToolDisplayInfo;
|
|
65
|
+
/**
|
|
66
|
+
* Format a short display string for the TUI when this tool is called.
|
|
67
|
+
* Return a concise summary of the args (e.g. the query, the file path).
|
|
68
|
+
* When absent, the TUI derives the detail from common arg fields (command, path, pattern).
|
|
69
|
+
*/
|
|
70
|
+
formatCall?: (args: Record<string, unknown>) => string;
|
|
71
|
+
/**
|
|
72
|
+
* Format result display for the TUI after execution completes.
|
|
73
|
+
* Return a summary string and/or structured body to render.
|
|
74
|
+
* When absent, defaults are computed based on tool kind.
|
|
75
|
+
* Extensions can further override via bus.onPipe("agent:tool-completed", ...).
|
|
76
|
+
*/
|
|
77
|
+
formatResult?: (args: Record<string, unknown>, result: ToolResult) => ToolResultDisplay;
|
|
44
78
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EventBus } from "./event-bus.js";
|
|
2
|
+
import type { HandlerRegistry } from "./utils/handler-registry.js";
|
|
2
3
|
export declare class ContextManager {
|
|
3
4
|
private exchanges;
|
|
4
5
|
private nextId;
|
|
@@ -7,7 +8,8 @@ export declare class ContextManager {
|
|
|
7
8
|
private pendingToolCalls;
|
|
8
9
|
private firstPrompt;
|
|
9
10
|
private agentShellActive;
|
|
10
|
-
|
|
11
|
+
private handlers;
|
|
12
|
+
constructor(bus: EventBus, handlers?: HandlerRegistry);
|
|
11
13
|
getCwd(): string;
|
|
12
14
|
/**
|
|
13
15
|
* Build the <shell_context> block for the agent prompt.
|
package/dist/context-manager.js
CHANGED
|
@@ -13,7 +13,13 @@ export class ContextManager {
|
|
|
13
13
|
pendingToolCalls = [];
|
|
14
14
|
firstPrompt = true;
|
|
15
15
|
agentShellActive = false; // true while user_shell command is executing
|
|
16
|
-
|
|
16
|
+
handlers = null;
|
|
17
|
+
constructor(bus, handlers) {
|
|
18
|
+
if (handlers) {
|
|
19
|
+
this.handlers = handlers;
|
|
20
|
+
// Extensions can advise this to inject extra context (e.g. terminal buffer)
|
|
21
|
+
handlers.define("context:build-extra", () => "");
|
|
22
|
+
}
|
|
17
23
|
this.currentCwd = process.cwd();
|
|
18
24
|
this.sessionStart = Date.now();
|
|
19
25
|
// ── Subscribe to shell events ──
|
|
@@ -291,6 +297,10 @@ export class ContextManager {
|
|
|
291
297
|
for (const ex of exchanges) {
|
|
292
298
|
out += "\n" + this.formatExchangeTruncated(ex);
|
|
293
299
|
}
|
|
300
|
+
// Allow extensions to inject extra context (e.g. terminal buffer snapshot)
|
|
301
|
+
const extra = this.handlers?.call("context:build-extra");
|
|
302
|
+
if (extra)
|
|
303
|
+
out += "\n" + extra + "\n";
|
|
294
304
|
out += "\n</shell_context>\n";
|
|
295
305
|
return out;
|
|
296
306
|
}
|
package/dist/core.d.ts
CHANGED
|
@@ -36,9 +36,7 @@ export interface AgentShellCore {
|
|
|
36
36
|
/** Activate the agent backend (call after extensions load). */
|
|
37
37
|
activateBackend(): void;
|
|
38
38
|
/** Convenience: emit agent:submit and await the response. */
|
|
39
|
-
query(text: string
|
|
40
|
-
mode?: string;
|
|
41
|
-
}): Promise<string>;
|
|
39
|
+
query(text: string): Promise<string>;
|
|
42
40
|
/** Convenience: emit agent:cancel-request. */
|
|
43
41
|
cancel(): void;
|
|
44
42
|
/** Build an ExtensionContext for loading extensions against this core. */
|
package/dist/core.js
CHANGED
|
@@ -25,6 +25,8 @@ import * as streamTransform from "./utils/stream-transform.js";
|
|
|
25
25
|
import * as settingsMod from "./settings.js";
|
|
26
26
|
import { resolveProvider, getProviderNames } from "./settings.js";
|
|
27
27
|
import { HandlerRegistry } from "./utils/handler-registry.js";
|
|
28
|
+
import { TerminalBuffer } from "./utils/terminal-buffer.js";
|
|
29
|
+
import { FloatingPanel } from "./utils/floating-panel.js";
|
|
28
30
|
// Re-export types that library consumers need
|
|
29
31
|
export { EventBus } from "./event-bus.js";
|
|
30
32
|
export { palette, setPalette, resetPalette } from "./utils/palette.js";
|
|
@@ -33,7 +35,7 @@ export { LlmClient } from "./utils/llm-client.js";
|
|
|
33
35
|
export function createCore(config) {
|
|
34
36
|
const bus = new EventBus();
|
|
35
37
|
const handlers = new HandlerRegistry();
|
|
36
|
-
const contextManager = new ContextManager(bus);
|
|
38
|
+
const contextManager = new ContextManager(bus, handlers);
|
|
37
39
|
// ── Resolve provider ─────────────────────────────────────────
|
|
38
40
|
const settings = settingsMod.getSettings();
|
|
39
41
|
let activeProvider = null;
|
|
@@ -216,17 +218,27 @@ export function createCore(config) {
|
|
|
216
218
|
bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
|
|
217
219
|
bus.emit("config:changed", {});
|
|
218
220
|
});
|
|
221
|
+
// ── Lazy singleton terminal buffer ──────────────────────────
|
|
222
|
+
let terminalBufferSingleton; // undefined = not yet created
|
|
223
|
+
const getTerminalBuffer = () => {
|
|
224
|
+
if (terminalBufferSingleton !== undefined)
|
|
225
|
+
return terminalBufferSingleton;
|
|
226
|
+
terminalBufferSingleton = TerminalBuffer.createWired(bus);
|
|
227
|
+
return terminalBufferSingleton;
|
|
228
|
+
};
|
|
219
229
|
return {
|
|
220
230
|
bus,
|
|
221
231
|
contextManager,
|
|
222
232
|
llmClient,
|
|
223
233
|
activateBackend() {
|
|
234
|
+
// Silent — backend info is shown in the startup banner.
|
|
235
|
+
// Runtime switches (config:switch-backend) still emit ui:info.
|
|
224
236
|
const preferred = settings.defaultBackend;
|
|
225
237
|
if (preferred && backends.has(preferred)) {
|
|
226
|
-
activateByName(preferred);
|
|
238
|
+
activateByName(preferred, true);
|
|
227
239
|
}
|
|
228
240
|
else if (backends.size > 0 && !agentLoop) {
|
|
229
|
-
activateByName(backends.keys().next().value);
|
|
241
|
+
activateByName(backends.keys().next().value, true);
|
|
230
242
|
}
|
|
231
243
|
else if (agentLoop) {
|
|
232
244
|
agentLoop.wire();
|
|
@@ -234,13 +246,10 @@ export function createCore(config) {
|
|
|
234
246
|
bus.emit("agent:info", { name: "agent-sh", version: "0.4", model: llmClient?.model, provider: activeProvider?.id, contextWindow: activeProvider?.contextWindow });
|
|
235
247
|
}
|
|
236
248
|
else if (backends.size > 0) {
|
|
237
|
-
activateByName(backends.keys().next().value);
|
|
238
|
-
}
|
|
239
|
-
if (activeBackendName) {
|
|
240
|
-
bus.emit("ui:info", { message: `Backend: ${activeBackendName}` });
|
|
249
|
+
activateByName(backends.keys().next().value, true);
|
|
241
250
|
}
|
|
242
251
|
},
|
|
243
|
-
async query(text
|
|
252
|
+
async query(text) {
|
|
244
253
|
return new Promise((resolve, reject) => {
|
|
245
254
|
let response = "";
|
|
246
255
|
let settled = false;
|
|
@@ -271,10 +280,7 @@ export function createCore(config) {
|
|
|
271
280
|
bus.on("agent:response-chunk", onChunk);
|
|
272
281
|
bus.on("agent:processing-done", onDone);
|
|
273
282
|
bus.on("agent:error", onError);
|
|
274
|
-
bus.emit("agent:submit", {
|
|
275
|
-
query: text,
|
|
276
|
-
modeInstruction: opts?.mode,
|
|
277
|
-
});
|
|
283
|
+
bus.emit("agent:submit", { query: text });
|
|
278
284
|
});
|
|
279
285
|
},
|
|
280
286
|
cancel() {
|
|
@@ -296,6 +302,11 @@ export function createCore(config) {
|
|
|
296
302
|
define: (name, fn) => handlers.define(name, fn),
|
|
297
303
|
advise: (name, wrapper) => handlers.advise(name, wrapper),
|
|
298
304
|
call: (name, ...args) => handlers.call(name, ...args),
|
|
305
|
+
get terminalBuffer() { return getTerminalBuffer(); },
|
|
306
|
+
createFloatingPanel: (config) => {
|
|
307
|
+
const tb = config.dimBackground !== false ? getTerminalBuffer() : null;
|
|
308
|
+
return new FloatingPanel(bus, { ...config, terminalBuffer: tb ?? undefined });
|
|
309
|
+
},
|
|
299
310
|
};
|
|
300
311
|
},
|
|
301
312
|
kill() {
|
package/dist/event-bus.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentMode } from "./types.js";
|
|
2
|
+
import type { ToolResultDisplay } from "./agent/types.js";
|
|
2
3
|
/**
|
|
3
4
|
* Typed event map — every event has a known payload shape.
|
|
4
5
|
*/
|
|
@@ -21,10 +22,23 @@ export interface ShellEvents {
|
|
|
21
22
|
};
|
|
22
23
|
"shell:agent-exec-start": Record<string, never>;
|
|
23
24
|
"shell:agent-exec-done": Record<string, never>;
|
|
25
|
+
"shell:pty-data": {
|
|
26
|
+
raw: string;
|
|
27
|
+
};
|
|
28
|
+
"shell:pty-write": {
|
|
29
|
+
data: string;
|
|
30
|
+
};
|
|
31
|
+
"shell:buffer-request": Record<string, never>;
|
|
32
|
+
"shell:buffer-snapshot": {
|
|
33
|
+
text: string;
|
|
34
|
+
altScreen: boolean;
|
|
35
|
+
cursor: {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
24
40
|
"agent:submit": {
|
|
25
41
|
query: string;
|
|
26
|
-
modeInstruction?: string;
|
|
27
|
-
modeLabel?: string;
|
|
28
42
|
};
|
|
29
43
|
"agent:cancel-request": {
|
|
30
44
|
silent?: boolean;
|
|
@@ -32,7 +46,6 @@ export interface ShellEvents {
|
|
|
32
46
|
"input-mode:register": import("./types.js").InputModeConfig;
|
|
33
47
|
"agent:query": {
|
|
34
48
|
query: string;
|
|
35
|
-
modeLabel?: string;
|
|
36
49
|
};
|
|
37
50
|
"agent:thinking-chunk": {
|
|
38
51
|
text: string;
|
|
@@ -63,21 +76,37 @@ export interface ShellEvents {
|
|
|
63
76
|
output: string;
|
|
64
77
|
exitCode: number | null;
|
|
65
78
|
};
|
|
79
|
+
"agent:tool-batch": {
|
|
80
|
+
groups: Array<{
|
|
81
|
+
kind: string;
|
|
82
|
+
tools: Array<{
|
|
83
|
+
name: string;
|
|
84
|
+
displayDetail?: string;
|
|
85
|
+
}>;
|
|
86
|
+
}>;
|
|
87
|
+
};
|
|
66
88
|
"agent:tool-started": {
|
|
67
89
|
title: string;
|
|
68
90
|
toolCallId?: string;
|
|
69
91
|
kind?: string;
|
|
92
|
+
icon?: string;
|
|
70
93
|
locations?: {
|
|
71
94
|
path: string;
|
|
72
95
|
line?: number | null;
|
|
73
96
|
}[];
|
|
74
97
|
rawInput?: unknown;
|
|
98
|
+
/** Pre-formatted display detail from tool's formatCall(). */
|
|
99
|
+
displayDetail?: string;
|
|
100
|
+
batchIndex?: number;
|
|
101
|
+
batchTotal?: number;
|
|
75
102
|
};
|
|
76
103
|
"agent:tool-completed": {
|
|
77
104
|
toolCallId?: string;
|
|
78
105
|
exitCode: number | null;
|
|
79
106
|
rawOutput?: unknown;
|
|
80
107
|
kind?: string;
|
|
108
|
+
/** Structured result display — set by formatResult or defaults, overridable via onPipe. */
|
|
109
|
+
resultDisplay?: ToolResultDisplay;
|
|
81
110
|
};
|
|
82
111
|
"agent:tool-output-chunk": {
|
|
83
112
|
chunk: string;
|
|
@@ -109,6 +138,14 @@ export interface ShellEvents {
|
|
|
109
138
|
"input:keypress": {
|
|
110
139
|
key: string;
|
|
111
140
|
};
|
|
141
|
+
"input:intercept": {
|
|
142
|
+
data: string;
|
|
143
|
+
consumed: boolean;
|
|
144
|
+
};
|
|
145
|
+
"shell:stdout-hold": Record<string, never>;
|
|
146
|
+
"shell:stdout-release": Record<string, never>;
|
|
147
|
+
"shell:stdout-show": Record<string, never>;
|
|
148
|
+
"shell:stdout-hide": Record<string, never>;
|
|
112
149
|
"agent:terminal-intercept": {
|
|
113
150
|
command: string;
|
|
114
151
|
cwd: string;
|
|
@@ -123,6 +160,7 @@ export interface ShellEvents {
|
|
|
123
160
|
command: string;
|
|
124
161
|
output: string;
|
|
125
162
|
cwd: string;
|
|
163
|
+
exitCode: number | null;
|
|
126
164
|
done: boolean;
|
|
127
165
|
};
|
|
128
166
|
"agent:info": {
|
|
@@ -13,4 +13,4 @@ import type { ExtensionContext } from "./types.js";
|
|
|
13
13
|
* Each module should export a default or named `activate(ctx)` function.
|
|
14
14
|
* Errors are non-fatal — logged via ui:error and skipped.
|
|
15
15
|
*/
|
|
16
|
-
export declare function loadExtensions(ctx: ExtensionContext, cliExtensions?: string[]): Promise<
|
|
16
|
+
export declare function loadExtensions(ctx: ExtensionContext, cliExtensions?: string[]): Promise<string[]>;
|
package/dist/extension-loader.js
CHANGED
|
@@ -102,9 +102,7 @@ export async function loadExtensions(ctx, cliExtensions) {
|
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
-
|
|
106
|
-
ctx.bus.emit("ui:info", { message: `Extensions: ${loaded.join(", ")}` });
|
|
107
|
-
}
|
|
105
|
+
return loaded;
|
|
108
106
|
}
|
|
109
107
|
/**
|
|
110
108
|
* Find an index file in a directory extension.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in overlay agent.
|
|
3
|
+
*
|
|
4
|
+
* Provides a hotkey (Ctrl+\) to summon the agent from anywhere — even
|
|
5
|
+
* inside vim, htop, or ssh. Composites a floating response box on top
|
|
6
|
+
* of the current terminal content.
|
|
7
|
+
*
|
|
8
|
+
* Requires: npm install @xterm/headless@5.5.0 @xterm/addon-serialize@0.13.0
|
|
9
|
+
*/
|
|
10
|
+
import type { ExtensionContext } from "../types.js";
|
|
11
|
+
export default function activate({ bus, createFloatingPanel }: ExtensionContext): void;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const BOLD = "\x1b[1m";
|
|
2
|
+
const CYAN = "\x1b[36m";
|
|
3
|
+
const RESET = "\x1b[0m";
|
|
4
|
+
export default function activate({ bus, createFloatingPanel }) {
|
|
5
|
+
const panel = createFloatingPanel({
|
|
6
|
+
trigger: "\x1c", // Ctrl+\
|
|
7
|
+
dimBackground: true,
|
|
8
|
+
autoDismissMs: 2000,
|
|
9
|
+
});
|
|
10
|
+
// ── Panel lifecycle ────────────────────────────────────────
|
|
11
|
+
panel.handlers.advise("panel:submit", (_next, query) => {
|
|
12
|
+
panel.setActive();
|
|
13
|
+
panel.appendLine(`${CYAN}${BOLD}❯${RESET} ${query}`);
|
|
14
|
+
panel.appendLine("");
|
|
15
|
+
bus.emit("agent:submit", { query });
|
|
16
|
+
});
|
|
17
|
+
// ── Stream agent response into panel ───────────────────────
|
|
18
|
+
bus.on("agent:response-chunk", (e) => {
|
|
19
|
+
if (!panel.active)
|
|
20
|
+
return;
|
|
21
|
+
for (const block of e.blocks) {
|
|
22
|
+
if (block.type === "text" && block.text) {
|
|
23
|
+
panel.appendText(block.text);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
bus.on("agent:tool-started", (e) => {
|
|
28
|
+
if (!panel.active)
|
|
29
|
+
return;
|
|
30
|
+
panel.appendLine(`▶ ${e.title}${e.displayDetail ? " " + e.displayDetail : ""}`);
|
|
31
|
+
});
|
|
32
|
+
bus.on("agent:tool-completed", (e) => {
|
|
33
|
+
if (!panel.active)
|
|
34
|
+
return;
|
|
35
|
+
const mark = e.exitCode === 0 ? " ✓" : ` ✗ exit ${e.exitCode}`;
|
|
36
|
+
panel.updateLastLine((line) => line + mark);
|
|
37
|
+
});
|
|
38
|
+
bus.on("agent:processing-done", () => {
|
|
39
|
+
if (!panel.active)
|
|
40
|
+
return;
|
|
41
|
+
panel.setDone();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in terminal buffer extension.
|
|
3
|
+
*
|
|
4
|
+
* Registers two agent tools:
|
|
5
|
+
* - terminal_read: get the current screen contents + cursor position
|
|
6
|
+
* - terminal_keys: send raw keystrokes into the user's live PTY
|
|
7
|
+
*
|
|
8
|
+
* Together these let the agent operate inside interactive programs
|
|
9
|
+
* (vim, htop, less, etc.) by reading the screen and typing keys.
|
|
10
|
+
*
|
|
11
|
+
* Requires: npm install @xterm/headless@5.5.0 @xterm/addon-serialize@0.13.0
|
|
12
|
+
*/
|
|
13
|
+
import type { ExtensionContext } from "../types.js";
|
|
14
|
+
export default function activate({ bus, terminalBuffer: tb, registerTool }: ExtensionContext): void;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/** Interpret C-style escape sequences (e.g. \r → CR, \x1b → ESC). */
|
|
2
|
+
function interpretEscapes(str) {
|
|
3
|
+
return str.replace(/\\(x[0-9a-fA-F]{2}|r|n|t|\\|0)/g, (_, seq) => {
|
|
4
|
+
if (seq === "r")
|
|
5
|
+
return "\r";
|
|
6
|
+
if (seq === "n")
|
|
7
|
+
return "\n";
|
|
8
|
+
if (seq === "t")
|
|
9
|
+
return "\t";
|
|
10
|
+
if (seq === "\\")
|
|
11
|
+
return "\\";
|
|
12
|
+
if (seq === "0")
|
|
13
|
+
return "\0";
|
|
14
|
+
if (seq.startsWith("x"))
|
|
15
|
+
return String.fromCharCode(parseInt(seq.slice(1), 16));
|
|
16
|
+
return seq;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function settle(ms = 100) {
|
|
20
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
export default function activate({ bus, terminalBuffer: tb, registerTool }) {
|
|
23
|
+
if (!tb)
|
|
24
|
+
return; // @xterm/headless not installed
|
|
25
|
+
registerTool({
|
|
26
|
+
name: "terminal_read",
|
|
27
|
+
description: "Read the current terminal screen contents. Returns clean text (ANSI stripped) " +
|
|
28
|
+
"with cursor position and whether an alternate-screen program (vim, htop, less) is active. " +
|
|
29
|
+
"Use this to see what the user sees before sending keystrokes with terminal_keys.",
|
|
30
|
+
input_schema: {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {},
|
|
33
|
+
},
|
|
34
|
+
showOutput: true,
|
|
35
|
+
getDisplayInfo: () => ({
|
|
36
|
+
kind: "read",
|
|
37
|
+
icon: "⊞",
|
|
38
|
+
locations: [],
|
|
39
|
+
}),
|
|
40
|
+
async execute() {
|
|
41
|
+
const { text, altScreen, cursorX, cursorY } = tb.readScreen();
|
|
42
|
+
const info = [
|
|
43
|
+
altScreen ? "mode: alternate screen" : "mode: normal",
|
|
44
|
+
`cursor: row=${cursorY} col=${cursorX}`,
|
|
45
|
+
].join(", ");
|
|
46
|
+
return {
|
|
47
|
+
content: `[${info}]\n\n${text}`,
|
|
48
|
+
exitCode: 0,
|
|
49
|
+
isError: false,
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
registerTool({
|
|
54
|
+
name: "terminal_keys",
|
|
55
|
+
description: "Send keystrokes to the user's live terminal. The keys are written directly to the PTY " +
|
|
56
|
+
"as if the user typed them. Use escape sequences for special keys:\n" +
|
|
57
|
+
" - Escape: \\x1b\n" +
|
|
58
|
+
" - Enter/Return: \\r\n" +
|
|
59
|
+
" - Tab: \\t\n" +
|
|
60
|
+
" - Ctrl+C: \\x03\n" +
|
|
61
|
+
" - Ctrl+D: \\x04\n" +
|
|
62
|
+
" - Ctrl+Z: \\x1a\n" +
|
|
63
|
+
" - Arrow keys: \\x1b[A (up), \\x1b[B (down), \\x1b[C (right), \\x1b[D (left)\n" +
|
|
64
|
+
" - Backspace: \\x7f\n\n" +
|
|
65
|
+
"Example: to quit vim without saving, send keys=\"\\x1b:q!\\r\" (Escape, :q!, Enter).\n" +
|
|
66
|
+
"Always call terminal_read after sending keys to verify the result.",
|
|
67
|
+
input_schema: {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: {
|
|
70
|
+
keys: {
|
|
71
|
+
type: "string",
|
|
72
|
+
description: "The keystrokes to send. Use \\x1b for Escape, \\r for Enter, \\t for Tab, " +
|
|
73
|
+
"\\x03 for Ctrl+C, etc. Regular characters are sent as-is.",
|
|
74
|
+
},
|
|
75
|
+
settle_ms: {
|
|
76
|
+
type: "number",
|
|
77
|
+
description: "Milliseconds to wait after sending keys for the terminal to settle before " +
|
|
78
|
+
"returning (default: 150). Increase for slow programs.",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
required: ["keys"],
|
|
82
|
+
},
|
|
83
|
+
showOutput: false,
|
|
84
|
+
getDisplayInfo: () => ({
|
|
85
|
+
kind: "execute",
|
|
86
|
+
icon: "⌨",
|
|
87
|
+
locations: [],
|
|
88
|
+
}),
|
|
89
|
+
formatCall: (args) => {
|
|
90
|
+
const keys = args.keys;
|
|
91
|
+
return keys
|
|
92
|
+
.replace(/\\x1b|\x1b/g, "ESC")
|
|
93
|
+
.replace(/\\r|\r/g, "⏎")
|
|
94
|
+
.replace(/\\n|\n/g, "↵")
|
|
95
|
+
.replace(/\\t|\t/g, "TAB")
|
|
96
|
+
.replace(/\\x03|\x03/g, "^C")
|
|
97
|
+
.replace(/\\x04|\x04/g, "^D")
|
|
98
|
+
.replace(/\\x7f|\x7f/g, "BS");
|
|
99
|
+
},
|
|
100
|
+
async execute(args) {
|
|
101
|
+
const raw = args.keys;
|
|
102
|
+
const keys = interpretEscapes(raw);
|
|
103
|
+
const settleMs = args.settle_ms ?? 150;
|
|
104
|
+
bus.emit("shell:stdout-show", {});
|
|
105
|
+
process.stdout.write("\n");
|
|
106
|
+
bus.emit("shell:pty-write", { data: keys });
|
|
107
|
+
await settle(settleMs);
|
|
108
|
+
const { text, altScreen, cursorX, cursorY } = tb.readScreen();
|
|
109
|
+
const info = [
|
|
110
|
+
altScreen ? "mode: alternate screen" : "mode: normal",
|
|
111
|
+
`cursor: row=${cursorY} col=${cursorX}`,
|
|
112
|
+
].join(", ");
|
|
113
|
+
return {
|
|
114
|
+
content: `Keys sent. Screen after:\n[${info}]\n\n${text}`,
|
|
115
|
+
exitCode: 0,
|
|
116
|
+
isError: false,
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|