@aigne/cli 1.11.9 → 1.13.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,51 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.13.0](https://github.com/AIGNE-io/aigne-framework/compare/cli-v1.12.0...cli-v1.13.0) (2025-06-24)
4
+
5
+
6
+ ### Features
7
+
8
+ * support observability for cli and blocklet ([#155](https://github.com/AIGNE-io/aigne-framework/issues/155)) ([5baa705](https://github.com/AIGNE-io/aigne-framework/commit/5baa705a33cfdba1efc5ccbe18674c27513ca97d))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * The following workspace dependencies were updated
14
+ * dependencies
15
+ * @aigne/agent-library bumped to 1.15.0
16
+ * @aigne/anthropic bumped to 0.3.4
17
+ * @aigne/bedrock bumped to 0.3.4
18
+ * @aigne/core bumped to 1.22.0
19
+ * @aigne/deepseek bumped to 0.3.4
20
+ * @aigne/gemini bumped to 0.3.4
21
+ * @aigne/ollama bumped to 0.3.4
22
+ * @aigne/open-router bumped to 0.3.4
23
+ * @aigne/openai bumped to 0.3.4
24
+ * @aigne/xai bumped to 0.3.4
25
+
26
+ ## [1.12.0](https://github.com/AIGNE-io/aigne-framework/compare/cli-v1.11.9...cli-v1.12.0) (2025-06-20)
27
+
28
+
29
+ ### Features
30
+
31
+ * **cli:** support pass named input to agent by --input-xxx ([#167](https://github.com/AIGNE-io/aigne-framework/issues/167)) ([cda5bb6](https://github.com/AIGNE-io/aigne-framework/commit/cda5bb6baab680787de1a042664fe34c17a84bb1))
32
+
33
+
34
+ ### Dependencies
35
+
36
+ * The following workspace dependencies were updated
37
+ * dependencies
38
+ * @aigne/agent-library bumped to 1.14.0
39
+ * @aigne/anthropic bumped to 0.3.3
40
+ * @aigne/bedrock bumped to 0.3.3
41
+ * @aigne/core bumped to 1.21.0
42
+ * @aigne/deepseek bumped to 0.3.3
43
+ * @aigne/gemini bumped to 0.3.3
44
+ * @aigne/ollama bumped to 0.3.3
45
+ * @aigne/open-router bumped to 0.3.3
46
+ * @aigne/openai bumped to 0.3.3
47
+ * @aigne/xai bumped to 0.3.3
48
+
3
49
  ## [1.11.9](https://github.com/AIGNE-io/aigne-framework/compare/cli-v1.11.8...cli-v1.11.9) (2025-06-19)
4
50
 
5
51
 
@@ -10,14 +10,15 @@ import { Listr, PRESET_TIMER } from "@aigne/listr2";
10
10
  import { availableMemories, availableModels } from "../constants.js";
11
11
  import { isV1Package, toAIGNEPackage } from "../utils/agent-v1.js";
12
12
  import { downloadAndExtract } from "../utils/download.js";
13
- import { createRunAIGNECommand, parseModelOption, runAgentWithAIGNE, } from "../utils/run-with-aigne.js";
13
+ import { createRunAIGNECommand, parseAgentInputByCommander, parseModelOption, runAgentWithAIGNE, } from "../utils/run-with-aigne.js";
14
14
  export function createRunCommand() {
15
15
  return createRunAIGNECommand()
16
16
  .description("Run AIGNE from the specified agent")
17
- .argument("[path]", "Path to the agents directory or URL to aigne project", ".")
17
+ .option("--url, --path <path_or_url>", "Path to the agents directory or URL to aigne project", ".")
18
18
  .option("--entry-agent <entry-agent>", "Name of the agent to run (defaults to the first agent found)")
19
19
  .option("--cache-dir <dir>", "Directory to download the package to (defaults to the ~/.aigne/xxx)")
20
- .action(async (path, options) => {
20
+ .action(async (options) => {
21
+ const { path } = options;
21
22
  if (options.logLevel)
22
23
  logger.level = options.logLevel;
23
24
  const { cacheDir, dir } = prepareDirs(path, options);
@@ -78,8 +79,9 @@ export function createRunCommand() {
78
79
  }).run();
79
80
  assert(aigne);
80
81
  assert(agent);
82
+ const input = await parseAgentInputByCommander(agent, options);
81
83
  try {
82
- await runAgentWithAIGNE(aigne, agent, { ...options });
84
+ await runAgentWithAIGNE(aigne, agent, { ...options, input });
83
85
  }
84
86
  finally {
85
87
  await aigne.shutdown();
@@ -4,6 +4,7 @@ import { type Listr } from "@aigne/listr2";
4
4
  import { type AIGNEListrTaskWrapper } from "../utils/listr.js";
5
5
  export interface TerminalTracerOptions {
6
6
  printRequest?: boolean;
7
+ outputKey?: string;
7
8
  }
8
9
  export declare class TerminalTracer {
9
10
  readonly context: Context;
@@ -24,6 +25,7 @@ export declare class TerminalTracer {
24
25
  time?: boolean;
25
26
  }): string;
26
27
  private marked;
28
+ get outputKey(): string;
27
29
  formatRequest(agent: Agent, _context: Context, m?: Message): string | undefined;
28
30
  formatResult(agent: Agent, context: Context, m?: Message): string;
29
31
  }
@@ -1,6 +1,6 @@
1
1
  import { EOL } from "node:os";
2
2
  import { inspect } from "node:util";
3
- import { AIAgent, ChatModel, } from "@aigne/core";
3
+ import { AIAgent, ChatModel, DEFAULT_OUTPUT_KEY, } from "@aigne/core";
4
4
  import { LogLevel, logger } from "@aigne/core/utils/logger.js";
5
5
  import { promiseWithResolvers } from "@aigne/core/utils/promise.js";
6
6
  import { omit } from "@aigne/core/utils/type-utils.js";
@@ -19,6 +19,7 @@ export class TerminalTracer {
19
19
  }
20
20
  tasks = {};
21
21
  async run(agent, input) {
22
+ await this.context.observer?.serve();
22
23
  const context = this.context.newContext({ reset: true });
23
24
  const listr = new AIGNEListr({
24
25
  formatRequest: () => this.options.printRequest ? this.formatRequest(agent, context, input) : undefined,
@@ -86,7 +87,7 @@ export class TerminalTracer {
86
87
  context.on("agentSucceed", onAgentSucceed);
87
88
  context.on("agentFailed", onAgentFailed);
88
89
  try {
89
- const result = await listr.run(() => context.invoke(agent, input, { streaming: true }));
90
+ const result = await listr.run(() => context.invoke(agent, input, { streaming: true, newContext: false }));
90
91
  return { result, context };
91
92
  }
92
93
  finally {
@@ -121,7 +122,19 @@ export class TerminalTracer {
121
122
  title += ` ${this.formatTimeUsage(task.startTime, task.endTime)}`;
122
123
  return title;
123
124
  }
124
- marked = new Marked().use(markedTerminal({ forceHyperLink: false }));
125
+ marked = new Marked().use({
126
+ // marked-terminal does not support code block meta, so we need to strip it
127
+ walkTokens: (token) => {
128
+ if (token.type === "code") {
129
+ if (typeof token.lang === "string") {
130
+ token.lang = token.lang.trim().split(/\s+/)[0];
131
+ }
132
+ }
133
+ },
134
+ }, markedTerminal({ forceHyperLink: false }));
135
+ get outputKey() {
136
+ return this.options.outputKey || DEFAULT_OUTPUT_KEY;
137
+ }
125
138
  formatRequest(agent, _context, m = {}) {
126
139
  if (!logger.enabled(LogLevel.INFO))
127
140
  return;
@@ -134,16 +147,19 @@ export class TerminalTracer {
134
147
  return [prefix, [text, json].filter(Boolean).join(EOL)].join(" ");
135
148
  }
136
149
  formatResult(agent, context, m = {}) {
150
+ const { isTTY } = process.stdout;
137
151
  const outputKey = agent instanceof AIAgent ? agent.outputKey : undefined;
138
152
  const prefix = logger.enabled(LogLevel.INFO)
139
153
  ? `${chalk.grey(figures.tick)} 🤖 ${this.formatTokenUsage(context.usage)}`
140
154
  : null;
141
155
  const msg = outputKey ? m[outputKey] : undefined;
142
156
  const message = outputKey ? omit(m, outputKey) : m;
143
- const text = msg && typeof msg === "string" ? this.marked.parse(msg, { async: false }).trim() : undefined;
144
- const json = Object.keys(message).length > 0
145
- ? inspect(message, { colors: process.stdout.isTTY })
157
+ const text = msg && typeof msg === "string"
158
+ ? isTTY
159
+ ? this.marked.parse(msg, { async: false }).trim()
160
+ : msg
146
161
  : undefined;
162
+ const json = Object.keys(message).length > 0 ? inspect(message, { colors: isTTY }) : undefined;
147
163
  return [prefix, text, json].filter(Boolean).join(EOL);
148
164
  }
149
165
  }
@@ -1,4 +1,4 @@
1
- import { AIGNE, type Agent, type ChatModelOptions } from "@aigne/core";
1
+ import { AIGNE, type Agent, type ChatModelOptions, type Message } from "@aigne/core";
2
2
  import { LogLevel } from "@aigne/core/utils/logger.js";
3
3
  import { type PromiseOrValue } from "@aigne/core/utils/type-utils.js";
4
4
  import { Command } from "commander";
@@ -10,23 +10,32 @@ export interface RunAIGNECommandOptions {
10
10
  topP?: number;
11
11
  presencePenalty?: number;
12
12
  frequencyPenalty?: number;
13
- input?: string;
13
+ input?: string[];
14
+ format?: "text" | "json" | "yaml";
15
+ output?: string;
14
16
  logLevel?: LogLevel;
17
+ force?: boolean;
15
18
  }
16
19
  export declare const createRunAIGNECommand: (name?: string) => Command;
20
+ export declare function parseAgentInputByCommander(agent: Agent, options?: RunAIGNECommandOptions & {
21
+ inputKey?: string;
22
+ argv?: string[];
23
+ }): Promise<Message>;
17
24
  export declare const parseModelOption: (model?: string) => {
18
25
  provider: string | undefined;
19
26
  name: string | undefined;
20
27
  };
21
- export declare function runWithAIGNE(agentCreator: ((aigne: AIGNE) => PromiseOrValue<Agent>) | Agent, { argv, chatLoopOptions, modelOptions, }?: {
28
+ export declare function runWithAIGNE(agentCreator: ((aigne: AIGNE) => PromiseOrValue<Agent>) | Agent, { argv, chatLoopOptions, modelOptions, outputKey, }?: {
22
29
  argv?: typeof process.argv;
23
30
  chatLoopOptions?: ChatLoopOptions;
24
31
  modelOptions?: ChatModelOptions;
32
+ outputKey?: string;
25
33
  }): Promise<void>;
26
- export declare function runAgentWithAIGNE(aigne: AIGNE, agent: Agent, { chatLoopOptions, modelOptions, ...options }?: {
34
+ export declare function runAgentWithAIGNE(aigne: AIGNE, agent: Agent, { outputKey, chatLoopOptions, modelOptions, ...options }?: {
35
+ outputKey?: string;
27
36
  chatLoopOptions?: ChatLoopOptions;
28
37
  modelOptions?: ChatModelOptions;
29
- } & RunAIGNECommandOptions): Promise<{
30
- result: import("@aigne/core").Message;
31
- context: import("@aigne/core").Context<import("@aigne/core").UserContext>;
38
+ input?: Message;
39
+ } & Omit<RunAIGNECommandOptions, "input">): Promise<{
40
+ result: Message;
32
41
  } | undefined>;
@@ -1,18 +1,24 @@
1
+ import assert from "node:assert";
1
2
  import { fstat } from "node:fs";
3
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
4
+ import { dirname, isAbsolute, join } from "node:path";
2
5
  import { isatty } from "node:tty";
3
6
  import { promisify } from "node:util";
4
- import { AIGNE, UserAgent } from "@aigne/core";
7
+ import { exists } from "@aigne/agent-library/utils/fs.js";
8
+ import { AIGNE, DEFAULT_OUTPUT_KEY, UserAgent, readAllString, } from "@aigne/core";
5
9
  import { loadModel } from "@aigne/core/loader/index.js";
6
10
  import { LogLevel, getLevelFromEnv, logger } from "@aigne/core/utils/logger.js";
7
- import { readAllString } from "@aigne/core/utils/stream-utils.js";
8
- import { tryOrThrow } from "@aigne/core/utils/type-utils.js";
11
+ import { isEmpty, isNonNullable, tryOrThrow, } from "@aigne/core/utils/type-utils.js";
9
12
  import { Command } from "commander";
10
13
  import PrettyError from "pretty-error";
11
- import { ZodError, z } from "zod";
14
+ import { parse } from "yaml";
15
+ import { ZodError, ZodObject, z } from "zod";
12
16
  import { availableModels } from "../constants.js";
13
17
  import { TerminalTracer } from "../tracer/terminal.js";
14
18
  import { DEFAULT_CHAT_INPUT_KEY, runChatLoopInTerminal, } from "./run-chat-loop.js";
15
19
  export const createRunAIGNECommand = (name = "run") => new Command(name)
20
+ .allowUnknownOption(true)
21
+ .allowExcessArguments(true)
16
22
  .description("Run agent with AIGNE in terminal")
17
23
  .option("--chat", "Run chat loop in terminal", false)
18
24
  .option("--model <provider[:model]>", `AI model to use in format 'provider[:model]' where model is optional. Examples: 'openai' or 'openai:gpt-4o-mini'. Available providers: ${availableModels.map((i) => i.name.toLowerCase().replace(/ChatModel$/i, "")).join(", ")} (default: openai)`)
@@ -20,13 +26,73 @@ export const createRunAIGNECommand = (name = "run") => new Command(name)
20
26
  .option("--top-p <top-p>", "Top P (nucleus sampling) parameter for the model (controls diversity). Range: 0.0-1.0", customZodError("--top-p", (s) => z.coerce.number().min(0).max(1).parse(s)))
21
27
  .option("--presence-penalty <presence-penalty>", "Presence penalty for the model (penalizes repeating the same tokens). Range: -2.0 to 2.0", customZodError("--presence-penalty", (s) => z.coerce.number().min(-2).max(2).parse(s)))
22
28
  .option("--frequency-penalty <frequency-penalty>", "Frequency penalty for the model (penalizes frequency of token usage). Range: -2.0 to 2.0", customZodError("--frequency-penalty", (s) => z.coerce.number().min(-2).max(2).parse(s)))
23
- .option("--input -i <input>", "Input to the agent")
29
+ .option("--input -i <input...>", "Input to the agent, use @<file> to read from a file")
30
+ .option("--format <format>", "Input format for the agent (available: text, json, yaml default: text)")
31
+ .option("--output -o <output>", "Output file to save the result (default: stdout)")
32
+ .option("--output-key <output-key>", "Key in the result to save to the output file", DEFAULT_OUTPUT_KEY)
33
+ .option("--force", "Truncate the output file if it exists, and create directory if the output path is not exists", false)
24
34
  .option("--log-level <level>", `Log level for detailed debugging information. Values: ${Object.values(LogLevel).join(", ")}`, customZodError("--log-level", (s) => z.nativeEnum(LogLevel).parse(s)), getLevelFromEnv(logger.options.ns) || LogLevel.INFO);
35
+ export async function parseAgentInputByCommander(agent, options = {}) {
36
+ const cmd = new Command()
37
+ .description(`Run agent ${agent.name} with AIGNE`)
38
+ .allowUnknownOption(true)
39
+ .allowExcessArguments(true);
40
+ const inputSchemaShape = agent.inputSchema instanceof ZodObject ? Object.keys(agent.inputSchema.shape) : [];
41
+ for (const option of inputSchemaShape) {
42
+ cmd.option(`--input-${option} <${option}>`);
43
+ }
44
+ const input = await new Promise((resolve, reject) => {
45
+ cmd
46
+ .action(async (agentInputOptions) => {
47
+ try {
48
+ const input = Object.fromEntries((await Promise.all(Object.entries(agentInputOptions).map(async ([key, value]) => {
49
+ let k = key.replace(/^input/, "");
50
+ k = k.charAt(0).toLowerCase() + k.slice(1);
51
+ if (!k)
52
+ return null;
53
+ if (typeof value === "string" && value.startsWith("@")) {
54
+ value = await readFile(value.slice(1), "utf8");
55
+ }
56
+ return [k, value];
57
+ }))).filter(isNonNullable));
58
+ resolve(input);
59
+ }
60
+ catch (error) {
61
+ reject(error);
62
+ }
63
+ })
64
+ .parseAsync(options.argv ?? process.argv)
65
+ .catch((error) => reject(error));
66
+ });
67
+ const rawInput = options.input ||
68
+ (isatty(process.stdin.fd) || !(await stdinHasData())
69
+ ? null
70
+ : [await readAllString(process.stdin)]);
71
+ if (rawInput?.length) {
72
+ for (let raw of rawInput) {
73
+ if (raw.startsWith("@")) {
74
+ raw = await readFile(raw.slice(1), "utf8");
75
+ }
76
+ if (options.format === "json") {
77
+ Object.assign(input, JSON.parse(raw));
78
+ }
79
+ else if (options.format === "yaml") {
80
+ Object.assign(input, parse(raw));
81
+ }
82
+ else {
83
+ Object.assign(input, typeof options.inputKey === "string"
84
+ ? { [options.inputKey]: raw }
85
+ : { [DEFAULT_CHAT_INPUT_KEY]: raw });
86
+ }
87
+ }
88
+ }
89
+ return input;
90
+ }
25
91
  export const parseModelOption = (model) => {
26
92
  const { provider, name } = model?.match(/(?<provider>[^:]+)(:(?<name>(\S+)))?/)?.groups ?? {};
27
93
  return { provider, name };
28
94
  };
29
- export async function runWithAIGNE(agentCreator, { argv = process.argv, chatLoopOptions, modelOptions, } = {}) {
95
+ export async function runWithAIGNE(agentCreator, { argv = process.argv, chatLoopOptions, modelOptions, outputKey, } = {}) {
30
96
  await createRunAIGNECommand()
31
97
  .showHelpAfterError(true)
32
98
  .showSuggestionAfterError(true)
@@ -44,7 +110,23 @@ export async function runWithAIGNE(agentCreator, { argv = process.argv, chatLoop
44
110
  const aigne = new AIGNE({ model });
45
111
  try {
46
112
  const agent = typeof agentCreator === "function" ? await agentCreator(aigne) : agentCreator;
47
- await runAgentWithAIGNE(aigne, agent, { ...options, chatLoopOptions, modelOptions });
113
+ const input = await parseAgentInputByCommander(agent, {
114
+ ...options,
115
+ inputKey: chatLoopOptions?.inputKey,
116
+ });
117
+ if (isEmpty(input)) {
118
+ const defaultInput = chatLoopOptions?.initialCall || chatLoopOptions?.defaultQuestion;
119
+ Object.assign(input, typeof defaultInput === "string"
120
+ ? { [chatLoopOptions?.inputKey || DEFAULT_CHAT_INPUT_KEY]: defaultInput }
121
+ : defaultInput);
122
+ }
123
+ await runAgentWithAIGNE(aigne, agent, {
124
+ ...options,
125
+ outputKey,
126
+ chatLoopOptions,
127
+ modelOptions,
128
+ input,
129
+ });
48
130
  }
49
131
  finally {
50
132
  await aigne.shutdown();
@@ -59,7 +141,24 @@ export async function runWithAIGNE(agentCreator, { argv = process.argv, chatLoop
59
141
  function customZodError(label, fn) {
60
142
  return ((...args) => tryOrThrow(() => fn(...args), (e) => new Error(`${label} ${e instanceof ZodError ? e.issues[0]?.message : e.message}`)));
61
143
  }
62
- export async function runAgentWithAIGNE(aigne, agent, { chatLoopOptions, modelOptions, ...options } = {}) {
144
+ export async function runAgentWithAIGNE(aigne, agent, { outputKey, chatLoopOptions, modelOptions, ...options } = {}) {
145
+ if (options.output) {
146
+ const outputPath = isAbsolute(options.output)
147
+ ? options.output
148
+ : join(process.cwd(), options.output);
149
+ if (await exists(outputPath)) {
150
+ const s = await stat(outputPath);
151
+ if (!s.isFile())
152
+ throw new Error(`Output path ${outputPath} is not a file`);
153
+ if (s.size > 0 && !options.force) {
154
+ throw new Error(`Output file ${outputPath} already exists. Use --force to overwrite.`);
155
+ }
156
+ }
157
+ else {
158
+ await mkdir(dirname(outputPath), { recursive: true });
159
+ }
160
+ await writeFile(outputPath, "", "utf8");
161
+ }
63
162
  if (options.chat) {
64
163
  if (!isatty(process.stdout.fd)) {
65
164
  throw new Error("--chat mode requires a TTY terminal");
@@ -70,19 +169,20 @@ export async function runAgentWithAIGNE(aigne, agent, { chatLoopOptions, modelOp
70
169
  });
71
170
  return;
72
171
  }
73
- const input = options.input ||
74
- (isatty(process.stdin.fd) || !(await stdinHasData())
75
- ? null
76
- : await readAllString(process.stdin)) ||
77
- chatLoopOptions?.initialCall ||
78
- chatLoopOptions?.defaultQuestion ||
79
- {};
80
172
  const tracer = new TerminalTracer(aigne.newContext(), {
81
173
  printRequest: logger.enabled(LogLevel.INFO),
174
+ outputKey,
82
175
  });
83
- return await tracer.run(agent, typeof input === "string"
84
- ? { [chatLoopOptions?.inputKey || DEFAULT_CHAT_INPUT_KEY]: input }
85
- : input);
176
+ assert(options.input);
177
+ const { result } = await tracer.run(agent, options.input);
178
+ if (options.output) {
179
+ const message = result[outputKey || DEFAULT_OUTPUT_KEY];
180
+ const content = typeof message === "string" ? message : JSON.stringify(result, null, 2);
181
+ const path = isAbsolute(options.output) ? options.output : join(process.cwd(), options.output);
182
+ await mkdir(dirname(path), { recursive: true });
183
+ await writeFile(path, content, "utf8");
184
+ }
185
+ return { result };
86
186
  }
87
187
  async function stdinHasData() {
88
188
  const stats = await promisify(fstat)(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/cli",
3
- "version": "1.11.9",
3
+ "version": "1.13.0",
4
4
  "description": "cli for AIGNE framework",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -49,17 +49,18 @@
49
49
  "pretty-error": "^4.0.0",
50
50
  "tar": "^7.4.3",
51
51
  "wrap-ansi": "^9.0.0",
52
+ "yaml": "^2.7.1",
52
53
  "zod": "^3.24.4",
53
- "@aigne/agent-library": "^1.13.2",
54
- "@aigne/bedrock": "^0.3.2",
55
- "@aigne/anthropic": "^0.3.2",
56
- "@aigne/core": "^1.20.1",
57
- "@aigne/gemini": "^0.3.2",
58
- "@aigne/deepseek": "^0.3.2",
59
- "@aigne/ollama": "^0.3.2",
60
- "@aigne/open-router": "^0.3.2",
61
- "@aigne/openai": "^0.3.2",
62
- "@aigne/xai": "^0.3.2"
54
+ "@aigne/agent-library": "^1.15.0",
55
+ "@aigne/anthropic": "^0.3.4",
56
+ "@aigne/bedrock": "^0.3.4",
57
+ "@aigne/core": "^1.22.0",
58
+ "@aigne/deepseek": "^0.3.4",
59
+ "@aigne/gemini": "^0.3.4",
60
+ "@aigne/ollama": "^0.3.4",
61
+ "@aigne/open-router": "^0.3.4",
62
+ "@aigne/openai": "^0.3.4",
63
+ "@aigne/xai": "^0.3.4"
63
64
  },
64
65
  "devDependencies": {
65
66
  "@types/archiver": "^6.0.3",