@vietor/easy-agent 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vietor Liu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # Easy Agent
2
+
3
+ ![](https://img.shields.io/badge/Node.js-22%2B-brightgreen?style=flat-square) [![npm]](https://www.npmjs.com/package/@vietor/easy-agent)
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.
8
+
9
+ ## Requirements
10
+
11
+ - Node.js ≥ 22
12
+
13
+ ## Install
14
+
15
+ Install globally with npm:
16
+
17
+ ```bash
18
+ npm install -g @vietor/easy-agent
19
+ ```
20
+
21
+ Or run it once without installing:
22
+
23
+ ```bash
24
+ npx @vietor/easy-agent
25
+ ```
26
+
27
+ ## Configuration
28
+
29
+ Create `~/.easy-agent.json` in your home directory:
30
+
31
+ ```json
32
+ {
33
+ "llm": {
34
+ "baseUrl": "https://api.example.com/v1",
35
+ "apiKey": "your-api-key",
36
+ "model": "your-model"
37
+ }
38
+ }
39
+ ```
40
+
41
+ `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
+
43
+ ### Optional: MCP servers
44
+
45
+ Add an `mcpServers` map to expose external tools through the Model Context Protocol. Each entry spawns a local process (stdio transport):
46
+
47
+ ```json
48
+ {
49
+ "llm": { ... },
50
+ "mcpServers": {
51
+ "chrome-devtools": {
52
+ "command": "npx",
53
+ "args": [
54
+ "chrome-devtools-mcp@latest",
55
+ "--auto-connect",
56
+ "--accept-insecure-certs"
57
+ ]
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ Only `command` is required; `args` and `env` are optional. 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 — the rest keep running.
64
+
65
+ ### Optional: Agent instructions
66
+
67
+ 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:
68
+
69
+ 1. **Global** — your home directory, applied to every session. Checks `~/.agents/AGENTS.md` first, then `~/.claude/CLAUDE.md`; uses the first one found.
70
+ 2. **Project** — your current working directory, applied per-project. Checks `./AGENTS.md` first, then `./CLAUDE.md`; uses the first one found.
71
+
72
+ Both global and project files are loaded and concatenated into the system prompt if they exist.
73
+
74
+ ### Optional: Skills
75
+
76
+ 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:
77
+
78
+ ```
79
+ ~/.claude/skills/
80
+ deploy/
81
+ SKILL.md
82
+ review/
83
+ SKILL.md
84
+ ```
85
+
86
+ A `SKILL.md` with YAML frontmatter:
87
+
88
+ ```markdown
89
+ ---
90
+ name: deploy
91
+ description: Deploy the app to production
92
+ ---
93
+
94
+ Run the deployment: build the project, then run `deploy.sh` with the `--prod` flag.
95
+ ```
96
+
97
+ Only `name` is required; if omitted the directory name is used. The body is the full prompt injected when the skill is invoked. Skills appear as `/`-prefixed commands in the TUI alongside built-in slash commands.
98
+
99
+ ## Usage
100
+
101
+ Launch the TUI:
102
+
103
+ ```bash
104
+ easy-agent
105
+ ```
106
+
107
+ 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 25 tool rounds per turn).
108
+
109
+ Built-in tools:
110
+
111
+ - **Shell** — run shell commands using this platform's native syntax.
112
+ - **FileRead** — read a file's full contents.
113
+ - **FileWrite** — create or fully overwrite a file.
114
+ - **FileEdit** — replace one exact, unique match within a file.
115
+ - **Glob** — list files, optionally filtered by a glob pattern.
116
+ - **Grep** — search file contents by regex.
117
+ - **WebFetch** — fetch a URL as markdown or text.
118
+
119
+ Slash commands:
120
+
121
+ - `/mcp` — list linked MCP servers, their status, and exposed tools.
122
+ - `/clear` — reset the conversation.
123
+ - `/compact` — compress the conversation into a summary to free context.
124
+ - `/export` — save the current session to `session-{timestamp}.jsonl`.
125
+ - `/quit` or `/exit` — leave the app.
package/dist/cli.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "./main.js";
3
+ main().catch((e) => {
4
+ console.error(e);
5
+ process.exit(1);
6
+ });
@@ -0,0 +1,68 @@
1
+ import { writeFileSync } from "node:fs";
2
+ export const exitCommand = {
3
+ name: "exit",
4
+ description: "Exit the session",
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
+ async execute(ctx, host) {
13
+ ctx.agent.clear();
14
+ host.clearLog();
15
+ },
16
+ };
17
+ export const mcpCommand = {
18
+ name: "mcp",
19
+ description: "List linked MCP servers",
20
+ async execute(ctx, host) {
21
+ const servers = ctx.mcp.list();
22
+ const text = servers.length
23
+ ? [
24
+ "MCP servers:",
25
+ ...servers.map((s) => `❯ ${s.name} ⋅ ${s.status} ∶ ${s.tools.join(", ") || "(no tools)"}`),
26
+ ].join("\n")
27
+ : "No MCP servers linked.";
28
+ host.info(text);
29
+ },
30
+ };
31
+ export const compactCommand = {
32
+ name: "compact",
33
+ description: "Compact the agent context",
34
+ async execute(ctx, host) {
35
+ host.thinking(true);
36
+ try {
37
+ await ctx.agent.compact();
38
+ host.info("context compacted");
39
+ }
40
+ catch (e) {
41
+ host.error(e.message);
42
+ }
43
+ finally {
44
+ host.thinking(false);
45
+ }
46
+ },
47
+ };
48
+ export const exportCommand = {
49
+ name: "export",
50
+ description: "Export the session to a JSONL file",
51
+ async execute(ctx, host) {
52
+ try {
53
+ const d = new Date();
54
+ const pad = (n) => String(n).padStart(2, "0");
55
+ const ts = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
56
+ const file = `session-${ts}.jsonl`;
57
+ const lines = ctx.agent
58
+ .export()
59
+ .map((m) => JSON.stringify(m))
60
+ .join("\n");
61
+ writeFileSync(file, lines + "\n", "utf-8");
62
+ host.info(`exported to ${file}`);
63
+ }
64
+ catch (e) {
65
+ host.error(e.message);
66
+ }
67
+ },
68
+ };
@@ -0,0 +1,27 @@
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
+ async execute(command, ctx, host) {
14
+ const cmd = this.commands.get(command);
15
+ if (!cmd) {
16
+ host.error(`unknown command: /${command}`);
17
+ return;
18
+ }
19
+ await cmd.execute(ctx, host);
20
+ }
21
+ }
22
+ export function registerBuiltinCommands(commands) {
23
+ commands.register(exitCommand);
24
+ commands.register({ ...exitCommand, name: "quit" });
25
+ for (const t of [clearCommand, mcpCommand, compactCommand, exportCommand])
26
+ commands.register(t);
27
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,31 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { z } from "zod";
5
+ const CONFIG_FILE = ".easy-agent.json";
6
+ const LLMConfig = z.object({
7
+ baseUrl: z.string(),
8
+ apiKey: z.string(),
9
+ model: z.string(),
10
+ });
11
+ const MCPServerConfig = z.object({
12
+ command: z.string(),
13
+ args: z.array(z.string()).optional(),
14
+ env: z.record(z.string(), z.string()).optional(),
15
+ });
16
+ const Config = z.object({
17
+ llm: LLMConfig,
18
+ mcpServers: z.record(z.string(), MCPServerConfig).optional(),
19
+ });
20
+ export function loadConfig() {
21
+ const path = join(homedir(), CONFIG_FILE);
22
+ const raw = JSON.parse(readFileSync(path, "utf-8"));
23
+ const result = Config.safeParse(raw);
24
+ if (!result.success) {
25
+ const issues = result.error.issues
26
+ .map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`)
27
+ .join("\n ");
28
+ throw new Error(`Invalid config ~/${CONFIG_FILE}:\n ${issues}`);
29
+ }
30
+ return result.data;
31
+ }
@@ -0,0 +1,116 @@
1
+ import { withAbort } from "../util/async.js";
2
+ const STALL_THRESHOLD = 3;
3
+ const COMPACT_PROMPT = "Summarize this conversation into a concise context summary. Preserve the user's goal, decisions made, files touched, and current progress. Write the summary in the same language the user used in the conversation. Begin your reply with \"Summary of conversation so far:\".";
4
+ export class Agent {
5
+ llm;
6
+ session;
7
+ tools;
8
+ constructor(llm, session, tools) {
9
+ this.llm = llm;
10
+ this.session = session;
11
+ this.tools = tools;
12
+ }
13
+ get contextTokens() {
14
+ return this.session.getEstimatedTokens();
15
+ }
16
+ clear() {
17
+ this.session.clear();
18
+ }
19
+ export() {
20
+ return this.session.export();
21
+ }
22
+ async compact() {
23
+ const history = this.session.toLLM().slice(1);
24
+ if (history.length === 0)
25
+ return;
26
+ const request = [
27
+ ...history,
28
+ { role: "user", content: COMPACT_PROMPT },
29
+ ];
30
+ const msg = await this.llm.chat(request, []);
31
+ this.session.compact(msg.content || "");
32
+ }
33
+ async run(userInput, onEvent, signal) {
34
+ this.session.createCheckpoint();
35
+ try {
36
+ this.session.add({ role: "user", content: userInput });
37
+ await this.loop(onEvent, signal);
38
+ }
39
+ finally {
40
+ this.session.removeCheckpoint();
41
+ }
42
+ }
43
+ async runSkill(skill, onEvent, signal) {
44
+ this.session.createCheckpoint();
45
+ try {
46
+ this.session.add({ role: "skill", name: skill.name, content: skill.prompt });
47
+ await this.loop(onEvent, signal);
48
+ }
49
+ finally {
50
+ this.session.removeCheckpoint();
51
+ }
52
+ }
53
+ async loop(onEvent, signal) {
54
+ await withAbort(async (aborted) => {
55
+ let lastSig = "";
56
+ let stall = 0;
57
+ while (true) {
58
+ let msg;
59
+ try {
60
+ msg = await this.llm.chat(this.session.toLLM(), this.tools.schemas(), (text) => onEvent?.({ type: "delta", text }), (attempt, max) => onEvent?.({ type: "retry", attempt, max }), (promptTokens, completionTokens) => onEvent?.({ type: "usage", promptTokens, completionTokens }), signal);
61
+ }
62
+ catch (e) {
63
+ if (aborted())
64
+ return;
65
+ onEvent?.({ type: "error", text: e.message });
66
+ return;
67
+ }
68
+ this.session.add(msg);
69
+ if (!msg.tool_calls?.length)
70
+ return;
71
+ if (aborted())
72
+ return;
73
+ const sig = msg.tool_calls
74
+ .map((c) => `${c.function.name}:${c.function.arguments}`)
75
+ .join("|");
76
+ stall = sig === lastSig ? stall + 1 : 1;
77
+ lastSig = sig;
78
+ if (stall >= STALL_THRESHOLD) {
79
+ onEvent?.({ type: "error", text: "agent stalled: repeated identical tool calls" });
80
+ return;
81
+ }
82
+ await Promise.all(msg.tool_calls.map(async (call) => {
83
+ let args = {};
84
+ let argsError = "";
85
+ if (call.function.arguments) {
86
+ try {
87
+ args = JSON.parse(call.function.arguments);
88
+ }
89
+ catch (e) {
90
+ argsError = `Error: invalid arguments: ${e.message}`;
91
+ }
92
+ }
93
+ const summary = this.tools.summarize(call.function.name, args);
94
+ onEvent?.({ type: "tool_start", id: call.id, name: call.function.name, summary });
95
+ if (aborted())
96
+ return;
97
+ const result = argsError
98
+ ? { content: argsError, isError: true }
99
+ : await this.tools.execute(call.function.name, args);
100
+ if (aborted())
101
+ return;
102
+ onEvent?.({ type: "tool_end", id: call.id, name: call.function.name, result: result.content, isError: result.isError });
103
+ this.session.add({ role: "tool", tool_call_id: call.id, content: result.content });
104
+ }));
105
+ if (aborted())
106
+ return;
107
+ }
108
+ }, {
109
+ signal,
110
+ onAbort: () => {
111
+ this.session.restoreCheckpoint();
112
+ onEvent?.({ type: "interrupted" });
113
+ },
114
+ });
115
+ }
116
+ }
@@ -0,0 +1,75 @@
1
+ function estimateTokens(text) {
2
+ if (!text)
3
+ return 0;
4
+ const cjk = (text.match(/[一-龥぀-ヿ가-힯]/g) || []).length;
5
+ const words = (text.match(/[a-zA-Z0-9']+/g) || []).length;
6
+ return Math.ceil(cjk * 1.6 + words * 1.3 + (text.length - cjk) * 0.3);
7
+ }
8
+ function messageText(msg) {
9
+ const parts = [];
10
+ if (typeof msg.content === "string")
11
+ parts.push(msg.content);
12
+ else if (Array.isArray(msg.content)) {
13
+ for (const p of msg.content) {
14
+ if (p.type === "text")
15
+ parts.push(p.text);
16
+ }
17
+ }
18
+ if ("tool_calls" in msg && msg.tool_calls) {
19
+ for (const tc of msg.tool_calls) {
20
+ if (tc.function?.name)
21
+ parts.push(tc.function.name);
22
+ if (tc.function?.arguments)
23
+ parts.push(tc.function.arguments);
24
+ }
25
+ }
26
+ return parts.join(" ");
27
+ }
28
+ export class Session {
29
+ system;
30
+ messages = [];
31
+ estimatedTokens = 0;
32
+ checkpoint;
33
+ checkpointTokens = 0;
34
+ constructor(system) {
35
+ this.system = system;
36
+ this.messages.push({ role: "system", content: system });
37
+ this.estimatedTokens = estimateTokens(system);
38
+ }
39
+ getEstimatedTokens() {
40
+ return this.estimatedTokens;
41
+ }
42
+ add(msg) {
43
+ this.messages.push(msg);
44
+ this.estimatedTokens += estimateTokens(messageText(msg));
45
+ }
46
+ toLLM() {
47
+ return this.messages.map((m) => (m.role === "skill" ? { role: "user", name: m.name, content: m.content } : m));
48
+ }
49
+ export() {
50
+ return this.messages.slice(1);
51
+ }
52
+ clear() {
53
+ this.messages = [{ role: "system", content: this.system }];
54
+ this.estimatedTokens = estimateTokens(this.system);
55
+ }
56
+ compact(summary) {
57
+ this.messages = [
58
+ { role: "system", content: this.system },
59
+ { role: "assistant", content: summary },
60
+ ];
61
+ this.estimatedTokens = estimateTokens(this.system) + estimateTokens(summary);
62
+ }
63
+ createCheckpoint() {
64
+ this.checkpoint = this.messages.slice();
65
+ this.checkpointTokens = this.estimatedTokens;
66
+ }
67
+ restoreCheckpoint() {
68
+ this.messages = this.checkpoint.slice();
69
+ this.estimatedTokens = this.checkpointTokens;
70
+ }
71
+ removeCheckpoint() {
72
+ this.checkpoint = undefined;
73
+ this.checkpointTokens = 0;
74
+ }
75
+ }
@@ -0,0 +1,74 @@
1
+ import OpenAI, { APIConnectionError } from "openai";
2
+ import { withRetry } from "../util/async.js";
3
+ const MAX_RETRIES = 3;
4
+ export class LLMClient {
5
+ client;
6
+ model;
7
+ constructor(config) {
8
+ this.client = new OpenAI({
9
+ apiKey: config.apiKey,
10
+ baseURL: config.baseUrl || undefined,
11
+ maxRetries: 0,
12
+ });
13
+ this.model = config.model;
14
+ }
15
+ async chat(messages, tools, onDelta, onRetry, onUsage, signal) {
16
+ return withRetry(() => this.streamOnce(messages, tools, onDelta, onUsage, signal), {
17
+ retries: MAX_RETRIES,
18
+ retryable: (e) => e instanceof APIConnectionError,
19
+ backoff: (attempt) => 1000 * 2 ** attempt,
20
+ onRetry,
21
+ signal,
22
+ });
23
+ }
24
+ async streamOnce(messages, tools, onDelta, onUsage, signal) {
25
+ let content = "";
26
+ const calls = new Map();
27
+ const stream = await this.client.chat.completions.create({
28
+ model: this.model,
29
+ messages,
30
+ tools,
31
+ stream: true,
32
+ stream_options: { include_usage: true },
33
+ }, { signal });
34
+ for await (const chunk of stream) {
35
+ if (chunk.usage) {
36
+ onUsage?.(chunk.usage.prompt_tokens ?? 0, chunk.usage.completion_tokens ?? 0);
37
+ }
38
+ const delta = chunk.choices[0]?.delta;
39
+ if (!delta)
40
+ continue;
41
+ if (delta.content) {
42
+ content += delta.content;
43
+ onDelta?.(delta.content);
44
+ }
45
+ if (delta.tool_calls) {
46
+ for (const tc of delta.tool_calls) {
47
+ let acc = calls.get(tc.index);
48
+ if (!acc) {
49
+ acc = { id: tc.id ?? "", name: "", arguments: "" };
50
+ calls.set(tc.index, acc);
51
+ }
52
+ if (tc.function?.name)
53
+ acc.name += tc.function.name;
54
+ if (tc.function?.arguments)
55
+ acc.arguments += tc.function.arguments;
56
+ }
57
+ }
58
+ }
59
+ const message = {
60
+ role: "assistant",
61
+ content: content || null,
62
+ };
63
+ if (calls.size) {
64
+ message.tool_calls = [...calls.entries()]
65
+ .sort((a, b) => a[0] - b[0])
66
+ .map(([, acc]) => ({
67
+ id: acc.id,
68
+ type: "function",
69
+ function: { name: acc.name, arguments: acc.arguments },
70
+ }));
71
+ }
72
+ return message;
73
+ }
74
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/main.js ADDED
@@ -0,0 +1,61 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { loadConfig } from "./config.js";
4
+ import { LLMClient } from "./llm/client.js";
5
+ import { Session } from "./core/session.js";
6
+ import { Agent } from "./core/agent.js";
7
+ import { ToolRegistry, registerBuiltinTools } from "./tools/registry.js";
8
+ import { CommandRegistry, registerBuiltinCommands } from "./cmds/registry.js";
9
+ import { tryLoadSkills } from "./skills/loader.js";
10
+ import { tryReadFileText, readFirstFileContent } from "./util/fs.js";
11
+ import { MCPServers } from "./mcp/server.js";
12
+ import { startApp } from "./tui/App.js";
13
+ const SYSTEM_PROMPT_BASE = `You are Easy Agent, an autonomous coding assistant running in the terminal. You complete tasks by calling tools, inspecting their results, and iterating until the work is done.
14
+
15
+ Environment:
16
+ - Platform: ${process.platform}
17
+ - Working directory: ${process.cwd()}
18
+
19
+ Tool use:
20
+ - Prefer dedicated tools (FileRead, FileEdit, Glob, Grep) over the Shell tool when they fit the task.
21
+ - Read a file before editing it; make minimal, surgical changes that match the surrounding code style.
22
+ - Reference code as file_path:line_number.
23
+
24
+ Output:
25
+ - Be concise and use GitHub-flavored markdown.
26
+ - State what you did and stop once the task is complete. Report outcomes faithfully, and do not narrate alternatives you will not pursue.`;
27
+ export async function main() {
28
+ const config = loadConfig();
29
+ const llm = new LLMClient(config.llm);
30
+ const tools = new ToolRegistry();
31
+ registerBuiltinTools(tools);
32
+ const commands = new CommandRegistry();
33
+ registerBuiltinCommands(commands);
34
+ const mcp = new MCPServers();
35
+ mcp
36
+ .connect(config.mcpServers)
37
+ .then((list) => {
38
+ for (const t of list)
39
+ tools.register(t);
40
+ })
41
+ .catch((e) => mcp.report(`MCP connect failed: ${e.message}`));
42
+ const globalSkills = readFirstFileContent([join(homedir(), ".agents", "skills"), join(homedir(), ".claude", "skills")], tryLoadSkills);
43
+ if (globalSkills) {
44
+ globalSkills.forEach((skill) => commands.register({
45
+ name: skill.name,
46
+ description: skill.description ?? skill.name,
47
+ execute: async (_, host) => {
48
+ await host.runSkill(skill);
49
+ },
50
+ }));
51
+ }
52
+ const globalPrompt = readFirstFileContent([join(homedir(), ".agents", "AGENTS.md"), join(homedir(), ".claude", "CLAUDE.md")], tryReadFileText);
53
+ const projectPrompt = readFirstFileContent([join(process.cwd(), "AGENTS.md"), join(process.cwd(), "CLAUDE.md")], tryReadFileText);
54
+ const systemPrompt = [SYSTEM_PROMPT_BASE, globalPrompt, projectPrompt]
55
+ .filter(Boolean)
56
+ .join("\n\n=================\n\n");
57
+ const session = new Session(systemPrompt);
58
+ const agent = new Agent(llm, session, tools);
59
+ const app = startApp(agent, commands, mcp);
60
+ await app.waitUntilExit().finally(() => mcp.kill());
61
+ }
@@ -0,0 +1,45 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+ import { getPackageInfo } from "../util/package.js";
4
+ function getClientINfo() {
5
+ const pkginfo = getPackageInfo();
6
+ return { name: pkginfo.name, version: pkginfo.version };
7
+ }
8
+ export class MCPClient {
9
+ name;
10
+ client = new Client(getClientINfo(), { capabilities: {} });
11
+ transport;
12
+ connectReject;
13
+ constructor(name, config) {
14
+ this.name = name;
15
+ this.transport = new StdioClientTransport({ ...config, stderr: "ignore" });
16
+ }
17
+ async connect() {
18
+ return new Promise((resolve, reject) => {
19
+ this.connectReject = reject;
20
+ this.client
21
+ .connect(this.transport)
22
+ .then(resolve, reject)
23
+ .finally(() => {
24
+ this.connectReject = undefined;
25
+ });
26
+ });
27
+ }
28
+ async listTools() {
29
+ return this.client.listTools().then((r) => r.tools);
30
+ }
31
+ async callTool(name, args) {
32
+ return this.client.callTool({ name, arguments: args });
33
+ }
34
+ kill() {
35
+ this.connectReject?.(new Error("aborted"));
36
+ this.connectReject = undefined;
37
+ const pid = this.transport.pid;
38
+ if (pid) {
39
+ try {
40
+ process.kill(pid, "SIGTERM");
41
+ }
42
+ catch { }
43
+ }
44
+ }
45
+ }