@aigne/cli 1.2.0 → 1.3.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,11 @@
1
- import { randomUUID } from "node:crypto";
2
- import { mkdir, rm } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
1
+ import { cp, mkdir, rm } from "node:fs/promises";
2
+ import { homedir } from "node:os";
4
3
  import { isAbsolute, join, resolve } from "node:path";
5
4
  import { ExecutionEngine } from "@aigne/core";
5
+ import { loadModel } from "@aigne/core/loader/index.js";
6
+ import { isNonNullable } from "@aigne/core/utils/type-utils.js";
6
7
  import { Command } from "commander";
8
+ import { isV1Package, toAIGNEPackage } from "../utils/agent-v1.js";
7
9
  import { downloadAndExtract } from "../utils/download.js";
8
10
  import { runChatLoopInTerminal } from "../utils/run-chat-loop.js";
9
11
  export function createRunCommand() {
@@ -11,6 +13,9 @@ export function createRunCommand() {
11
13
  .description("Run a chat loop with the specified agent")
12
14
  .argument("[path]", "Path to the agents directory or URL to aigne project", ".")
13
15
  .option("--agent <agent>", "Name of the agent to use (defaults to the first agent found)")
16
+ .option("--download-dir <dir>", "Directory to download the package to (defaults to the ~/.aigne/xxx)")
17
+ .option("--model-provider <provider>", "Model provider to use, available providers: openai, claude, xai (defaults to the aigne.yaml definition or openai)")
18
+ .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
19
  .action(async (path, options) => {
15
20
  if (path.startsWith("http")) {
16
21
  await downloadAndRunPackage(path, options);
@@ -23,7 +28,13 @@ export function createRunCommand() {
23
28
  .showSuggestionAfterError(true);
24
29
  }
25
30
  async function runEngine(originalPath, path, options) {
26
- const engine = await ExecutionEngine.load({ path });
31
+ if (options.modelName && !options.modelProvider) {
32
+ throw new Error("please specify --model-provider when using the --model-name option");
33
+ }
34
+ const model = options.modelProvider
35
+ ? await loadModel({ provider: options.modelProvider, name: options.modelName })
36
+ : undefined;
37
+ const engine = await ExecutionEngine.load({ path, model });
27
38
  let agent;
28
39
  if (options.agent) {
29
40
  agent = engine.agents[options.agent];
@@ -45,13 +56,33 @@ async function runEngine(originalPath, path, options) {
45
56
  await runChatLoopInTerminal(user, {});
46
57
  }
47
58
  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);
59
+ let dir;
60
+ let downloadDir;
61
+ if (options.downloadDir) {
62
+ dir = isAbsolute(options.downloadDir)
63
+ ? options.downloadDir
64
+ : resolve(process.cwd(), options.downloadDir);
65
+ downloadDir = join(dir, ".download");
66
+ }
67
+ else {
68
+ dir = getLocalPackagePathFromUrl(url);
69
+ downloadDir = getLocalPackagePathFromUrl(url, { subdir: ".download" });
53
70
  }
54
- finally {
55
- await rm(dir, { recursive: true, force: true });
71
+ // clean up the download directory
72
+ await rm(downloadDir, { recursive: true, force: true });
73
+ await mkdir(dir, { recursive: true });
74
+ await mkdir(downloadDir, { recursive: true });
75
+ await downloadAndExtract(url, downloadDir);
76
+ if (await isV1Package(downloadDir)) {
77
+ await toAIGNEPackage(downloadDir, dir);
56
78
  }
79
+ else {
80
+ await cp(downloadDir, dir, { recursive: true, force: true });
81
+ }
82
+ await runEngine(url, dir, options);
83
+ }
84
+ function getLocalPackagePathFromUrl(url, { subdir } = {}) {
85
+ const root = [homedir(), ".aigne", subdir].filter(isNonNullable);
86
+ const u = new URL(url);
87
+ return join(...root, u.hostname, u.pathname);
57
88
  }
@@ -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([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.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",