@aigne/cli 1.1.0 → 1.2.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.0](https://github.com/AIGNE-io/aigne-framework/compare/cli-v1.1.0...cli-v1.2.0) (2025-04-15)
4
+
5
+
6
+ ### Features
7
+
8
+ * add TerminalTracer for better UX in terminal ([#56](https://github.com/AIGNE-io/aigne-framework/issues/56)) ([9875a5d](https://github.com/AIGNE-io/aigne-framework/commit/9875a5d46abb55073340ffae841fed6bd6b83ff4))
9
+ * **cli:** support run agents from remote URL ([#60](https://github.com/AIGNE-io/aigne-framework/issues/60)) ([5f49920](https://github.com/AIGNE-io/aigne-framework/commit/5f4992089d36f9e780ba55a912a1d35508cad28e))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * remove usage of new Node.js exists API for compatibility ([#57](https://github.com/AIGNE-io/aigne-framework/issues/57)) ([c10cc08](https://github.com/AIGNE-io/aigne-framework/commit/c10cc086d8ecd0744f38cdb1367d4c8816b723b3))
15
+
3
16
  ## [1.1.0](https://github.com/AIGNE-io/aigne-framework/compare/cli-v1.0.0...cli-v1.1.0) (2025-04-08)
4
17
 
5
18
 
package/dist/cli.js CHANGED
@@ -1,3 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ import PrettyError from "pretty-error";
2
3
  import { createAIGNECommand } from "./commands/aigne.js";
3
- createAIGNECommand().parse();
4
+ createAIGNECommand()
5
+ .parseAsync()
6
+ .catch((error) => {
7
+ console.error(new PrettyError().render(error));
8
+ process.exit(1);
9
+ });
@@ -1,35 +1,57 @@
1
- import { isAbsolute, resolve } from "node:path";
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdir, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { isAbsolute, join, resolve } from "node:path";
2
5
  import { ExecutionEngine } from "@aigne/core";
3
- import { runChatLoopInTerminal } from "@aigne/core/utils/run-chat-loop.js";
4
6
  import { Command } from "commander";
7
+ import { downloadAndExtract } from "../utils/download.js";
8
+ import { runChatLoopInTerminal } from "../utils/run-chat-loop.js";
5
9
  export function createRunCommand() {
6
10
  return new Command("run")
7
11
  .description("Run a chat loop with the specified agent")
8
- .argument("[path]", "Path to the agents directory", ".")
12
+ .argument("[path]", "Path to the agents directory or URL to aigne project", ".")
9
13
  .option("--agent <agent>", "Name of the agent to use (defaults to the first agent found)")
10
14
  .action(async (path, options) => {
11
- const absolutePath = isAbsolute(path) ? path : resolve(process.cwd(), path);
12
- const engine = await ExecutionEngine.load({ path: absolutePath });
13
- let agent;
14
- if (options.agent) {
15
- agent = engine.agents[options.agent];
16
- if (!agent) {
17
- console.error(`Agent "${options.agent}" not found.`);
18
- console.log("Available agents:");
19
- for (const agent of engine.agents) {
20
- console.log(`- ${agent.name}`);
21
- }
22
- throw new Error(`Agent "${options.agent}" not found`);
23
- }
15
+ if (path.startsWith("http")) {
16
+ await downloadAndRunPackage(path, options);
17
+ return;
24
18
  }
25
- else {
26
- agent = engine.agents[0];
27
- if (!agent)
28
- throw new Error("No agents found in the specified path");
29
- }
30
- const user = engine.call(agent);
31
- await runChatLoopInTerminal(user, {});
19
+ const absolutePath = isAbsolute(path) ? path : resolve(process.cwd(), path);
20
+ await runEngine(path, absolutePath, options);
32
21
  })
33
22
  .showHelpAfterError(true)
34
23
  .showSuggestionAfterError(true);
35
24
  }
25
+ async function runEngine(originalPath, path, options) {
26
+ const engine = await ExecutionEngine.load({ path });
27
+ let agent;
28
+ if (options.agent) {
29
+ agent = engine.agents[options.agent];
30
+ if (!agent) {
31
+ console.error(`Agent "${options.agent}" not found in ${originalPath}`);
32
+ console.log("Available agents:");
33
+ for (const agent of engine.agents) {
34
+ console.log(`- ${agent.name}`);
35
+ }
36
+ throw new Error(`Agent "${options.agent}" not found in ${originalPath}`);
37
+ }
38
+ }
39
+ else {
40
+ agent = engine.agents[0];
41
+ if (!agent)
42
+ throw new Error(`No agents found in ${originalPath}`);
43
+ }
44
+ const user = engine.call(agent);
45
+ await runChatLoopInTerminal(user, {});
46
+ }
47
+ async function downloadAndRunPackage(url, options) {
48
+ const dir = join(tmpdir(), randomUUID());
49
+ try {
50
+ await mkdir(dir, { recursive: true });
51
+ await downloadAndExtract(url, dir);
52
+ await runEngine(url, dir, options);
53
+ }
54
+ finally {
55
+ await rm(dir, { recursive: true, force: true });
56
+ }
57
+ }
@@ -0,0 +1,52 @@
1
+ import { type Agent, type Context, type Message } from "@aigne/core";
2
+ import type { ContextUsage } from "@aigne/core/execution-engine/usage";
3
+ import { type DefaultRenderer, Listr, type ListrRenderer, type ListrTaskWrapper } from "@aigne/listr2";
4
+ export interface TerminalTracerOptions {
5
+ verbose?: boolean;
6
+ }
7
+ export declare class TerminalTracer {
8
+ readonly context: Context;
9
+ readonly options: TerminalTracerOptions;
10
+ constructor(context: Context, options?: TerminalTracerOptions);
11
+ private spinner;
12
+ private tasks;
13
+ run(agent: Agent, input: Message): Promise<{
14
+ result: any;
15
+ context: Context;
16
+ }>;
17
+ protected newListr(): MyListr;
18
+ formatAgentStartedOutput(agent: Agent, data: Message): string;
19
+ formatAgentSucceedOutput(agent: Agent, data: Message): string;
20
+ formatAgentFailedOutput(agent: Agent, data: Error): string;
21
+ formatMessage(data: unknown): string;
22
+ formatTokenUsage(usage: Partial<ContextUsage>, extra?: {
23
+ [key: string]: string;
24
+ }): string;
25
+ formatTimeUsage(startTime: number, endTime: number): string;
26
+ formatTaskTitle(agent: Agent, { task, usage, time }?: {
27
+ task?: Task;
28
+ usage?: boolean;
29
+ time?: boolean;
30
+ }): string;
31
+ }
32
+ type Task = ReturnType<typeof Promise.withResolvers<void>> & {
33
+ listr: ReturnType<typeof Promise.withResolvers<{
34
+ ctx: object;
35
+ subtask: Listr;
36
+ taskWrapper: ListrTaskWrapper<unknown, typeof DefaultRenderer, typeof ListrRenderer>;
37
+ }>>;
38
+ startTime?: number;
39
+ endTime?: number;
40
+ usage?: Partial<ContextUsage>;
41
+ extraTitleMetadata?: {
42
+ [key: string]: string;
43
+ };
44
+ };
45
+ declare class MyListr extends Listr {
46
+ private taskPromise;
47
+ private isTaskPromiseResolved;
48
+ resolveWaitingTask(): void;
49
+ add(...args: Parameters<Listr["add"]>): ReturnType<Listr["add"]>;
50
+ waitTaskAndRun(ctx?: unknown): Promise<any>;
51
+ }
52
+ export {};
@@ -0,0 +1,179 @@
1
+ import { inspect } from "node:util";
2
+ import { ChatModel, } from "@aigne/core";
3
+ import { Listr, ListrDefaultRendererLogLevels, Spinner, figures, } from "@aigne/listr2";
4
+ import chalk from "chalk";
5
+ import { z } from "zod";
6
+ import { parseDuration } from "../utils/time.js";
7
+ const DEBUG_DEPTH = z.number().int().default(2).safeParse(Number(process.env.DEBUG_DEPTH)).data;
8
+ export class TerminalTracer {
9
+ context;
10
+ options;
11
+ constructor(context, options = {}) {
12
+ this.context = context;
13
+ this.options = options;
14
+ }
15
+ spinner = new Spinner();
16
+ tasks = {};
17
+ async run(agent, input) {
18
+ try {
19
+ this.spinner.start();
20
+ const context = this.context.newContext({ reset: true });
21
+ const listr = this.newListr();
22
+ context.on("agentStarted", async ({ contextId, parentContextId, agent, input, timestamp }) => {
23
+ const task = {
24
+ ...Promise.withResolvers(),
25
+ listr: Promise.withResolvers(),
26
+ startTime: timestamp,
27
+ };
28
+ this.tasks[contextId] = task;
29
+ const listrTask = {
30
+ title: this.formatTaskTitle(agent),
31
+ task: (ctx, taskWrapper) => {
32
+ const subtask = taskWrapper.newListr([{ task: () => task.promise }]);
33
+ task.listr.resolve({ subtask, taskWrapper, ctx });
34
+ return subtask;
35
+ },
36
+ rendererOptions: {
37
+ persistentOutput: true,
38
+ outputBar: Number.POSITIVE_INFINITY,
39
+ bottomBar: Number.POSITIVE_INFINITY,
40
+ },
41
+ };
42
+ const parentTask = parentContextId ? this.tasks[parentContextId] : undefined;
43
+ if (parentTask) {
44
+ parentTask.listr.promise.then(({ subtask }) => {
45
+ subtask.add(listrTask);
46
+ });
47
+ }
48
+ else {
49
+ listr.add(listrTask);
50
+ }
51
+ const { taskWrapper } = await task.listr.promise;
52
+ if (this.options.verbose) {
53
+ taskWrapper.output = this.formatAgentStartedOutput(agent, input);
54
+ }
55
+ });
56
+ context.on("agentSucceed", async ({ agent, contextId, parentContextId, output, timestamp }) => {
57
+ const task = this.tasks[contextId];
58
+ if (!task)
59
+ return;
60
+ task.endTime = timestamp;
61
+ const { taskWrapper, ctx } = await task.listr.promise;
62
+ if (agent instanceof ChatModel) {
63
+ const { usage, model } = output;
64
+ task.usage = usage;
65
+ task.extraTitleMetadata ??= {};
66
+ if (model)
67
+ task.extraTitleMetadata.model = model;
68
+ }
69
+ taskWrapper.title = this.formatTaskTitle(agent, { task, usage: true, time: true });
70
+ if (this.options.verbose) {
71
+ taskWrapper.output = this.formatAgentSucceedOutput(agent, output);
72
+ }
73
+ if (!parentContextId || !this.tasks[parentContextId]) {
74
+ Object.assign(ctx, output);
75
+ }
76
+ task.resolve();
77
+ });
78
+ context.on("agentFailed", async ({ agent, contextId, error, timestamp }) => {
79
+ const task = this.tasks[contextId];
80
+ if (!task)
81
+ return;
82
+ task.endTime = timestamp;
83
+ const { taskWrapper } = await task.listr.promise;
84
+ taskWrapper.title = this.formatTaskTitle(agent, { task, usage: true, time: true });
85
+ taskWrapper.output = this.formatAgentFailedOutput(agent, error);
86
+ task.reject(error);
87
+ });
88
+ const [result] = await Promise.all([
89
+ listr.waitTaskAndRun(),
90
+ context.call(agent, input).finally(() => {
91
+ listr.resolveWaitingTask();
92
+ }),
93
+ ]);
94
+ return { result, context };
95
+ }
96
+ finally {
97
+ this.spinner.stop();
98
+ }
99
+ }
100
+ newListr() {
101
+ return new MyListr([], {
102
+ concurrent: true,
103
+ rendererOptions: {
104
+ collapseSubtasks: false,
105
+ writeBottomBarDirectly: true,
106
+ icon: {
107
+ [ListrDefaultRendererLogLevels.PENDING]: () => this.spinner.fetch(),
108
+ [ListrDefaultRendererLogLevels.OUTPUT_WITH_BOTTOMBAR]: "",
109
+ },
110
+ },
111
+ });
112
+ }
113
+ formatAgentStartedOutput(agent, data) {
114
+ return `\
115
+ ${chalk.grey(figures.pointer)} call agent ${agent.name} started with input:
116
+ ${this.formatMessage(data)}`;
117
+ }
118
+ formatAgentSucceedOutput(agent, data) {
119
+ return `\
120
+ ${chalk.grey(figures.tick)} call agent ${agent.name} succeed with output:
121
+ ${this.formatMessage(data)}`;
122
+ }
123
+ formatAgentFailedOutput(agent, data) {
124
+ return `\
125
+ ${chalk.grey(figures.cross)} call agent ${agent.name} failed with error:
126
+ ${this.formatMessage(data)}`;
127
+ }
128
+ formatMessage(data) {
129
+ return inspect(data, { colors: true, depth: DEBUG_DEPTH });
130
+ }
131
+ formatTokenUsage(usage, extra) {
132
+ const items = [
133
+ [chalk.yellow(usage.inputTokens), chalk.grey("input tokens")],
134
+ [chalk.cyan(usage.outputTokens), chalk.grey("output tokens")],
135
+ usage.agentCalls ? [chalk.magenta(usage.agentCalls), chalk.grey("agent calls")] : undefined,
136
+ ];
137
+ const content = items.filter((i) => !!i).map((i) => i.join(" "));
138
+ if (extra) {
139
+ content.unshift(...Object.entries(extra)
140
+ .filter(([k, v]) => k && v)
141
+ .map(([k, v]) => `${chalk.grey(k)}: ${v}`));
142
+ }
143
+ return `${chalk.grey("(")}${content.join(chalk.green(", "))}${chalk.grey(")")}`;
144
+ }
145
+ formatTimeUsage(startTime, endTime) {
146
+ const duration = endTime - startTime;
147
+ return chalk.grey(`[${parseDuration(duration)}]`);
148
+ }
149
+ formatTaskTitle(agent, { task, usage, time } = {}) {
150
+ let title = `call agent ${agent.name}`;
151
+ if (usage && task?.usage)
152
+ title += ` ${this.formatTokenUsage(task.usage, task.extraTitleMetadata)}`;
153
+ if (time && task?.startTime && task.endTime)
154
+ title += ` ${this.formatTimeUsage(task.startTime, task.endTime)}`;
155
+ return title;
156
+ }
157
+ }
158
+ class MyListr extends Listr {
159
+ taskPromise = Promise.withResolvers();
160
+ isTaskPromiseResolved = false;
161
+ resolveWaitingTask() {
162
+ if (!this.isTaskPromiseResolved) {
163
+ this.taskPromise.resolve();
164
+ this.isTaskPromiseResolved = true;
165
+ }
166
+ }
167
+ add(...args) {
168
+ const result = super.add(...args);
169
+ this.resolveWaitingTask();
170
+ return result;
171
+ }
172
+ async waitTaskAndRun(ctx) {
173
+ if (!this.tasks.length)
174
+ await this.taskPromise.promise;
175
+ if (!this.tasks.length)
176
+ return ctx;
177
+ return super.run(ctx);
178
+ }
179
+ }
@@ -0,0 +1 @@
1
+ export declare function downloadAndExtract(url: string, dir: string): Promise<void>;
@@ -0,0 +1,21 @@
1
+ import { Readable } from "node:stream";
2
+ import { finished } from "node:stream/promises";
3
+ import { x } from "tar";
4
+ export async function downloadAndExtract(url, dir) {
5
+ const response = await fetch(url).catch((error) => {
6
+ throw new Error(`Failed to download package from ${url}: ${error.message}`);
7
+ });
8
+ if (!response.ok) {
9
+ throw new Error(`Failed to download package from ${url}: ${response.statusText}`);
10
+ }
11
+ if (!response.body) {
12
+ throw new Error(`Failed to download package from ${url}: Unexpected to get empty response`);
13
+ }
14
+ try {
15
+ await finished(Readable.fromWeb(response.body).pipe(x({ C: dir })));
16
+ }
17
+ catch (error) {
18
+ error.message = `Failed to extract package from ${url}: ${error.message}`;
19
+ throw error;
20
+ }
21
+ }
@@ -0,0 +1,9 @@
1
+ import { type Message, type UserAgent as input } from "@aigne/core";
2
+ export interface ChatLoopOptions {
3
+ initialCall?: Message | string;
4
+ welcome?: string;
5
+ defaultQuestion?: string;
6
+ inputKey?: string;
7
+ verbose?: boolean;
8
+ }
9
+ export declare function runChatLoopInTerminal(userAgent: input, options?: ChatLoopOptions): Promise<void>;
@@ -0,0 +1,73 @@
1
+ import { inspect } from "node:util";
2
+ import { MESSAGE_KEY, createMessage } from "@aigne/core";
3
+ import { logger } from "@aigne/core/utils/logger.js";
4
+ import { figures } from "@aigne/listr2";
5
+ import chalk from "chalk";
6
+ import inquirer from "inquirer";
7
+ import { TerminalTracer } from "../tracer/terminal.js";
8
+ export async function runChatLoopInTerminal(userAgent, options = {}) {
9
+ options.verbose ??= logger.enabled("aigne:core");
10
+ // Disable the logger, use TerminalTracer instead
11
+ logger.disable();
12
+ let prompt;
13
+ if (options?.welcome)
14
+ console.log(options.welcome);
15
+ if (options?.initialCall) {
16
+ await callAgent(userAgent, options.initialCall, { ...options });
17
+ }
18
+ for (let i = 0;; i++) {
19
+ prompt = inquirer.prompt([
20
+ {
21
+ type: "input",
22
+ name: "question",
23
+ message: "💬",
24
+ default: i === 0 ? options?.defaultQuestion : undefined,
25
+ },
26
+ ]);
27
+ let question;
28
+ try {
29
+ question = (await prompt).question;
30
+ }
31
+ catch {
32
+ // ignore abort error from inquirer
33
+ }
34
+ if (!question?.trim())
35
+ continue;
36
+ const cmd = COMMANDS[question.trim()];
37
+ if (cmd) {
38
+ const result = cmd();
39
+ if (result.message)
40
+ console.log(result.message);
41
+ if (result?.exit)
42
+ break;
43
+ continue;
44
+ }
45
+ await callAgent(userAgent, question, { ...options });
46
+ }
47
+ }
48
+ async function callAgent(userAgent, input, options) {
49
+ const tracer = new TerminalTracer(userAgent.context, { verbose: options.verbose });
50
+ const { result, context } = await tracer.run(userAgent, options.inputKey && typeof input === "string"
51
+ ? { [options.inputKey]: input }
52
+ : createMessage(input));
53
+ console.log(`
54
+ ${chalk.grey(figures.tick)} 💬 ${inspect(input, { colors: true })}
55
+ ${chalk.grey(figures.tick)} 🤖 ${tracer.formatTokenUsage(context.usage)}
56
+ ${formatAIResponse(result)}
57
+ `);
58
+ }
59
+ const COMMANDS = {
60
+ "/exit": () => ({ exit: true }),
61
+ "/help": () => ({
62
+ message: `\
63
+ Commands:
64
+ /exit - exit the chat loop
65
+ /help - show this help message
66
+ `,
67
+ }),
68
+ };
69
+ function formatAIResponse({ [MESSAGE_KEY]: msg, ...message } = {}) {
70
+ const text = msg && typeof msg === "string" ? msg : undefined;
71
+ const json = Object.keys(message).length > 0 ? inspect(message, { colors: true }) : undefined;
72
+ return [text, json].filter(Boolean).join("\n");
73
+ }
@@ -0,0 +1 @@
1
+ export declare function parseDuration(duration: number): string;
@@ -0,0 +1,12 @@
1
+ export function parseDuration(duration) {
2
+ const milliseconds = duration % 1000;
3
+ const seconds = Math.floor(duration / 1000);
4
+ const minutes = Math.floor(seconds / 60);
5
+ const ms = Math.round(milliseconds / 10)
6
+ .toString()
7
+ .padStart(2, "0");
8
+ const s = `${Number.parseFloat(`${seconds % 60}.${ms}`)}s`;
9
+ if (minutes === 0)
10
+ return s;
11
+ return `${minutes}m${s}`;
12
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/cli",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "cli for AIGNE framework",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -34,27 +34,31 @@
34
34
  }
35
35
  },
36
36
  "dependencies": {
37
- "@modelcontextprotocol/sdk": "^1.8.0",
37
+ "@aigne/listr2": "^1.0.7",
38
+ "@modelcontextprotocol/sdk": "^1.9.0",
38
39
  "chalk": "^5.4.1",
39
40
  "commander": "^13.1.0",
40
41
  "express": "^5.1.0",
41
42
  "gradient-string": "^3.0.0",
42
- "inquirer": "^12.5.0",
43
- "openai": "^4.91.1",
43
+ "inquirer": "^12.5.2",
44
+ "openai": "^4.93.0",
45
+ "pretty-error": "^4.0.0",
46
+ "tar": "^7.4.3",
44
47
  "zod": "^3.24.2",
45
- "@aigne/core": "^1.6.0"
48
+ "@aigne/core": "^1.7.0"
46
49
  },
47
50
  "devDependencies": {
48
- "@types/bun": "^1.2.8",
51
+ "@types/archiver": "^6.0.3",
52
+ "@types/bun": "^1.2.9",
49
53
  "@types/express": "^5.0.1",
50
54
  "@types/gradient-string": "^1.1.6",
51
55
  "@types/node": "^22.14.0",
56
+ "archiver": "^7.0.1",
52
57
  "detect-port": "^2.1.0",
53
58
  "npm-run-all": "^4.1.5",
54
59
  "rimraf": "^6.0.1",
55
- "typescript": "^5.8.2",
56
- "ufo": "^1.6.0",
57
- "@aigne/test-utils": "^1.0.0"
60
+ "typescript": "^5.8.3",
61
+ "ufo": "^1.6.1"
58
62
  },
59
63
  "scripts": {
60
64
  "lint": "tsc --noEmit",