@vietor/easy-agent 0.3.1 → 0.4.2
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 +31 -22
- package/dist/cli.js +0 -0
- package/dist/cmds/builtin.js +17 -56
- package/dist/config.js +1 -1
- package/dist/main.js +26 -44
- package/dist/tui/App.js +29 -18
- package/dist/tui/AppHeader.js +4 -3
- package/dist/tui/LogList.js +9 -0
- package/dist/tui/PromptOrCommandInput.js +8 -6
- package/dist/tui/TodoView.js +19 -0
- package/dist/util/package.js +2 -3
- package/package.json +18 -21
- package/dist/cmds/registry.js +0 -30
- package/dist/cmds/types.js +0 -1
- package/dist/core/agent.js +0 -134
- package/dist/core/conversation.js +0 -82
- package/dist/core/logstore.js +0 -49
- package/dist/core/session.js +0 -181
- package/dist/llm/client.js +0 -80
- package/dist/llm/types.js +0 -1
- package/dist/mcp/client.js +0 -81
- package/dist/mcp/server.js +0 -124
- package/dist/skills/loader.js +0 -43
- package/dist/skills/types.js +0 -1
- package/dist/tools/ask_user.js +0 -24
- package/dist/tools/file_edit.js +0 -47
- package/dist/tools/file_read.js +0 -43
- package/dist/tools/file_write.js +0 -22
- package/dist/tools/glob.js +0 -29
- package/dist/tools/grep.js +0 -73
- package/dist/tools/registry.js +0 -52
- package/dist/tools/shell.js +0 -34
- package/dist/tools/types.js +0 -1
- package/dist/tools/web_fetch.js +0 -141
- package/dist/util/async.js +0 -58
- package/dist/util/fs.js +0 -17
- package/dist/util/process.js +0 -43
- package/dist/util/ripgrep.js +0 -17
package/README.md
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @vietor/easy-agent
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[npm]: https://img.shields.io/npm/v/@vietor/easy-agent.svg?style=flat-square
|
|
6
|
-
|
|
7
|
-
An autonomous coding agent in the terminal.
|
|
3
|
+
Terminal-based AI agent CLI with a conversational TUI, powered by `@vietor/easy-agent-core`.
|
|
8
4
|
|
|
9
5
|
## Requirements
|
|
10
6
|
|
|
@@ -12,7 +8,7 @@ An autonomous coding agent in the terminal.
|
|
|
12
8
|
|
|
13
9
|
## Install
|
|
14
10
|
|
|
15
|
-
Install globally
|
|
11
|
+
Install globally:
|
|
16
12
|
|
|
17
13
|
```bash
|
|
18
14
|
npm install -g @vietor/easy-agent
|
|
@@ -31,18 +27,18 @@ Create `~/.easy-agent.json` in your home directory:
|
|
|
31
27
|
```json
|
|
32
28
|
{
|
|
33
29
|
"llm": {
|
|
34
|
-
"baseUrl": "https://api.
|
|
30
|
+
"baseUrl": "https://api.openai.com/v1",
|
|
35
31
|
"apiKey": "your-api-key",
|
|
36
|
-
"model": "
|
|
32
|
+
"model": "gpt-4o"
|
|
37
33
|
}
|
|
38
34
|
}
|
|
39
35
|
```
|
|
40
36
|
|
|
41
37
|
`llm.baseUrl`, `llm.apiKey`, and `llm.model` are all required. Point `baseUrl` at any OpenAI-compatible endpoint (OpenAI, Azure OpenAI, local servers, etc.) and set `model` to a model that endpoint serves.
|
|
42
38
|
|
|
43
|
-
###
|
|
39
|
+
### MCP servers (optional)
|
|
44
40
|
|
|
45
|
-
Add an `mcpServers` map to expose external tools through the Model Context Protocol. Each entry is either a local process (stdio) or a remote endpoint (
|
|
41
|
+
Add an `mcpServers` map to expose external tools through the Model Context Protocol. Each entry is either a local process (stdio) or a remote endpoint (Streamable HTTP):
|
|
46
42
|
|
|
47
43
|
```json
|
|
48
44
|
{
|
|
@@ -52,7 +48,7 @@ Add an `mcpServers` map to expose external tools through the Model Context Proto
|
|
|
52
48
|
"command": "npx",
|
|
53
49
|
"args": ["chrome-devtools-mcp@latest", "--auto-connect", "--accept-insecure-certs"]
|
|
54
50
|
},
|
|
55
|
-
"
|
|
51
|
+
"api-server": {
|
|
56
52
|
"type": "http",
|
|
57
53
|
"url": "https://mcp.example.com/mcp",
|
|
58
54
|
"headers": { "Authorization": "Bearer your-token" }
|
|
@@ -61,9 +57,9 @@ Add an `mcpServers` map to expose external tools through the Model Context Proto
|
|
|
61
57
|
}
|
|
62
58
|
```
|
|
63
59
|
|
|
64
|
-
A stdio server omits `type` (or sets `"stdio"`); only `command` is required, `args` and `env` are optional. A remote server sets `type` to `"
|
|
60
|
+
A stdio server omits `type` (or sets `"stdio"`); only `command` is required, `args` and `env` are optional. A remote server sets `type` to `"http"` with a required `url`; `headers` is optional and sent with every request (use it for static auth tokens). Either entry accepts an optional `enabled` (defaults to `true`); set `false` to keep a server configured but skip starting it. MCP tools become available to the agent as `MCP__<server>__<tool>`. If a server fails to connect within 30s, it is disabled and the error is shown in the TUI — for stdio servers the captured stderr tail is included to help diagnose startup failures — and the rest keep running.
|
|
65
61
|
|
|
66
|
-
###
|
|
62
|
+
### Agent instructions (optional)
|
|
67
63
|
|
|
68
64
|
Easy Agent reads instructions files and appends them to the system prompt, so you can set persistent rules, conventions, or preferences. It looks in two places:
|
|
69
65
|
|
|
@@ -72,7 +68,7 @@ Easy Agent reads instructions files and appends them to the system prompt, so yo
|
|
|
72
68
|
|
|
73
69
|
Both global and project files are loaded and concatenated into the system prompt if they exist.
|
|
74
70
|
|
|
75
|
-
###
|
|
71
|
+
### Skills (optional)
|
|
76
72
|
|
|
77
73
|
Skills are reusable prompts that register themselves as slash commands. Create a subdirectory for each skill under `~/.agents/skills/` (or `~/.claude/skills/`) with a `SKILL.md` file:
|
|
78
74
|
|
|
@@ -107,7 +103,7 @@ easy-agent
|
|
|
107
103
|
|
|
108
104
|
Type a prompt and press Enter. The agent streams its reply and calls tools as needed, showing each tool call and a one-line preview of its result. It iterates until the task is done (capped at 50 tool rounds per turn).
|
|
109
105
|
|
|
110
|
-
Built-in tools
|
|
106
|
+
### Built-in tools
|
|
111
107
|
|
|
112
108
|
- **Shell** — run shell commands using this platform's native syntax.
|
|
113
109
|
- **FileRead** — read a file with line numbers; supports `offset`/`limit` for paging large files.
|
|
@@ -117,11 +113,24 @@ Built-in tools:
|
|
|
117
113
|
- **Grep** — search file contents by regex with `glob`/`type` filters, context lines, case-insensitive, and `files_with_matches`/`count` output modes.
|
|
118
114
|
- **WebFetch** — fetch a URL as markdown or text.
|
|
119
115
|
- **AskUser** — ask the user a question and wait for their answer.
|
|
116
|
+
- **TodoWrite** — track multi-step work as a task list (pending / in_progress / completed), shown live as a panel in the TUI.
|
|
117
|
+
|
|
118
|
+
### Slash commands
|
|
120
119
|
|
|
121
|
-
|
|
120
|
+
| Command | Description |
|
|
121
|
+
|---------|-------------|
|
|
122
|
+
| `/mcp` | List linked MCP servers, their status, and exposed tools |
|
|
123
|
+
| `/clear` | Reset the conversation |
|
|
124
|
+
| `/compact` | Compress the conversation into a summary to free context |
|
|
125
|
+
| `/export` | Save the current conversation to `conversation-{timestamp}.jsonl` |
|
|
126
|
+
| `/quit` or `/exit` | Leave the app |
|
|
122
127
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
-
|
|
128
|
+
## Build from source
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
git clone <repo-url>
|
|
132
|
+
cd easy-agent
|
|
133
|
+
pnpm install
|
|
134
|
+
pnpm build # build core → CLI
|
|
135
|
+
pnpm --filter @vietor/easy-agent dev # hot-reload dev mode
|
|
136
|
+
```
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/cmds/builtin.js
CHANGED
|
@@ -2,67 +2,28 @@ import { writeFileSync } from "node:fs";
|
|
|
2
2
|
export const exitCommand = {
|
|
3
3
|
name: "exit",
|
|
4
4
|
description: "Exit the conversation",
|
|
5
|
-
async execute(_ctx, host) {
|
|
6
|
-
host.exit();
|
|
7
|
-
},
|
|
8
|
-
};
|
|
9
|
-
export const clearCommand = {
|
|
10
|
-
name: "clear",
|
|
11
|
-
description: "Clear the conversation and log",
|
|
12
5
|
async execute(ctx) {
|
|
13
|
-
ctx.session.
|
|
14
|
-
},
|
|
15
|
-
};
|
|
16
|
-
export const mcpCommand = {
|
|
17
|
-
name: "mcp",
|
|
18
|
-
description: "List linked MCP servers",
|
|
19
|
-
async execute(ctx, host) {
|
|
20
|
-
const servers = ctx.mcp.list();
|
|
21
|
-
const text = servers.length
|
|
22
|
-
? [
|
|
23
|
-
"MCP servers:",
|
|
24
|
-
...servers.map((s) => `❯ ${s.name} ⋅ ${s.type} ⋅ ${s.status} ∶ ${s.tools.join(", ") || "(no tools)"}`),
|
|
25
|
-
].join("\n")
|
|
26
|
-
: "No MCP servers linked.";
|
|
27
|
-
host.info(text);
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
export const compactCommand = {
|
|
31
|
-
name: "compact",
|
|
32
|
-
description: "Compact the agent context",
|
|
33
|
-
async execute(ctx, host) {
|
|
34
|
-
host.thinking(true);
|
|
35
|
-
try {
|
|
36
|
-
const ok = await ctx.session.compact();
|
|
37
|
-
if (ok)
|
|
38
|
-
host.info("context compacted");
|
|
39
|
-
}
|
|
40
|
-
catch (e) {
|
|
41
|
-
host.error(e.message);
|
|
42
|
-
}
|
|
43
|
-
finally {
|
|
44
|
-
host.thinking(false);
|
|
45
|
-
}
|
|
6
|
+
ctx.session.local.set("exitRequested", true);
|
|
46
7
|
},
|
|
47
8
|
};
|
|
48
9
|
export const exportCommand = {
|
|
49
10
|
name: "export",
|
|
50
11
|
description: "Export the conversation to a JSONL file",
|
|
51
|
-
async execute(ctx
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
host.info(`exported to ${file}`);
|
|
63
|
-
}
|
|
64
|
-
catch (e) {
|
|
65
|
-
host.error(e.message);
|
|
66
|
-
}
|
|
12
|
+
async execute(ctx) {
|
|
13
|
+
const d = new Date();
|
|
14
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
15
|
+
const ts = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
16
|
+
const file = `conversation-${ts}.jsonl`;
|
|
17
|
+
const lines = ctx.session
|
|
18
|
+
.export()
|
|
19
|
+
.map((m) => JSON.stringify(m))
|
|
20
|
+
.join("\n");
|
|
21
|
+
writeFileSync(file, lines + "\n", "utf-8");
|
|
22
|
+
ctx.message(`exported to ${file}`);
|
|
67
23
|
},
|
|
68
24
|
};
|
|
25
|
+
export const builtinCommands = [
|
|
26
|
+
exitCommand,
|
|
27
|
+
{ ...exitCommand, name: "quit" },
|
|
28
|
+
exportCommand,
|
|
29
|
+
];
|
package/dist/config.js
CHANGED
|
@@ -16,7 +16,7 @@ const StdioServerConfig = z.object({
|
|
|
16
16
|
enabled: z.boolean().optional(),
|
|
17
17
|
});
|
|
18
18
|
const RemoteServerConfig = z.object({
|
|
19
|
-
type: z.enum(["
|
|
19
|
+
type: z.enum(["http"]),
|
|
20
20
|
url: z.string().url(),
|
|
21
21
|
headers: z.record(z.string(), z.string()).optional(),
|
|
22
22
|
enabled: z.boolean().optional(),
|
package/dist/main.js
CHANGED
|
@@ -1,61 +1,43 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { loadConfig } from "./config.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { ToolRegistry, registerBuiltinTools } from "./tools/registry.js";
|
|
7
|
-
import { CommandRegistry, registerBuiltinCommands } from "./cmds/registry.js";
|
|
8
|
-
import { tryLoadSkills } from "./skills/loader.js";
|
|
9
|
-
import { tryReadFileText, readFirstFileContent } from "./util/fs.js";
|
|
10
|
-
import { MCPServers } from "./mcp/server.js";
|
|
4
|
+
import { tryLoadSkills, tryReadFileText, startSession, } from "@vietor/easy-agent-core";
|
|
5
|
+
import { builtinCommands } from "./cmds/builtin.js";
|
|
11
6
|
import { startApp } from "./tui/App.js";
|
|
12
7
|
const SYSTEM_PROMPT_BASE = [
|
|
13
|
-
"You are Easy Agent, an autonomous
|
|
14
|
-
`Environment:
|
|
15
|
-
- Platform: ${process.platform}
|
|
16
|
-
- Working directory: ${process.cwd()}`,
|
|
17
|
-
[
|
|
18
|
-
"Tool use:",
|
|
19
|
-
"- Prefer dedicated tools (FileRead, FileWrite, FileEdit, Glob, Grep, WebFetch) over the Shell tool when they fit the task.",
|
|
20
|
-
"- Read a file before editing it; make minimal, surgical changes that match the surrounding code style.",
|
|
21
|
-
"- Reference code as file_path:line_number.",
|
|
22
|
-
"- When a decision belongs to the user, call AskUser and wait for the answer rather than listing options in prose. Ask when there are multiple reasonable approaches, an irreversible or consequential action, or the request is ambiguous; when you have enough to proceed, act without asking.",
|
|
23
|
-
...(process.platform === "linux"
|
|
24
|
-
? ["- For privileged shell commands, use `sudo -n` (non-interactive); if it reports a password is required, do not retry - surface the command for the user to run manually."]
|
|
25
|
-
: []),
|
|
26
|
-
].join("\n"),
|
|
8
|
+
"You are Easy Agent, an autonomous assistant. You complete tasks by calling tools, inspecting their results, and iterating until the work is done.",
|
|
27
9
|
`Output:
|
|
28
10
|
- Be concise and use GitHub-flavored markdown.
|
|
29
11
|
- State what you did and stop once the task is complete; report outcomes faithfully.
|
|
30
|
-
-
|
|
12
|
+
- Reference code as file_path:line_number.`,
|
|
13
|
+
`Environment:
|
|
14
|
+
- Platform: ${process.platform}
|
|
15
|
+
- Working directory: ${process.cwd()}`,
|
|
16
|
+
`Decision making:
|
|
17
|
+
- When a decision belongs to the user, call AskUser and wait for the answer rather than listing options in prose. Ask when there are multiple reasonable approaches, an irreversible or consequential action, or the request is ambiguous; when you have enough to proceed, act without asking.`,
|
|
31
18
|
].join("\n\n");
|
|
32
19
|
export async function main() {
|
|
33
20
|
const config = loadConfig();
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
mcp
|
|
41
|
-
.connect(config.mcpServers)
|
|
42
|
-
.catch((e) => mcp.report(`MCP connect failed: ${e.message}`));
|
|
43
|
-
const globalSkills = readFirstFileContent([join(homedir(), ".agents", "skills"), join(homedir(), ".claude", "skills")], tryLoadSkills);
|
|
44
|
-
if (globalSkills) {
|
|
45
|
-
globalSkills.forEach((skill) => commands.register({
|
|
46
|
-
name: skill.name,
|
|
47
|
-
description: skill.description ?? skill.name,
|
|
48
|
-
execute: async (_, host) => {
|
|
49
|
-
await host.runSkill(skill);
|
|
50
|
-
},
|
|
51
|
-
}));
|
|
52
|
-
}
|
|
53
|
-
const globalPrompt = readFirstFileContent([join(homedir(), ".agents", "AGENTS.md"), join(homedir(), ".claude", "CLAUDE.md")], tryReadFileText);
|
|
54
|
-
const projectPrompt = readFirstFileContent([join(process.cwd(), "AGENTS.md"), join(process.cwd(), "CLAUDE.md")], tryReadFileText);
|
|
21
|
+
const globalSkills = tryLoadSkills(join(homedir(), ".agents", "skills"))
|
|
22
|
+
?? tryLoadSkills(join(homedir(), ".claude", "skills"));
|
|
23
|
+
const globalPrompt = tryReadFileText(join(homedir(), ".agents", "AGENTS.md"))
|
|
24
|
+
?? tryReadFileText(join(homedir(), ".claude", "CLAUDE.md"));
|
|
25
|
+
const projectPrompt = tryReadFileText(join(process.cwd(), "AGENTS.md"))
|
|
26
|
+
?? tryReadFileText(join(process.cwd(), "CLAUDE.md"));
|
|
55
27
|
const systemPrompt = [SYSTEM_PROMPT_BASE, globalPrompt, projectPrompt]
|
|
56
28
|
.filter(Boolean)
|
|
57
29
|
.join("\n\n=================\n\n");
|
|
58
|
-
const session =
|
|
30
|
+
const session = await startSession({
|
|
31
|
+
systemPrompt,
|
|
32
|
+
llmConfig: config.llm,
|
|
33
|
+
mcpServers: config.mcpServers,
|
|
34
|
+
skills: globalSkills,
|
|
35
|
+
commands: builtinCommands,
|
|
36
|
+
builtinTools: {
|
|
37
|
+
askUser: true,
|
|
38
|
+
todoWrite: true
|
|
39
|
+
}
|
|
40
|
+
});
|
|
59
41
|
const app = startApp(session);
|
|
60
42
|
await app.waitUntilExit().finally(() => session.dispose());
|
|
61
43
|
}
|
package/dist/tui/App.js
CHANGED
|
@@ -2,33 +2,30 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
3
|
import { Box, render, Text, useApp, useInput } from "ink";
|
|
4
4
|
import { Markdown } from "./components/Markdown.js";
|
|
5
|
-
import {
|
|
5
|
+
import { LogList } from "./LogList.js";
|
|
6
|
+
import { TodoView } from "./TodoView.js";
|
|
6
7
|
import { AppHeader } from "./AppHeader.js";
|
|
7
8
|
import { PromptOrCommandInput } from "./PromptOrCommandInput.js";
|
|
8
9
|
import { QuestionView } from "./QuestionView.js";
|
|
9
10
|
import { Spinner } from "./Spinner.js";
|
|
10
11
|
import { compactDisplay } from "../util/format.js";
|
|
11
|
-
const STREAM_FRAME_MS =
|
|
12
|
+
const STREAM_FRAME_MS = 120;
|
|
12
13
|
export function App({ session }) {
|
|
13
14
|
const { exit } = useApp();
|
|
14
|
-
useSyncExternalStore(session.subscribe, session.getSnapshot);
|
|
15
|
-
const [
|
|
16
|
-
const [
|
|
17
|
-
const [usage, setUsage] = useState({ prompt: 0, completion: 0 });
|
|
18
|
-
const [, setTick] = useState(0);
|
|
15
|
+
const version = useSyncExternalStore(session.subscribe, session.getSnapshot);
|
|
16
|
+
const [runState, setRunState] = useState({ running: false, elapsed: 0, promptTokens: 0, completionTokens: 0 });
|
|
17
|
+
const [streamingText, setStreamingText] = useState("");
|
|
19
18
|
const streamingRef = useRef("");
|
|
20
19
|
const renderTimerRef = useRef(undefined);
|
|
21
20
|
const allCmds = useMemo(() => session.commandSchemas, [session]);
|
|
22
|
-
const pendingQuestion = session.logEntries.find((e) => e.kind === "question" && e.answer === null);
|
|
21
|
+
const pendingQuestion = useMemo(() => session.logEntries.find((e) => e.kind === "question" && e.answer === null), [session, version]);
|
|
23
22
|
useEffect(() => {
|
|
24
23
|
session.setCallbacks({
|
|
25
24
|
onStreaming: (text) => {
|
|
26
25
|
streamingRef.current = text;
|
|
27
26
|
scheduleStreamingRender();
|
|
28
27
|
},
|
|
29
|
-
onRunStateChange:
|
|
30
|
-
onElapsedChange: (s) => setElapsed(s),
|
|
31
|
-
onUsageChange: (p, c) => setUsage({ prompt: p, completion: c }),
|
|
28
|
+
onRunStateChange: setRunState,
|
|
32
29
|
});
|
|
33
30
|
}, []);
|
|
34
31
|
const scheduleStreamingRender = () => {
|
|
@@ -36,7 +33,7 @@ export function App({ session }) {
|
|
|
36
33
|
return;
|
|
37
34
|
renderTimerRef.current = setTimeout(() => {
|
|
38
35
|
renderTimerRef.current = undefined;
|
|
39
|
-
|
|
36
|
+
setStreamingText(streamingRef.current);
|
|
40
37
|
}, STREAM_FRAME_MS);
|
|
41
38
|
};
|
|
42
39
|
useInput((_input, key) => {
|
|
@@ -49,26 +46,40 @@ export function App({ session }) {
|
|
|
49
46
|
session.abort();
|
|
50
47
|
}
|
|
51
48
|
else if (key.ctrl && _input === "c") {
|
|
52
|
-
if (running)
|
|
49
|
+
if (runState.running)
|
|
53
50
|
session.abort();
|
|
54
51
|
else
|
|
55
52
|
exit();
|
|
56
53
|
}
|
|
57
54
|
});
|
|
58
|
-
async function handleCommand(name) {
|
|
59
|
-
await session.executeCommand(name,
|
|
55
|
+
async function handleCommand(name, args) {
|
|
56
|
+
await session.executeCommand(name, args);
|
|
57
|
+
if (session.local.get("exitRequested"))
|
|
58
|
+
exit();
|
|
60
59
|
}
|
|
61
60
|
async function handlePrompt(text) {
|
|
62
61
|
if (session.isCommand(text)) {
|
|
63
|
-
await handleCommand(text);
|
|
62
|
+
await handleCommand(text, "");
|
|
64
63
|
}
|
|
65
64
|
else {
|
|
66
65
|
await session.startPrompt(text);
|
|
67
66
|
}
|
|
68
67
|
}
|
|
69
|
-
|
|
68
|
+
let runningView = null;
|
|
69
|
+
if (runState.running) {
|
|
70
|
+
if (pendingQuestion) {
|
|
71
|
+
runningView = (_jsx(QuestionView, { question: pendingQuestion, onAnswer: (ans) => session.submitAnswer(pendingQuestion.id, ans) }));
|
|
72
|
+
}
|
|
73
|
+
else if (streamingText) {
|
|
74
|
+
runningView = (_jsx(Box, { marginTop: 1, paddingLeft: 1, paddingRight: 1, children: _jsx(Markdown, { color: "green", children: streamingText }) }));
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
runningView = (_jsx(Box, { marginTop: 1, paddingLeft: 1, children: _jsx(Spinner, { label: "thinking", elapsed: runState.elapsed, promptTokens: runState.promptTokens, completionTokens: runState.completionTokens }) }));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return (_jsxs(Box, { flexDirection: "column", minWidth: 80, children: [_jsx(AppHeader, {}), _jsx(LogList, { session: session }), session.todos.length > 0 ? _jsx(TodoView, { todos: session.todos }) : null, runningView, !runState.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] }));
|
|
70
81
|
}
|
|
71
82
|
export function startApp(session) {
|
|
72
83
|
process.stdout.write("[2J[H");
|
|
73
|
-
return render(_jsx(App, { session: session }), { exitOnCtrlC: false });
|
|
84
|
+
return render(_jsx(App, { session: session }), { exitOnCtrlC: false, incrementalRendering: true });
|
|
74
85
|
}
|
package/dist/tui/AppHeader.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo } from "react";
|
|
2
3
|
import { Box, Text } from "ink";
|
|
3
4
|
import { getPackageInfo } from "../util/package.js";
|
|
4
|
-
export function AppHeader() {
|
|
5
|
+
export const AppHeader = memo(function AppHeader() {
|
|
5
6
|
const pkginfo = getPackageInfo();
|
|
6
|
-
return (_jsxs(Box, { flexDirection: "column",
|
|
7
|
-
}
|
|
7
|
+
return (_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() })] }));
|
|
8
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useSyncExternalStore } from "react";
|
|
3
|
+
import { Box } from "ink";
|
|
4
|
+
import { LogView } from "./LogView.js";
|
|
5
|
+
export const LogList = memo(function LogList({ session }) {
|
|
6
|
+
useSyncExternalStore(session.subscribe, session.getSnapshot);
|
|
7
|
+
const entries = session.logEntries;
|
|
8
|
+
return (_jsx(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: entries.map((entry, i) => (_jsx(LogView, { entry: entry }, i))) }));
|
|
9
|
+
});
|
|
@@ -6,7 +6,7 @@ const MAX_ITEMS = 4;
|
|
|
6
6
|
export function PromptOrCommandInput({ commands, onCommand, onPrompt }) {
|
|
7
7
|
const [input, setInput] = useState("");
|
|
8
8
|
const showMenu = input.startsWith("/");
|
|
9
|
-
const prefix = showMenu ? input.slice(1) : "";
|
|
9
|
+
const prefix = showMenu ? input.slice(1).split(/\s+/)[0] : "";
|
|
10
10
|
const filtered = useMemo(() => {
|
|
11
11
|
if (!showMenu)
|
|
12
12
|
return [];
|
|
@@ -16,10 +16,7 @@ export function PromptOrCommandInput({ commands, onCommand, onPrompt }) {
|
|
|
16
16
|
}, [showMenu, prefix, commands]);
|
|
17
17
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
18
18
|
useEffect(() => {
|
|
19
|
-
|
|
20
|
-
setSelectedIndex(0);
|
|
21
|
-
else if (selectedIndex >= filtered.length)
|
|
22
|
-
setSelectedIndex(Math.max(0, filtered.length - 1));
|
|
19
|
+
setSelectedIndex((prev) => Math.max(0, Math.min(prev, Math.max(0, filtered.length - 1))));
|
|
23
20
|
}, [filtered.length]);
|
|
24
21
|
useInput((_input, key) => {
|
|
25
22
|
if (key.upArrow) {
|
|
@@ -48,9 +45,14 @@ export function PromptOrCommandInput({ commands, onCommand, onPrompt }) {
|
|
|
48
45
|
if (!text)
|
|
49
46
|
return;
|
|
50
47
|
if (text.startsWith("/")) {
|
|
48
|
+
const [name, ...rest] = text.slice(1).split(/\s+/);
|
|
49
|
+
const args = rest.join(" ");
|
|
51
50
|
if (filtered.length > 0) {
|
|
52
51
|
const cmd = filtered[selectedIndex];
|
|
53
|
-
onCommand(cmd ? cmd.name :
|
|
52
|
+
onCommand(cmd ? cmd.name : name, args);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
onCommand(name, args);
|
|
54
56
|
}
|
|
55
57
|
return;
|
|
56
58
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo } from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
const ICONS = {
|
|
5
|
+
pending: "○",
|
|
6
|
+
in_progress: "◐",
|
|
7
|
+
completed: "✓",
|
|
8
|
+
};
|
|
9
|
+
const COLORS = {
|
|
10
|
+
pending: "gray",
|
|
11
|
+
in_progress: "yellow",
|
|
12
|
+
completed: "green",
|
|
13
|
+
};
|
|
14
|
+
export const TodoView = memo(function TodoView({ todos }) {
|
|
15
|
+
if (todos.length === 0)
|
|
16
|
+
return null;
|
|
17
|
+
const done = todos.filter((t) => t.status === "completed").length;
|
|
18
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 1, paddingRight: 1, children: [_jsx(Text, { dimColor: true, children: `Tasks [${done}/${todos.length}]` }), todos.map((t, i) => (_jsx(Text, { color: COLORS[t.status], strikethrough: t.status === "completed", children: `${ICONS[t.status]} ${t.content}` }, i)))] }));
|
|
19
|
+
});
|
package/dist/util/package.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
const __dirname = import.meta.dirname;
|
|
4
4
|
const MAX_PARENT_TRAVERSAL = 10;
|
|
@@ -7,9 +7,8 @@ function findPackageJson() {
|
|
|
7
7
|
let current = __dirname;
|
|
8
8
|
for (let i = 0; i < MAX_PARENT_TRAVERSAL; i++) {
|
|
9
9
|
const pkgPath = join(current, "package.json");
|
|
10
|
-
if (existsSync(pkgPath))
|
|
10
|
+
if (existsSync(pkgPath))
|
|
11
11
|
return pkgPath;
|
|
12
|
-
}
|
|
13
12
|
const parent = dirname(current);
|
|
14
13
|
if (parent === current)
|
|
15
14
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vietor/easy-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,40 +9,37 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"dist"
|
|
11
11
|
],
|
|
12
|
-
"
|
|
13
|
-
"build": "tsc",
|
|
14
|
-
"start": "tsc && node dist/cli.js",
|
|
15
|
-
"dev": "tsx src/cli.ts",
|
|
16
|
-
"prepublishOnly": "tsc"
|
|
17
|
-
},
|
|
18
|
-
"keywords": [
|
|
19
|
-
"ai",
|
|
20
|
-
"cli"
|
|
21
|
-
],
|
|
22
|
-
"author": "",
|
|
12
|
+
"description": "Terminal-based AI agent CLI with conversational TUI (Ink/React) powered by easy-agent-core",
|
|
23
13
|
"license": "MIT",
|
|
24
|
-
"
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/vietor/easy-agent.git"
|
|
17
|
+
},
|
|
25
18
|
"dependencies": {
|
|
26
|
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
27
|
-
"@vscode/ripgrep": "^1.18.0",
|
|
28
|
-
"htmlparser2": "^12.0.0",
|
|
29
19
|
"ink": "^7.1.0",
|
|
30
20
|
"ink-text-input": "^6.0.0",
|
|
31
21
|
"marked": "^18.0.5",
|
|
32
|
-
"openai": "^6.45.0",
|
|
33
22
|
"react": "^19.2.7",
|
|
34
23
|
"string-width": "^8.2.1",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
24
|
+
"zod": "^4.4.3",
|
|
25
|
+
"@vietor/easy-agent-core": "0.4.2"
|
|
37
26
|
},
|
|
38
27
|
"devDependencies": {
|
|
39
28
|
"@types/node": "^22.0.0",
|
|
40
29
|
"@types/react": "^19.2.17",
|
|
41
|
-
"@types/turndown": "^5.0.6",
|
|
42
30
|
"tsx": "^4.23.0",
|
|
43
31
|
"typescript": "^6.0.3"
|
|
44
32
|
},
|
|
45
33
|
"engines": {
|
|
46
34
|
"node": ">=22.0.0"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"ai",
|
|
38
|
+
"cli"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc",
|
|
42
|
+
"dev": "tsx src/cli.ts",
|
|
43
|
+
"start": "node dist/cli.js"
|
|
47
44
|
}
|
|
48
|
-
}
|
|
45
|
+
}
|
package/dist/cmds/registry.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { exitCommand, clearCommand, mcpCommand, compactCommand, exportCommand } from "./builtin.js";
|
|
2
|
-
export class CommandRegistry {
|
|
3
|
-
commands = new Map();
|
|
4
|
-
register(command) {
|
|
5
|
-
this.commands.set(command.name, command);
|
|
6
|
-
}
|
|
7
|
-
schemas() {
|
|
8
|
-
return [...this.commands.values()].map((t) => ({
|
|
9
|
-
name: t.name,
|
|
10
|
-
description: t.description,
|
|
11
|
-
}));
|
|
12
|
-
}
|
|
13
|
-
exists(name) {
|
|
14
|
-
return this.commands.has(name);
|
|
15
|
-
}
|
|
16
|
-
async execute(command, ctx, host) {
|
|
17
|
-
const cmd = this.commands.get(command);
|
|
18
|
-
if (!cmd) {
|
|
19
|
-
host.error(`unknown command: /${command}`);
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
await cmd.execute(ctx, host);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
export function registerBuiltinCommands(commands) {
|
|
26
|
-
commands.register(exitCommand);
|
|
27
|
-
commands.register({ ...exitCommand, name: "quit" });
|
|
28
|
-
for (const t of [clearCommand, mcpCommand, compactCommand, exportCommand])
|
|
29
|
-
commands.register(t);
|
|
30
|
-
}
|
package/dist/cmds/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|