@vietor/easy-agent 0.4.6 → 0.4.8

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
@@ -37,6 +37,25 @@ Create `~/.easy-agent.json` in your home directory:
37
37
 
38
38
  `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.
39
39
 
40
+ #### `wireApi` (optional)
41
+
42
+ Selects the wire protocol the client speaks. Valid values: `"completions"` | `"anthropic"`. Defaults to `"completions"`.
43
+
44
+ - `"completions"` - OpenAI Chat Completions compatible endpoint (the default). `reasoningEffort` is sent as `reasoning_effort`.
45
+ - `"anthropic"` - Anthropic Messages API via the official SDK. Point `baseUrl` at an Anthropic-compatible endpoint (e.g. `https://api.anthropic.com`) and `model` at a Claude model. `reasoningEffort` enables extended thinking (`"high"` = 16k budget, `"max"` = 32k).
46
+
47
+ ```json
48
+ {
49
+ "llm": {
50
+ "baseUrl": "https://api.anthropic.com",
51
+ "apiKey": "your-api-key",
52
+ "model": "claude-sonnet-5",
53
+ "wireApi": "anthropic",
54
+ "reasoningEffort": "high"
55
+ }
56
+ }
57
+ ```
58
+
40
59
  #### `reasoningEffort` (optional)
41
60
 
42
61
  Controls the depth of model reasoning. Valid values: `"high"` | `"max"`. Defaults to `"high"`. When set to `"max"` the model spends more tokens on deeper reasoning before responding — useful for complex logic, math, or multi-step analysis. The current setting is shown in the TUI header (e.g. ` · reasoning max`). Requires a model that supports the `reasoning_effort` parameter.
@@ -85,7 +104,7 @@ Both global and project files are loaded and concatenated into the system prompt
85
104
 
86
105
  ### Skills (optional)
87
106
 
88
- 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:
107
+ Skills are reusable prompts that register themselves as slash commands. Create a subdirectory for each skill under `~/.easy-agent/skills/` with a `SKILL.md` file (falls back to `~/.claude/skills/`):
89
108
 
90
109
  ```
91
110
  ~/.easy-agent/skills/
@@ -121,7 +140,9 @@ easy-agent --resume # list all saved sessions for this directory
121
140
 
122
141
  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).
123
142
 
124
- 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).
143
+ The TUI header shows the model name, version, and configured reasoning effort (e.g. `deepseek-v4-flash · reasoning high`). While the agent is running, a spinner displays the thinking and reply elapsed time alongside token counts (e.g. `⠋ thinking · think 2s · reply 0s · ↑1.2k · ↓0.4k`). If the LLM emits extended thinking blocks, a collapsible reasoning panel appears — press <kbd>t</kbd> to expand/collapse it.
144
+
145
+ A status bar at the bottom shows the context token usage with a progress bar and percentage, along with keyboard shortcut hints: <kbd>ESC</kbd> to abort a running task, <kbd>t</kbd> to toggle reasoning (when available), and <kbd>/</kbd> for commands.
125
146
 
126
147
  ### Built-in tools
127
148
 
package/dist/config.js CHANGED
@@ -8,6 +8,7 @@ const LLMConfig = z.object({
8
8
  apiKey: z.string(),
9
9
  model: z.string(),
10
10
  reasoningEffort: z.enum(["high", "max"]).default("high"),
11
+ wireApi: z.enum(["completions", "anthropic"]).default("completions"),
11
12
  });
12
13
  const StdioServerConfig = z.object({
13
14
  type: z.literal("stdio").optional(),
package/dist/tui/App.js CHANGED
@@ -14,7 +14,7 @@ export function App({ session }) {
14
14
  const { exit } = useApp();
15
15
  const { columns } = useWindowSize();
16
16
  const view = useSyncExternalStore(session.subscribe, session.getSnapshot);
17
- const [runState, setRunState] = useState({ running: false, elapsed: 0, thinkingElapsed: 0, replyElapsed: 0, promptTokens: 0, completionTokens: 0 });
17
+ const [runState, setRunState] = useState({ running: false, elapsed: 0, thinkingElapsed: 0, replyElapsed: 0, inputTokens: 0, outputTokens: 0 });
18
18
  const [streamingText, setStreamingText] = useState("");
19
19
  const streamingRef = useRef("");
20
20
  const renderTimerRef = useRef(undefined);
@@ -101,10 +101,10 @@ export function App({ session }) {
101
101
  }
102
102
  else {
103
103
  const spinnerLabel = streamingText ? "replying" : "thinking";
104
- runningView = (_jsxs(_Fragment, { children: [reasoningText ? renderReasoning(reasoningText, showReasoning) : null, streamingText ? (_jsx(Box, { marginTop: 1, paddingLeft: 1, paddingRight: 1, borderStyle: "single", borderTop: false, borderRight: false, borderBottom: false, borderColor: "gray", children: _jsx(Markdown, { children: streamingText }) })) : null, _jsx(Box, { marginTop: 1, paddingLeft: 1, children: _jsx(Spinner, { label: spinnerLabel, thinkingElapsed: runState.thinkingElapsed, replyElapsed: runState.replyElapsed, promptTokens: runState.promptTokens, completionTokens: runState.completionTokens }) })] }));
104
+ runningView = (_jsxs(_Fragment, { children: [reasoningText ? renderReasoning(reasoningText, showReasoning) : null, streamingText ? (_jsx(Box, { marginTop: 1, paddingLeft: 1, paddingRight: 1, borderStyle: "single", borderTop: false, borderRight: false, borderBottom: false, borderColor: "gray", children: _jsx(Markdown, { children: streamingText }) })) : null, _jsx(Box, { marginTop: 1, paddingLeft: 1, children: _jsx(Spinner, { label: spinnerLabel, thinkingElapsed: runState.thinkingElapsed, replyElapsed: runState.replyElapsed, inputTokens: runState.inputTokens, outputTokens: runState.outputTokens }) })] }));
105
105
  }
106
106
  }
107
- return (_jsxs(Box, { width: columns, flexDirection: "column", children: [_jsx(AppHeader, { cwd: session.cwd, model: session.model, reasoningEffort: session.reasoningEffort }), view.timeline.length > 0 ? (_jsx(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: view.timeline.map((entry, i) => (_jsx(TimelineView, { entry: entry }, i))) })) : null, view.todos.length > 0 ? _jsx(TodoView, { todos: view.todos }) : null, runningView, !runState.running ? (_jsx(PromptOrCommandInput, { commands: allCmds, onCommand: handleCommand, onPrompt: handlePrompt })) : null, _jsx(StatusBar, { contextTokens: session.contextTokens, contextLimit: session.compactThreshold, running: runState.running, questionPending: !!pendingQuestion, reasoningAvailable: !!reasoningText })] }));
107
+ return (_jsxs(Box, { width: columns, flexDirection: "column", children: [_jsx(AppHeader, { cwd: session.cwd, model: session.model, reasoningEffort: session.reasoningEffort }), view.timeline.length > 0 ? (_jsx(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: view.timeline.map((entry, i) => (_jsx(TimelineView, { entry: entry }, i))) })) : null, runningView, view.todos.length > 0 ? _jsx(TodoView, { todos: view.todos }) : null, !runState.running ? (_jsx(PromptOrCommandInput, { commands: allCmds, onCommand: handleCommand, onPrompt: handlePrompt })) : null, _jsx(StatusBar, { contextTokens: session.contextTokens, contextLimit: session.compactThreshold, running: runState.running, questionPending: !!pendingQuestion, reasoningAvailable: !!reasoningText })] }));
108
108
  }
109
109
  function renderReasoning(text, expanded) {
110
110
  const lines = text.split("\n");
@@ -3,11 +3,11 @@ import { useEffect, useState } from "react";
3
3
  import { Text } from "ink";
4
4
  import { timeDisplay, compactDisplay } from "../util/format.js";
5
5
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
6
- export function Spinner({ label, thinkingElapsed, replyElapsed, promptTokens, completionTokens, }) {
6
+ export function Spinner({ label, thinkingElapsed, replyElapsed, inputTokens, outputTokens, }) {
7
7
  const [frame, setFrame] = useState(0);
8
8
  useEffect(() => {
9
9
  const id = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), 80);
10
10
  return () => clearInterval(id);
11
11
  }, []);
12
- return (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: SPINNER_FRAMES[frame] }), _jsxs(Text, { children: [" ", label] }), _jsxs(Text, { dimColor: true, children: [" \u00B7 think ", timeDisplay(thinkingElapsed), " \u00B7 reply ", timeDisplay(replyElapsed), " \u00B7 \u2191", compactDisplay(promptTokens), " \u00B7 \u2193", compactDisplay(completionTokens)] })] }));
12
+ return (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: SPINNER_FRAMES[frame] }), _jsxs(Text, { children: [" ", label] }), _jsxs(Text, { dimColor: true, children: [" \u00B7 think ", timeDisplay(thinkingElapsed), " \u00B7 reply ", timeDisplay(replyElapsed), " \u00B7 \u2191", compactDisplay(inputTokens), " \u00B7 \u2193", compactDisplay(outputTokens)] })] }));
13
13
  }
@@ -14,5 +14,5 @@ const COLORS = {
14
14
  export const TodoView = memo(function TodoView({ todos }) {
15
15
  const done = todos.filter((t) => t.status === "completed").length;
16
16
  const headerColor = done === todos.length ? "green" : "cyan";
17
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 1, paddingRight: 1, children: [_jsx(Box, { borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, borderColor: "gray" }), _jsx(Text, { color: headerColor, 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)))] }));
17
+ return (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: [_jsx(Box, { borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, borderColor: "gray" }), _jsx(Text, { color: headerColor, 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)))] }));
18
18
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vietor/easy-agent",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "type": "module",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {
@@ -23,7 +23,7 @@
23
23
  "react": "^19.2.7",
24
24
  "string-width": "^8.2.1",
25
25
  "zod": "^4.4.3",
26
- "@vietor/easy-agent-core": "0.4.6"
26
+ "@vietor/easy-agent-core": "0.4.8"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^22.0.0",
@@ -1,15 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Component } from "react";
3
- import { Box, Text } from "ink";
4
- export class ErrorBoundary extends Component {
5
- state = { error: null };
6
- static getDerivedStateFromError(error) {
7
- return { error };
8
- }
9
- render() {
10
- if (this.state.error) {
11
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "red", bold: true, children: "\u2717 TUI Error" }), _jsx(Text, { dimColor: true, children: this.state.error.message })] }));
12
- }
13
- return this.props.children;
14
- }
15
- }
@@ -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 { 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
- });