@aigne/cli 1.27.1-1 → 1.28.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,31 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.28.0](https://github.com/AIGNE-io/aigne-framework/compare/cli-v1.27.0...cli-v1.28.0) (2025-07-31)
4
+
5
+
6
+ ### Features
7
+
8
+ * **cli:** support dynamic download and execution of doc-smith app ([#293](https://github.com/AIGNE-io/aigne-framework/issues/293)) ([4c40077](https://github.com/AIGNE-io/aigne-framework/commit/4c40077bacef076bc4b098879e948ef866218e39))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * The following workspace dependencies were updated
14
+ * dependencies
15
+ * @aigne/agent-library bumped to 1.21.7
16
+ * @aigne/agentic-memory bumped to 1.0.7
17
+ * @aigne/aigne-hub bumped to 0.3.1
18
+ * @aigne/anthropic bumped to 0.10.3
19
+ * @aigne/bedrock bumped to 0.8.7
20
+ * @aigne/core bumped to 1.40.0
21
+ * @aigne/deepseek bumped to 0.7.7
22
+ * @aigne/default-memory bumped to 1.0.7
23
+ * @aigne/gemini bumped to 0.8.7
24
+ * @aigne/ollama bumped to 0.7.7
25
+ * @aigne/open-router bumped to 0.7.7
26
+ * @aigne/openai bumped to 0.10.7
27
+ * @aigne/xai bumped to 0.7.7
28
+
3
29
  ## [1.27.0](https://github.com/AIGNE-io/aigne-framework/compare/cli-v1.26.0...cli-v1.27.0) (2025-07-30)
4
30
 
5
31
 
@@ -1,2 +1,19 @@
1
+ import { AIGNE, type Message } from "@aigne/core";
1
2
  import type { CommandModule } from "yargs";
2
3
  export declare function createAppCommands(): CommandModule[];
4
+ export declare function invokeCLIAgentFromDir(options: {
5
+ dir: string;
6
+ agent: string;
7
+ input: Message & {
8
+ input?: string[];
9
+ format?: "yaml" | "json";
10
+ };
11
+ }): Promise<void>;
12
+ export declare function loadApplication({ name, dir, }: {
13
+ name: string;
14
+ dir?: string;
15
+ }): Promise<{
16
+ aigne: AIGNE;
17
+ dir: string;
18
+ version: string;
19
+ }>;
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert";
2
2
  import { spawnSync } from "node:child_process";
3
- import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
+ import { readFile, stat, writeFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { extname, join } from "node:path";
6
6
  import { isatty } from "node:tty";
@@ -9,11 +9,12 @@ import { pick } from "@aigne/core/utils/type-utils.js";
9
9
  import { Listr, PRESET_TIMER } from "@aigne/listr2";
10
10
  import { joinURL } from "ufo";
11
11
  import { parse } from "yaml";
12
- import { ZodObject } from "zod";
12
+ import { ZodObject, ZodString } from "zod";
13
13
  import { availableModels } from "../constants.js";
14
14
  import { downloadAndExtract } from "../utils/download.js";
15
15
  import { loadAIGNE } from "../utils/load-aigne.js";
16
16
  import { runAgentWithAIGNE, stdinHasData } from "../utils/run-with-aigne.js";
17
+ import { serveMCPServerFromDir } from "./serve-mcp.js";
17
18
  const NPM_PACKAGE_CACHE_TIME_MS = 1000 * 60 * 60 * 24; // 1 day
18
19
  const builtinApps = [
19
20
  {
@@ -28,72 +29,10 @@ export function createAppCommands() {
28
29
  describe: app.describe,
29
30
  aliases: app.aliases,
30
31
  builder: async (yargs) => {
31
- const { aigne, dir, version } = await loadApplication({ name: "doc-smith" });
32
- for (const agent of aigne.agents) {
33
- const inputSchema = Object.entries(agent.inputSchema instanceof ZodObject ? agent.inputSchema.shape : {});
34
- yargs.command(agent.name, agent.description || "", (yargs) => {
35
- for (const [option, config] of inputSchema) {
36
- yargs.option(option, {
37
- // TODO: support more types
38
- type: "string",
39
- description: config.description,
40
- });
41
- if (!(config.isNullable() || config.isOptional())) {
42
- yargs.demandOption(option);
43
- }
44
- }
45
- yargs
46
- .option("input", {
47
- type: "array",
48
- description: "Input to the agent, use @<file> to read from a file",
49
- alias: ["i"],
50
- })
51
- .option("format", {
52
- type: "string",
53
- description: 'Input format, can be "json" or "yaml"',
54
- choices: ["json", "yaml"],
55
- });
56
- }, async (argv) => {
57
- try {
58
- const aigne = await loadAIGNE(dir);
59
- const _agent = aigne.agents[agent.name];
60
- assert(_agent, `Agent ${agent.name} not found in ${app.name}`);
61
- const input = pick(argv, inputSchema.map(([key]) => key));
62
- const rawInput = argv.input ||
63
- (isatty(process.stdin.fd) || !(await stdinHasData())
64
- ? null
65
- : [await readAllString(process.stdin)].filter(Boolean));
66
- if (rawInput) {
67
- for (let raw of rawInput) {
68
- let format = argv.format;
69
- if (raw.startsWith("@")) {
70
- raw = await readFile(raw.slice(1), "utf8");
71
- if (!format) {
72
- const ext = extname(raw);
73
- if (ext === ".json")
74
- format = "json";
75
- else if (ext === ".yaml" || ext === ".yml")
76
- format = "yaml";
77
- }
78
- }
79
- const inputKey = agent instanceof AIAgent ? agent.inputKey : undefined;
80
- if (format === "json") {
81
- Object.assign(input, JSON.parse(raw));
82
- }
83
- else if (format === "yaml") {
84
- Object.assign(input, parse(raw));
85
- }
86
- else if (inputKey) {
87
- Object.assign(input, { [inputKey]: raw });
88
- }
89
- }
90
- }
91
- await runAgentWithAIGNE(aigne, _agent, { input });
92
- }
93
- finally {
94
- await aigne.shutdown();
95
- }
96
- });
32
+ const { aigne, dir, version } = await loadApplication({ name: app.name });
33
+ yargs.command(serveMcpCommandModule({ name: app.name, dir }));
34
+ for (const agent of aigne.cli?.agents ?? []) {
35
+ yargs.command(agentCommandModule({ dir, agent }));
97
36
  }
98
37
  yargs.version(`${app.name} v${version}`);
99
38
  return yargs.demandCommand();
@@ -101,9 +40,126 @@ export function createAppCommands() {
101
40
  handler: () => { },
102
41
  }));
103
42
  }
104
- async function loadApplication({ name, }) {
43
+ const serveMcpCommandModule = ({ name, dir, }) => ({
44
+ command: "serve-mcp",
45
+ describe: `Serve ${name} a MCP server (streamable http)`,
46
+ builder: (yargs) => {
47
+ return yargs
48
+ .option("host", {
49
+ describe: "Host to run the MCP server on, use 0.0.0.0 to publicly expose the server",
50
+ type: "string",
51
+ default: "localhost",
52
+ })
53
+ .option("port", {
54
+ describe: "Port to run the MCP server on",
55
+ type: "number",
56
+ })
57
+ .option("pathname", {
58
+ describe: "Pathname to the service",
59
+ type: "string",
60
+ default: "/mcp",
61
+ });
62
+ },
63
+ handler: async (options) => {
64
+ await serveMCPServerFromDir({ ...options, dir });
65
+ },
66
+ });
67
+ const agentCommandModule = ({ dir, agent, }) => {
68
+ const inputSchema = agent.inputSchema instanceof ZodObject ? agent.inputSchema.shape : {};
69
+ return {
70
+ command: agent.name,
71
+ describe: agent.description || "",
72
+ builder: (yargs) => {
73
+ for (const [option, config] of Object.entries(inputSchema)) {
74
+ yargs.option(option, {
75
+ // TODO: support more types
76
+ type: "string",
77
+ description: config.description,
78
+ });
79
+ if (!(config.isNullable() || config.isOptional())) {
80
+ yargs.demandOption(option);
81
+ }
82
+ }
83
+ return yargs
84
+ .option("input", {
85
+ type: "array",
86
+ description: "Input to the agent, use @<file> to read from a file",
87
+ alias: ["i"],
88
+ })
89
+ .option("format", {
90
+ type: "string",
91
+ description: 'Input format, can be "json" or "yaml"',
92
+ choices: ["json", "yaml"],
93
+ });
94
+ },
95
+ handler: async (input) => {
96
+ await invokeCLIAgentFromDir({ dir, agent: agent.name, input });
97
+ },
98
+ };
99
+ };
100
+ export async function invokeCLIAgentFromDir(options) {
101
+ const aigne = await loadAIGNE(options.dir);
102
+ try {
103
+ const agent = aigne.cli.agents[options.agent];
104
+ assert(agent, `Agent ${options.agent} not found in ${options.dir}`);
105
+ const inputSchema = agent.inputSchema instanceof ZodObject ? agent.inputSchema.shape : {};
106
+ const input = Object.fromEntries(await Promise.all(Object.entries(pick(options.input, Object.keys(inputSchema))).map(async ([key, val]) => {
107
+ if (typeof val === "string" && val.startsWith("@")) {
108
+ const schema = inputSchema[key];
109
+ val = await readFileAsInput(val, {
110
+ format: schema instanceof ZodString ? "raw" : undefined,
111
+ });
112
+ }
113
+ return [key, val];
114
+ })));
115
+ const rawInput = options.input.input ||
116
+ (isatty(process.stdin.fd) || !(await stdinHasData())
117
+ ? null
118
+ : [await readAllString(process.stdin)].filter(Boolean));
119
+ if (rawInput) {
120
+ for (const raw of rawInput) {
121
+ const parsed = raw.startsWith("@")
122
+ ? await readFileAsInput(raw, { format: options.input.format })
123
+ : raw;
124
+ if (typeof parsed !== "string") {
125
+ Object.assign(input, parsed);
126
+ }
127
+ else {
128
+ const inputKey = agent instanceof AIAgent ? agent.inputKey : undefined;
129
+ if (inputKey) {
130
+ Object.assign(input, { [inputKey]: parsed });
131
+ }
132
+ }
133
+ }
134
+ }
135
+ await runAgentWithAIGNE(aigne, agent, { input });
136
+ }
137
+ finally {
138
+ await aigne.shutdown();
139
+ }
140
+ }
141
+ async function readFileAsInput(value, { format } = {}) {
142
+ if (value.startsWith("@")) {
143
+ const ext = extname(value);
144
+ value = await readFile(value.slice(1), "utf8");
145
+ if (!format) {
146
+ if (ext === ".json")
147
+ format = "json";
148
+ else if (ext === ".yaml" || ext === ".yml")
149
+ format = "yaml";
150
+ }
151
+ }
152
+ if (format === "json") {
153
+ return JSON.parse(value);
154
+ }
155
+ else if (format === "yaml") {
156
+ return parse(value);
157
+ }
158
+ return value;
159
+ }
160
+ export async function loadApplication({ name, dir, }) {
105
161
  name = `@aigne/${name}`;
106
- const dir = join(homedir(), ".aigne", "registry.npmjs.org", name);
162
+ dir ??= join(homedir(), ".aigne", "registry.npmjs.org", name);
107
163
  const check = await isInstallationAvailable(dir);
108
164
  if (check?.available) {
109
165
  return {
@@ -124,7 +180,7 @@ async function loadApplication({ name, }) {
124
180
  title: "Downloading application",
125
181
  skip: (ctx) => ctx.version === check?.version,
126
182
  task: async (ctx) => {
127
- await downloadApplication({ url: ctx.url, dir });
183
+ await downloadAndExtract(ctx.url, dir, { strip: 1 });
128
184
  },
129
185
  },
130
186
  {
@@ -148,12 +204,7 @@ async function loadApplication({ name, }) {
148
204
  };
149
205
  }
150
206
  async function isInstallationAvailable(dir, { cacheTimeMs = NPM_PACKAGE_CACHE_TIME_MS } = {}) {
151
- const s = await stat(join(dir, "package.json")).catch((error) => {
152
- if (error.code === "ENOENT") {
153
- return null;
154
- }
155
- throw error;
156
- });
207
+ const s = await stat(join(dir, "package.json")).catch(() => null);
157
208
  if (!s)
158
209
  return null;
159
210
  const version = safeParseJSON(await readFile(join(dir, "package.json"), "utf-8"))?.version;
@@ -166,10 +217,6 @@ async function isInstallationAvailable(dir, { cacheTimeMs = NPM_PACKAGE_CACHE_TI
166
217
  const available = installedAt ? now - installedAt < cacheTimeMs : false;
167
218
  return { version, available };
168
219
  }
169
- async function downloadApplication({ url, dir }) {
170
- await mkdir(dir, { recursive: true });
171
- await downloadAndExtract(url, dir, { strip: 1 });
172
- }
173
220
  async function installDependencies(dir) {
174
221
  const { stderr, status } = spawnSync("npm", ["install", "--omit", "dev"], {
175
222
  cwd: dir,
@@ -197,7 +244,5 @@ function safeParseJSON(raw) {
197
244
  try {
198
245
  return JSON.parse(raw);
199
246
  }
200
- catch {
201
- return null;
202
- }
247
+ catch { }
203
248
  }
@@ -31,7 +31,8 @@ export function createRunCommand({ aigneFilePath, } = {}) {
31
31
  .option("cache-dir", {
32
32
  describe: "Directory to download the package to (defaults to the ~/.aigne/xxx)",
33
33
  type: "string",
34
- });
34
+ })
35
+ .strict(false);
35
36
  },
36
37
  handler: async (argv) => {
37
38
  const options = argv;
@@ -5,7 +5,14 @@ interface ServeMCPOptions {
5
5
  port?: number;
6
6
  pathname: string;
7
7
  }
8
+ export declare const DEFAULT_PORT: () => number;
8
9
  export declare function createServeMCPCommand({ aigneFilePath, }?: {
9
10
  aigneFilePath?: string;
10
11
  }): CommandModule<unknown, ServeMCPOptions>;
12
+ export declare function serveMCPServerFromDir(options: {
13
+ dir: string;
14
+ host: string;
15
+ port?: number;
16
+ pathname: string;
17
+ }): Promise<void>;
11
18
  export {};
@@ -2,7 +2,7 @@ import { isAbsolute, resolve } from "node:path";
2
2
  import { tryOrThrow } from "@aigne/core/utils/type-utils.js";
3
3
  import { loadAIGNE } from "../utils/load-aigne.js";
4
4
  import { serveMCPServer } from "../utils/serve-mcp.js";
5
- const DEFAULT_PORT = () => tryOrThrow(() => {
5
+ export const DEFAULT_PORT = () => tryOrThrow(() => {
6
6
  const { PORT } = process.env;
7
7
  if (!PORT)
8
8
  return 3000;
@@ -41,15 +41,18 @@ export function createServeMCPCommand({ aigneFilePath, } = {}) {
41
41
  handler: async (options) => {
42
42
  const path = aigneFilePath || options.path;
43
43
  const absolutePath = isAbsolute(path) ? path : resolve(process.cwd(), path);
44
- const port = options.port || DEFAULT_PORT();
45
- const aigne = await loadAIGNE(absolutePath);
46
- await serveMCPServer({
47
- aigne,
48
- host: options.host,
49
- port,
50
- pathname: options.pathname,
51
- });
52
- console.log(`MCP server is running on http://${options.host}:${port}${options.pathname}`);
44
+ await serveMCPServerFromDir({ ...options, dir: absolutePath });
53
45
  },
54
46
  };
55
47
  }
48
+ export async function serveMCPServerFromDir(options) {
49
+ const port = options.port || DEFAULT_PORT();
50
+ const aigne = await loadAIGNE(options.dir);
51
+ await serveMCPServer({
52
+ aigne,
53
+ host: options.host,
54
+ port,
55
+ pathname: options.pathname,
56
+ });
57
+ console.log(`MCP server is running on http://${options.host}:${port}${options.pathname}`);
58
+ }
@@ -1,3 +1,4 @@
1
+ import { mkdir } from "node:fs/promises";
1
2
  import { Readable } from "node:stream";
2
3
  import { finished } from "node:stream/promises";
3
4
  import { x } from "tar";
@@ -12,6 +13,7 @@ export async function downloadAndExtract(url, dir, options = {}) {
12
13
  throw new Error(`Failed to download package from ${url}: Unexpected to get empty response`);
13
14
  }
14
15
  try {
16
+ await mkdir(dir, { recursive: true });
15
17
  await finished(Readable.fromWeb(response.body).pipe(x({ C: dir, ...options })));
16
18
  }
17
19
  catch (error) {
@@ -72,7 +72,7 @@ export function createMcpServer(aigne) {
72
72
  capabilities: { tools: {} },
73
73
  instructions: aigne.description,
74
74
  });
75
- for (const agent of aigne.agents) {
75
+ for (const agent of aigne.mcpServer?.agents ?? []) {
76
76
  const schema = agent.inputSchema;
77
77
  if (!(schema instanceof ZodObject))
78
78
  throw new Error("Agent input schema must be a ZodObject");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/cli",
3
- "version": "1.27.1-1",
3
+ "version": "1.28.0",
4
4
  "description": "cli for AIGNE framework",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -70,20 +70,20 @@
70
70
  "yaml": "^2.8.0",
71
71
  "yargs": "^18.0.0",
72
72
  "zod": "^3.25.67",
73
- "@aigne/agentic-memory": "^1.0.6",
74
- "@aigne/agent-library": "^1.21.6",
75
- "@aigne/bedrock": "^0.8.6",
76
- "@aigne/aigne-hub": "^0.3.0",
77
- "@aigne/core": "^1.39.0",
78
- "@aigne/deepseek": "^0.7.6",
79
- "@aigne/anthropic": "^0.10.2",
80
- "@aigne/default-memory": "^1.0.6",
81
- "@aigne/gemini": "^0.8.6",
73
+ "@aigne/agentic-memory": "^1.0.7",
74
+ "@aigne/agent-library": "^1.21.7",
75
+ "@aigne/aigne-hub": "^0.3.1",
76
+ "@aigne/anthropic": "^0.10.3",
77
+ "@aigne/bedrock": "^0.8.7",
78
+ "@aigne/core": "^1.40.0",
79
+ "@aigne/default-memory": "^1.0.7",
80
+ "@aigne/gemini": "^0.8.7",
81
+ "@aigne/deepseek": "^0.7.7",
82
82
  "@aigne/observability-api": "^0.8.2",
83
- "@aigne/open-router": "^0.7.6",
84
- "@aigne/openai": "^0.10.6",
85
- "@aigne/xai": "^0.7.6",
86
- "@aigne/ollama": "^0.7.6"
83
+ "@aigne/ollama": "^0.7.7",
84
+ "@aigne/open-router": "^0.7.7",
85
+ "@aigne/openai": "^0.10.7",
86
+ "@aigne/xai": "^0.7.7"
87
87
  },
88
88
  "devDependencies": {
89
89
  "@types/archiver": "^6.0.3",
@@ -4,6 +4,6 @@ chat_model:
4
4
  temperature: 0.8
5
5
  agents:
6
6
  - chat.yaml
7
- tools:
7
+ skills:
8
8
  - sandbox.js
9
9
  - filesystem.yaml
@@ -5,5 +5,5 @@ instructions: |
5
5
  Your goal is to assist users in finding the information they need and to engage in friendly conversation.
6
6
  input_key: message
7
7
  memory: true
8
- tools:
8
+ skills:
9
9
  - sandbox.js