@aigne/cli 1.3.1-1 → 1.5.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,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.5.0](https://github.com/AIGNE-io/aigne-framework/compare/cli-v1.4.0...cli-v1.5.0) (2025-04-22)
4
+
5
+
6
+ ### Features
7
+
8
+ * **cli:** render output message with markdown highlight ([#76](https://github.com/AIGNE-io/aigne-framework/issues/76)) ([b2a793a](https://github.com/AIGNE-io/aigne-framework/commit/b2a793a638e5f95d3f68be80f907da40bd7e624a))
9
+ * **stream:** add streaming output support for agent ([#73](https://github.com/AIGNE-io/aigne-framework/issues/73)) ([5f3ea4b](https://github.com/AIGNE-io/aigne-framework/commit/5f3ea4bccda7c8c457d6e9518b3d6a8b254ec041))
10
+
11
+ ## [1.4.0](https://github.com/AIGNE-io/aigne-framework/compare/cli-v1.3.0...cli-v1.4.0) (2025-04-20)
12
+
13
+
14
+ ### Features
15
+
16
+ * **core:** add model adapters for DeepSeek, Gemini, OpenRouter, and Ollama ([#53](https://github.com/AIGNE-io/aigne-framework/issues/53)) ([5d40546](https://github.com/AIGNE-io/aigne-framework/commit/5d40546bd5ddb70233d27ea3b20e5711b2af320a))
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * **cli:** display progressing for `run` command ([#68](https://github.com/AIGNE-io/aigne-framework/issues/68)) ([e3d2193](https://github.com/AIGNE-io/aigne-framework/commit/e3d21930bc2cf20edeb0ad7123e9e87e3e0ea653))
22
+ * **cli:** ensure dir exists before extract package ([#70](https://github.com/AIGNE-io/aigne-framework/issues/70)) ([5ebe56d](https://github.com/AIGNE-io/aigne-framework/commit/5ebe56d3483d4309d9e39ab0566d353b3787edce))
23
+
3
24
  ## [1.3.0](https://github.com/AIGNE-io/aigne-framework/compare/cli-v1.2.0...cli-v1.3.0) (2025-04-17)
4
25
 
5
26
 
@@ -1,8 +1,10 @@
1
- import { type Agent, type Context, type Message } from "@aigne/core";
1
+ import { type Agent, type Context, MESSAGE_KEY, type Message } from "@aigne/core";
2
2
  import type { ContextUsage } from "@aigne/core/execution-engine/usage";
3
3
  import { type DefaultRenderer, Listr, type ListrRenderer, type ListrTaskWrapper } from "@aigne/listr2";
4
+ import { promiseWithResolvers } from "../utils/promise-with-resolvers.js";
4
5
  export interface TerminalTracerOptions {
5
6
  verbose?: boolean;
7
+ aiResponsePrefix?: (context: Context) => string;
6
8
  }
7
9
  export declare class TerminalTracer {
8
10
  readonly context: Context;
@@ -11,10 +13,10 @@ export declare class TerminalTracer {
11
13
  private spinner;
12
14
  private tasks;
13
15
  run(agent: Agent, input: Message): Promise<{
14
- result: any;
16
+ result: Message;
15
17
  context: Context;
16
18
  }>;
17
- protected newListr(): MyListr;
19
+ protected wrap(str: string): string;
18
20
  formatAgentStartedOutput(agent: Agent, data: Message): string;
19
21
  formatAgentSucceedOutput(agent: Agent, data: Message): string;
20
22
  formatAgentFailedOutput(agent: Agent, data: Error): string;
@@ -28,9 +30,11 @@ export declare class TerminalTracer {
28
30
  usage?: boolean;
29
31
  time?: boolean;
30
32
  }): string;
33
+ private marked;
34
+ formatAIResponse({ [MESSAGE_KEY]: msg, ...message }?: Message): string;
31
35
  }
32
- type Task = ReturnType<typeof Promise.withResolvers<void>> & {
33
- listr: ReturnType<typeof Promise.withResolvers<{
36
+ type Task = ReturnType<typeof promiseWithResolvers<void>> & {
37
+ listr: ReturnType<typeof promiseWithResolvers<{
34
38
  ctx: object;
35
39
  subtask: Listr;
36
40
  taskWrapper: ListrTaskWrapper<unknown, typeof DefaultRenderer, typeof ListrRenderer>;
@@ -42,11 +46,4 @@ type Task = ReturnType<typeof Promise.withResolvers<void>> & {
42
46
  [key: string]: string;
43
47
  };
44
48
  };
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
49
  export {};
@@ -1,8 +1,14 @@
1
+ import { EOL } from "node:os";
1
2
  import { inspect } from "node:util";
2
- import { ChatModel, } from "@aigne/core";
3
+ import { ChatModel, MESSAGE_KEY, } from "@aigne/core";
4
+ import { mergeAgentResponseChunk, readableStreamToAsyncIterator, } from "@aigne/core/utils/stream-utils.js";
3
5
  import { Listr, ListrDefaultRendererLogLevels, Spinner, figures, } from "@aigne/listr2";
4
6
  import chalk from "chalk";
7
+ import { Marked } from "marked";
8
+ import { markedTerminal } from "marked-terminal";
9
+ import wrap from "wrap-ansi";
5
10
  import { z } from "zod";
11
+ import { promiseWithResolvers } from "../utils/promise-with-resolvers.js";
6
12
  import { parseDuration } from "../utils/time.js";
7
13
  const DEBUG_DEPTH = z.number().int().default(2).safeParse(Number(process.env.DEBUG_DEPTH)).data;
8
14
  export class TerminalTracer {
@@ -15,91 +21,18 @@ export class TerminalTracer {
15
21
  spinner = new Spinner();
16
22
  tasks = {};
17
23
  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([], {
24
+ this.spinner.start();
25
+ const context = this.context.newContext({ reset: true });
26
+ const listr = new AIGNEListr({
27
+ formatResult: (result) => {
28
+ return [
29
+ this.wrap(this.options.aiResponsePrefix?.(context) || ""),
30
+ this.wrap(this.formatAIResponse(result)),
31
+ ];
32
+ },
33
+ }, [], {
102
34
  concurrent: true,
35
+ forceTTY: process.env.CI === "true",
103
36
  rendererOptions: {
104
37
  collapseSubtasks: false,
105
38
  writeBottomBarDirectly: true,
@@ -109,6 +42,92 @@ export class TerminalTracer {
109
42
  },
110
43
  },
111
44
  });
45
+ const onAgentStarted = async ({ contextId, parentContextId, agent, input, timestamp, }) => {
46
+ const task = {
47
+ ...promiseWithResolvers(),
48
+ listr: promiseWithResolvers(),
49
+ startTime: timestamp,
50
+ };
51
+ this.tasks[contextId] = task;
52
+ const listrTask = {
53
+ title: this.formatTaskTitle(agent),
54
+ task: (ctx, taskWrapper) => {
55
+ const subtask = taskWrapper.newListr([{ task: () => task.promise }]);
56
+ task.listr.resolve({ subtask, taskWrapper, ctx });
57
+ return subtask;
58
+ },
59
+ rendererOptions: {
60
+ persistentOutput: true,
61
+ outputBar: Number.POSITIVE_INFINITY,
62
+ bottomBar: Number.POSITIVE_INFINITY,
63
+ },
64
+ };
65
+ const parentTask = parentContextId ? this.tasks[parentContextId] : undefined;
66
+ if (parentTask) {
67
+ parentTask.listr.promise.then(({ subtask }) => {
68
+ subtask.add(listrTask);
69
+ });
70
+ }
71
+ else {
72
+ listr.add(listrTask);
73
+ }
74
+ const { taskWrapper } = await task.listr.promise;
75
+ if (this.options.verbose) {
76
+ taskWrapper.output = this.formatAgentStartedOutput(agent, input);
77
+ }
78
+ };
79
+ const onAgentSucceed = async ({ agent, contextId, parentContextId, output, timestamp, }) => {
80
+ const task = this.tasks[contextId];
81
+ if (!task)
82
+ return;
83
+ task.endTime = timestamp;
84
+ const { taskWrapper, ctx } = await task.listr.promise;
85
+ if (agent instanceof ChatModel) {
86
+ const { usage, model } = output;
87
+ task.usage = usage;
88
+ task.extraTitleMetadata ??= {};
89
+ if (model)
90
+ task.extraTitleMetadata.model = model;
91
+ }
92
+ taskWrapper.title = this.formatTaskTitle(agent, { task, usage: true, time: true });
93
+ if (this.options.verbose) {
94
+ taskWrapper.output = this.formatAgentSucceedOutput(agent, output);
95
+ }
96
+ if (!parentContextId || !this.tasks[parentContextId]) {
97
+ Object.assign(ctx, output);
98
+ }
99
+ task.resolve();
100
+ };
101
+ const onAgentFailed = async ({ agent, contextId, error, timestamp, }) => {
102
+ const task = this.tasks[contextId];
103
+ if (!task)
104
+ return;
105
+ task.endTime = timestamp;
106
+ const { taskWrapper } = await task.listr.promise;
107
+ taskWrapper.title = this.formatTaskTitle(agent, { task, usage: true, time: true });
108
+ taskWrapper.output = this.formatAgentFailedOutput(agent, error);
109
+ task.reject(error);
110
+ };
111
+ context.on("agentStarted", onAgentStarted);
112
+ context.on("agentSucceed", onAgentSucceed);
113
+ context.on("agentFailed", onAgentFailed);
114
+ try {
115
+ const stream = await context.call(agent, input, { streaming: true });
116
+ const result = await listr.run(stream);
117
+ return { result, context };
118
+ }
119
+ finally {
120
+ this.spinner.stop();
121
+ context.off("agentStarted", onAgentStarted);
122
+ context.off("agentSucceed", onAgentSucceed);
123
+ context.off("agentFailed", onAgentFailed);
124
+ }
125
+ }
126
+ wrap(str) {
127
+ return wrap(str, process.stdout.columns ?? 80, {
128
+ hard: true,
129
+ trim: false,
130
+ });
112
131
  }
113
132
  formatAgentStartedOutput(agent, data) {
114
133
  return `\
@@ -154,26 +173,49 @@ ${this.formatMessage(data)}`;
154
173
  title += ` ${this.formatTimeUsage(task.startTime, task.endTime)}`;
155
174
  return title;
156
175
  }
176
+ marked = new Marked().use(markedTerminal());
177
+ formatAIResponse({ [MESSAGE_KEY]: msg, ...message } = {}) {
178
+ const text = msg && typeof msg === "string" ? this.marked.parse(msg, { async: false }).trim() : undefined;
179
+ const json = Object.keys(message).length > 0 ? inspect(message, { colors: true }) : undefined;
180
+ return [text, json].filter(Boolean).join("\n");
181
+ }
157
182
  }
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
- }
183
+ class AIGNEListr extends Listr {
184
+ myOptions;
185
+ result = {};
186
+ isStreamRunning = false;
187
+ constructor(myOptions, ...args) {
188
+ super(...args);
189
+ this.myOptions = myOptions;
190
+ const renderer = new this.rendererClass(this.tasks, this.rendererClassOptions, this.events);
191
+ const spinner = renderer.spinner;
192
+ // Override the `create` method of renderer to customize the output
193
+ const create = renderer.create;
194
+ renderer.create = (...args) => {
195
+ const [tasks, output] = create.call(renderer, ...args);
196
+ const l = [
197
+ "",
198
+ tasks,
199
+ "",
200
+ ...[this.myOptions.formatResult(this.result)].flat(),
201
+ this.isStreamRunning ? spinner.fetch() : "",
202
+ ];
203
+ return [l.join(EOL), output];
204
+ };
205
+ // @ts-ignore initialize the renderer
206
+ this.renderer = renderer;
166
207
  }
167
- add(...args) {
168
- const result = super.add(...args);
169
- this.resolveWaitingTask();
170
- return result;
208
+ async run(stream) {
209
+ this.add({ task: () => this.extractStream(stream) });
210
+ return await super.run().then(() => ({ ...this.result }));
171
211
  }
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);
212
+ async extractStream(stream) {
213
+ this.isStreamRunning = true;
214
+ this.result = {};
215
+ for await (const value of readableStreamToAsyncIterator(stream)) {
216
+ mergeAgentResponseChunk(this.result, value);
217
+ }
218
+ this.isStreamRunning = false;
219
+ return this.result;
178
220
  }
179
221
  }
@@ -0,0 +1,6 @@
1
+ export interface PromiseWithResolvers<T> {
2
+ promise: Promise<T>;
3
+ resolve: (value: T | PromiseLike<T>) => void;
4
+ reject: (reason?: unknown) => void;
5
+ }
6
+ export declare function promiseWithResolvers<T = void>(): PromiseWithResolvers<T>;
@@ -0,0 +1,9 @@
1
+ export function promiseWithResolvers() {
2
+ let resolve;
3
+ let reject;
4
+ const promise = new Promise((res, rej) => {
5
+ resolve = res;
6
+ reject = rej;
7
+ });
8
+ return { promise, resolve, reject };
9
+ }
@@ -1,5 +1,4 @@
1
- import { inspect } from "node:util";
2
- import { MESSAGE_KEY, createMessage } from "@aigne/core";
1
+ import { createMessage } from "@aigne/core";
3
2
  import { logger } from "@aigne/core/utils/logger.js";
4
3
  import { figures } from "@aigne/listr2";
5
4
  import chalk from "chalk";
@@ -49,14 +48,15 @@ export async function runChatLoopInTerminal(userAgent, options = {}) {
49
48
  }
50
49
  }
51
50
  async function callAgent(userAgent, input, options) {
52
- const tracer = new TerminalTracer(userAgent.context, { verbose: options.verbose });
53
- const { result, context } = await tracer.run(userAgent, options.inputKey && typeof input === "string"
51
+ const tracer = new TerminalTracer(userAgent.context, {
52
+ verbose: options.verbose,
53
+ aiResponsePrefix: (context) => {
54
+ return `${chalk.grey(figures.tick)} 🤖 ${tracer.formatTokenUsage(context.usage)}`;
55
+ },
56
+ });
57
+ await tracer.run(userAgent, options.inputKey && typeof input === "string"
54
58
  ? { [options.inputKey]: input }
55
59
  : createMessage(input));
56
- console.log(`
57
- ${chalk.grey(figures.tick)} 🤖 ${tracer.formatTokenUsage(context.usage)}
58
- ${formatAIResponse(result)}
59
- `);
60
60
  }
61
61
  const COMMANDS = {
62
62
  "/exit": () => ({ exit: true }),
@@ -68,8 +68,3 @@ Commands:
68
68
  `,
69
69
  }),
70
70
  };
71
- function formatAIResponse({ [MESSAGE_KEY]: msg, ...message } = {}) {
72
- const text = msg && typeof msg === "string" ? msg : undefined;
73
- const json = Object.keys(message).length > 0 ? inspect(message, { colors: true }) : undefined;
74
- return [text, json].filter(Boolean).join("\n");
75
- }
@@ -3,6 +3,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
4
4
  import express from "express";
5
5
  import { ZodObject } from "zod";
6
+ import { promiseWithResolvers } from "./promise-with-resolvers.js";
6
7
  export async function serveMCPServer({ engine, port }) {
7
8
  const server = new McpServer({
8
9
  name: engine.name || "aigne-mcp-server",
@@ -53,7 +54,7 @@ export async function serveMCPServer({ engine, port }) {
53
54
  .status(error instanceof HttpError ? error.status : 500)
54
55
  .json({ error: { message: error.message } });
55
56
  }));
56
- const { promise, resolve, reject } = Promise.withResolvers();
57
+ const { promise, resolve, reject } = promiseWithResolvers();
57
58
  const httpServer = app.listen(port, (error) => {
58
59
  if (error)
59
60
  reject(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/cli",
3
- "version": "1.3.1-1",
3
+ "version": "1.5.0",
4
4
  "description": "cli for AIGNE framework",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -34,7 +34,7 @@
34
34
  }
35
35
  },
36
36
  "dependencies": {
37
- "@aigne/listr2": "^1.0.7",
37
+ "@aigne/listr2": "^1.0.8",
38
38
  "@modelcontextprotocol/sdk": "^1.9.0",
39
39
  "chalk": "^5.4.1",
40
40
  "commander": "^13.1.0",
@@ -42,12 +42,14 @@
42
42
  "glob": "^11.0.1",
43
43
  "gradient-string": "^3.0.0",
44
44
  "inquirer": "^12.5.2",
45
+ "marked": "^15.0.9",
46
+ "marked-terminal": "^7.3.0",
45
47
  "openai": "^4.94.0",
46
48
  "prettier": "^3.5.3",
47
49
  "pretty-error": "^4.0.0",
48
50
  "tar": "^7.4.3",
49
51
  "zod": "^3.24.2",
50
- "@aigne/core": "^1.8.0"
52
+ "@aigne/core": "^1.10.0"
51
53
  },
52
54
  "devDependencies": {
53
55
  "@types/archiver": "^6.0.3",