@vietor/easy-agent 0.4.2 → 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 CHANGED
@@ -27,15 +27,25 @@ Create `~/.easy-agent.json` in your home directory:
27
27
  ```json
28
28
  {
29
29
  "llm": {
30
- "baseUrl": "https://api.openai.com/v1",
30
+ "baseUrl": "https://api.deepseek.com/v1",
31
31
  "apiKey": "your-api-key",
32
- "model": "gpt-4o"
32
+ "model": "deepseek-v4-flash"
33
33
  }
34
34
  }
35
35
  ```
36
36
 
37
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.
38
38
 
39
+ ### Proxy
40
+
41
+ The agent automatically routes HTTP requests through a proxy when the standard environment variables are set:
42
+
43
+ - `HTTPS_PROXY` / `https_proxy` — proxy URL for HTTPS (preferred)
44
+ - `HTTP_PROXY` / `http_proxy` — proxy URL for HTTP (fallback)
45
+ - `NO_PROXY` / `no_proxy` — comma-separated hosts/domains to bypass the proxy
46
+
47
+ No extra configuration is needed — just set the env vars before launching `easy-agent`.
48
+
39
49
  ### MCP servers (optional)
40
50
 
41
51
  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):
@@ -63,17 +73,17 @@ A stdio server omits `type` (or sets `"stdio"`); only `command` is required, `ar
63
73
 
64
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:
65
75
 
66
- 1. **Global** — your home directory, applied to every conversation. Checks `~/.agents/AGENTS.md` first, then `~/.claude/CLAUDE.md`; uses the first one found.
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.
67
77
  2. **Project** — your current working directory, applied per-project. Checks `./AGENTS.md` first, then `./CLAUDE.md`; uses the first one found.
68
78
 
69
79
  Both global and project files are loaded and concatenated into the system prompt if they exist.
70
80
 
71
81
  ### Skills (optional)
72
82
 
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:
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:
74
84
 
75
85
  ```
76
- ~/.claude/skills/
86
+ ~/.easy-agent/skills/
77
87
  deploy/
78
88
  SKILL.md
79
89
  review/
@@ -98,11 +108,16 @@ Only `name` is required; if omitted the directory name is used. The body is the
98
108
  Launch the TUI:
99
109
 
100
110
  ```bash
101
- 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
102
115
  ```
103
116
 
104
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).
105
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
+
106
121
  ### Built-in tools
107
122
 
108
123
  - **Shell** — run shell commands using this platform's native syntax.
@@ -128,7 +143,7 @@ Type a prompt and press Enter. The agent streams its reply and calls tools as ne
128
143
  ## Build from source
129
144
 
130
145
  ```bash
131
- git clone <repo-url>
146
+ git clone https://github.com/vietor/easy-agent.git
132
147
  cd easy-agent
133
148
  pnpm install
134
149
  pnpm build # build core → CLI
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { main } from "./main.js";
3
- main().catch((e) => {
3
+ main(process.argv.slice(2)).catch((e) => {
4
4
  console.error(e);
5
5
  process.exit(1);
6
6
  });
@@ -3,7 +3,7 @@ export const exitCommand = {
3
3
  name: "exit",
4
4
  description: "Exit the conversation",
5
5
  async execute(ctx) {
6
- ctx.session.local.set("exitRequested", true);
6
+ ctx.session.localStore.set("exitRequested", true);
7
7
  },
8
8
  };
9
9
  export const exportCommand = {
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
- import { tryLoadSkills, tryReadFileText, startSession, } from "@vietor/easy-agent-core";
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,18 +18,71 @@ 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
- export async function main() {
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
- const globalSkills = tryLoadSkills(join(homedir(), ".agents", "skills"))
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(), ".agents", "AGENTS.md"))
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"));
27
82
  const systemPrompt = [SYSTEM_PROMPT_BASE, globalPrompt, projectPrompt]
28
83
  .filter(Boolean)
29
84
  .join("\n\n=================\n\n");
30
- const session = await startSession({
85
+ const session = await createSession({
31
86
  systemPrompt,
32
87
  llmConfig: config.llm,
33
88
  mcpServers: config.mcpServers,
@@ -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,32 +1,42 @@
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, Text, useApp, useInput } from "ink";
3
+ import { Box, render, useApp, useInput, useWindowSize } from "ink";
4
4
  import { Markdown } from "./components/Markdown.js";
5
- import { LogList } from "./LogList.js";
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 { compactDisplay } from "../util/format.js";
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 version = useSyncExternalStore(session.subscribe, session.getSnapshot);
15
+ const { columns } = useWindowSize();
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("");
18
19
  const streamingRef = useRef("");
19
20
  const renderTimerRef = useRef(undefined);
20
21
  const allCmds = useMemo(() => session.commandSchemas, [session]);
21
- const pendingQuestion = useMemo(() => session.logEntries.find((e) => e.kind === "question" && e.answer === null), [session, version]);
22
+ const pendingQuestion = session.getPendingQuestion();
22
23
  useEffect(() => {
23
- session.setCallbacks({
24
- onStreaming: (text) => {
25
- streamingRef.current = text;
26
- scheduleStreamingRender();
27
- },
28
- onRunStateChange: setRunState,
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,12 +64,14 @@ export function App({ session }) {
54
64
  });
55
65
  async function handleCommand(name, args) {
56
66
  await session.executeCommand(name, args);
57
- if (session.local.get("exitRequested"))
67
+ if (session.localStore.get("exitRequested") != null)
58
68
  exit();
59
69
  }
60
70
  async function handlePrompt(text) {
61
- if (session.isCommand(text)) {
62
- await handleCommand(text, "");
71
+ const [first, ...rest] = text.split(/\s+/);
72
+ if (first.startsWith("/") || session.isCommand(first)) {
73
+ const name = first.startsWith("/") ? first.slice(1) : first;
74
+ await handleCommand(name, rest.join(" "));
63
75
  }
64
76
  else {
65
77
  await session.startPrompt(text);
@@ -71,13 +83,13 @@ export function App({ session }) {
71
83
  runningView = (_jsx(QuestionView, { question: pendingQuestion, onAnswer: (ans) => session.submitAnswer(pendingQuestion.id, ans) }));
72
84
  }
73
85
  else if (streamingText) {
74
- 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 }) }));
75
87
  }
76
88
  else {
77
89
  runningView = (_jsx(Box, { marginTop: 1, paddingLeft: 1, children: _jsx(Spinner, { label: "thinking", elapsed: runState.elapsed, promptTokens: runState.promptTokens, completionTokens: runState.completionTokens }) }));
78
90
  }
79
91
  }
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] }));
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] }));
81
93
  }
82
94
  export function startApp(session) {
83
95
  process.stdout.write("");
@@ -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, { 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
+ 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: "\u276F " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit })] })] }));
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
+ });
@@ -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 { TimelineView } from "./TimelineView.js";
5
+ export const TimelineList = memo(function TimelineList({ session }) {
6
+ const view = useSyncExternalStore(session.subscribe, session.getSnapshot);
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
+ });
@@ -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 LogView = memo(function Entry({ entry }) {
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.2",
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.2"
25
+ "@vietor/easy-agent-core": "0.4.4"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.0.0",
@@ -1,9 +0,0 @@
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
- });