@vietor/easy-agent 0.2.1 → 0.3.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.
- package/README.md +15 -13
- package/dist/cmds/builtin.js +10 -10
- package/dist/config.js +25 -3
- package/dist/core/agent.js +48 -30
- package/dist/core/conversation.js +82 -0
- package/dist/core/logstore.js +49 -0
- package/dist/core/session.js +173 -67
- package/dist/llm/client.js +12 -6
- package/dist/main.js +24 -24
- package/dist/mcp/client.js +44 -8
- package/dist/mcp/server.js +49 -10
- package/dist/tools/ask_user.js +24 -0
- package/dist/tools/file_edit.js +13 -5
- package/dist/tools/file_read.js +28 -3
- package/dist/tools/glob.js +3 -3
- package/dist/tools/grep.js +51 -14
- package/dist/tools/registry.js +4 -3
- package/dist/tools/shell.js +2 -2
- package/dist/tools/web_fetch.js +3 -2
- package/dist/tui/App.js +32 -110
- package/dist/tui/AppHeader.js +1 -1
- package/dist/tui/LogView.js +11 -7
- package/dist/tui/PromptOrCommandInput.js +14 -12
- package/dist/tui/QuestionView.js +45 -0
- package/dist/tui/Spinner.js +2 -2
- package/dist/util/package.js +5 -5
- package/dist/util/process.js +12 -2
- package/dist/util/ripgrep.js +3 -3
- package/package.json +2 -2
- package/dist/tui/LogStore.js +0 -35
package/dist/tools/glob.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolveCwd, runRgLines } from "../util/ripgrep.js";
|
|
2
2
|
const DESCRIPTION = [
|
|
3
3
|
"List files under a directory, optionally filtered by a glob pattern (e.g. **/*.ts); omit pattern to list every file.",
|
|
4
|
-
"
|
|
4
|
+
"Includes hidden files (e.g. .env, .gitignore); skips node_modules and .git.",
|
|
5
5
|
"Returns paths relative to the root, one per line.",
|
|
6
6
|
].join(" ");
|
|
7
7
|
export const globTool = {
|
|
@@ -15,14 +15,14 @@ export const globTool = {
|
|
|
15
15
|
},
|
|
16
16
|
required: [],
|
|
17
17
|
},
|
|
18
|
-
async execute(args) {
|
|
18
|
+
async execute(args, ctx) {
|
|
19
19
|
const cwd = resolveCwd(args.path);
|
|
20
20
|
const rgArgs = ["--files"];
|
|
21
21
|
const pattern = args.pattern;
|
|
22
22
|
if (pattern)
|
|
23
23
|
rgArgs.push("-g", pattern);
|
|
24
24
|
rgArgs.push(".");
|
|
25
|
-
const files = await runRgLines(rgArgs, cwd);
|
|
25
|
+
const files = await runRgLines(rgArgs, cwd, ctx.signal);
|
|
26
26
|
return files.length ? files.join("\n") : "(no matches)";
|
|
27
27
|
},
|
|
28
28
|
summaryArg: ["pattern", "path"],
|
package/dist/tools/grep.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { resolveCwd, runRgLines } from "../util/ripgrep.js";
|
|
2
|
-
const
|
|
2
|
+
const DEFAULT_HEAD_LIMIT = 200;
|
|
3
3
|
const DESCRIPTION = [
|
|
4
4
|
"Search file contents under a directory recursively for a regex pattern (RE2 syntax).",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
5
|
+
"Includes hidden files (e.g. .env, .gitignore); skips node_modules and .git.",
|
|
6
|
+
"By default returns matching lines as path:line:content, capped at 200 lines; use output_mode, head_limit, and context options to control output.",
|
|
7
7
|
].join(" ");
|
|
8
8
|
export const grepTool = {
|
|
9
9
|
name: "Grep",
|
|
@@ -13,23 +13,60 @@ export const grepTool = {
|
|
|
13
13
|
properties: {
|
|
14
14
|
pattern: { type: "string" },
|
|
15
15
|
path: { type: "string", description: "root directory, defaults to cwd" },
|
|
16
|
+
glob: { type: "string", description: "glob pattern to filter files (e.g. *.ts)" },
|
|
17
|
+
type: { type: "string", description: "file type to search (e.g. ts, js, py, rust, go)" },
|
|
18
|
+
output_mode: {
|
|
19
|
+
type: "string",
|
|
20
|
+
enum: ["content", "files_with_matches", "count"],
|
|
21
|
+
description: "content (default, matching lines), files_with_matches (file paths only), count (match counts per file)",
|
|
22
|
+
},
|
|
23
|
+
ignore_case: { type: "boolean", description: "case-insensitive match" },
|
|
24
|
+
before: { type: "number", description: "lines to show before each match" },
|
|
25
|
+
after: { type: "number", description: "lines to show after each match" },
|
|
26
|
+
context: { type: "number", description: "lines to show before and after each match" },
|
|
27
|
+
only_matching: { type: "boolean", description: "print only the matched (non-empty) parts" },
|
|
28
|
+
multiline: { type: "boolean", description: "allow patterns to span newlines" },
|
|
29
|
+
head_limit: { type: "number", description: "max output lines (default 200)" },
|
|
16
30
|
},
|
|
17
31
|
required: ["pattern"],
|
|
18
32
|
},
|
|
19
|
-
async execute(args) {
|
|
33
|
+
async execute(args, ctx) {
|
|
20
34
|
const cwd = resolveCwd(args.path);
|
|
21
|
-
const rgArgs = [
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
35
|
+
const rgArgs = ["--line-number", "--with-filename", "--no-heading"];
|
|
36
|
+
if (args.ignore_case)
|
|
37
|
+
rgArgs.push("-i");
|
|
38
|
+
if (args.only_matching)
|
|
39
|
+
rgArgs.push("-o");
|
|
40
|
+
if (args.multiline)
|
|
41
|
+
rgArgs.push("-U", "--multiline-dotall");
|
|
42
|
+
const context = args.context;
|
|
43
|
+
if (context)
|
|
44
|
+
rgArgs.push("-C", String(context));
|
|
45
|
+
else {
|
|
46
|
+
const before = args.before;
|
|
47
|
+
const after = args.after;
|
|
48
|
+
if (before)
|
|
49
|
+
rgArgs.push("-B", String(before));
|
|
50
|
+
if (after)
|
|
51
|
+
rgArgs.push("-A", String(after));
|
|
52
|
+
}
|
|
53
|
+
if (args.glob)
|
|
54
|
+
rgArgs.push("-g", args.glob);
|
|
55
|
+
if (args.type)
|
|
56
|
+
rgArgs.push("-t", args.type);
|
|
57
|
+
const output_mode = args.output_mode || "content";
|
|
58
|
+
if (output_mode === "files_with_matches")
|
|
59
|
+
rgArgs.push("-l");
|
|
60
|
+
else if (output_mode === "count")
|
|
61
|
+
rgArgs.push("-c");
|
|
62
|
+
rgArgs.push(args.pattern, ".");
|
|
63
|
+
const lines = await runRgLines(rgArgs, cwd, ctx.signal);
|
|
29
64
|
if (!lines.length)
|
|
30
65
|
return "(no matches)";
|
|
31
|
-
|
|
32
|
-
|
|
66
|
+
const headLimit = args.head_limit || DEFAULT_HEAD_LIMIT;
|
|
67
|
+
if (lines.length > headLimit) {
|
|
68
|
+
return lines.slice(0, headLimit).join("\n") + `\n(${lines.length - headLimit} more matches, truncated)`;
|
|
69
|
+
}
|
|
33
70
|
return lines.join("\n");
|
|
34
71
|
},
|
|
35
72
|
summaryArg: ["pattern", "path"],
|
package/dist/tools/registry.js
CHANGED
|
@@ -5,6 +5,7 @@ import { fileEditTool } from "./file_edit.js";
|
|
|
5
5
|
import { globTool } from "./glob.js";
|
|
6
6
|
import { grepTool } from "./grep.js";
|
|
7
7
|
import { webFetchTool } from "./web_fetch.js";
|
|
8
|
+
import { askUserTool } from "./ask_user.js";
|
|
8
9
|
export class ToolRegistry {
|
|
9
10
|
tools = new Map();
|
|
10
11
|
register(tool) {
|
|
@@ -20,12 +21,12 @@ export class ToolRegistry {
|
|
|
20
21
|
},
|
|
21
22
|
}));
|
|
22
23
|
}
|
|
23
|
-
async execute(name, args) {
|
|
24
|
+
async execute(name, args, ctx) {
|
|
24
25
|
const tool = this.tools.get(name);
|
|
25
26
|
if (!tool)
|
|
26
27
|
return { content: `Error: unknown tool ${name}`, isError: true };
|
|
27
28
|
try {
|
|
28
|
-
const r = await tool.execute(args);
|
|
29
|
+
const r = await tool.execute(args, ctx);
|
|
29
30
|
return typeof r === "string" ? { content: r } : r;
|
|
30
31
|
}
|
|
31
32
|
catch (e) {
|
|
@@ -46,6 +47,6 @@ export class ToolRegistry {
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
export function registerBuiltinTools(tools) {
|
|
49
|
-
for (const t of [shellTool, fileReadTool, fileWriteTool, fileEditTool, globTool, grepTool, webFetchTool])
|
|
50
|
+
for (const t of [shellTool, fileReadTool, fileWriteTool, fileEditTool, globTool, grepTool, webFetchTool, askUserTool])
|
|
50
51
|
tools.register(t);
|
|
51
52
|
}
|
package/dist/tools/shell.js
CHANGED
|
@@ -22,9 +22,9 @@ export const shellTool = {
|
|
|
22
22
|
properties: { command: { type: "string" } },
|
|
23
23
|
required: ["command"],
|
|
24
24
|
},
|
|
25
|
-
async execute(args) {
|
|
25
|
+
async execute(args, ctx) {
|
|
26
26
|
const command = args.command;
|
|
27
|
-
const r = await runProcess(shell, [...shellArgs, commandPrefix + command]);
|
|
27
|
+
const r = await runProcess(shell, [...shellArgs, commandPrefix + command], {}, ctx.signal);
|
|
28
28
|
if (r.status === 0 && !r.error) {
|
|
29
29
|
return r.stdout || "(no output)";
|
|
30
30
|
}
|
package/dist/tools/web_fetch.js
CHANGED
|
@@ -76,7 +76,7 @@ function htmlToMarkdown(html) {
|
|
|
76
76
|
return turndown.turndown(html);
|
|
77
77
|
}
|
|
78
78
|
function mimeFrom(contentType) {
|
|
79
|
-
return contentType.split(";", 1)[0]
|
|
79
|
+
return contentType.split(";", 1)[0].trim().toLowerCase();
|
|
80
80
|
}
|
|
81
81
|
function isTextualMime(mime) {
|
|
82
82
|
return (!mime ||
|
|
@@ -110,7 +110,7 @@ export const webFetchTool = {
|
|
|
110
110
|
},
|
|
111
111
|
required: ["url"],
|
|
112
112
|
},
|
|
113
|
-
async execute(args) {
|
|
113
|
+
async execute(args, ctx) {
|
|
114
114
|
const url = args.url;
|
|
115
115
|
const format = (args.format || "markdown").toLowerCase();
|
|
116
116
|
let res;
|
|
@@ -120,6 +120,7 @@ export const webFetchTool = {
|
|
|
120
120
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
|
|
121
121
|
},
|
|
122
122
|
redirect: "follow",
|
|
123
|
+
signal: ctx.signal,
|
|
123
124
|
});
|
|
124
125
|
}
|
|
125
126
|
catch (e) {
|
package/dist/tui/App.js
CHANGED
|
@@ -3,150 +3,72 @@ import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "reac
|
|
|
3
3
|
import { Box, render, Text, useApp, useInput } from "ink";
|
|
4
4
|
import { Markdown } from "./components/Markdown.js";
|
|
5
5
|
import { LogView } from "./LogView.js";
|
|
6
|
-
import { LogStore } from "./LogStore.js";
|
|
7
6
|
import { AppHeader } from "./AppHeader.js";
|
|
8
7
|
import { PromptOrCommandInput } from "./PromptOrCommandInput.js";
|
|
8
|
+
import { QuestionView } from "./QuestionView.js";
|
|
9
9
|
import { Spinner } from "./Spinner.js";
|
|
10
10
|
import { compactDisplay } from "../util/format.js";
|
|
11
11
|
const STREAM_FRAME_MS = 240;
|
|
12
|
-
export function App({
|
|
12
|
+
export function App({ session }) {
|
|
13
13
|
const { exit } = useApp();
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const [
|
|
14
|
+
useSyncExternalStore(session.subscribe, session.getSnapshot);
|
|
15
|
+
const [running, setRunning] = useState(false);
|
|
16
|
+
const [elapsed, setElapsed] = useState(0);
|
|
17
|
+
const [usage, setUsage] = useState({ prompt: 0, completion: 0 });
|
|
17
18
|
const [, setTick] = useState(0);
|
|
18
19
|
const streamingRef = useRef("");
|
|
19
20
|
const renderTimerRef = useRef(undefined);
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const timerRef = useRef(undefined);
|
|
23
|
-
const [elapsed, setElapsed] = useState(0);
|
|
24
|
-
const [usage, setUsage] = useState({ prompt: 0, completion: 0 });
|
|
25
|
-
const allCmds = useMemo(() => commands.schemas(), []);
|
|
21
|
+
const allCmds = useMemo(() => session.commandSchemas, [session]);
|
|
22
|
+
const pendingQuestion = session.logEntries.find((e) => e.kind === "question" && e.answer === null);
|
|
26
23
|
useEffect(() => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
session.setCallbacks({
|
|
25
|
+
onStreaming: (text) => {
|
|
26
|
+
streamingRef.current = text;
|
|
27
|
+
scheduleStreamingRender();
|
|
28
|
+
},
|
|
29
|
+
onRunStateChange: (r) => setRunning(r),
|
|
30
|
+
onElapsedChange: (s) => setElapsed(s),
|
|
31
|
+
onUsageChange: (p, c) => setUsage({ prompt: p, completion: c }),
|
|
32
|
+
});
|
|
33
33
|
}, []);
|
|
34
|
-
const cancelStreamingRender = () => {
|
|
35
|
-
if (renderTimerRef.current) {
|
|
36
|
-
clearTimeout(renderTimerRef.current);
|
|
37
|
-
renderTimerRef.current = undefined;
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
34
|
const scheduleStreamingRender = () => {
|
|
41
35
|
if (renderTimerRef.current)
|
|
42
36
|
return;
|
|
43
37
|
renderTimerRef.current = setTimeout(() => {
|
|
44
38
|
renderTimerRef.current = undefined;
|
|
45
|
-
setStatus("streaming");
|
|
46
39
|
setTick((t) => t + 1);
|
|
47
40
|
}, STREAM_FRAME_MS);
|
|
48
41
|
};
|
|
49
|
-
const flushStreaming = () => {
|
|
50
|
-
cancelStreamingRender();
|
|
51
|
-
if (streamingRef.current) {
|
|
52
|
-
store.append({ kind: "assistant", text: streamingRef.current });
|
|
53
|
-
streamingRef.current = "";
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
const onEvent = (e) => {
|
|
57
|
-
if (e.type === "delta") {
|
|
58
|
-
streamingRef.current += e.text;
|
|
59
|
-
scheduleStreamingRender();
|
|
60
|
-
}
|
|
61
|
-
else if (e.type === "tool_start") {
|
|
62
|
-
flushStreaming();
|
|
63
|
-
setStatus("thinking");
|
|
64
|
-
store.append({ kind: "tool", id: e.id, name: e.name, summary: e.summary, result: null });
|
|
65
|
-
}
|
|
66
|
-
else if (e.type === "retry") {
|
|
67
|
-
cancelStreamingRender();
|
|
68
|
-
streamingRef.current = "";
|
|
69
|
-
setStatus("thinking");
|
|
70
|
-
store.append({ kind: "retry", attempt: e.attempt, max: e.max });
|
|
71
|
-
}
|
|
72
|
-
else if (e.type === "tool_end") {
|
|
73
|
-
setStatus("thinking");
|
|
74
|
-
store.setToolResult(e.id, e.result, e.isError);
|
|
75
|
-
}
|
|
76
|
-
else if (e.type === "error") {
|
|
77
|
-
flushStreaming();
|
|
78
|
-
store.append({ kind: "error", text: e.text });
|
|
79
|
-
}
|
|
80
|
-
else if (e.type === "interrupted") {
|
|
81
|
-
flushStreaming();
|
|
82
|
-
store.append({ kind: "interrupted" });
|
|
83
|
-
}
|
|
84
|
-
else if (e.type === "usage") {
|
|
85
|
-
setUsage({ prompt: e.promptTokens, completion: e.completionTokens });
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
42
|
useInput((_input, key) => {
|
|
43
|
+
if (pendingQuestion) {
|
|
44
|
+
if (key.ctrl && _input === "c")
|
|
45
|
+
session.abort();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
89
48
|
if (key.escape) {
|
|
90
|
-
|
|
49
|
+
session.abort();
|
|
91
50
|
}
|
|
92
51
|
else if (key.ctrl && _input === "c") {
|
|
93
|
-
if (
|
|
94
|
-
|
|
52
|
+
if (running)
|
|
53
|
+
session.abort();
|
|
95
54
|
else
|
|
96
55
|
exit();
|
|
97
56
|
}
|
|
98
57
|
});
|
|
99
58
|
async function handleCommand(name) {
|
|
100
|
-
|
|
101
|
-
exit,
|
|
102
|
-
clearLog: () => store.clear(),
|
|
103
|
-
info: (t) => store.append({ kind: "system", text: t }),
|
|
104
|
-
error: (t) => store.append({ kind: "error", text: t }),
|
|
105
|
-
thinking: (on) => setStatus(on ? "thinking" : "idle"),
|
|
106
|
-
runSkill: (s) => handleSkill(s),
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
async function runAgent(entry, run) {
|
|
110
|
-
store.append(entry);
|
|
111
|
-
setStatus("thinking");
|
|
112
|
-
streamingRef.current = "";
|
|
113
|
-
startRef.current = Date.now();
|
|
114
|
-
setElapsed(0);
|
|
115
|
-
setUsage({ prompt: 0, completion: 0 });
|
|
116
|
-
timerRef.current = setInterval(() => {
|
|
117
|
-
setElapsed(Math.floor((Date.now() - startRef.current) / 1000));
|
|
118
|
-
}, 1000);
|
|
119
|
-
const controller = new AbortController();
|
|
120
|
-
abortRef.current = controller;
|
|
121
|
-
try {
|
|
122
|
-
await run(controller.signal);
|
|
123
|
-
flushStreaming();
|
|
124
|
-
}
|
|
125
|
-
catch (e) {
|
|
126
|
-
flushStreaming();
|
|
127
|
-
store.append({ kind: "error", text: e.message });
|
|
128
|
-
}
|
|
129
|
-
finally {
|
|
130
|
-
clearInterval(timerRef.current);
|
|
131
|
-
timerRef.current = undefined;
|
|
132
|
-
abortRef.current = null;
|
|
133
|
-
setStatus("idle");
|
|
134
|
-
}
|
|
59
|
+
await session.executeCommand(name, { exit, setRunning });
|
|
135
60
|
}
|
|
136
61
|
async function handlePrompt(text) {
|
|
137
|
-
if (
|
|
62
|
+
if (session.isCommand(text)) {
|
|
138
63
|
await handleCommand(text);
|
|
139
64
|
}
|
|
140
65
|
else {
|
|
141
|
-
await
|
|
66
|
+
await session.startPrompt(text);
|
|
142
67
|
}
|
|
143
68
|
}
|
|
144
|
-
|
|
145
|
-
await runAgent({ kind: "skill", name: skill.name }, (signal) => agent.runSkill(skill, onEvent, signal));
|
|
146
|
-
}
|
|
147
|
-
return (_jsxs(Box, { flexDirection: "column", minWidth: 80, children: [_jsx(AppHeader, {}), _jsx(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: log.map((entry, i) => (_jsx(LogView, { entry: entry }, i))) }), status === "streaming" && streamingRef.current ? (_jsx(Box, { paddingLeft: 1, paddingRight: 1, children: _jsx(Markdown, { color: "green", children: streamingRef.current }) })) : null, status === "thinking" ? (_jsx(Box, { marginTop: 1, paddingLeft: 1, children: _jsx(Spinner, { label: "thinking", elapsed: elapsed, promptTokens: usage.prompt, completionTokens: usage.completion }) })) : null, status === "idle" ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { dimColor: true, children: ["[CTX ", compactDisplay(agent.contextTokens), "] \u00B7 ESC to stop \u00B7 \"/quit\" to leave"] }), _jsx(PromptOrCommandInput, { commands: allCmds, onCommand: handleCommand, onPrompt: handlePrompt })] })) : null] }));
|
|
69
|
+
return (_jsxs(Box, { flexDirection: "column", minWidth: 80, children: [_jsx(AppHeader, {}), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: [session.logEntries.length === 0 ? (_jsx(Box, { paddingTop: 2 })) : null, session.logEntries.map((entry, i) => (_jsx(LogView, { entry: entry }, i)))] }), running && pendingQuestion ? (_jsx(QuestionView, { question: pendingQuestion, onAnswer: (ans) => session.submitAnswer(pendingQuestion.id, ans) })) : running && streamingRef.current ? (_jsx(Box, { marginTop: 1, paddingLeft: 1, paddingRight: 1, children: _jsx(Markdown, { color: "green", children: streamingRef.current }) })) : running ? (_jsx(Box, { marginTop: 1, paddingLeft: 1, children: _jsx(Spinner, { label: "thinking", elapsed: elapsed, promptTokens: usage.prompt, completionTokens: usage.completion }) })) : null, !running ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { dimColor: true, children: ["[CTX ", compactDisplay(session.contextTokens), "] \u00B7 ESC to stop \u00B7 type / for commands"] }), _jsx(PromptOrCommandInput, { commands: allCmds, onCommand: handleCommand, onPrompt: handlePrompt })] })) : null] }));
|
|
148
70
|
}
|
|
149
|
-
export function startApp(
|
|
150
|
-
process.stdout.write("
|
|
151
|
-
return render(_jsx(App, {
|
|
71
|
+
export function startApp(session) {
|
|
72
|
+
process.stdout.write("[2J[H");
|
|
73
|
+
return render(_jsx(App, { session: session }), { exitOnCtrlC: false });
|
|
152
74
|
}
|
package/dist/tui/AppHeader.js
CHANGED
|
@@ -3,5 +3,5 @@ import { Box, Text } from "ink";
|
|
|
3
3
|
import { getPackageInfo } from "../util/package.js";
|
|
4
4
|
export function AppHeader() {
|
|
5
5
|
const pkginfo = getPackageInfo();
|
|
6
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "red", bold: true, children: "Easy Agent" }), _jsxs(Text, { dimColor: true, children: [" v", pkginfo.version] })] }), _jsx(Text, { dimColor: true, children: process.cwd() })] }));
|
|
6
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: "red", bold: true, children: "Easy Agent" }), _jsxs(Text, { dimColor: true, children: [" v", pkginfo.version] })] }), _jsx(Text, { dimColor: true, children: process.cwd() })] }), _jsx(Box, { borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, borderColor: "gray" })] }));
|
|
7
7
|
}
|
package/dist/tui/LogView.js
CHANGED
|
@@ -14,20 +14,24 @@ function preview(isError, text) {
|
|
|
14
14
|
export const LogView = memo(function Entry({ entry }) {
|
|
15
15
|
switch (entry.kind) {
|
|
16
16
|
case "user":
|
|
17
|
-
return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children:
|
|
17
|
+
return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "\u276F " }), entry.text] }) }));
|
|
18
18
|
case "skill":
|
|
19
|
-
return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "magenta", children:
|
|
19
|
+
return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "magenta", children: "\u25C8 " }), _jsx(Text, { dimColor: true, children: "skill " }), _jsx(Text, { color: "magenta", bold: true, children: entry.name })] }) }));
|
|
20
20
|
case "assistant":
|
|
21
21
|
return (_jsx(Box, { marginTop: 1, children: _jsx(Markdown, { color: "green", children: entry.text }) }));
|
|
22
22
|
case "tool":
|
|
23
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "yellow", children:
|
|
23
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "\u25CF " }), _jsx(Text, { color: "yellow", bold: true, children: entry.name }), entry.summary ? _jsxs(Text, { dimColor: true, children: [" ", entry.summary] }) : null] }), entry.result !== null ? (_jsx(Text, { color: entry.isError ? "red" : "gray", children: ` ${preview(entry.isError ?? false, entry.result)}` })) : null] }));
|
|
24
24
|
case "retry":
|
|
25
|
-
return (_jsx(Box, { children: _jsx(Text, { color: "yellow", children:
|
|
25
|
+
return (_jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "\u21BB " }), _jsxs(Text, { dimColor: true, children: ["retry ", entry.attempt, "/", entry.max] })] }) }));
|
|
26
26
|
case "error":
|
|
27
|
-
return (_jsx(Box, { children:
|
|
27
|
+
return (_jsx(Box, { children: _jsxs(Text, { color: "red", children: [_jsx(Text, { bold: true, children: "\u2717 " }), entry.text] }) }));
|
|
28
28
|
case "interrupted":
|
|
29
|
-
return (_jsx(Box, { children: _jsx(Text, { color: "yellow", children: "\u25FC interrupted" }) }));
|
|
29
|
+
return (_jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "\u25FC " }), _jsx(Text, { dimColor: true, children: "interrupted" })] }) }));
|
|
30
|
+
case "question":
|
|
31
|
+
if (entry.answer === null)
|
|
32
|
+
return null;
|
|
33
|
+
return (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: `? ${entry.text}` }), _jsx(Text, { dimColor: true, children: ` › ${entry.answer || "(skipped)"}` })] }));
|
|
30
34
|
case "system":
|
|
31
|
-
return (_jsx(Box, { children: _jsx(Text, { color: "
|
|
35
|
+
return (_jsx(Box, { children: _jsx(Text, { color: "blue", children: entry.text }) }));
|
|
32
36
|
}
|
|
33
37
|
});
|
|
@@ -32,6 +32,16 @@ export function PromptOrCommandInput({ commands, onCommand, onPrompt }) {
|
|
|
32
32
|
setInput("");
|
|
33
33
|
}
|
|
34
34
|
}, { isActive: showMenu });
|
|
35
|
+
const visibleItems = useMemo(() => {
|
|
36
|
+
if (!showMenu || filtered.length === 0)
|
|
37
|
+
return { start: 0, items: [] };
|
|
38
|
+
const total = filtered.length;
|
|
39
|
+
const half = Math.floor(MAX_ITEMS / 2);
|
|
40
|
+
let start = Math.max(0, selectedIndex - half);
|
|
41
|
+
if (start + MAX_ITEMS > total)
|
|
42
|
+
start = Math.max(0, total - MAX_ITEMS);
|
|
43
|
+
return { start, items: filtered.slice(start, start + MAX_ITEMS) };
|
|
44
|
+
}, [showMenu, filtered, selectedIndex]);
|
|
35
45
|
const onSubmit = (value) => {
|
|
36
46
|
const text = value.trim();
|
|
37
47
|
setInput("");
|
|
@@ -46,16 +56,8 @@ export function PromptOrCommandInput({ commands, onCommand, onPrompt }) {
|
|
|
46
56
|
}
|
|
47
57
|
onPrompt(text);
|
|
48
58
|
};
|
|
49
|
-
return (_jsxs(Box, { flexDirection: "column", children: [showMenu && filtered.length > 0 ? (_jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", children: (() => {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (start + MAX_ITEMS > total)
|
|
54
|
-
start = Math.max(0, total - MAX_ITEMS);
|
|
55
|
-
const visible = filtered.slice(start, start + MAX_ITEMS);
|
|
56
|
-
return visible.map((cmd, i) => {
|
|
57
|
-
const realIdx = start + i;
|
|
58
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { color: realIdx === selectedIndex ? "cyan" : undefined, children: [realIdx === selectedIndex ? "▸ " : " ", "/", cmd.name] }), _jsxs(Text, { dimColor: true, children: [" ", cmd.description] })] }, cmd.name));
|
|
59
|
-
});
|
|
60
|
-
})() })) : null, _jsxs(Box, { borderStyle: "single", borderLeft: false, borderRight: false, borderColor: "gray", children: [_jsx(Text, { color: "gray", children: "\u276F " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit })] })] }));
|
|
59
|
+
return (_jsxs(Box, { flexDirection: "column", children: [showMenu && filtered.length > 0 ? (_jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", children: visibleItems.items.map((cmd, i) => {
|
|
60
|
+
const realIdx = visibleItems.start + i;
|
|
61
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: realIdx === selectedIndex ? "cyan" : undefined, children: [realIdx === selectedIndex ? "▸ " : " ", "/", cmd.name] }), _jsxs(Text, { dimColor: true, children: [" ", cmd.description] })] }, cmd.name));
|
|
62
|
+
}) })) : null, _jsxs(Box, { borderStyle: "single", borderLeft: false, borderRight: false, borderColor: "gray", children: [_jsx(Text, { color: "gray", children: "\u276F " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit })] })] }));
|
|
61
63
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
const CUSTOM_LABEL = "✎ Custom input";
|
|
6
|
+
export function QuestionView({ question, onAnswer }) {
|
|
7
|
+
const hasOptions = question.options.length > 0;
|
|
8
|
+
const items = hasOptions ? [...question.options, CUSTOM_LABEL] : [];
|
|
9
|
+
const [selected, setSelected] = useState(0);
|
|
10
|
+
const [mode, setMode] = useState(hasOptions ? "select" : "input");
|
|
11
|
+
const [text, setText] = useState("");
|
|
12
|
+
useInput((input, key) => {
|
|
13
|
+
if (mode === "select") {
|
|
14
|
+
if (key.upArrow) {
|
|
15
|
+
setSelected((i) => (i <= 0 ? items.length - 1 : i - 1));
|
|
16
|
+
}
|
|
17
|
+
else if (key.downArrow) {
|
|
18
|
+
setSelected((i) => (i >= items.length - 1 ? 0 : i + 1));
|
|
19
|
+
}
|
|
20
|
+
else if (key.return) {
|
|
21
|
+
if (selected === items.length - 1)
|
|
22
|
+
setMode("input");
|
|
23
|
+
else
|
|
24
|
+
onAnswer(items[selected]);
|
|
25
|
+
}
|
|
26
|
+
else if (key.escape) {
|
|
27
|
+
onAnswer("");
|
|
28
|
+
}
|
|
29
|
+
else if (input && !key.ctrl && !key.meta) {
|
|
30
|
+
setText(input);
|
|
31
|
+
setMode("input");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else if (key.escape) {
|
|
35
|
+
if (hasOptions)
|
|
36
|
+
setMode("select");
|
|
37
|
+
else
|
|
38
|
+
onAnswer("");
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
if (mode === "input") {
|
|
42
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 1, paddingRight: 1, children: [_jsx(Text, { color: "cyan", children: `? ${question.text}` }), _jsxs(Box, { borderStyle: "single", borderLeft: false, borderRight: false, borderColor: "gray", children: [_jsx(Text, { color: "gray", children: "\u276F " }), _jsx(TextInput, { value: text, onChange: setText, onSubmit: () => onAnswer(text) })] })] }));
|
|
43
|
+
}
|
|
44
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 1, paddingRight: 1, children: [_jsx(Text, { color: "cyan", children: `? ${question.text}` }), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", children: items.map((item, i) => (_jsx(Box, { children: _jsxs(Text, { color: i === selected ? "cyan" : undefined, children: [i === selected ? "▸ " : " ", item] }) }, item))) })] }));
|
|
45
|
+
}
|
package/dist/tui/Spinner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { Text } from "ink";
|
|
4
4
|
import { timeDisplay, compactDisplay } from "../util/format.js";
|
|
@@ -9,5 +9,5 @@ export function Spinner({ label, elapsed, promptTokens, completionTokens, }) {
|
|
|
9
9
|
const id = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), 80);
|
|
10
10
|
return () => clearInterval(id);
|
|
11
11
|
}, []);
|
|
12
|
-
return (_jsxs(Text, { color: "
|
|
12
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: SPINNER_FRAMES[frame] }), _jsxs(Text, { children: [" ", label] }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", timeDisplay(elapsed), " \u00B7 \u2191", compactDisplay(promptTokens), " \u00B7 \u2193", compactDisplay(completionTokens)] })] }));
|
|
13
13
|
}
|
package/dist/util/package.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from
|
|
2
|
-
import { dirname, join } from
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
3
|
const __dirname = import.meta.dirname;
|
|
4
4
|
const MAX_PARENT_TRAVERSAL = 10;
|
|
5
5
|
let _pkg = null;
|
|
6
6
|
function findPackageJson() {
|
|
7
7
|
let current = __dirname;
|
|
8
8
|
for (let i = 0; i < MAX_PARENT_TRAVERSAL; i++) {
|
|
9
|
-
const pkgPath = join(current,
|
|
9
|
+
const pkgPath = join(current, "package.json");
|
|
10
10
|
if (existsSync(pkgPath)) {
|
|
11
11
|
return pkgPath;
|
|
12
12
|
}
|
|
@@ -15,12 +15,12 @@ function findPackageJson() {
|
|
|
15
15
|
break;
|
|
16
16
|
current = parent;
|
|
17
17
|
}
|
|
18
|
-
throw new Error(
|
|
18
|
+
throw new Error("Cannot find package.json");
|
|
19
19
|
}
|
|
20
20
|
export function getPackageInfo() {
|
|
21
21
|
if (_pkg === null) {
|
|
22
22
|
const pkgPath = findPackageJson();
|
|
23
|
-
_pkg = JSON.parse(readFileSync(pkgPath,
|
|
23
|
+
_pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
24
24
|
}
|
|
25
25
|
return _pkg;
|
|
26
26
|
}
|
package/dist/util/process.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
const MAX_BUFFER = 10 * 1024 * 1024;
|
|
3
|
-
export function runProcess(cmd, args, opts = {}) {
|
|
3
|
+
export function runProcess(cmd, args, opts = {}, signal) {
|
|
4
4
|
return new Promise((resolve) => {
|
|
5
5
|
const child = spawn(cmd, args, {
|
|
6
6
|
cwd: opts.cwd,
|
|
7
7
|
stdio: ["ignore", "pipe", "pipe"],
|
|
8
8
|
});
|
|
9
|
+
const onAbort = () => child.kill();
|
|
10
|
+
if (signal) {
|
|
11
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
12
|
+
if (signal.aborted)
|
|
13
|
+
onAbort();
|
|
14
|
+
}
|
|
9
15
|
const outChunks = [];
|
|
10
16
|
const errChunks = [];
|
|
11
17
|
let size = 0;
|
|
@@ -21,8 +27,12 @@ export function runProcess(cmd, args, opts = {}) {
|
|
|
21
27
|
child.stderr?.on("data", (c) => {
|
|
22
28
|
errChunks.push(c);
|
|
23
29
|
});
|
|
24
|
-
child.on("error", (error) =>
|
|
30
|
+
child.on("error", (error) => {
|
|
31
|
+
signal?.removeEventListener("abort", onAbort);
|
|
32
|
+
resolve({ stdout: "", stderr: "", status: null, error });
|
|
33
|
+
});
|
|
25
34
|
child.on("close", (status) => {
|
|
35
|
+
signal?.removeEventListener("abort", onAbort);
|
|
26
36
|
const stdout = Buffer.concat(outChunks).toString("utf-8");
|
|
27
37
|
const stderr = Buffer.concat(errChunks).toString("utf-8");
|
|
28
38
|
resolve(overflow
|
package/dist/util/ripgrep.js
CHANGED
|
@@ -5,9 +5,9 @@ export function resolveCwd(path) {
|
|
|
5
5
|
const root = path || ".";
|
|
6
6
|
return isAbsolute(root) ? root : join(process.cwd(), root);
|
|
7
7
|
}
|
|
8
|
-
export async function runRgLines(args, cwd) {
|
|
9
|
-
const rgArgs = ["--hidden", "--path-separator", "/", "-g", "!.git/**", ...args];
|
|
10
|
-
const r = await runProcess(rgPath, rgArgs, { cwd });
|
|
8
|
+
export async function runRgLines(args, cwd, signal) {
|
|
9
|
+
const rgArgs = ["--hidden", "--path-separator", "/", "-g", "!.git/**", "-g", "!node_modules/**", ...args];
|
|
10
|
+
const r = await runProcess(rgPath, rgArgs, { cwd }, signal);
|
|
11
11
|
if (r.error)
|
|
12
12
|
throw r.error;
|
|
13
13
|
if (r.status !== 0 && r.status !== 1) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vietor/easy-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"cli"
|
|
21
21
|
],
|
|
22
22
|
"author": "",
|
|
23
|
-
"license": "
|
|
23
|
+
"license": "MIT",
|
|
24
24
|
"packageManager": "pnpm@10.30.3",
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
package/dist/tui/LogStore.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
export class LogStore {
|
|
2
|
-
entries = [];
|
|
3
|
-
listeners = new Set();
|
|
4
|
-
getSnapshot = () => this.entries;
|
|
5
|
-
subscribe = (listener) => {
|
|
6
|
-
this.listeners.add(listener);
|
|
7
|
-
return () => {
|
|
8
|
-
this.listeners.delete(listener);
|
|
9
|
-
};
|
|
10
|
-
};
|
|
11
|
-
append(entry) {
|
|
12
|
-
this.entries = [...this.entries, entry];
|
|
13
|
-
this.emit();
|
|
14
|
-
}
|
|
15
|
-
clear() {
|
|
16
|
-
this.entries = [];
|
|
17
|
-
this.emit();
|
|
18
|
-
}
|
|
19
|
-
setToolResult(id, result, isError) {
|
|
20
|
-
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
21
|
-
const entry = this.entries[i];
|
|
22
|
-
if (entry.kind === "tool" && entry.id === id && entry.result === null) {
|
|
23
|
-
const copy = [...this.entries];
|
|
24
|
-
copy[i] = { ...entry, result, isError };
|
|
25
|
-
this.entries = copy;
|
|
26
|
-
this.emit();
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
emit() {
|
|
32
|
-
for (const listener of this.listeners)
|
|
33
|
-
listener();
|
|
34
|
-
}
|
|
35
|
-
}
|