@vietor/easy-agent 0.4.3 → 0.4.5
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 +9 -4
- package/dist/cli.js +1 -1
- package/dist/main.js +75 -19
- package/dist/tui/App.js +23 -13
- package/dist/tui/AppHeader.js +4 -3
- package/dist/tui/PromptOrCommandInput.js +1 -1
- package/dist/tui/StatusBar.js +8 -0
- package/dist/tui/{LogList.js → TimelineList.js} +4 -4
- package/dist/tui/{LogView.js → TimelineView.js} +3 -3
- package/dist/util/sessionStore.js +104 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -73,17 +73,17 @@ A stdio server omits `type` (or sets `"stdio"`); only `command` is required, `ar
|
|
|
73
73
|
|
|
74
74
|
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:
|
|
75
75
|
|
|
76
|
-
1. **Global** — your home directory, applied to every conversation. Checks `~/.
|
|
76
|
+
1. **Global** — your home directory, applied to every conversation. Checks `~/.easy-agent/AGENTS.md` first, then `~/.claude/CLAUDE.md`; uses the first one found.
|
|
77
77
|
2. **Project** — your current working directory, applied per-project. Checks `./AGENTS.md` first, then `./CLAUDE.md`; uses the first one found.
|
|
78
78
|
|
|
79
79
|
Both global and project files are loaded and concatenated into the system prompt if they exist.
|
|
80
80
|
|
|
81
81
|
### Skills (optional)
|
|
82
82
|
|
|
83
|
-
Skills are reusable prompts that register themselves as slash commands. Create a subdirectory for each skill under `~/.
|
|
83
|
+
Skills are reusable prompts that register themselves as slash commands. Create a subdirectory for each skill under `~/.easy-agent/skills/` (or `~/.claude/skills/`) with a `SKILL.md` file:
|
|
84
84
|
|
|
85
85
|
```
|
|
86
|
-
~/.
|
|
86
|
+
~/.easy-agent/skills/
|
|
87
87
|
deploy/
|
|
88
88
|
SKILL.md
|
|
89
89
|
review/
|
|
@@ -108,11 +108,16 @@ Only `name` is required; if omitted the directory name is used. The body is the
|
|
|
108
108
|
Launch the TUI:
|
|
109
109
|
|
|
110
110
|
```bash
|
|
111
|
-
easy-agent
|
|
111
|
+
easy-agent # start a new session
|
|
112
|
+
easy-agent --continue # resume the most recent session
|
|
113
|
+
easy-agent --resume <id> # resume a specific session by ID
|
|
114
|
+
easy-agent --resume # list all saved sessions for this directory
|
|
112
115
|
```
|
|
113
116
|
|
|
114
117
|
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).
|
|
115
118
|
|
|
119
|
+
The TUI automatically adapts to your terminal width. A status bar at the bottom shows the current context token count, with hints for keyboard shortcuts (<kbd>ESC</kbd> to abort a running task, <kbd>/</kbd> for commands).
|
|
120
|
+
|
|
116
121
|
### Built-in tools
|
|
117
122
|
|
|
118
123
|
- **Shell** — run shell commands using this platform's native syntax.
|
package/dist/cli.js
CHANGED
package/dist/main.js
CHANGED
|
@@ -1,30 +1,78 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { homedir } from "node:os";
|
|
2
3
|
import { join } from "node:path";
|
|
4
|
+
import { Command } from "commander";
|
|
3
5
|
import { loadConfig } from "./config.js";
|
|
4
|
-
import { tryLoadSkills, tryReadFileText, createSession
|
|
6
|
+
import { tryLoadSkills, tryReadFileText, createSession } from "@vietor/easy-agent-core";
|
|
5
7
|
import { builtinCommands } from "./cmds/builtin.js";
|
|
6
8
|
import { startApp } from "./tui/App.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
import { getPackageInfo } from "./util/package.js";
|
|
10
|
+
import { FileSessionPersistence } from "./util/sessionStore.js";
|
|
11
|
+
function buildSystemPromptBase(cwd) {
|
|
12
|
+
return [
|
|
13
|
+
"You are Easy Agent, an autonomous assistant. You complete tasks by calling tools, inspecting their results, and iterating until the work is done.",
|
|
14
|
+
`Output:
|
|
10
15
|
- Be concise and use GitHub-flavored markdown.
|
|
11
16
|
- State what you did and stop once the task is complete; report outcomes faithfully.
|
|
12
17
|
- Reference code as file_path:line_number.`,
|
|
13
|
-
|
|
18
|
+
`Environment:
|
|
14
19
|
- Platform: ${process.platform}
|
|
15
|
-
- Working directory: ${
|
|
16
|
-
|
|
20
|
+
- Working directory: ${cwd}`,
|
|
21
|
+
`Decision making:
|
|
17
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.`,
|
|
18
|
-
].join("\n\n");
|
|
19
|
-
|
|
23
|
+
].join("\n\n");
|
|
24
|
+
}
|
|
25
|
+
async function listSessions(store) {
|
|
26
|
+
const sessions = await store.listSessions();
|
|
27
|
+
if (!sessions.length) {
|
|
28
|
+
console.log("No previous sessions found in this directory.");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.log("Previous sessions (most recent first):");
|
|
32
|
+
for (const s of sessions) {
|
|
33
|
+
const title = s.title ? ` ${s.title}` : "";
|
|
34
|
+
console.log(` ${s.id} ${new Date(s.updatedAt).toLocaleString()}${title}`);
|
|
35
|
+
}
|
|
36
|
+
console.log("\nResume with: easy-agent --resume <id>");
|
|
37
|
+
}
|
|
38
|
+
export async function main(argv = []) {
|
|
39
|
+
const pkg = getPackageInfo();
|
|
40
|
+
const program = new Command();
|
|
41
|
+
program
|
|
42
|
+
.name("easy-agent")
|
|
43
|
+
.version(pkg.version)
|
|
44
|
+
.description("Terminal-based AI agent CLI with conversational TUI")
|
|
45
|
+
.option("-c, --continue", "Continue the most recent session")
|
|
46
|
+
.option("-r, --resume [id]", "Resume a session by ID (omit to list sessions)")
|
|
47
|
+
.parse(argv, { from: "user" });
|
|
48
|
+
const opts = program.opts();
|
|
49
|
+
const cwd = process.cwd();
|
|
50
|
+
const store = new FileSessionPersistence(cwd);
|
|
51
|
+
if (opts.resume !== undefined && typeof opts.resume !== "string") {
|
|
52
|
+
await listSessions(store);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
20
55
|
const config = loadConfig();
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
56
|
+
let sessionId;
|
|
57
|
+
let resume = false;
|
|
58
|
+
if (opts.continue) {
|
|
59
|
+
const sessions = await store.listSessions();
|
|
60
|
+
if (sessions.length) {
|
|
61
|
+
sessionId = sessions[0].id;
|
|
62
|
+
resume = true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (opts.resume && typeof opts.resume === "string") {
|
|
66
|
+
sessionId = opts.resume;
|
|
67
|
+
resume = true;
|
|
68
|
+
}
|
|
69
|
+
if (!sessionId)
|
|
70
|
+
sessionId = randomUUID();
|
|
71
|
+
const globalSkills = tryLoadSkills(join(homedir(), ".easy-agent", "skills")) ?? tryLoadSkills(join(homedir(), ".claude", "skills"));
|
|
72
|
+
const globalPrompt = tryReadFileText(join(homedir(), ".easy-agent", "AGENTS.md")) ??
|
|
73
|
+
tryReadFileText(join(homedir(), ".claude", "CLAUDE.md"));
|
|
74
|
+
const projectPrompt = tryReadFileText(join(cwd, "AGENTS.md")) ?? tryReadFileText(join(cwd, "CLAUDE.md"));
|
|
75
|
+
const systemPrompt = [buildSystemPromptBase(cwd), globalPrompt, projectPrompt]
|
|
28
76
|
.filter(Boolean)
|
|
29
77
|
.join("\n\n=================\n\n");
|
|
30
78
|
const session = await createSession({
|
|
@@ -35,9 +83,17 @@ export async function main() {
|
|
|
35
83
|
commands: builtinCommands,
|
|
36
84
|
builtinTools: {
|
|
37
85
|
askUser: true,
|
|
38
|
-
todoWrite: true
|
|
39
|
-
}
|
|
86
|
+
todoWrite: true,
|
|
87
|
+
},
|
|
88
|
+
cwd: cwd,
|
|
89
|
+
sessionId,
|
|
90
|
+
persistence: store,
|
|
40
91
|
});
|
|
92
|
+
if (resume)
|
|
93
|
+
await session.restore();
|
|
41
94
|
const app = startApp(session);
|
|
42
|
-
await app.waitUntilExit().finally(() =>
|
|
95
|
+
await app.waitUntilExit().finally(() => {
|
|
96
|
+
session.dispose();
|
|
97
|
+
console.log(["Resume this session with:", `easy-agent --resume ${sessionId}`].join("\n"));
|
|
98
|
+
});
|
|
43
99
|
}
|
package/dist/tui/App.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
-
import { Box, render,
|
|
3
|
+
import { Box, render, useApp, useInput, useWindowSize } from "ink";
|
|
4
4
|
import { Markdown } from "./components/Markdown.js";
|
|
5
|
-
import {
|
|
5
|
+
import { TimelineList } from "./TimelineList.js";
|
|
6
6
|
import { TodoView } from "./TodoView.js";
|
|
7
7
|
import { AppHeader } from "./AppHeader.js";
|
|
8
8
|
import { PromptOrCommandInput } from "./PromptOrCommandInput.js";
|
|
9
9
|
import { QuestionView } from "./QuestionView.js";
|
|
10
10
|
import { Spinner } from "./Spinner.js";
|
|
11
|
-
import {
|
|
11
|
+
import { StatusBar } from "./StatusBar.js";
|
|
12
12
|
const STREAM_FRAME_MS = 120;
|
|
13
13
|
export function App({ session }) {
|
|
14
14
|
const { exit } = useApp();
|
|
15
|
+
const { columns } = useWindowSize();
|
|
15
16
|
const view = useSyncExternalStore(session.subscribe, session.getSnapshot);
|
|
16
17
|
const [runState, setRunState] = useState({ running: false, elapsed: 0, promptTokens: 0, completionTokens: 0 });
|
|
17
18
|
const [streamingText, setStreamingText] = useState("");
|
|
@@ -20,13 +21,22 @@ export function App({ session }) {
|
|
|
20
21
|
const allCmds = useMemo(() => session.commandSchemas, [session]);
|
|
21
22
|
const pendingQuestion = session.getPendingQuestion();
|
|
22
23
|
useEffect(() => {
|
|
23
|
-
session.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
const unsub = session.subscribeEvents((e) => {
|
|
25
|
+
switch (e.type) {
|
|
26
|
+
case "assistant_delta":
|
|
27
|
+
streamingRef.current += e.text;
|
|
28
|
+
scheduleStreamingRender();
|
|
29
|
+
break;
|
|
30
|
+
case "assistant":
|
|
31
|
+
streamingRef.current = "";
|
|
32
|
+
setStreamingText("");
|
|
33
|
+
break;
|
|
34
|
+
case "state":
|
|
35
|
+
setRunState(e);
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
29
38
|
});
|
|
39
|
+
return unsub;
|
|
30
40
|
}, []);
|
|
31
41
|
const scheduleStreamingRender = () => {
|
|
32
42
|
if (renderTimerRef.current)
|
|
@@ -54,7 +64,7 @@ export function App({ session }) {
|
|
|
54
64
|
});
|
|
55
65
|
async function handleCommand(name, args) {
|
|
56
66
|
await session.executeCommand(name, args);
|
|
57
|
-
if (session.localStore.get("exitRequested"))
|
|
67
|
+
if (session.localStore.get("exitRequested") != null)
|
|
58
68
|
exit();
|
|
59
69
|
}
|
|
60
70
|
async function handlePrompt(text) {
|
|
@@ -73,13 +83,13 @@ export function App({ session }) {
|
|
|
73
83
|
runningView = (_jsx(QuestionView, { question: pendingQuestion, onAnswer: (ans) => session.submitAnswer(pendingQuestion.id, ans) }));
|
|
74
84
|
}
|
|
75
85
|
else if (streamingText) {
|
|
76
|
-
runningView = (_jsx(Box, { marginTop: 1, paddingLeft: 1, paddingRight: 1, children: _jsx(Markdown, { color: "green", children: streamingText }) }));
|
|
86
|
+
runningView = (_jsx(Box, { marginTop: 1, paddingLeft: 1, paddingRight: 1, borderStyle: "single", borderTop: false, borderRight: false, borderBottom: false, borderColor: "gray", children: _jsx(Markdown, { color: "green", children: streamingText }) }));
|
|
77
87
|
}
|
|
78
88
|
else {
|
|
79
89
|
runningView = (_jsx(Box, { marginTop: 1, paddingLeft: 1, children: _jsx(Spinner, { label: "thinking", elapsed: runState.elapsed, promptTokens: runState.promptTokens, completionTokens: runState.completionTokens }) }));
|
|
80
90
|
}
|
|
81
91
|
}
|
|
82
|
-
return (_jsxs(Box, { flexDirection: "column",
|
|
92
|
+
return (_jsxs(Box, { width: columns, flexDirection: "column", children: [_jsx(AppHeader, { cwd: session.cwd }), _jsx(TimelineList, { session: session }), view.todos.length > 0 ? _jsx(TodoView, { todos: view.todos }) : null, runningView, !runState.running ? (_jsxs(_Fragment, { children: [_jsx(StatusBar, { contextTokens: session.contextTokens }), _jsx(PromptOrCommandInput, { commands: allCmds, onCommand: handleCommand, onPrompt: handlePrompt })] })) : null] }));
|
|
83
93
|
}
|
|
84
94
|
export function startApp(session) {
|
|
85
95
|
process.stdout.write("[2J[H");
|
package/dist/tui/AppHeader.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { memo } from "react";
|
|
3
|
-
import { Box, Text } from "ink";
|
|
3
|
+
import { Box, Text, useWindowSize } from "ink";
|
|
4
4
|
import { getPackageInfo } from "../util/package.js";
|
|
5
|
-
export const AppHeader = memo(function AppHeader() {
|
|
5
|
+
export const AppHeader = memo(function AppHeader({ cwd }) {
|
|
6
|
+
const { columns } = useWindowSize();
|
|
6
7
|
const pkginfo = getPackageInfo();
|
|
7
|
-
return (_jsxs(Box, {
|
|
8
|
+
return (_jsxs(Box, { width: columns, paddingX: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Easy Agent" }), _jsxs(Text, { dimColor: true, children: [" v", pkginfo.version] })] }), _jsx(Text, { dimColor: true, children: cwd })] }));
|
|
8
9
|
});
|
|
@@ -61,5 +61,5 @@ export function PromptOrCommandInput({ commands, onCommand, onPrompt }) {
|
|
|
61
61
|
return (_jsxs(Box, { flexDirection: "column", children: [showMenu && filtered.length > 0 ? (_jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", children: visibleItems.items.map((cmd, i) => {
|
|
62
62
|
const realIdx = visibleItems.start + i;
|
|
63
63
|
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));
|
|
64
|
-
}) })) : null, _jsxs(Box, { borderStyle: "single", borderLeft: false, borderRight: false, borderColor: "gray", children: [_jsx(Text, { color: "gray", children: "
|
|
64
|
+
}) })) : null, _jsxs(Box, { borderStyle: "single", borderLeft: false, borderRight: false, borderColor: "gray", children: [_jsx(Text, { color: "gray", children: "> " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit })] })] }));
|
|
65
65
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { memo } from "react";
|
|
3
|
+
import { Box, Text, useWindowSize } from "ink";
|
|
4
|
+
import { compactDisplay } from "../util/format.js";
|
|
5
|
+
export const StatusBar = memo(function StatusBar({ contextTokens }) {
|
|
6
|
+
const { columns } = useWindowSize();
|
|
7
|
+
return (_jsx(Box, { width: columns, paddingX: 1, flexDirection: "row", children: _jsxs(Text, { dimColor: true, children: ["Context: ", compactDisplay(contextTokens), " tokens \u00B7 ESC to stop \u00B7 / for commands"] }) }));
|
|
8
|
+
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { memo, useSyncExternalStore } from "react";
|
|
3
3
|
import { Box } from "ink";
|
|
4
|
-
import {
|
|
5
|
-
export const
|
|
4
|
+
import { TimelineView } from "./TimelineView.js";
|
|
5
|
+
export const TimelineList = memo(function TimelineList({ session }) {
|
|
6
6
|
const view = useSyncExternalStore(session.subscribe, session.getSnapshot);
|
|
7
|
-
const entries = view.
|
|
8
|
-
return (_jsx(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: entries.map((entry, i) => (_jsx(
|
|
7
|
+
const entries = view.timeline;
|
|
8
|
+
return (_jsx(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: entries.map((entry, i) => (_jsx(TimelineView, { entry: entry }, i))) }));
|
|
9
9
|
});
|
|
@@ -11,7 +11,7 @@ function preview(isError, text) {
|
|
|
11
11
|
const byteCount = Buffer.byteLength(text, "utf-8");
|
|
12
12
|
return `Result: ${byteCount} bytes, ${lineCount} lines`;
|
|
13
13
|
}
|
|
14
|
-
export const
|
|
14
|
+
export const TimelineView = memo(function Entry({ entry }) {
|
|
15
15
|
switch (entry.kind) {
|
|
16
16
|
case "user":
|
|
17
17
|
return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "\u276F " }), entry.text] }) }));
|
|
@@ -20,11 +20,11 @@ export const LogView = memo(function Entry({ entry }) {
|
|
|
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: [_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] }));
|
|
23
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, 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
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: _jsxs(Text, { color: "red", children: [_jsx(Text, { bold: true, children: "\u2717 " }), entry.text] }) }));
|
|
27
|
+
return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: [_jsx(Text, { bold: true, children: "\u2717 " }), entry.text] }) }));
|
|
28
28
|
case "interrupted":
|
|
29
29
|
return (_jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "\u25FC " }), _jsx(Text, { dimColor: true, children: "interrupted" })] }) }));
|
|
30
30
|
case "question":
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
function encodeCwd(cwd) {
|
|
5
|
+
return cwd.replace(/[\/\\:]/g, "-");
|
|
6
|
+
}
|
|
7
|
+
export class FileSessionPersistence {
|
|
8
|
+
cwd;
|
|
9
|
+
dir;
|
|
10
|
+
constructor(cwd) {
|
|
11
|
+
this.cwd = cwd;
|
|
12
|
+
this.dir = join(homedir(), ".easy-agent", "projects", encodeCwd(cwd));
|
|
13
|
+
}
|
|
14
|
+
file(sessionId) {
|
|
15
|
+
return join(this.dir, `${sessionId}.jsonl`);
|
|
16
|
+
}
|
|
17
|
+
ensureDir() {
|
|
18
|
+
if (!existsSync(this.dir))
|
|
19
|
+
mkdirSync(this.dir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
async load(sessionId) {
|
|
22
|
+
const path = this.file(sessionId);
|
|
23
|
+
if (!existsSync(path))
|
|
24
|
+
return null;
|
|
25
|
+
const messages = [];
|
|
26
|
+
let todos = [];
|
|
27
|
+
for (const line of readFileSync(path, "utf-8").split("\n")) {
|
|
28
|
+
if (!line.trim())
|
|
29
|
+
continue;
|
|
30
|
+
let rec;
|
|
31
|
+
try {
|
|
32
|
+
rec = JSON.parse(line);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const r = rec;
|
|
38
|
+
if (r.t === "m" && r.m)
|
|
39
|
+
messages.push(r.m);
|
|
40
|
+
else if (r.t === "todo" && r.todos)
|
|
41
|
+
todos = r.todos;
|
|
42
|
+
}
|
|
43
|
+
return { messages, todos };
|
|
44
|
+
}
|
|
45
|
+
async saveAll(sessionId, state) {
|
|
46
|
+
this.ensureDir();
|
|
47
|
+
const lines = state.messages
|
|
48
|
+
.map((m) => JSON.stringify({ t: "m", m }))
|
|
49
|
+
.concat([JSON.stringify({ t: "todo", todos: state.todos })]);
|
|
50
|
+
writeFileSync(this.file(sessionId), lines.join("\n") + "\n", "utf-8");
|
|
51
|
+
}
|
|
52
|
+
async listSessions() {
|
|
53
|
+
if (!existsSync(this.dir))
|
|
54
|
+
return [];
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const name of readdirSync(this.dir)) {
|
|
57
|
+
if (!name.endsWith(".jsonl"))
|
|
58
|
+
continue;
|
|
59
|
+
const id = name.slice(0, -6);
|
|
60
|
+
const path = join(this.dir, name);
|
|
61
|
+
try {
|
|
62
|
+
const stat = statSync(path);
|
|
63
|
+
out.push({
|
|
64
|
+
id,
|
|
65
|
+
title: this.readTitle(path),
|
|
66
|
+
createdAt: stat.birthtimeMs || stat.mtimeMs,
|
|
67
|
+
updatedAt: stat.mtimeMs,
|
|
68
|
+
cwd: this.cwd,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch { }
|
|
72
|
+
}
|
|
73
|
+
return out.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
74
|
+
}
|
|
75
|
+
async delete(sessionId) {
|
|
76
|
+
const path = this.file(sessionId);
|
|
77
|
+
if (existsSync(path))
|
|
78
|
+
unlinkSync(path);
|
|
79
|
+
}
|
|
80
|
+
readTitle(path) {
|
|
81
|
+
const first = this.readFirstUser(path);
|
|
82
|
+
if (!first)
|
|
83
|
+
return undefined;
|
|
84
|
+
const oneline = first.replace(/\s+/g, " ").trim();
|
|
85
|
+
return oneline.length > 60 ? oneline.slice(0, 60) + "…" : oneline;
|
|
86
|
+
}
|
|
87
|
+
readFirstUser(path) {
|
|
88
|
+
for (const line of readFileSync(path, "utf-8").split("\n")) {
|
|
89
|
+
if (!line.trim())
|
|
90
|
+
continue;
|
|
91
|
+
let rec;
|
|
92
|
+
try {
|
|
93
|
+
rec = JSON.parse(line);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const r = rec;
|
|
99
|
+
if (r.t === "m" && r.m && r.m.role === "user" && typeof r.m.content === "string")
|
|
100
|
+
return r.m.content;
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vietor/easy-agent",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -16,13 +16,14 @@
|
|
|
16
16
|
"url": "https://github.com/vietor/easy-agent.git"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
+
"commander": "^13.0.0",
|
|
19
20
|
"ink": "^7.1.0",
|
|
20
21
|
"ink-text-input": "^6.0.0",
|
|
21
22
|
"marked": "^18.0.5",
|
|
22
23
|
"react": "^19.2.7",
|
|
23
24
|
"string-width": "^8.2.1",
|
|
24
25
|
"zod": "^4.4.3",
|
|
25
|
-
"@vietor/easy-agent-core": "0.4.
|
|
26
|
+
"@vietor/easy-agent-core": "0.4.5"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
29
|
"@types/node": "^22.0.0",
|