@vietor/easy-agent 0.1.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/LICENSE +21 -0
- package/README.md +125 -0
- package/dist/cli.js +6 -0
- package/dist/cmds/builtin.js +68 -0
- package/dist/cmds/registry.js +27 -0
- package/dist/cmds/types.js +1 -0
- package/dist/config.js +31 -0
- package/dist/core/agent.js +116 -0
- package/dist/core/session.js +75 -0
- package/dist/llm/client.js +74 -0
- package/dist/llm/types.js +1 -0
- package/dist/main.js +61 -0
- package/dist/mcp/client.js +45 -0
- package/dist/mcp/server.js +85 -0
- package/dist/skills/loader.js +43 -0
- package/dist/skills/types.js +1 -0
- package/dist/tools/file_edit.js +39 -0
- package/dist/tools/file_read.js +18 -0
- package/dist/tools/file_write.js +22 -0
- package/dist/tools/glob.js +29 -0
- package/dist/tools/grep.js +36 -0
- package/dist/tools/registry.js +51 -0
- package/dist/tools/shell.js +34 -0
- package/dist/tools/types.js +1 -0
- package/dist/tools/web_fetch.js +140 -0
- package/dist/tui/App.js +147 -0
- package/dist/tui/AppHeader.js +7 -0
- package/dist/tui/LogStore.js +35 -0
- package/dist/tui/LogView.js +33 -0
- package/dist/tui/PromptOrCommandInput.js +61 -0
- package/dist/tui/Spinner.js +13 -0
- package/dist/tui/components/Markdown.js +210 -0
- package/dist/util/async.js +58 -0
- package/dist/util/format.js +12 -0
- package/dist/util/fs.js +17 -0
- package/dist/util/package.js +26 -0
- package/dist/util/process.js +33 -0
- package/dist/util/ripgrep.js +17 -0
- package/package.json +45 -0
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo } from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { Markdown } from "./components/Markdown.js";
|
|
5
|
+
function preview(isError, text) {
|
|
6
|
+
if (isError) {
|
|
7
|
+
const previewText = text.replace(/\n/g, " ").replace(/\s+/g, " ").trim();
|
|
8
|
+
return previewText.length > 100 ? previewText.slice(0, 100) + "…" : previewText;
|
|
9
|
+
}
|
|
10
|
+
const lineCount = text.length === 0 ? 0 : (text.match(/\n/g) || []).length + 1;
|
|
11
|
+
const byteCount = Buffer.byteLength(text, "utf-8");
|
|
12
|
+
return `Result: ${byteCount} bytes, ${lineCount} lines`;
|
|
13
|
+
}
|
|
14
|
+
export const LogView = memo(function Entry({ entry }) {
|
|
15
|
+
switch (entry.kind) {
|
|
16
|
+
case "user":
|
|
17
|
+
return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: `❯ ${entry.text}` }) }));
|
|
18
|
+
case "skill":
|
|
19
|
+
return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "magenta", children: `◈ skill: ${entry.name}` }) }));
|
|
20
|
+
case "assistant":
|
|
21
|
+
return (_jsx(Box, { marginTop: 1, children: _jsx(Markdown, { color: "green", children: entry.text }) }));
|
|
22
|
+
case "tool":
|
|
23
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: `● ${entry.name}${entry.summary ? ` ${entry.summary}` : ""}` }), entry.result !== null ? (_jsx(Text, { color: entry.isError ? "red" : "gray", children: ` ${preview(entry.isError ?? false, entry.result)}` })) : null] }));
|
|
24
|
+
case "retry":
|
|
25
|
+
return (_jsx(Box, { children: _jsx(Text, { color: "yellow", children: `↻ Retry ${entry.attempt}/${entry.max}` }) }));
|
|
26
|
+
case "error":
|
|
27
|
+
return (_jsx(Box, { children: _jsx(Text, { color: "red", children: `✗ ${entry.text}` }) }));
|
|
28
|
+
case "interrupted":
|
|
29
|
+
return (_jsx(Box, { children: _jsx(Text, { color: "yellow", children: "\u25FC interrupted" }) }));
|
|
30
|
+
case "system":
|
|
31
|
+
return (_jsx(Box, { children: _jsx(Text, { color: "gray", children: entry.text }) }));
|
|
32
|
+
}
|
|
33
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
const MAX_ITEMS = 4;
|
|
6
|
+
export function PromptOrCommandInput({ commands, onCommand, onPrompt }) {
|
|
7
|
+
const [input, setInput] = useState("");
|
|
8
|
+
const showMenu = input.startsWith("/");
|
|
9
|
+
const prefix = showMenu ? input.slice(1) : "";
|
|
10
|
+
const filtered = useMemo(() => {
|
|
11
|
+
if (!showMenu)
|
|
12
|
+
return [];
|
|
13
|
+
if (prefix === "")
|
|
14
|
+
return commands;
|
|
15
|
+
return commands.filter((c) => c.name.startsWith(prefix));
|
|
16
|
+
}, [showMenu, prefix, commands]);
|
|
17
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (filtered.length > 0 && selectedIndex === -1)
|
|
20
|
+
setSelectedIndex(0);
|
|
21
|
+
else if (selectedIndex >= filtered.length)
|
|
22
|
+
setSelectedIndex(Math.max(0, filtered.length - 1));
|
|
23
|
+
}, [filtered.length]);
|
|
24
|
+
useInput((_input, key) => {
|
|
25
|
+
if (key.upArrow) {
|
|
26
|
+
setSelectedIndex((i) => (i <= 0 ? filtered.length - 1 : i - 1));
|
|
27
|
+
}
|
|
28
|
+
else if (key.downArrow) {
|
|
29
|
+
setSelectedIndex((i) => (i >= filtered.length - 1 ? 0 : i + 1));
|
|
30
|
+
}
|
|
31
|
+
else if (key.escape) {
|
|
32
|
+
setInput("");
|
|
33
|
+
}
|
|
34
|
+
}, { isActive: showMenu });
|
|
35
|
+
const onSubmit = (value) => {
|
|
36
|
+
const text = value.trim();
|
|
37
|
+
setInput("");
|
|
38
|
+
if (!text)
|
|
39
|
+
return;
|
|
40
|
+
if (text.startsWith("/")) {
|
|
41
|
+
if (filtered.length > 0) {
|
|
42
|
+
const cmd = filtered[selectedIndex];
|
|
43
|
+
onCommand(cmd ? cmd.name : prefix);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
onPrompt(text);
|
|
48
|
+
};
|
|
49
|
+
return (_jsxs(Box, { flexDirection: "column", children: [showMenu && filtered.length > 0 ? (_jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", children: (() => {
|
|
50
|
+
const total = filtered.length;
|
|
51
|
+
const half = Math.floor(MAX_ITEMS / 2);
|
|
52
|
+
let start = Math.max(0, selectedIndex - half);
|
|
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 })] })] }));
|
|
61
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Text } from "ink";
|
|
4
|
+
import { timeDisplay, compactDisplay } from "../util/format.js";
|
|
5
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
6
|
+
export function Spinner({ label, elapsed, promptTokens, completionTokens, }) {
|
|
7
|
+
const [frame, setFrame] = useState(0);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const id = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), 80);
|
|
10
|
+
return () => clearInterval(id);
|
|
11
|
+
}, []);
|
|
12
|
+
return (_jsxs(Text, { color: "gray", children: [SPINNER_FRAMES[frame], " ", label, " \u00B7 ", timeDisplay(elapsed), " \u00B7 \u2191", compactDisplay(promptTokens), " \u00B7 \u2193", compactDisplay(completionTokens)] }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, useMemo } from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { lexer } from "marked";
|
|
5
|
+
import stringWidth from "string-width";
|
|
6
|
+
const HEADING_COLOR = ["magentaBright", "cyanBright", "blue", "yellow", "green", "gray"];
|
|
7
|
+
export function Markdown({ children, color }) {
|
|
8
|
+
const tokens = useMemo(() => lexer(children, { gfm: true }), [children]);
|
|
9
|
+
return _jsx(Box, { flexDirection: "column", children: renderBlocks(tokens, color) });
|
|
10
|
+
}
|
|
11
|
+
function renderBlocks(tokens, color) {
|
|
12
|
+
return tokens.map((token, i) => (_jsx(Fragment, { children: renderBlock(token, color) }, i)));
|
|
13
|
+
}
|
|
14
|
+
function renderBlock(token, color) {
|
|
15
|
+
switch (token.type) {
|
|
16
|
+
case "space":
|
|
17
|
+
return _jsx(Text, { children: " " });
|
|
18
|
+
case "hr":
|
|
19
|
+
return (_jsx(Box, { borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, borderColor: "gray" }));
|
|
20
|
+
case "heading":
|
|
21
|
+
return (_jsx(Box, { children: _jsx(Text, { bold: true, underline: token.depth === 1, color: HEADING_COLOR[token.depth - 1], children: renderInline(token.tokens) }) }));
|
|
22
|
+
case "code":
|
|
23
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", children: [token.lang ? _jsx(Text, { dimColor: true, children: token.lang }) : null, _jsx(Text, { children: token.text })] }));
|
|
24
|
+
case "blockquote":
|
|
25
|
+
return (_jsx(Box, { borderStyle: "single", borderTop: false, borderBottom: false, borderRight: false, borderColor: "gray", paddingLeft: 1, children: renderBlocks(token.tokens, color) }));
|
|
26
|
+
case "list":
|
|
27
|
+
return renderList(token, color);
|
|
28
|
+
case "table":
|
|
29
|
+
return renderTable(token, color);
|
|
30
|
+
case "paragraph":
|
|
31
|
+
return _jsx(Text, { color: color, children: renderInline(token.tokens) });
|
|
32
|
+
case "text":
|
|
33
|
+
return _jsx(Text, { color: color, children: token.tokens ? renderInline(token.tokens) : token.text });
|
|
34
|
+
case "html":
|
|
35
|
+
return _jsx(Text, { dimColor: true, children: token.text });
|
|
36
|
+
default:
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function renderInline(tokens) {
|
|
41
|
+
if (!tokens || tokens.length === 0)
|
|
42
|
+
return null;
|
|
43
|
+
return tokens.map((token, i) => {
|
|
44
|
+
const tok = token;
|
|
45
|
+
switch (tok.type) {
|
|
46
|
+
case "text":
|
|
47
|
+
return _jsx(Text, { children: tok.tokens ? renderInline(tok.tokens) : tok.text }, i);
|
|
48
|
+
case "strong":
|
|
49
|
+
return _jsx(Text, { bold: true, children: renderInline(tok.tokens) }, i);
|
|
50
|
+
case "em":
|
|
51
|
+
return _jsx(Text, { italic: true, children: renderInline(tok.tokens) }, i);
|
|
52
|
+
case "codespan":
|
|
53
|
+
return _jsx(Text, { color: "cyan", children: tok.text }, i);
|
|
54
|
+
case "del":
|
|
55
|
+
return _jsx(Text, { strikethrough: true, children: renderInline(tok.tokens) }, i);
|
|
56
|
+
case "link":
|
|
57
|
+
return _jsx(Text, { color: "blue", underline: true, children: renderInline(tok.tokens) }, i);
|
|
58
|
+
case "image":
|
|
59
|
+
return _jsx(Text, { color: "magenta", children: tok.text || tok.href }, i);
|
|
60
|
+
case "br":
|
|
61
|
+
return _jsx(Text, { children: "\n" }, i);
|
|
62
|
+
case "escape":
|
|
63
|
+
return _jsx(Text, { children: tok.text }, i);
|
|
64
|
+
case "html":
|
|
65
|
+
return _jsx(Text, { dimColor: true, children: tok.text }, i);
|
|
66
|
+
default:
|
|
67
|
+
return _jsx(Text, { children: tok.text ?? "" }, i);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function renderList(token, color) {
|
|
72
|
+
const markers = token.items.map((item, i) => {
|
|
73
|
+
if (item.task)
|
|
74
|
+
return item.checked ? "[x]" : "[ ]";
|
|
75
|
+
if (token.ordered)
|
|
76
|
+
return `${(token.start === "" ? i + 1 : Number(token.start) + i)}.`;
|
|
77
|
+
return "•";
|
|
78
|
+
});
|
|
79
|
+
const markerWidth = Math.max(1, ...markers.map((s) => stringWidth(s)));
|
|
80
|
+
return (_jsx(Box, { flexDirection: "column", children: token.items.map((item, i) => (_jsxs(Box, { marginTop: token.loose && i > 0 ? 1 : 0, children: [_jsxs(Text, { color: color, children: [padAlign(markers[i], markerWidth, "right"), " "] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: renderBlocks(item.tokens, color) })] }, i))) }));
|
|
81
|
+
}
|
|
82
|
+
function renderTable(token, color) {
|
|
83
|
+
const aligns = token.align;
|
|
84
|
+
const cols = token.header.length;
|
|
85
|
+
const overhead = 3 * cols + 1;
|
|
86
|
+
const available = Math.max(1, (process.stdout.columns ?? 80) - 2 - overhead);
|
|
87
|
+
const natural = new Array(cols).fill(0);
|
|
88
|
+
for (const row of [token.header, ...token.rows]) {
|
|
89
|
+
for (let c = 0; c < cols; c++) {
|
|
90
|
+
const w = stringWidth(tokensToText(row[c].tokens));
|
|
91
|
+
if (w > natural[c])
|
|
92
|
+
natural[c] = w;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const widths = fitWidths(natural, available);
|
|
96
|
+
const renderRow = (row, bold = false) => {
|
|
97
|
+
const wrapped = row.map((tc, c) => wrapText(tokensToText(tc.tokens), widths[c]));
|
|
98
|
+
const height = Math.max(1, ...wrapped.map(lines => lines.length));
|
|
99
|
+
return Array.from({ length: height }, (_, r) => (_jsx(Text, { children: [
|
|
100
|
+
...row.flatMap((_, c) => [
|
|
101
|
+
_jsx(Text, { dimColor: true, children: "\u2502" }, `s${c}`),
|
|
102
|
+
_jsx(Text, { color: color, bold: bold, children: ` ${padAlign(wrapped[c][r] ?? "", widths[c], aligns[c])} ` }, `c${c}`),
|
|
103
|
+
]),
|
|
104
|
+
_jsx(Text, { dimColor: true, children: "\u2502" }, "e"),
|
|
105
|
+
] }, r)));
|
|
106
|
+
};
|
|
107
|
+
const border = (l, m, r) => l + widths.map(w => "─".repeat(w + 2)).join(m) + r;
|
|
108
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: border("┌", "┬", "┐") }), _jsx(Fragment, { children: renderRow(token.header, true) }), _jsx(Text, { dimColor: true, children: border("├", "┼", "┤") }), token.rows.map((row, i) => (_jsx(Fragment, { children: renderRow(row) }, i))), _jsx(Text, { dimColor: true, children: border("└", "┴", "┘") })] }));
|
|
109
|
+
}
|
|
110
|
+
function tokensToText(tokens) {
|
|
111
|
+
if (!tokens)
|
|
112
|
+
return "";
|
|
113
|
+
return tokens.map(tokenToText).join("");
|
|
114
|
+
}
|
|
115
|
+
function tokenToText(token) {
|
|
116
|
+
const t = token;
|
|
117
|
+
switch (t.type) {
|
|
118
|
+
case "text":
|
|
119
|
+
return t.tokens ? tokensToText(t.tokens) : t.text;
|
|
120
|
+
case "strong":
|
|
121
|
+
case "em":
|
|
122
|
+
case "del":
|
|
123
|
+
return tokensToText(t.tokens);
|
|
124
|
+
case "codespan":
|
|
125
|
+
return t.text;
|
|
126
|
+
case "link":
|
|
127
|
+
return tokensToText(t.tokens);
|
|
128
|
+
case "image":
|
|
129
|
+
return t.text || t.href;
|
|
130
|
+
case "br":
|
|
131
|
+
return "";
|
|
132
|
+
case "escape":
|
|
133
|
+
return t.text;
|
|
134
|
+
case "html":
|
|
135
|
+
return t.text;
|
|
136
|
+
default:
|
|
137
|
+
return t.text ?? "";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function padAlign(text, width, align) {
|
|
141
|
+
const pad = Math.max(0, width - stringWidth(text));
|
|
142
|
+
if (align === "right")
|
|
143
|
+
return " ".repeat(pad) + text;
|
|
144
|
+
if (align === "center")
|
|
145
|
+
return " ".repeat(Math.floor(pad / 2)) + text + " ".repeat(Math.ceil(pad / 2));
|
|
146
|
+
return text + " ".repeat(pad);
|
|
147
|
+
}
|
|
148
|
+
function fitWidths(widths, available) {
|
|
149
|
+
const fitted = widths.slice();
|
|
150
|
+
let total = fitted.reduce((a, b) => a + b, 0);
|
|
151
|
+
if (total <= available)
|
|
152
|
+
return fitted;
|
|
153
|
+
while (total > available) {
|
|
154
|
+
let max = 0;
|
|
155
|
+
for (let i = 1; i < fitted.length; i++)
|
|
156
|
+
if (fitted[i] > fitted[max])
|
|
157
|
+
max = i;
|
|
158
|
+
if (fitted[max] <= 1)
|
|
159
|
+
break;
|
|
160
|
+
fitted[max]--;
|
|
161
|
+
total--;
|
|
162
|
+
}
|
|
163
|
+
return fitted;
|
|
164
|
+
}
|
|
165
|
+
function wrapText(text, width) {
|
|
166
|
+
if (width <= 0 || stringWidth(text) <= width)
|
|
167
|
+
return [text];
|
|
168
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
169
|
+
const lines = [];
|
|
170
|
+
let line = "";
|
|
171
|
+
let lineW = 0;
|
|
172
|
+
for (const word of words) {
|
|
173
|
+
const w = stringWidth(word);
|
|
174
|
+
if (lineW > 0 && lineW + 1 + w <= width) {
|
|
175
|
+
line += " " + word;
|
|
176
|
+
lineW += 1 + w;
|
|
177
|
+
}
|
|
178
|
+
else if (w <= width) {
|
|
179
|
+
if (lineW > 0)
|
|
180
|
+
lines.push(line);
|
|
181
|
+
line = word;
|
|
182
|
+
lineW = w;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
if (lineW > 0)
|
|
186
|
+
lines.push(line);
|
|
187
|
+
line = "";
|
|
188
|
+
lineW = 0;
|
|
189
|
+
let cur = "";
|
|
190
|
+
let curW = 0;
|
|
191
|
+
for (const ch of word) {
|
|
192
|
+
const cw = stringWidth(ch);
|
|
193
|
+
if (cur && curW + cw > width) {
|
|
194
|
+
lines.push(cur);
|
|
195
|
+
cur = ch;
|
|
196
|
+
curW = cw;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
cur += ch;
|
|
200
|
+
curW += cw;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
line = cur;
|
|
204
|
+
lineW = curW;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (lineW > 0 || lines.length === 0)
|
|
208
|
+
lines.push(line);
|
|
209
|
+
return lines;
|
|
210
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export async function withRetry(fn, opts) {
|
|
2
|
+
for (let attempt = 0;; attempt++) {
|
|
3
|
+
try {
|
|
4
|
+
return await fn();
|
|
5
|
+
}
|
|
6
|
+
catch (e) {
|
|
7
|
+
if (attempt < opts.retries && opts.retryable(e)) {
|
|
8
|
+
opts.onRetry?.(attempt + 1, opts.retries);
|
|
9
|
+
await trySleep(opts.backoff(attempt), opts.signal);
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
throw e;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function trySleep(ms, signal) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
if (signal?.aborted) {
|
|
19
|
+
reject(new Error("aborted"));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const onAbort = () => {
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
reject(new Error("aborted"));
|
|
25
|
+
};
|
|
26
|
+
const timer = setTimeout(() => {
|
|
27
|
+
signal?.removeEventListener("abort", onAbort);
|
|
28
|
+
resolve();
|
|
29
|
+
}, ms);
|
|
30
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export async function withAbort(fn, opts) {
|
|
34
|
+
const onAbort = () => opts.onAbort?.();
|
|
35
|
+
if (opts.signal?.aborted)
|
|
36
|
+
onAbort();
|
|
37
|
+
else
|
|
38
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
39
|
+
try {
|
|
40
|
+
return await fn(() => !!opts.signal?.aborted);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function withTimeout(p, ms) {
|
|
47
|
+
let timer;
|
|
48
|
+
const timed = new Promise((_, reject) => {
|
|
49
|
+
timer = setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms);
|
|
50
|
+
});
|
|
51
|
+
return Promise.race([
|
|
52
|
+
p.finally(() => {
|
|
53
|
+
if (timer)
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
}),
|
|
56
|
+
timed,
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const timeFormatter = new Intl.NumberFormat("en-US", { style: "unit", unit: "second", unitDisplay: "narrow" });
|
|
2
|
+
const compactFormatter = new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 2 });
|
|
3
|
+
export function timeDisplay(value) {
|
|
4
|
+
if (!value)
|
|
5
|
+
return "0s";
|
|
6
|
+
return timeFormatter.format(value);
|
|
7
|
+
}
|
|
8
|
+
export function compactDisplay(value) {
|
|
9
|
+
if (!value)
|
|
10
|
+
return "0";
|
|
11
|
+
return compactFormatter.format(value);
|
|
12
|
+
}
|
package/dist/util/fs.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
export function tryReadFileText(path) {
|
|
3
|
+
if (existsSync(path)) {
|
|
4
|
+
const content = readFileSync(path, "utf-8").trim();
|
|
5
|
+
if (content)
|
|
6
|
+
return content;
|
|
7
|
+
}
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
export function readFirstFileContent(paths, fn) {
|
|
11
|
+
for (const p of paths) {
|
|
12
|
+
const content = fn(p);
|
|
13
|
+
if (content)
|
|
14
|
+
return content;
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
const __dirname = import.meta.dirname;
|
|
4
|
+
const MAX_PARENT_TRAVERSAL = 10;
|
|
5
|
+
let _pkg = null;
|
|
6
|
+
function findPackageJson() {
|
|
7
|
+
let current = __dirname;
|
|
8
|
+
for (let i = 0; i < MAX_PARENT_TRAVERSAL; i++) {
|
|
9
|
+
const pkgPath = join(current, 'package.json');
|
|
10
|
+
if (existsSync(pkgPath)) {
|
|
11
|
+
return pkgPath;
|
|
12
|
+
}
|
|
13
|
+
const parent = dirname(current);
|
|
14
|
+
if (parent === current)
|
|
15
|
+
break;
|
|
16
|
+
current = parent;
|
|
17
|
+
}
|
|
18
|
+
throw new Error('Cannot find package.json');
|
|
19
|
+
}
|
|
20
|
+
export function getPackageInfo() {
|
|
21
|
+
if (_pkg === null) {
|
|
22
|
+
const pkgPath = findPackageJson();
|
|
23
|
+
_pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
24
|
+
}
|
|
25
|
+
return _pkg;
|
|
26
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
const MAX_BUFFER = 10 * 1024 * 1024;
|
|
3
|
+
export function runProcess(cmd, args, opts = {}) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
const child = spawn(cmd, args, {
|
|
6
|
+
cwd: opts.cwd,
|
|
7
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
8
|
+
});
|
|
9
|
+
const outChunks = [];
|
|
10
|
+
const errChunks = [];
|
|
11
|
+
let size = 0;
|
|
12
|
+
let overflow = false;
|
|
13
|
+
child.stdout?.on("data", (c) => {
|
|
14
|
+
outChunks.push(c);
|
|
15
|
+
size += c.length;
|
|
16
|
+
if (size > MAX_BUFFER) {
|
|
17
|
+
overflow = true;
|
|
18
|
+
child.kill();
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
child.stderr?.on("data", (c) => {
|
|
22
|
+
errChunks.push(c);
|
|
23
|
+
});
|
|
24
|
+
child.on("error", (error) => resolve({ stdout: "", stderr: "", status: null, error }));
|
|
25
|
+
child.on("close", (status) => {
|
|
26
|
+
const stdout = Buffer.concat(outChunks).toString("utf-8");
|
|
27
|
+
const stderr = Buffer.concat(errChunks).toString("utf-8");
|
|
28
|
+
resolve(overflow
|
|
29
|
+
? { stdout, stderr, status, error: new Error("output exceeded maxBuffer") }
|
|
30
|
+
: { stdout, stderr, status });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { isAbsolute, join } from "node:path";
|
|
2
|
+
import { rgPath } from "@vscode/ripgrep";
|
|
3
|
+
import { runProcess } from "./process.js";
|
|
4
|
+
export function resolveCwd(path) {
|
|
5
|
+
const root = path || ".";
|
|
6
|
+
return isAbsolute(root) ? root : join(process.cwd(), root);
|
|
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 });
|
|
11
|
+
if (r.error)
|
|
12
|
+
throw r.error;
|
|
13
|
+
if (r.status !== 0 && r.status !== 1) {
|
|
14
|
+
throw new Error((r.stderr || "").trim() || `ripgrep exited with ${r.status}`);
|
|
15
|
+
}
|
|
16
|
+
return r.stdout.split("\n").filter(Boolean).map((f) => f.replace(/^\.\//, ""));
|
|
17
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vietor/easy-agent",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"easy-agent": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"start": "tsc && node dist/cli.js",
|
|
15
|
+
"dev": "tsx src/cli.ts",
|
|
16
|
+
"prepublishOnly": "tsc"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [],
|
|
19
|
+
"author": "",
|
|
20
|
+
"license": "ISC",
|
|
21
|
+
"packageManager": "pnpm@10.30.3",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
24
|
+
"@vscode/ripgrep": "^1.18.0",
|
|
25
|
+
"htmlparser2": "^12.0.0",
|
|
26
|
+
"ink": "^7.1.0",
|
|
27
|
+
"ink-text-input": "^6.0.0",
|
|
28
|
+
"marked": "^18.0.5",
|
|
29
|
+
"openai": "^4.67.0",
|
|
30
|
+
"react": "^19.2.7",
|
|
31
|
+
"string-width": "^8.2.1",
|
|
32
|
+
"turndown": "^7.2.4",
|
|
33
|
+
"zod": "^4.4.3"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.0.0",
|
|
37
|
+
"@types/react": "^19.2.17",
|
|
38
|
+
"@types/turndown": "^5.0.6",
|
|
39
|
+
"tsx": "^4.19.0",
|
|
40
|
+
"typescript": "^5.6.0"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=22.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|