agent-sh 0.12.26 → 0.13.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 +13 -2
- package/dist/agent/agent-loop.d.ts +3 -5
- package/dist/agent/agent-loop.js +44 -100
- package/dist/agent/conversation-state.d.ts +9 -0
- package/dist/agent/conversation-state.js +38 -1
- package/dist/agent/history-file.d.ts +6 -0
- package/dist/agent/history-file.js +1 -1
- package/dist/agent/host-types.d.ts +125 -0
- package/dist/agent/index.d.ts +12 -4
- package/dist/agent/index.js +357 -6
- package/dist/agent/nuclear-form.d.ts +7 -0
- package/dist/{extensions → agent}/providers/deepseek.d.ts +2 -2
- package/dist/{extensions → agent}/providers/deepseek.js +5 -4
- package/dist/{extensions → agent}/providers/openai-compatible.d.ts +2 -2
- package/dist/{extensions → agent}/providers/openai.d.ts +2 -2
- package/dist/{extensions → agent}/providers/openai.js +3 -2
- package/dist/{extensions → agent}/providers/openrouter.d.ts +2 -2
- package/dist/{extensions → agent}/providers/openrouter.js +4 -3
- package/dist/agent/skills.js +51 -7
- package/dist/agent/subagent.d.ts +1 -1
- package/dist/agent/system-prompt.js +14 -17
- package/dist/agent/tool-protocol.d.ts +1 -1
- package/dist/agent/tool-protocol.js +5 -3
- package/dist/agent/tool-registry.d.ts +9 -4
- package/dist/agent/tool-registry.js +27 -4
- package/dist/agent/tools/bash.d.ts +1 -1
- package/dist/agent/tools/bash.js +3 -2
- package/dist/agent/tools/edit-file.js +0 -1
- package/dist/agent/tools/glob.js +1 -1
- package/dist/agent/tools/grep.js +1 -1
- package/dist/agent/tools/pwsh.d.ts +1 -1
- package/dist/agent/tools/pwsh.js +1 -2
- package/dist/agent/tools/read-file.js +7 -4
- package/dist/agent/tools/write-file.js +0 -1
- package/dist/agent/types.d.ts +17 -2
- package/dist/cli/auth/cli.d.ts +1 -0
- package/dist/cli/auth/cli.js +216 -0
- package/dist/cli/auth/keys.d.ts +31 -0
- package/dist/cli/auth/keys.js +102 -0
- package/dist/{index.js → cli/index.js} +29 -32
- package/dist/{init.js → cli/init.js} +1 -1
- package/dist/{install.js → cli/install.js} +114 -5
- package/dist/cli/subcommands.d.ts +1 -0
- package/dist/cli/subcommands.js +17 -0
- package/dist/{event-bus.d.ts → core/event-bus.d.ts} +7 -13
- package/dist/{extension-loader.d.ts → core/extension-loader.d.ts} +1 -1
- package/dist/{extension-loader.js → core/extension-loader.js} +62 -70
- package/dist/{core.d.ts → core/index.d.ts} +18 -15
- package/dist/{core.js → core/index.js} +18 -92
- package/dist/{settings.d.ts → core/settings.d.ts} +7 -0
- package/dist/{settings.js → core/settings.js} +1 -0
- package/dist/core/types.d.ts +49 -0
- package/dist/core/types.js +1 -0
- package/dist/extensions/file-autocomplete.d.ts +1 -1
- package/dist/extensions/index.d.ts +7 -14
- package/dist/extensions/index.js +2 -19
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +7 -2
- package/dist/shell/host-types.d.ts +114 -0
- package/dist/shell/host-types.js +1 -0
- package/dist/shell/index.d.ts +8 -7
- package/dist/shell/index.js +58 -9
- package/dist/shell/input-handler.d.ts +7 -1
- package/dist/shell/input-handler.js +5 -2
- package/dist/shell/output-parser.d.ts +1 -1
- package/dist/{extensions → shell}/shell-context.d.ts +1 -1
- package/dist/{extensions → shell}/shell-context.js +18 -12
- package/dist/shell/shell.d.ts +6 -4
- package/dist/shell/shell.js +33 -109
- package/dist/shell/strategies/bash.d.ts +2 -0
- package/dist/shell/strategies/bash.js +68 -0
- package/dist/shell/strategies/fish.d.ts +2 -0
- package/dist/shell/strategies/fish.js +65 -0
- package/dist/shell/strategies/index.d.ts +13 -0
- package/dist/shell/strategies/index.js +17 -0
- package/dist/shell/strategies/types.d.ts +50 -0
- package/dist/shell/strategies/types.js +9 -0
- package/dist/shell/strategies/zsh.d.ts +2 -0
- package/dist/shell/strategies/zsh.js +72 -0
- package/dist/shell/tui-input-view.js +14 -3
- package/dist/{extensions → shell}/tui-renderer.d.ts +1 -1
- package/dist/{extensions → shell}/tui-renderer.js +27 -55
- package/dist/utils/box-frame.d.ts +4 -0
- package/dist/utils/box-frame.js +17 -6
- package/dist/utils/compositor.d.ts +1 -1
- package/dist/utils/compositor.js +2 -1
- package/dist/{executor.js → utils/executor.js} +1 -1
- package/dist/utils/floating-panel.d.ts +17 -5
- package/dist/utils/floating-panel.js +218 -70
- package/dist/utils/llm-facade.d.ts +7 -3
- package/dist/utils/stream-transform.d.ts +1 -1
- package/dist/utils/terminal-buffer.d.ts +1 -1
- package/dist/utils/tool-display.js +4 -0
- package/dist/utils/tool-interactive.d.ts +1 -1
- package/dist/utils/tty.d.ts +7 -0
- package/dist/utils/tty.js +15 -0
- package/examples/extensions/ash-acp-bridge/README.md +4 -1
- package/examples/extensions/ash-acp-bridge/src/index.ts +654 -0
- package/examples/extensions/ash-mcp-bridge/index.ts +1 -1
- package/examples/extensions/ashi/README.md +250 -0
- package/examples/extensions/ashi/package.json +60 -0
- package/examples/extensions/ashi/src/autocomplete.ts +91 -0
- package/examples/extensions/ashi/src/capture.ts +34 -0
- package/examples/extensions/ashi/src/cli.ts +126 -0
- package/examples/extensions/ashi/src/commands.ts +82 -0
- package/examples/extensions/ashi/src/compaction.ts +157 -0
- package/examples/extensions/ashi/src/components.ts +332 -0
- package/examples/extensions/ashi/src/default-renderers.ts +153 -0
- package/examples/extensions/ashi/src/display-config.ts +62 -0
- package/examples/extensions/ashi/src/frontend.ts +735 -0
- package/examples/extensions/ashi/src/hooks.ts +136 -0
- package/examples/extensions/ashi/src/multi-session-store.ts +146 -0
- package/examples/extensions/ashi/src/session-commands.ts +76 -0
- package/examples/extensions/ashi/src/session-store.ts +264 -0
- package/examples/extensions/ashi/src/status-footer.ts +66 -0
- package/examples/extensions/ashi/src/theme.ts +151 -0
- package/examples/extensions/ashi/tsconfig.json +14 -0
- package/examples/extensions/emacs-buffer.ts +364 -0
- package/examples/extensions/interactive-prompts.ts +114 -69
- package/examples/extensions/latex-images.ts +3 -3
- package/examples/extensions/opencode-bridge/index.ts +1 -1
- package/examples/extensions/overlay-agent.ts +35 -10
- package/examples/extensions/peer-mesh.ts +1 -1
- package/examples/extensions/pi-bridge/index.ts +0 -1
- package/examples/extensions/questionnaire.ts +2 -1
- package/examples/extensions/rtk-proxy.ts +3 -3
- package/examples/extensions/solarized-theme.ts +3 -3
- package/examples/extensions/subagents.ts +6 -6
- package/examples/extensions/terminal-buffer.ts +174 -33
- package/examples/extensions/tmux-pane.ts +6 -4
- package/examples/extensions/tunnel-vision.ts +405 -0
- package/examples/extensions/user-shell.ts +1 -1
- package/examples/extensions/web-access.ts +8 -113
- package/package.json +26 -22
- package/dist/extensions/agent-backend.d.ts +0 -14
- package/dist/extensions/agent-backend.js +0 -307
- package/dist/types.d.ts +0 -227
- /package/dist/{types.js → agent/host-types.js} +0 -0
- /package/dist/{extensions → agent}/providers/openai-compatible.js +0 -0
- /package/dist/{index.d.ts → cli/index.d.ts} +0 -0
- /package/dist/{init.d.ts → cli/init.d.ts} +0 -0
- /package/dist/{install.d.ts → cli/install.d.ts} +0 -0
- /package/dist/{event-bus.js → core/event-bus.js} +0 -0
- /package/dist/{executor.d.ts → utils/executor.d.ts} +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { highlight, supportsLanguage } from "cli-highlight";
|
|
3
|
+
import type { EditorTheme, MarkdownTheme, SelectListTheme } from "@earendil-works/pi-tui";
|
|
4
|
+
|
|
5
|
+
/** Bundled dark palette, lifted from pi-coding-agent's dark.json. */
|
|
6
|
+
const VARS: Record<string, string> = {
|
|
7
|
+
cyan: "#00d7ff",
|
|
8
|
+
blue: "#5f87ff",
|
|
9
|
+
green: "#b5bd68",
|
|
10
|
+
red: "#cc6666",
|
|
11
|
+
yellow: "#ffff00",
|
|
12
|
+
gray: "#808080",
|
|
13
|
+
dimGray: "#666666",
|
|
14
|
+
darkGray: "#505050",
|
|
15
|
+
accent: "#8abeb7",
|
|
16
|
+
selectedBg: "#3a3a4a",
|
|
17
|
+
userMsgBg: "#343541",
|
|
18
|
+
toolPendingBg: "#282832",
|
|
19
|
+
toolSuccessBg: "#283228",
|
|
20
|
+
toolErrorBg: "#3c2828",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const RAW = {
|
|
24
|
+
accent: "accent",
|
|
25
|
+
border: "blue",
|
|
26
|
+
borderAccent: "cyan",
|
|
27
|
+
borderMuted: "darkGray",
|
|
28
|
+
success: "green",
|
|
29
|
+
error: "red",
|
|
30
|
+
warning: "yellow",
|
|
31
|
+
muted: "gray",
|
|
32
|
+
dim: "dimGray",
|
|
33
|
+
text: "",
|
|
34
|
+
thinkingText: "gray",
|
|
35
|
+
selectedBg: "selectedBg",
|
|
36
|
+
userMessageBg: "userMsgBg",
|
|
37
|
+
userMessageText: "",
|
|
38
|
+
toolPendingBg: "toolPendingBg",
|
|
39
|
+
toolSuccessBg: "toolSuccessBg",
|
|
40
|
+
toolErrorBg: "toolErrorBg",
|
|
41
|
+
toolTitle: "",
|
|
42
|
+
toolOutput: "gray",
|
|
43
|
+
mdHeading: "#f0c674",
|
|
44
|
+
mdLink: "#81a2be",
|
|
45
|
+
mdLinkUrl: "dimGray",
|
|
46
|
+
mdCode: "accent",
|
|
47
|
+
mdCodeBlock: "green",
|
|
48
|
+
mdCodeBlockBorder: "gray",
|
|
49
|
+
mdQuote: "gray",
|
|
50
|
+
mdQuoteBorder: "gray",
|
|
51
|
+
mdHr: "gray",
|
|
52
|
+
mdListBullet: "accent",
|
|
53
|
+
toolDiffAdded: "green",
|
|
54
|
+
toolDiffRemoved: "red",
|
|
55
|
+
toolDiffContext: "gray",
|
|
56
|
+
bashMode: "green",
|
|
57
|
+
} as const;
|
|
58
|
+
|
|
59
|
+
export type ThemeColor = keyof typeof RAW;
|
|
60
|
+
|
|
61
|
+
function resolve(v: string): string {
|
|
62
|
+
if (v === "") return "";
|
|
63
|
+
if (v.startsWith("#")) return v;
|
|
64
|
+
const hex = VARS[v];
|
|
65
|
+
if (!hex) throw new Error(`Unknown theme var: ${v}`);
|
|
66
|
+
return hex;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function hexToRgb(hex: string): [number, number, number] {
|
|
70
|
+
const c = hex.replace("#", "");
|
|
71
|
+
return [parseInt(c.slice(0, 2), 16), parseInt(c.slice(2, 4), 16), parseInt(c.slice(4, 6), 16)];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function fgAnsi(hex: string): string {
|
|
75
|
+
if (hex === "") return "\x1b[39m";
|
|
76
|
+
const [r, g, b] = hexToRgb(hex);
|
|
77
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function bgAnsi(hex: string): string {
|
|
81
|
+
if (hex === "") return "\x1b[49m";
|
|
82
|
+
const [r, g, b] = hexToRgb(hex);
|
|
83
|
+
return `\x1b[48;2;${r};${g};${b}m`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class Theme {
|
|
87
|
+
private fgCodes = new Map<ThemeColor, string>();
|
|
88
|
+
private bgCodes = new Map<ThemeColor, string>();
|
|
89
|
+
constructor() {
|
|
90
|
+
for (const k of Object.keys(RAW) as ThemeColor[]) {
|
|
91
|
+
const hex = resolve(RAW[k]);
|
|
92
|
+
this.fgCodes.set(k, fgAnsi(hex));
|
|
93
|
+
this.bgCodes.set(k, bgAnsi(hex));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
fg(color: ThemeColor, text: string): string { return `${this.fgCodes.get(color)}${text}\x1b[39m`; }
|
|
97
|
+
bg(color: ThemeColor, text: string): string { return `${this.bgCodes.get(color)}${text}\x1b[49m`; }
|
|
98
|
+
bgCode(color: ThemeColor): string { return this.bgCodes.get(color) ?? ""; }
|
|
99
|
+
bold(text: string): string { return chalk.bold(text); }
|
|
100
|
+
italic(text: string): string { return chalk.italic(text); }
|
|
101
|
+
underline(text: string): string { return chalk.underline(text); }
|
|
102
|
+
strikethrough(text: string): string { return chalk.strikethrough(text); }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const theme = new Theme();
|
|
106
|
+
|
|
107
|
+
export function markdownTheme(): MarkdownTheme {
|
|
108
|
+
return {
|
|
109
|
+
heading: (t) => theme.fg("mdHeading", t),
|
|
110
|
+
link: (t) => theme.fg("mdLink", t),
|
|
111
|
+
linkUrl: (t) => theme.fg("mdLinkUrl", t),
|
|
112
|
+
code: (t) => theme.fg("mdCode", t),
|
|
113
|
+
codeBlock: (t) => theme.fg("mdCodeBlock", t),
|
|
114
|
+
codeBlockBorder: (t) => theme.fg("mdCodeBlockBorder", t),
|
|
115
|
+
quote: (t) => theme.fg("mdQuote", t),
|
|
116
|
+
quoteBorder: (t) => theme.fg("mdQuoteBorder", t),
|
|
117
|
+
hr: (t) => theme.fg("mdHr", t),
|
|
118
|
+
listBullet: (t) => theme.fg("mdListBullet", t),
|
|
119
|
+
bold: (t) => theme.bold(t),
|
|
120
|
+
italic: (t) => theme.italic(t),
|
|
121
|
+
underline: (t) => theme.underline(t),
|
|
122
|
+
strikethrough: (t) => theme.strikethrough(t),
|
|
123
|
+
highlightCode: (src: string, lang?: string): string[] => {
|
|
124
|
+
const validLang = lang && supportsLanguage(lang) ? lang : undefined;
|
|
125
|
+
if (!validLang) return src.split("\n").map((l) => theme.fg("mdCodeBlock", l));
|
|
126
|
+
try {
|
|
127
|
+
return highlight(src, { language: validLang, ignoreIllegals: true }).split("\n");
|
|
128
|
+
} catch {
|
|
129
|
+
return src.split("\n").map((l) => theme.fg("mdCodeBlock", l));
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function selectListTheme(): SelectListTheme {
|
|
136
|
+
return {
|
|
137
|
+
selectedPrefix: (t) => theme.fg("accent", t),
|
|
138
|
+
selectedText: (t) => theme.fg("accent", t),
|
|
139
|
+
description: (t) => theme.fg("muted", t),
|
|
140
|
+
scrollInfo: (t) => theme.fg("muted", t),
|
|
141
|
+
noMatch: (t) => theme.fg("muted", t),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function editorTheme(): EditorTheme {
|
|
146
|
+
return {
|
|
147
|
+
borderColor: (t) => theme.fg("borderMuted", t),
|
|
148
|
+
selectList: selectListTheme(),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emacs buffer extension.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the terminal-buffer extension but for a running Emacs server,
|
|
5
|
+
* trading PTY screen-scraping for structural access via `emacsclient -e`.
|
|
6
|
+
*
|
|
7
|
+
* Registers three agent tools (only when `emacsclient` is available and
|
|
8
|
+
* a server is reachable):
|
|
9
|
+
*
|
|
10
|
+
* - emacs_read : structured snapshot of the selected window —
|
|
11
|
+
* buffer, file, mode, point, narrowing, modeline,
|
|
12
|
+
* echo area, and the visible region (window-start
|
|
13
|
+
* to window-end). Optional all-windows mode.
|
|
14
|
+
*
|
|
15
|
+
* - emacs_keys : send a `kbd`-notation key sequence
|
|
16
|
+
* (e.g. "C-x C-s", "SPC f f"). Goes through Emacs's
|
|
17
|
+
* own key parser, so failed chords don't leak as
|
|
18
|
+
* literal text and Doom leaders work without timing
|
|
19
|
+
* tricks.
|
|
20
|
+
*
|
|
21
|
+
* - emacs_eval : evaluate arbitrary elisp inside the running Emacs.
|
|
22
|
+
* Use for structural operations (buffer edits,
|
|
23
|
+
* window manipulation, calling commands directly).
|
|
24
|
+
*
|
|
25
|
+
* All three round-trip results through a temp file as JSON. Requires
|
|
26
|
+
* Emacs 27+ for `json-serialize`.
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* ash -e ./examples/extensions/emacs-buffer.ts
|
|
30
|
+
*
|
|
31
|
+
* # Or install permanently
|
|
32
|
+
* cp examples/extensions/emacs-buffer.ts ~/.agent-sh/extensions/
|
|
33
|
+
*/
|
|
34
|
+
import * as fs from "node:fs";
|
|
35
|
+
import * as os from "node:os";
|
|
36
|
+
import * as path from "node:path";
|
|
37
|
+
import { spawnSync } from "node:child_process";
|
|
38
|
+
import type { ExtensionContext } from "agent-sh/types";
|
|
39
|
+
|
|
40
|
+
function emacsclientAvailable(): boolean {
|
|
41
|
+
// `emacsclient -e t` exits 0 only if a server is actually reachable.
|
|
42
|
+
const r = spawnSync("emacsclient", ["-e", "t"], { encoding: "utf-8" });
|
|
43
|
+
return r.status === 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function evalToJson<T = unknown>(body: string): T {
|
|
47
|
+
const out = path.join(
|
|
48
|
+
os.tmpdir(),
|
|
49
|
+
`agent-sh-emacs-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.json`,
|
|
50
|
+
);
|
|
51
|
+
// Compute the result *before* with-temp-file: inside its body, current-buffer
|
|
52
|
+
// is the temp buffer, and execute-kbd-macro can shift current-buffer to the
|
|
53
|
+
// user's live buffer mid-flight, causing (insert ...) to write JSON into it.
|
|
54
|
+
const wrapped = `(let ((__result (progn ${body}))) (with-temp-file ${JSON.stringify(out)} (insert (json-serialize __result))))`;
|
|
55
|
+
const r = spawnSync("emacsclient", ["-e", wrapped], { encoding: "utf-8" });
|
|
56
|
+
if (r.status !== 0) {
|
|
57
|
+
try { fs.unlinkSync(out); } catch { /* ignore */ }
|
|
58
|
+
throw new Error(`emacsclient failed: ${(r.stderr || r.stdout || "").trim()}`);
|
|
59
|
+
}
|
|
60
|
+
let json: string;
|
|
61
|
+
try {
|
|
62
|
+
json = fs.readFileSync(out, "utf-8");
|
|
63
|
+
} finally {
|
|
64
|
+
try { fs.unlinkSync(out); } catch { /* ignore */ }
|
|
65
|
+
}
|
|
66
|
+
return JSON.parse(json) as T;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Used by emacs_eval, where the result might not be JSON-serializable.
|
|
70
|
+
function evalPrinted(body: string): string {
|
|
71
|
+
const r = spawnSync("emacsclient", ["-e", body], { encoding: "utf-8" });
|
|
72
|
+
if (r.status !== 0) {
|
|
73
|
+
throw new Error(`emacsclient failed: ${(r.stderr || r.stdout || "").trim()}`);
|
|
74
|
+
}
|
|
75
|
+
return r.stdout.replace(/\n$/, "");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface WindowSnapshot {
|
|
79
|
+
selected: boolean;
|
|
80
|
+
buffer: string;
|
|
81
|
+
file: string | null;
|
|
82
|
+
mode: string;
|
|
83
|
+
modified: boolean;
|
|
84
|
+
narrowed: boolean;
|
|
85
|
+
point: number;
|
|
86
|
+
line: number;
|
|
87
|
+
column: number;
|
|
88
|
+
window_start: number;
|
|
89
|
+
window_end: number;
|
|
90
|
+
visible: string;
|
|
91
|
+
modeline: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface EmacsSnapshot {
|
|
95
|
+
windows: WindowSnapshot[];
|
|
96
|
+
echo_area: string | null;
|
|
97
|
+
minibuffer_active: boolean;
|
|
98
|
+
minibuffer_prompt: string | null;
|
|
99
|
+
minibuffer_contents: string | null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Plist for one window. Conventions: t / :false for booleans (json-serialize
|
|
103
|
+
// would otherwise map nil → {}, not null), :null for explicit nulls.
|
|
104
|
+
const WINDOW_PLIST = `
|
|
105
|
+
(let* ((buf (window-buffer w))
|
|
106
|
+
(s (window-start w))
|
|
107
|
+
(e (window-end w t)))
|
|
108
|
+
(with-current-buffer buf
|
|
109
|
+
(save-excursion
|
|
110
|
+
(goto-char (window-point w))
|
|
111
|
+
(list
|
|
112
|
+
:selected (if (eq w (selected-window)) t :false)
|
|
113
|
+
:buffer (buffer-name)
|
|
114
|
+
:file (or (buffer-file-name) :null)
|
|
115
|
+
:mode (symbol-name major-mode)
|
|
116
|
+
:modified (if (buffer-modified-p) t :false)
|
|
117
|
+
:narrowed (if (or (/= (point-min) 1) (/= (point-max) (1+ (buffer-size)))) t :false)
|
|
118
|
+
:point (point)
|
|
119
|
+
:line (line-number-at-pos (point))
|
|
120
|
+
:column (current-column)
|
|
121
|
+
:window_start s
|
|
122
|
+
:window_end e
|
|
123
|
+
:visible (buffer-substring-no-properties s e)
|
|
124
|
+
:modeline (substring-no-properties (format-mode-line mode-line-format nil w))))))
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
function snapshotElisp(allWindows: boolean): string {
|
|
128
|
+
const winList = allWindows
|
|
129
|
+
? "(window-list)"
|
|
130
|
+
: "(list (selected-window))";
|
|
131
|
+
return `
|
|
132
|
+
(list
|
|
133
|
+
:windows (vconcat
|
|
134
|
+
(mapcar (lambda (w) ${WINDOW_PLIST}) ${winList}))
|
|
135
|
+
:echo_area (or (current-message) :null)
|
|
136
|
+
:minibuffer_active (if (active-minibuffer-window) t :false)
|
|
137
|
+
:minibuffer_prompt (or (and (active-minibuffer-window)
|
|
138
|
+
(with-current-buffer (window-buffer (minibuffer-window))
|
|
139
|
+
(or (minibuffer-prompt) "")))
|
|
140
|
+
:null)
|
|
141
|
+
:minibuffer_contents (or (and (active-minibuffer-window)
|
|
142
|
+
(with-current-buffer (window-buffer (minibuffer-window))
|
|
143
|
+
(minibuffer-contents-no-properties)))
|
|
144
|
+
:null))
|
|
145
|
+
`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function snapshot(allWindows: boolean): EmacsSnapshot {
|
|
149
|
+
return evalToJson<EmacsSnapshot>(snapshotElisp(allWindows));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function renderWindow(w: WindowSnapshot, idx: number): string {
|
|
153
|
+
const tag = w.selected ? "selected" : `window ${idx}`;
|
|
154
|
+
const flags: string[] = [];
|
|
155
|
+
if (w.modified) flags.push("modified");
|
|
156
|
+
if (w.narrowed) flags.push("narrowed");
|
|
157
|
+
const flagsStr = flags.length ? ` [${flags.join(", ")}]` : "";
|
|
158
|
+
const fileStr = w.file ? ` file=${w.file}` : "";
|
|
159
|
+
const visible = markCursor(w.visible, w.point - w.window_start);
|
|
160
|
+
|
|
161
|
+
return [
|
|
162
|
+
`── ${tag}${flagsStr} ──`,
|
|
163
|
+
`buffer=${w.buffer}${fileStr} mode=${w.mode}`,
|
|
164
|
+
`point=${w.point} line=${w.line} col=${w.column}`,
|
|
165
|
+
`modeline: ${w.modeline}`,
|
|
166
|
+
`visible (${w.window_start}..${w.window_end}):`,
|
|
167
|
+
visible,
|
|
168
|
+
].join("\n");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function markCursor(visible: string, offset: number): string {
|
|
172
|
+
if (offset < 0 || offset > visible.length) return visible;
|
|
173
|
+
return visible.slice(0, offset) + "▮" + visible.slice(offset);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function renderSnapshot(snap: EmacsSnapshot): string {
|
|
177
|
+
const parts = snap.windows.map((w, i) => renderWindow(w, i));
|
|
178
|
+
if (snap.minibuffer_active && snap.minibuffer_prompt !== null) {
|
|
179
|
+
parts.push(
|
|
180
|
+
`── minibuffer ──\n${snap.minibuffer_prompt}${snap.minibuffer_contents ?? ""}`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if (snap.echo_area) {
|
|
184
|
+
parts.push(`── echo area ──\n${snap.echo_area}`);
|
|
185
|
+
}
|
|
186
|
+
return parts.join("\n\n");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export default function activate(ctx: ExtensionContext): void {
|
|
190
|
+
const { registerTool } = ctx.agent;
|
|
191
|
+
if (!emacsclientAvailable()) return;
|
|
192
|
+
|
|
193
|
+
registerTool({
|
|
194
|
+
name: "emacs_read",
|
|
195
|
+
description:
|
|
196
|
+
"Read the state of the user's running Emacs: selected window's buffer, " +
|
|
197
|
+
"file path, major mode, point (line/column), narrowing, modeline, the " +
|
|
198
|
+
"currently visible region (window-start to window-end) with a ▮ cursor " +
|
|
199
|
+
"marker, plus the echo area / minibuffer if active. With all_windows=true, " +
|
|
200
|
+
"returns the same data for every visible window. Use this to ground answers " +
|
|
201
|
+
"in what the user is actually looking at, not just guessing from filenames. " +
|
|
202
|
+
"Far more reliable than terminal_read for Emacs — it sees structure, not pixels.",
|
|
203
|
+
input_schema: {
|
|
204
|
+
type: "object",
|
|
205
|
+
properties: {
|
|
206
|
+
all_windows: {
|
|
207
|
+
type: "boolean",
|
|
208
|
+
description:
|
|
209
|
+
"Include every visible window in the current frame, not just the " +
|
|
210
|
+
"selected one. Default: false.",
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
showOutput: true,
|
|
215
|
+
getDisplayInfo: () => ({ kind: "read" as const, icon: "⌬", locations: [] }),
|
|
216
|
+
|
|
217
|
+
async execute(args) {
|
|
218
|
+
const all = (args.all_windows as boolean) ?? false;
|
|
219
|
+
try {
|
|
220
|
+
const snap = snapshot(all);
|
|
221
|
+
return { content: renderSnapshot(snap), exitCode: 0, isError: false };
|
|
222
|
+
} catch (e) {
|
|
223
|
+
return {
|
|
224
|
+
content: `emacs_read failed: ${(e as Error).message}`,
|
|
225
|
+
exitCode: 1,
|
|
226
|
+
isError: true,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
registerTool({
|
|
233
|
+
name: "emacs_keys",
|
|
234
|
+
description:
|
|
235
|
+
"Send a key sequence to the user's running Emacs, parsed by Emacs itself " +
|
|
236
|
+
"via `kbd`. Use Emacs `kbd` notation:\n" +
|
|
237
|
+
" C-x C-s — Ctrl+x Ctrl+s (save)\n" +
|
|
238
|
+
" M-x — Meta/Alt+x\n" +
|
|
239
|
+
" C-M-f — Ctrl+Meta+f\n" +
|
|
240
|
+
" SPC f f — Doom/Spacemacs leader find-file (no timing tricks needed)\n" +
|
|
241
|
+
" RET ESC TAB DEL — named keys\n" +
|
|
242
|
+
" <up> <down> — arrow keys\n\n" +
|
|
243
|
+
"Why this beats terminal_keys for Emacs: the key parser is authoritative, so " +
|
|
244
|
+
"C-c works as a prefix without queueing garbage, leader keys resolve without " +
|
|
245
|
+
"inter-key delays, and failed chords surface as a `kbd` parse error instead of " +
|
|
246
|
+
"leaking into the buffer as text. Returns a fresh emacs_read snapshot after " +
|
|
247
|
+
"the keys execute.",
|
|
248
|
+
input_schema: {
|
|
249
|
+
type: "object",
|
|
250
|
+
properties: {
|
|
251
|
+
keys: {
|
|
252
|
+
type: "string",
|
|
253
|
+
description:
|
|
254
|
+
"A `kbd`-style key sequence, e.g. \"C-x C-s\", \"M-x find-file RET\", \"SPC f f\".",
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
required: ["keys"],
|
|
258
|
+
},
|
|
259
|
+
showOutput: false,
|
|
260
|
+
getDisplayInfo: () => ({ kind: "execute" as const, icon: "⌥", locations: [] }),
|
|
261
|
+
formatCall: (args) => `keys: ${args.keys}`,
|
|
262
|
+
|
|
263
|
+
async execute(args) {
|
|
264
|
+
const keys = args.keys as string;
|
|
265
|
+
try {
|
|
266
|
+
// condition-case so kbd parse / runtime errors surface structurally
|
|
267
|
+
// rather than as a non-zero emacsclient exit.
|
|
268
|
+
const body = `
|
|
269
|
+
(condition-case err
|
|
270
|
+
(progn
|
|
271
|
+
(execute-kbd-macro (kbd ${JSON.stringify(keys)}))
|
|
272
|
+
${snapshotElisp(false).trim()})
|
|
273
|
+
(error (list :error (error-message-string err))))
|
|
274
|
+
`;
|
|
275
|
+
const result = evalToJson<EmacsSnapshot | { error: string }>(body);
|
|
276
|
+
if ("error" in result) {
|
|
277
|
+
return {
|
|
278
|
+
content: `emacs_keys error: ${result.error}`,
|
|
279
|
+
exitCode: 1,
|
|
280
|
+
isError: true,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
content: `Keys sent.\n\n${renderSnapshot(result)}`,
|
|
285
|
+
exitCode: 0,
|
|
286
|
+
isError: false,
|
|
287
|
+
};
|
|
288
|
+
} catch (e) {
|
|
289
|
+
return {
|
|
290
|
+
content: `emacs_keys failed: ${(e as Error).message}`,
|
|
291
|
+
exitCode: 1,
|
|
292
|
+
isError: true,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
registerTool({
|
|
299
|
+
name: "emacs_eval",
|
|
300
|
+
description:
|
|
301
|
+
"Evaluate elisp inside the user's running Emacs. The high-leverage tool: " +
|
|
302
|
+
"buffer edits, window manipulation, calling named commands, reading any " +
|
|
303
|
+
"data structure Emacs knows. Returns the printed value plus a fresh " +
|
|
304
|
+
"emacs_read snapshot.\n\n" +
|
|
305
|
+
"Useful idioms:\n" +
|
|
306
|
+
" (with-current-buffer \"foo.org\" (buffer-substring-no-properties (point-min) (point-max)))\n" +
|
|
307
|
+
" (with-current-buffer (window-buffer (selected-window)) (save-buffer))\n" +
|
|
308
|
+
" (call-interactively '+default/find-file)\n" +
|
|
309
|
+
" (split-window-right)\n\n" +
|
|
310
|
+
"Caveat: this mutates the user's live editor. The change is undoable in the " +
|
|
311
|
+
"buffer (C-/) but not all elisp side effects are reversible — be deliberate.",
|
|
312
|
+
input_schema: {
|
|
313
|
+
type: "object",
|
|
314
|
+
properties: {
|
|
315
|
+
elisp: {
|
|
316
|
+
type: "string",
|
|
317
|
+
description:
|
|
318
|
+
"Elisp form(s) to evaluate. Multiple forms are allowed; only the last " +
|
|
319
|
+
"form's value is returned in the printed-value section.",
|
|
320
|
+
},
|
|
321
|
+
skip_snapshot: {
|
|
322
|
+
type: "boolean",
|
|
323
|
+
description:
|
|
324
|
+
"If true, don't return a post-eval emacs_read snapshot. Useful for " +
|
|
325
|
+
"pure read-only evals where the snapshot would be noise. Default: false.",
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
required: ["elisp"],
|
|
329
|
+
},
|
|
330
|
+
showOutput: false,
|
|
331
|
+
getDisplayInfo: () => ({ kind: "execute" as const, icon: "λ", locations: [] }),
|
|
332
|
+
formatCall: (args) => {
|
|
333
|
+
const elisp = (args.elisp as string).trim().split("\n")[0];
|
|
334
|
+
return elisp.length > 80 ? elisp.slice(0, 77) + "..." : elisp;
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
async execute(args) {
|
|
338
|
+
const elisp = args.elisp as string;
|
|
339
|
+
const skipSnap = (args.skip_snapshot as boolean) ?? false;
|
|
340
|
+
try {
|
|
341
|
+
const printed = evalPrinted(elisp);
|
|
342
|
+
let suffix = "";
|
|
343
|
+
if (!skipSnap) {
|
|
344
|
+
try {
|
|
345
|
+
suffix = "\n\n" + renderSnapshot(snapshot(false));
|
|
346
|
+
} catch (e) {
|
|
347
|
+
suffix = `\n\n(snapshot failed: ${(e as Error).message})`;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
content: `=> ${printed}${suffix}`,
|
|
352
|
+
exitCode: 0,
|
|
353
|
+
isError: false,
|
|
354
|
+
};
|
|
355
|
+
} catch (e) {
|
|
356
|
+
return {
|
|
357
|
+
content: `emacs_eval failed: ${(e as Error).message}`,
|
|
358
|
+
exitCode: 1,
|
|
359
|
+
isError: true,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
}
|