@vietor/easy-agent 0.4.3 → 0.4.4
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 +63 -4
- package/dist/tui/App.js +23 -13
- package/dist/tui/AppHeader.js +3 -2
- 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 +2 -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,9 +1,11 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { homedir } from "node:os";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { loadConfig } from "./config.js";
|
|
4
5
|
import { tryLoadSkills, tryReadFileText, createSession, } from "@vietor/easy-agent-core";
|
|
5
6
|
import { builtinCommands } from "./cmds/builtin.js";
|
|
6
7
|
import { startApp } from "./tui/App.js";
|
|
8
|
+
import { FileSessionPersistence } from "./util/sessionStore.js";
|
|
7
9
|
const SYSTEM_PROMPT_BASE = [
|
|
8
10
|
"You are Easy Agent, an autonomous assistant. You complete tasks by calling tools, inspecting their results, and iterating until the work is done.",
|
|
9
11
|
`Output:
|
|
@@ -16,11 +18,64 @@ const SYSTEM_PROMPT_BASE = [
|
|
|
16
18
|
`Decision making:
|
|
17
19
|
- 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
20
|
].join("\n\n");
|
|
19
|
-
|
|
21
|
+
function parseArgs(argv) {
|
|
22
|
+
let mode = "new";
|
|
23
|
+
let id;
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const a = argv[i];
|
|
26
|
+
if (a === "-c" || a === "--continue") {
|
|
27
|
+
mode = "continue";
|
|
28
|
+
}
|
|
29
|
+
else if (a === "-r" || a === "--resume") {
|
|
30
|
+
mode = "resume";
|
|
31
|
+
const next = argv[i + 1];
|
|
32
|
+
if (next && !next.startsWith("-")) {
|
|
33
|
+
id = next;
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { mode, id };
|
|
39
|
+
}
|
|
40
|
+
async function listSessions(store) {
|
|
41
|
+
const sessions = await store.listSessions();
|
|
42
|
+
if (!sessions.length) {
|
|
43
|
+
console.log("No previous sessions found in this directory.");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
console.log("Previous sessions (most recent first):");
|
|
47
|
+
for (const s of sessions) {
|
|
48
|
+
const title = s.title ? ` ${s.title}` : "";
|
|
49
|
+
console.log(` ${s.id} ${new Date(s.updatedAt).toLocaleString()}${title}`);
|
|
50
|
+
}
|
|
51
|
+
console.log("\nResume with: easy-agent --resume <id>");
|
|
52
|
+
}
|
|
53
|
+
export async function main(argv = []) {
|
|
54
|
+
const { mode, id } = parseArgs(argv);
|
|
55
|
+
const store = new FileSessionPersistence(process.cwd());
|
|
56
|
+
if (mode === "resume" && !id) {
|
|
57
|
+
await listSessions(store);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
20
60
|
const config = loadConfig();
|
|
21
|
-
|
|
61
|
+
let sessionId;
|
|
62
|
+
let resume = false;
|
|
63
|
+
if (mode === "continue") {
|
|
64
|
+
const sessions = await store.listSessions();
|
|
65
|
+
if (sessions.length) {
|
|
66
|
+
sessionId = sessions[0].id;
|
|
67
|
+
resume = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else if (mode === "resume" && id) {
|
|
71
|
+
sessionId = id;
|
|
72
|
+
resume = true;
|
|
73
|
+
}
|
|
74
|
+
if (!sessionId)
|
|
75
|
+
sessionId = randomUUID();
|
|
76
|
+
const globalSkills = tryLoadSkills(join(homedir(), ".easy-agent", "skills"))
|
|
22
77
|
?? tryLoadSkills(join(homedir(), ".claude", "skills"));
|
|
23
|
-
const globalPrompt = tryReadFileText(join(homedir(), ".
|
|
78
|
+
const globalPrompt = tryReadFileText(join(homedir(), ".easy-agent", "AGENTS.md"))
|
|
24
79
|
?? tryReadFileText(join(homedir(), ".claude", "CLAUDE.md"));
|
|
25
80
|
const projectPrompt = tryReadFileText(join(process.cwd(), "AGENTS.md"))
|
|
26
81
|
?? tryReadFileText(join(process.cwd(), "CLAUDE.md"));
|
|
@@ -36,8 +91,12 @@ export async function main() {
|
|
|
36
91
|
builtinTools: {
|
|
37
92
|
askUser: true,
|
|
38
93
|
todoWrite: true
|
|
39
|
-
}
|
|
94
|
+
},
|
|
95
|
+
sessionId,
|
|
96
|
+
persistence: store,
|
|
40
97
|
});
|
|
98
|
+
if (resume)
|
|
99
|
+
await session.restore();
|
|
41
100
|
const app = startApp(session);
|
|
42
101
|
await app.waitUntilExit().finally(() => session.dispose());
|
|
43
102
|
}
|
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, {}), _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
5
|
export const AppHeader = memo(function AppHeader() {
|
|
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: process.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.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"react": "^19.2.7",
|
|
23
23
|
"string-width": "^8.2.1",
|
|
24
24
|
"zod": "^4.4.3",
|
|
25
|
-
"@vietor/easy-agent-core": "0.4.
|
|
25
|
+
"@vietor/easy-agent-core": "0.4.4"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.0.0",
|