@aigne/cli 1.2.0 → 1.3.1-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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.0](https://github.com/AIGNE-io/aigne-framework/compare/cli-v1.2.0...cli-v1.3.0) (2025-04-17)
4
+
5
+
6
+ ### Features
7
+
8
+ * **ci:** support coverage examples with model matrix ([#59](https://github.com/AIGNE-io/aigne-framework/issues/59)) ([1edd704](https://github.com/AIGNE-io/aigne-framework/commit/1edd70426b80a69e3751b2d5fe818297711d0777))
9
+ * **cli:** support convert agents from studio ([#64](https://github.com/AIGNE-io/aigne-framework/issues/64)) ([f544bc7](https://github.com/AIGNE-io/aigne-framework/commit/f544bc77a2fb07e034b317ceb6a46aadd35830c9))
10
+ * **cli:** support model and download customization for aigne run ([#61](https://github.com/AIGNE-io/aigne-framework/issues/61)) ([51f6619](https://github.com/AIGNE-io/aigne-framework/commit/51f6619e6c591a84f1f2339b26ef66d89fa9486e))
11
+
3
12
  ## [1.2.0](https://github.com/AIGNE-io/aigne-framework/compare/cli-v1.1.0...cli-v1.2.0) (2025-04-15)
4
13
 
5
14
 
@@ -1,9 +1,13 @@
1
- import { randomUUID } from "node:crypto";
2
- import { mkdir, rm } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
1
+ import assert from "node:assert";
2
+ import { cp, mkdir, rm } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
4
  import { isAbsolute, join, resolve } from "node:path";
5
5
  import { ExecutionEngine } from "@aigne/core";
6
+ import { loadModel } from "@aigne/core/loader/index.js";
7
+ import { isNonNullable } from "@aigne/core/utils/type-utils.js";
8
+ import { Listr, PRESET_TIMER } from "@aigne/listr2";
6
9
  import { Command } from "commander";
10
+ import { isV1Package, toAIGNEPackage } from "../utils/agent-v1.js";
7
11
  import { downloadAndExtract } from "../utils/download.js";
8
12
  import { runChatLoopInTerminal } from "../utils/run-chat-loop.js";
9
13
  export function createRunCommand() {
@@ -11,47 +15,117 @@ export function createRunCommand() {
11
15
  .description("Run a chat loop with the specified agent")
12
16
  .argument("[path]", "Path to the agents directory or URL to aigne project", ".")
13
17
  .option("--agent <agent>", "Name of the agent to use (defaults to the first agent found)")
18
+ .option("--download-dir <dir>", "Directory to download the package to (defaults to the ~/.aigne/xxx)")
19
+ .option("--model-provider <provider>", "Model provider to use, available providers: openai, claude, xai (defaults to the aigne.yaml definition or openai)")
20
+ .option("--model-name <model>", "Model name to use, available models depend on the provider (defaults to the aigne.yaml definition or gpt-4o-mini)")
14
21
  .action(async (path, options) => {
15
- if (path.startsWith("http")) {
16
- await downloadAndRunPackage(path, options);
17
- return;
18
- }
19
- const absolutePath = isAbsolute(path) ? path : resolve(process.cwd(), path);
20
- await runEngine(path, absolutePath, options);
22
+ const { downloadDir, dir } = prepareDirs(path, options);
23
+ const { engine, agent } = await new Listr([
24
+ {
25
+ title: "Prepare environment",
26
+ task: (_, task) => {
27
+ if (downloadDir) {
28
+ return task.newListr([
29
+ {
30
+ title: "Download package",
31
+ task: () => downloadPackage(path, downloadDir),
32
+ },
33
+ {
34
+ title: "Extract package",
35
+ task: () => extractPackage(downloadDir, dir),
36
+ },
37
+ ]);
38
+ }
39
+ },
40
+ },
41
+ {
42
+ title: "Initialize execution engine",
43
+ task: async (ctx) => {
44
+ const engine = await runEngine(dir, options);
45
+ ctx.engine = engine;
46
+ },
47
+ },
48
+ {
49
+ task: (ctx) => {
50
+ const { engine } = ctx;
51
+ assert(engine);
52
+ let agent;
53
+ if (options.agent) {
54
+ agent = engine.agents[options.agent];
55
+ if (!agent) {
56
+ console.error(`Agent "${options.agent}" not found in ${path}`);
57
+ console.log("Available agents:");
58
+ for (const agent of engine.agents) {
59
+ console.log(`- ${agent.name}`);
60
+ }
61
+ throw new Error(`Agent "${options.agent}" not found in ${path}`);
62
+ }
63
+ }
64
+ else {
65
+ agent = engine.agents[0];
66
+ if (!agent)
67
+ throw new Error(`No agents found in ${path}`);
68
+ }
69
+ ctx.agent = agent;
70
+ },
71
+ },
72
+ ], {
73
+ rendererOptions: {
74
+ collapseSubtasks: false,
75
+ timer: PRESET_TIMER,
76
+ },
77
+ }).run();
78
+ assert(engine);
79
+ assert(agent);
80
+ const user = engine.call(agent);
81
+ await runChatLoopInTerminal(user, {});
82
+ await engine.shutdown();
21
83
  })
22
84
  .showHelpAfterError(true)
23
85
  .showSuggestionAfterError(true);
24
86
  }
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
- }
87
+ async function runEngine(path, options) {
88
+ if (options.modelName && !options.modelProvider) {
89
+ throw new Error("please specify --model-provider when using the --model-name option");
90
+ }
91
+ const model = options.modelProvider
92
+ ? await loadModel({ provider: options.modelProvider, name: options.modelName })
93
+ : undefined;
94
+ return await ExecutionEngine.load({ path, model });
95
+ }
96
+ async function downloadPackage(url, downloadDir) {
97
+ await rm(downloadDir, { recursive: true, force: true });
98
+ await mkdir(downloadDir, { recursive: true });
99
+ await downloadAndExtract(url, downloadDir);
100
+ }
101
+ async function extractPackage(downloadDir, dir) {
102
+ if (await isV1Package(downloadDir)) {
103
+ await toAIGNEPackage(downloadDir, dir);
38
104
  }
39
105
  else {
40
- agent = engine.agents[0];
41
- if (!agent)
42
- throw new Error(`No agents found in ${originalPath}`);
106
+ await cp(downloadDir, dir, { recursive: true, force: true });
43
107
  }
44
- const user = engine.call(agent);
45
- await runChatLoopInTerminal(user, {});
46
108
  }
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);
109
+ function prepareDirs(path, options) {
110
+ let dir;
111
+ let downloadDir;
112
+ if (!path.startsWith("http")) {
113
+ dir = isAbsolute(path) ? path : resolve(process.cwd(), path);
53
114
  }
54
- finally {
55
- await rm(dir, { recursive: true, force: true });
115
+ else if (options.downloadDir) {
116
+ dir = isAbsolute(options.downloadDir)
117
+ ? options.downloadDir
118
+ : resolve(process.cwd(), options.downloadDir);
119
+ downloadDir = join(dir, ".download");
56
120
  }
121
+ else {
122
+ dir = getLocalPackagePathFromUrl(path);
123
+ downloadDir = getLocalPackagePathFromUrl(path, { subdir: ".download" });
124
+ }
125
+ return { downloadDir, dir };
126
+ }
127
+ function getLocalPackagePathFromUrl(url, { subdir } = {}) {
128
+ const root = [homedir(), ".aigne", subdir].filter(isNonNullable);
129
+ const u = new URL(url);
130
+ return join(...root, u.hostname, u.pathname);
57
131
  }
@@ -0,0 +1,134 @@
1
+ export declare function isV1Package(src: string): Promise<boolean>;
2
+ export declare function toAIGNEPackage(src: string, dst: string): Promise<void>;
3
+ export type Parameter = {
4
+ id: string;
5
+ key?: string;
6
+ hidden?: boolean;
7
+ required?: boolean;
8
+ placeholder?: string;
9
+ } & ({
10
+ type?: "string" | "number" | "boolean" | "language";
11
+ } | {
12
+ type?: "select";
13
+ options?: {
14
+ id: string;
15
+ label?: string;
16
+ value?: string;
17
+ }[];
18
+ });
19
+ export interface VariableTypeBase {
20
+ id: string;
21
+ name?: string;
22
+ description?: string;
23
+ required?: boolean;
24
+ hidden?: boolean;
25
+ }
26
+ export type OutputVariable = VariableTypeBase & ({
27
+ type?: undefined;
28
+ } | {
29
+ type: "string";
30
+ defaultValue?: string;
31
+ } | {
32
+ type: "number";
33
+ defaultValue?: number;
34
+ } | {
35
+ type: "boolean";
36
+ defaultValue?: boolean;
37
+ } | {
38
+ type: "object";
39
+ properties?: OutputVariable[];
40
+ } | {
41
+ type: "array";
42
+ element?: OutputVariable;
43
+ });
44
+ export type AgentV1 = {
45
+ id: string;
46
+ name?: string;
47
+ description?: string;
48
+ parameters?: Parameter[];
49
+ outputVariables?: OutputVariable[];
50
+ } & ({
51
+ type: "prompt";
52
+ prompts?: Prompt[];
53
+ temperature?: number;
54
+ topP?: number;
55
+ presencePenalty?: number;
56
+ frequencyPenalty?: number;
57
+ maxTokens?: number;
58
+ model?: string;
59
+ } | {
60
+ type: "router";
61
+ defaultToolId?: string;
62
+ prompt?: string;
63
+ decisionType?: "ai";
64
+ routes?: Tool[];
65
+ temperature?: number;
66
+ topP?: number;
67
+ presencePenalty?: number;
68
+ frequencyPenalty?: number;
69
+ maxTokens?: number;
70
+ model?: string;
71
+ } | {
72
+ type: "image";
73
+ prompt?: string;
74
+ model?: string;
75
+ n?: number;
76
+ quality?: string;
77
+ style?: string;
78
+ size?: string;
79
+ modelSettings?: {
80
+ [key: string]: unknown;
81
+ };
82
+ } | {
83
+ type: "api";
84
+ requestParameters?: {
85
+ id: string;
86
+ key?: string;
87
+ value?: string;
88
+ }[];
89
+ requestMethod?: string;
90
+ requestUrl?: string;
91
+ requestHeaders: {
92
+ id: string;
93
+ key?: string;
94
+ value?: string;
95
+ }[];
96
+ } | {
97
+ type: "function";
98
+ code?: string;
99
+ } | {
100
+ type: "callAgent";
101
+ agents?: Tool[];
102
+ });
103
+ export interface ProjectDefinitionV1 {
104
+ project: {
105
+ name?: string;
106
+ description?: string;
107
+ };
108
+ agents: AgentV1[];
109
+ }
110
+ export type Prompt = {
111
+ type: "message";
112
+ data: PromptMessage;
113
+ visibility?: "hidden";
114
+ } | {
115
+ type: "executeBlock";
116
+ visibility?: "hidden";
117
+ };
118
+ export type PromptMessage = {
119
+ id: string;
120
+ role: Role;
121
+ content?: string;
122
+ name?: string;
123
+ };
124
+ export type Role = "system" | "user" | "assistant";
125
+ export type Tool = {
126
+ blockletDid?: string;
127
+ projectId?: string;
128
+ id: string;
129
+ from?: "assistant" | "blockletAPI" | "knowledge";
130
+ parameters?: {
131
+ [key: string]: unknown;
132
+ };
133
+ functionName?: string;
134
+ };
@@ -0,0 +1,209 @@
1
+ import { readFile, stat, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { glob } from "glob";
4
+ import { parse, stringify } from "yaml";
5
+ export async function isV1Package(src) {
6
+ return stat(join(src, "project.yaml"))
7
+ .then((res) => res.isFile())
8
+ .catch((error) => {
9
+ if (error.code === "ENOENT")
10
+ return false;
11
+ throw error;
12
+ });
13
+ }
14
+ export async function toAIGNEPackage(src, dst) {
15
+ const definition = await loadAgentV1Package(src);
16
+ const aigne = {
17
+ chat_model: {
18
+ name: "gpt-4o-mini", // TODO: get from config
19
+ },
20
+ agents: [],
21
+ };
22
+ for (const agent of definition.agents) {
23
+ const { content } = await assistantToAigneV2(agent, definition);
24
+ const filename = getAgentFilename(agent);
25
+ await writeFile(join(dst, filename), content);
26
+ aigne.agents.push(filename);
27
+ }
28
+ await writeFile(join(dst, "aigne.yaml"), stringify(aigne));
29
+ }
30
+ async function loadAgentV1Package(path) {
31
+ const agentFilePaths = await glob("prompts/**/*.yaml", {
32
+ cwd: path,
33
+ });
34
+ const project = parse(await readFile(join(path, "project.yaml"), "utf8"));
35
+ const definition = {
36
+ project,
37
+ agents: [],
38
+ };
39
+ for (const filename of agentFilePaths) {
40
+ const agent = parse(await readFile(join(path, filename), "utf8"));
41
+ definition.agents.push(agent);
42
+ }
43
+ return definition;
44
+ }
45
+ function assistantToAigneV2(agent, project) {
46
+ const converter = RUNNABLE_MAP[agent.type];
47
+ if (!converter)
48
+ throw new Error(`Unsupported runnable type: ${agent.type}`);
49
+ return converter(agent, project);
50
+ }
51
+ const RUNNABLE_MAP = {
52
+ prompt: (agent) => {
53
+ if (agent.type !== "prompt")
54
+ throw new Error(`Expected runnable type 'prompt', but got '${agent.type}'`);
55
+ const obj = {
56
+ name: agent.name || agent.id,
57
+ description: agent.description,
58
+ input_schema: convertInputSchema(agent),
59
+ output_schema: convertOutputSchema(agent),
60
+ instructions: agent.prompts
61
+ ?.filter((i) => i.visibility !== "hidden" && i.type === "message")
62
+ .map((i) => i.data.content) // TODO: should support multiple messages with different roles
63
+ .join("\n"),
64
+ };
65
+ return {
66
+ content: stringify(obj),
67
+ };
68
+ },
69
+ function: async (agent) => {
70
+ if (agent.type !== "function")
71
+ throw new Error(`Expected runnable type 'function', but got '${agent.type}'`);
72
+ const inputNames = agent.parameters?.map((i) => i.key).filter(Boolean) ?? [];
73
+ return {
74
+ content: await formatCode(`\
75
+ export default async function agent({${inputNames.join(", ")}}) {
76
+ ${agent.code}
77
+ }
78
+
79
+ agent.agent_name = ${JSON.stringify(agent.name || agent.id)};
80
+
81
+ agent.description = ${agent.description ? JSON.stringify(agent.description) : "undefined"};
82
+
83
+ agent.input_schema = ${JSON.stringify(convertInputSchema(agent))};
84
+
85
+ agent.output_schema = ${JSON.stringify(convertOutputSchema(agent))};
86
+ `),
87
+ };
88
+ },
89
+ router: (agent, project) => {
90
+ if (agent.type !== "router")
91
+ throw new Error(`Expected runnable type 'router', but got '${agent.type}'`);
92
+ return {
93
+ content: stringify({
94
+ name: agent.name || agent.id,
95
+ description: agent.description,
96
+ instructions: agent.prompt,
97
+ input_schema: convertInputSchema(agent),
98
+ output_schema: convertOutputSchema(agent),
99
+ tools: agent.routes?.map((i) => {
100
+ const tool = project.agents.find((j) => j.id === i.id);
101
+ if (!tool)
102
+ throw new Error(`Tool ${i.id} not found in project definition`);
103
+ return getAgentFilename(tool);
104
+ }),
105
+ tool_choice: "router",
106
+ }),
107
+ };
108
+ },
109
+ };
110
+ function getAgentFilename(agent) {
111
+ switch (agent.type) {
112
+ case "prompt":
113
+ case "router":
114
+ return `${agent.name || agent.id}.yaml`;
115
+ case "function":
116
+ return `${agent.name || agent.id}.js`;
117
+ default:
118
+ throw new Error(`Unsupported agent type: ${agent.type}`);
119
+ }
120
+ }
121
+ async function formatCode(code) {
122
+ const [prettier, typescriptPlugin, estreePlugin] = await Promise.all([
123
+ import("prettier"),
124
+ import("prettier/plugins/typescript"),
125
+ import("prettier/plugins/estree"),
126
+ ]);
127
+ return prettier.format(code, {
128
+ parser: "typescript",
129
+ plugins: [typescriptPlugin, estreePlugin.default],
130
+ printWidth: 120,
131
+ useTabs: false,
132
+ tabWidth: 2,
133
+ trailingComma: "es5",
134
+ bracketSameLine: true,
135
+ semi: true,
136
+ singleQuote: true,
137
+ });
138
+ }
139
+ function convertInputSchema(agent) {
140
+ const parameters = (agent.parameters ?? []).filter((i) => !!i.key && !i.hidden);
141
+ const properties = parameters.map((i) => [i.key, parameterToJsonSchema(i)]);
142
+ return {
143
+ type: "object",
144
+ properties: Object.fromEntries(properties),
145
+ required: parameters.filter((i) => i.required).map((i) => i.key),
146
+ };
147
+ }
148
+ function parameterToJsonSchema(parameter) {
149
+ switch (parameter.type) {
150
+ case undefined:
151
+ case "string":
152
+ case "language":
153
+ return {
154
+ type: "string",
155
+ description: parameter.placeholder,
156
+ };
157
+ case "select":
158
+ return {
159
+ type: "string",
160
+ enum: parameter.options?.map((i) => i.value),
161
+ description: parameter.placeholder,
162
+ };
163
+ case "number":
164
+ case "boolean":
165
+ return {
166
+ type: parameter.type,
167
+ description: parameter.placeholder,
168
+ };
169
+ default:
170
+ throw new Error(`Unsupported parameter type: ${parameter.type}`);
171
+ }
172
+ }
173
+ function convertOutputSchema(agent) {
174
+ const outputs = agent.outputVariables?.filter((i) => !!i.name && !i.hidden) ?? [];
175
+ const properties = outputs
176
+ .map((i) => [i.name, variableTypeToJsonSchema(i)])
177
+ .filter((i) => !!i[1]);
178
+ return {
179
+ type: "object",
180
+ properties: Object.fromEntries(properties),
181
+ required: outputs.filter((i) => i.required).map((i) => i.name),
182
+ };
183
+ }
184
+ function variableTypeToJsonSchema(output) {
185
+ if (output.type === "object") {
186
+ if (!output.properties)
187
+ return undefined;
188
+ return {
189
+ type: "object",
190
+ properties: Object.fromEntries(output.properties
191
+ .filter((i) => !!i.name)
192
+ .map((i) => [i.name, variableTypeToJsonSchema(i)])
193
+ .filter((i) => !!i[1])),
194
+ required: output.properties.filter((i) => i.required && i.name).map((i) => i.name),
195
+ };
196
+ }
197
+ if (output.type === "array") {
198
+ if (!output.element)
199
+ return undefined;
200
+ return {
201
+ type: "array",
202
+ items: variableTypeToJsonSchema(output.element),
203
+ };
204
+ }
205
+ return {
206
+ type: output.type || "string",
207
+ description: output.description,
208
+ };
209
+ }
@@ -5,5 +5,6 @@ export interface ChatLoopOptions {
5
5
  defaultQuestion?: string;
6
6
  inputKey?: string;
7
7
  verbose?: boolean;
8
+ skipLoop?: boolean;
8
9
  }
9
10
  export declare function runChatLoopInTerminal(userAgent: input, options?: ChatLoopOptions): Promise<void>;
@@ -6,14 +6,17 @@ import chalk from "chalk";
6
6
  import inquirer from "inquirer";
7
7
  import { TerminalTracer } from "../tracer/terminal.js";
8
8
  export async function runChatLoopInTerminal(userAgent, options = {}) {
9
+ const { initialCall = process.env.INITIAL_CALL, skipLoop = process.env.SKIP_LOOP === "true" } = options;
9
10
  options.verbose ??= logger.enabled("aigne:core");
10
11
  // Disable the logger, use TerminalTracer instead
11
12
  logger.disable();
12
13
  let prompt;
13
14
  if (options?.welcome)
14
15
  console.log(options.welcome);
15
- if (options?.initialCall) {
16
- await callAgent(userAgent, options.initialCall, { ...options });
16
+ if (initialCall) {
17
+ await callAgent(userAgent, initialCall, { ...options });
18
+ if (skipLoop)
19
+ return;
17
20
  }
18
21
  for (let i = 0;; i++) {
19
22
  prompt = inquirer.prompt([
@@ -51,7 +54,6 @@ async function callAgent(userAgent, input, options) {
51
54
  ? { [options.inputKey]: input }
52
55
  : createMessage(input));
53
56
  console.log(`
54
- ${chalk.grey(figures.tick)} 💬 ${inspect(input, { colors: true })}
55
57
  ${chalk.grey(figures.tick)} 🤖 ${tracer.formatTokenUsage(context.usage)}
56
58
  ${formatAIResponse(result)}
57
59
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.1-0",
4
4
  "description": "cli for AIGNE framework",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -39,20 +39,23 @@
39
39
  "chalk": "^5.4.1",
40
40
  "commander": "^13.1.0",
41
41
  "express": "^5.1.0",
42
+ "glob": "^11.0.1",
42
43
  "gradient-string": "^3.0.0",
43
44
  "inquirer": "^12.5.2",
44
- "openai": "^4.93.0",
45
+ "openai": "^4.94.0",
46
+ "prettier": "^3.5.3",
45
47
  "pretty-error": "^4.0.0",
46
48
  "tar": "^7.4.3",
47
49
  "zod": "^3.24.2",
48
- "@aigne/core": "^1.7.0"
50
+ "@aigne/core": "^1.8.0"
49
51
  },
50
52
  "devDependencies": {
51
53
  "@types/archiver": "^6.0.3",
52
54
  "@types/bun": "^1.2.9",
53
55
  "@types/express": "^5.0.1",
56
+ "@types/glob": "^8.1.0",
54
57
  "@types/gradient-string": "^1.1.6",
55
- "@types/node": "^22.14.0",
58
+ "@types/node": "^22.14.1",
56
59
  "archiver": "^7.0.1",
57
60
  "detect-port": "^2.1.0",
58
61
  "npm-run-all": "^4.1.5",