@aigne/cli 1.36.4 → 1.38.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,64 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.38.0](https://github.com/AIGNE-io/aigne-framework/compare/cli-v1.37.1...cli-v1.38.0) (2025-08-20)
4
+
5
+
6
+ ### Features
7
+
8
+ * add ImageModel/ImageAgent support ([#383](https://github.com/AIGNE-io/aigne-framework/issues/383)) ([96a2093](https://github.com/AIGNE-io/aigne-framework/commit/96a209368d91d98f47db6de1e404640368a86fa8))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * The following workspace dependencies were updated
14
+ * dependencies
15
+ * @aigne/agent-library bumped to 1.21.24
16
+ * @aigne/agentic-memory bumped to 1.0.24
17
+ * @aigne/aigne-hub bumped to 0.6.6
18
+ * @aigne/core bumped to 1.53.0
19
+ * @aigne/default-memory bumped to 1.1.6
20
+ * @aigne/openai bumped to 0.12.0
21
+
22
+ ## [1.37.1](https://github.com/AIGNE-io/aigne-framework/compare/cli-v1.37.0...cli-v1.37.1) (2025-08-20)
23
+
24
+
25
+ ### Dependencies
26
+
27
+ * The following workspace dependencies were updated
28
+ * dependencies
29
+ * @aigne/agent-library bumped to 1.21.23
30
+ * @aigne/agentic-memory bumped to 1.0.23
31
+ * @aigne/aigne-hub bumped to 0.6.5
32
+ * @aigne/core bumped to 1.52.0
33
+ * @aigne/default-memory bumped to 1.1.5
34
+ * @aigne/observability-api bumped to 0.9.1
35
+ * @aigne/openai bumped to 0.11.5
36
+
37
+ ## [1.37.0](https://github.com/AIGNE-io/aigne-framework/compare/cli-v1.36.4...cli-v1.37.0) (2025-08-18)
38
+
39
+
40
+ ### Features
41
+
42
+ * **cli:** add support for array inputs in CLI arguments ([#378](https://github.com/AIGNE-io/aigne-framework/issues/378)) ([827ae11](https://github.com/AIGNE-io/aigne-framework/commit/827ae112de8d1a2e997b272b759090b6e5b8d395))
43
+ * **cli:** support hide or collapse task for agents in CLI ([#381](https://github.com/AIGNE-io/aigne-framework/issues/381)) ([05b372d](https://github.com/AIGNE-io/aigne-framework/commit/05b372d431a862f7cdfa2a90bb4b7b2379bf97ab))
44
+
45
+
46
+ ### Bug Fixes
47
+
48
+ * **cli:** only log API requests at info level and above ([#376](https://github.com/AIGNE-io/aigne-framework/issues/376)) ([03fc4d9](https://github.com/AIGNE-io/aigne-framework/commit/03fc4d9aad6e81aeae3b2eb02a62f7acade3bd77))
49
+
50
+
51
+ ### Dependencies
52
+
53
+ * The following workspace dependencies were updated
54
+ * dependencies
55
+ * @aigne/agent-library bumped to 1.21.22
56
+ * @aigne/agentic-memory bumped to 1.0.22
57
+ * @aigne/aigne-hub bumped to 0.6.4
58
+ * @aigne/core bumped to 1.51.0
59
+ * @aigne/default-memory bumped to 1.1.4
60
+ * @aigne/openai bumped to 0.11.4
61
+
3
62
  ## [1.36.4](https://github.com/AIGNE-io/aigne-framework/compare/cli-v1.36.3...cli-v1.36.4) (2025-08-16)
4
63
 
5
64
 
@@ -2,18 +2,14 @@ import assert from "node:assert";
2
2
  import { spawn } from "node:child_process";
3
3
  import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
- import { extname, join } from "node:path";
6
- import { isatty } from "node:tty";
7
- import { loadModel } from "@aigne/aigne-hub";
8
- import { AIAgent, AIGNE, readAllString } from "@aigne/core";
9
- import { pick } from "@aigne/core/utils/type-utils.js";
5
+ import { join } from "node:path";
6
+ import { AIGNE } from "@aigne/core";
10
7
  import { Listr, PRESET_TIMER } from "@aigne/listr2";
11
8
  import { joinURL } from "ufo";
12
- import { parse } from "yaml";
13
- import { ZodBoolean, ZodNumber, ZodObject, ZodString, ZodType } from "zod";
14
9
  import { downloadAndExtract } from "../utils/download.js";
15
10
  import { loadAIGNE } from "../utils/load-aigne.js";
16
- import { runAgentWithAIGNE, stdinHasData } from "../utils/run-with-aigne.js";
11
+ import { runAgentWithAIGNE } from "../utils/run-with-aigne.js";
12
+ import { parseAgentInput, withAgentInputSchema } from "../utils/yargs.js";
17
13
  import { serveMCPServerFromDir } from "./serve-mcp.js";
18
14
  const NPM_PACKAGE_CACHE_TIME_MS = 1000 * 60 * 60 * 24; // 1 day
19
15
  const builtinApps = [
@@ -85,120 +81,38 @@ const upgradeCommandModule = ({ name, dir, isLatest, version, }) => ({
85
81
  },
86
82
  });
87
83
  const agentCommandModule = ({ dir, agent, }) => {
88
- const inputSchema = agent.inputSchema instanceof ZodObject ? agent.inputSchema.shape : {};
89
84
  return {
90
85
  command: agent.name,
91
86
  aliases: agent.alias || [],
92
87
  describe: agent.description || "",
93
- builder: (yargs) => {
94
- for (const [option, config] of Object.entries(inputSchema)) {
95
- const innerType = innerZodType(config);
96
- yargs.option(option, {
97
- // TODO: support more types
98
- type: innerType instanceof ZodBoolean
99
- ? "boolean"
100
- : innerType instanceof ZodNumber
101
- ? "number"
102
- : "string",
103
- description: config.description,
104
- });
105
- if (!(config.isNullable() || config.isOptional())) {
106
- yargs.demandOption(option);
107
- }
108
- }
109
- return yargs
110
- .option("input", {
111
- type: "array",
112
- description: "Input to the agent, use @<file> to read from a file",
113
- alias: ["i"],
114
- })
115
- .option("format", {
116
- type: "string",
117
- description: 'Input format, can be "json" or "yaml"',
118
- choices: ["json", "yaml"],
119
- });
120
- },
88
+ builder: async (yargs) => withAgentInputSchema(yargs, agent),
121
89
  handler: async (input) => {
122
90
  await invokeCLIAgentFromDir({ dir, agent: agent.name, input });
123
91
  },
124
92
  };
125
93
  };
126
- function innerZodType(type) {
127
- if ("innerType" in type._def && type._def.innerType instanceof ZodType) {
128
- return innerZodType(type._def.innerType);
129
- }
130
- return type;
131
- }
132
94
  export async function invokeCLIAgentFromDir(options) {
133
95
  const aigne = await loadAIGNE({
134
96
  path: options.dir,
135
- options: { model: options.input.model },
97
+ modelOptions: { model: options.input.model },
136
98
  });
137
99
  try {
138
100
  const agent = aigne.cli.agents[options.agent];
139
101
  assert(agent, `Agent ${options.agent} not found in ${options.dir}`);
140
- const inputSchema = agent.inputSchema instanceof ZodObject ? agent.inputSchema.shape : {};
141
- const input = Object.fromEntries(await Promise.all(Object.entries(pick(options.input, Object.keys(inputSchema))).map(async ([key, val]) => {
142
- if (typeof val === "string" && val.startsWith("@")) {
143
- const schema = inputSchema[key];
144
- val = await readFileAsInput(val, {
145
- format: schema instanceof ZodString ? "raw" : undefined,
146
- });
147
- }
148
- return [key, val];
149
- })));
150
- const rawInput = options.input.input ||
151
- (isatty(process.stdin.fd) || !(await stdinHasData())
152
- ? null
153
- : [await readAllString(process.stdin)].filter(Boolean));
154
- if (rawInput) {
155
- for (const raw of rawInput) {
156
- const parsed = raw.startsWith("@")
157
- ? await readFileAsInput(raw, { format: options.input.format })
158
- : raw;
159
- if (typeof parsed !== "string") {
160
- Object.assign(input, parsed);
161
- }
162
- else {
163
- const inputKey = agent instanceof AIAgent ? agent.inputKey : undefined;
164
- if (inputKey) {
165
- Object.assign(input, { [inputKey]: parsed });
166
- }
167
- }
168
- }
169
- }
102
+ const input = await parseAgentInput(options.input, agent);
170
103
  await runAgentWithAIGNE(aigne, agent, { input });
171
104
  }
172
105
  finally {
173
106
  await aigne.shutdown();
174
107
  }
175
108
  }
176
- async function readFileAsInput(value, { format } = {}) {
177
- if (value.startsWith("@")) {
178
- const ext = extname(value);
179
- value = await readFile(value.slice(1), "utf8");
180
- if (!format) {
181
- if (ext === ".json")
182
- format = "json";
183
- else if (ext === ".yaml" || ext === ".yml")
184
- format = "yaml";
185
- }
186
- }
187
- if (format === "json") {
188
- return JSON.parse(value);
189
- }
190
- else if (format === "yaml") {
191
- return parse(value);
192
- }
193
- return value;
194
- }
195
109
  export async function loadApplication({ name, dir, forceUpgrade = false, }) {
196
110
  name = `@aigne/${name}`;
197
111
  dir ??= join(homedir(), ".aigne", "registry.npmjs.org", name);
198
112
  const check = forceUpgrade ? undefined : await isInstallationAvailable(dir);
199
113
  if (check?.available) {
200
114
  return {
201
- aigne: await AIGNE.load(dir, { loadModel }),
115
+ aigne: await AIGNE.load(dir),
202
116
  dir,
203
117
  version: check.version,
204
118
  isCache: true,
@@ -241,7 +155,7 @@ export async function loadApplication({ name, dir, forceUpgrade = false, }) {
241
155
  },
242
156
  }).run();
243
157
  return {
244
- aigne: await AIGNE.load(dir, { loadModel }),
158
+ aigne: await AIGNE.load(dir),
245
159
  dir,
246
160
  version: result.version,
247
161
  };
@@ -1,10 +1,12 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { readFile, writeFile } from "node:fs/promises";
3
- import { AIGNE_ENV_FILE, AIGNE_HUB_URL, connectToAIGNEHub, isTest } from "@aigne/aigne-hub";
3
+ import { AIGNE_HUB_URL } from "@aigne/aigne-hub";
4
4
  import chalk from "chalk";
5
5
  import Table from "cli-table3";
6
6
  import inquirer from "inquirer";
7
7
  import { parse, stringify } from "yaml";
8
+ import { AIGNE_ENV_FILE, isTest } from "../utils/aigne-hub/constants.js";
9
+ import { connectToAIGNEHub } from "../utils/aigne-hub/credential.js";
8
10
  import { getUserInfo } from "../utils/aigne-hub-user.js";
9
11
  import { getUrlOrigin } from "../utils/get-url-origin.js";
10
12
  const formatNumber = (balance) => {
@@ -78,12 +78,10 @@ export function createRunCommand({ aigneFilePath, } = {}) {
78
78
  }
79
79
  const aigne = await loadAIGNE({
80
80
  path: dir,
81
- options: {
81
+ modelOptions: {
82
82
  ...options,
83
83
  model: options.model || process.env.MODEL,
84
84
  aigneHubUrl: options?.aigneHubUrl,
85
- },
86
- actionOptions: {
87
85
  inquirerPromptFn: (prompt) => {
88
86
  if (prompt.type === "input") {
89
87
  return task
@@ -53,7 +53,7 @@ export async function serveMCPServerFromDir(options) {
53
53
  const port = options.port || DEFAULT_PORT();
54
54
  const aigne = await loadAIGNE({
55
55
  path: options.dir,
56
- options: { aigneHubUrl: options.aigneHubUrl },
56
+ modelOptions: { aigneHubUrl: options.aigneHubUrl },
57
57
  });
58
58
  await serveMCPServer({
59
59
  aigne,
@@ -24,7 +24,7 @@ export function createTestCommand({ aigneFilePath, } = {}) {
24
24
  const absolutePath = isAbsolute(path) ? path : resolve(process.cwd(), path);
25
25
  const aigne = await loadAIGNE({
26
26
  path: absolutePath,
27
- options: { aigneHubUrl: options?.aigneHubUrl },
27
+ modelOptions: { aigneHubUrl: options?.aigneHubUrl },
28
28
  });
29
29
  assert(aigne.rootDir);
30
30
  spawnSync("node", ["--test"], { cwd: aigne.rootDir, stdio: "inherit" });
@@ -45,6 +45,8 @@ type Task = ReturnType<typeof promiseWithResolvers<void>> & {
45
45
  subtask: Listr;
46
46
  taskWrapper: AIGNEListrTaskWrapper;
47
47
  }>>;
48
+ agent: Agent;
49
+ input: Message;
48
50
  startTime?: number;
49
51
  endTime?: number;
50
52
  usage?: Partial<ContextUsage>;
@@ -1,6 +1,6 @@
1
1
  import { EOL } from "node:os";
2
2
  import { inspect } from "node:util";
3
- import { AIAgent, ChatModel, DEFAULT_OUTPUT_KEY, UserAgent, } from "@aigne/core";
3
+ import { AIAgent, ChatModel, DEFAULT_OUTPUT_KEY, mergeContextUsage, newEmptyContextUsage, UserAgent, } from "@aigne/core";
4
4
  import { promiseWithResolvers } from "@aigne/core/utils/promise.js";
5
5
  import { flat, omit } from "@aigne/core/utils/type-utils.js";
6
6
  import { figures } from "@aigne/listr2";
@@ -28,13 +28,39 @@ export class TerminalTracer {
28
28
  formatResult: (result, options) => [this.formatResult(agent, context, result, options)].filter(Boolean),
29
29
  }, [], { concurrent: true });
30
30
  this.listr = listr;
31
+ const collapsedMap = new Map();
32
+ const hideContextIds = new Set();
31
33
  const onStart = async ({ context, agent, ...event }) => {
32
34
  if (agent instanceof UserAgent)
33
35
  return;
36
+ if (agent.taskRenderMode === "hide") {
37
+ hideContextIds.add(context.id);
38
+ return;
39
+ }
40
+ else if (agent.taskRenderMode === "collapse") {
41
+ collapsedMap.set(context.id, {
42
+ ancestor: { contextId: context.id },
43
+ usage: newEmptyContextUsage(),
44
+ models: new Set(),
45
+ });
46
+ }
47
+ if (context.parentId) {
48
+ if (hideContextIds.has(context.parentId)) {
49
+ hideContextIds.add(context.id);
50
+ return;
51
+ }
52
+ const collapsed = collapsedMap.get(context.parentId);
53
+ if (collapsed) {
54
+ collapsedMap.set(context.id, collapsed);
55
+ return;
56
+ }
57
+ }
34
58
  const contextId = context.id;
35
59
  const parentContextId = context.parentId;
36
60
  const task = {
37
61
  ...promiseWithResolvers(),
62
+ agent,
63
+ input: event.input,
38
64
  listr: promiseWithResolvers(),
39
65
  startTime: Date.now(),
40
66
  };
@@ -66,6 +92,34 @@ export class TerminalTracer {
66
92
  const onSuccess = async ({ context, agent, output, ...event }) => {
67
93
  const contextId = context.id;
68
94
  const parentContextId = context.parentId;
95
+ const collapsed = collapsedMap.get(contextId);
96
+ if (collapsed) {
97
+ if (agent instanceof ChatModel) {
98
+ const { usage, model } = output;
99
+ if (usage)
100
+ mergeContextUsage(collapsed.usage, usage);
101
+ if (model)
102
+ collapsed.models.add(model);
103
+ }
104
+ const task = this.tasks[collapsed.ancestor.contextId];
105
+ if (task) {
106
+ task.usage = collapsed.usage;
107
+ task.extraTitleMetadata ??= {};
108
+ if (collapsed.models.size)
109
+ task.extraTitleMetadata.model = [...collapsed.models].join(",");
110
+ const { taskWrapper } = await task.listr.promise;
111
+ taskWrapper.title = await this.formatTaskTitle(task.agent, {
112
+ input: task.input,
113
+ task,
114
+ usage: Boolean(task.usage.inputTokens || task.usage.outputTokens || task.usage.aigneHubCredits),
115
+ time: context.id === collapsed.ancestor.contextId,
116
+ });
117
+ if (context.id === collapsed.ancestor.contextId) {
118
+ task?.resolve();
119
+ }
120
+ return;
121
+ }
122
+ }
69
123
  const task = this.tasks[contextId];
70
124
  if (!task)
71
125
  return;
@@ -173,6 +227,9 @@ export class TerminalTracer {
173
227
  const items = [
174
228
  [chalk.yellow(usage.inputTokens), chalk.grey("input tokens")],
175
229
  [chalk.cyan(usage.outputTokens), chalk.grey("output tokens")],
230
+ usage.aigneHubCredits
231
+ ? [chalk.blue(usage.aigneHubCredits.toFixed()), chalk.grey("AIGNE Hub credits")]
232
+ : undefined,
176
233
  usage.agentCalls ? [chalk.magenta(usage.agentCalls), chalk.grey("agent calls")] : undefined,
177
234
  ];
178
235
  const content = items.filter((i) => !!i).map((i) => i.join(" "));
@@ -0,0 +1,6 @@
1
+ export declare const WELLKNOWN_SERVICE_PATH_PREFIX = "/.well-known/service";
2
+ export declare const ACCESS_KEY_SESSION_API = "/api/access-key/session";
3
+ export declare const AIGNE_HUB_PROVIDER = "aignehub";
4
+ export declare const DEFAULT_AIGNE_HUB_PROVIDER_MODEL = "aignehub:openai/gpt-5-mini";
5
+ export declare const isTest: string | boolean;
6
+ export declare const AIGNE_ENV_FILE: string;
@@ -0,0 +1,12 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ const ACCESS_KEY_PREFIX = "/api/access-key";
4
+ export const WELLKNOWN_SERVICE_PATH_PREFIX = "/.well-known/service";
5
+ export const ACCESS_KEY_SESSION_API = `${ACCESS_KEY_PREFIX}/session`;
6
+ const DEFAULT_AIGNE_HUB_MODEL = "openai/gpt-5-mini";
7
+ export const AIGNE_HUB_PROVIDER = "aignehub";
8
+ export const DEFAULT_AIGNE_HUB_PROVIDER_MODEL = `${AIGNE_HUB_PROVIDER}:${DEFAULT_AIGNE_HUB_MODEL}`;
9
+ export const isTest = process.env.CI || process.env.NODE_ENV === "test";
10
+ const TEST_AIGNE_ENV_FILE = join(homedir(), ".aigne", "test-aigne-hub-connected.yaml");
11
+ const PROD_AIGNE_ENV_FILE = join(homedir(), ".aigne", "aigne-hub-connected.yaml");
12
+ export const AIGNE_ENV_FILE = isTest ? TEST_AIGNE_ENV_FILE : PROD_AIGNE_ENV_FILE;
@@ -0,0 +1,23 @@
1
+ import type { CreateConnectOptions, FetchResult, LoadCredentialOptions } from "./type.js";
2
+ export declare const fetchConfigs: ({ connectUrl, sessionId, fetchInterval, fetchTimeout, }: {
3
+ connectUrl: string;
4
+ sessionId: string;
5
+ fetchInterval: number;
6
+ fetchTimeout: number;
7
+ }) => Promise<any>;
8
+ export declare function createConnect({ connectUrl, openPage, fetchInterval, retry, source, connectAction, wrapSpinner, closeOnSuccess, intervalFetchConfig, appName, appLogo, }: CreateConnectOptions): Promise<FetchResult>;
9
+ export declare function connectToAIGNEHub(url: string): Promise<{
10
+ apiKey: string;
11
+ url: string;
12
+ } | {
13
+ apiKey: undefined;
14
+ url: undefined;
15
+ }>;
16
+ export declare const checkConnectionStatus: (host: string) => Promise<{
17
+ apiKey: any;
18
+ url: any;
19
+ }>;
20
+ export declare function loadAIGNEHubCredential(options?: LoadCredentialOptions): Promise<{
21
+ apiKey?: string;
22
+ url?: string;
23
+ } | undefined>;
@@ -0,0 +1,206 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { AIGNE_HUB_BLOCKLET_DID, AIGNE_HUB_URL, getAIGNEHubMountPoint } from "@aigne/aigne-hub";
6
+ import { logger } from "@aigne/core/utils/logger.js";
7
+ import inquirer from "inquirer";
8
+ import open from "open";
9
+ import pWaitFor from "p-wait-for";
10
+ import { joinURL, withQuery } from "ufo";
11
+ import { parse, stringify } from "yaml";
12
+ import { ACCESS_KEY_SESSION_API, AIGNE_ENV_FILE, isTest, WELLKNOWN_SERVICE_PATH_PREFIX, } from "./constants.js";
13
+ import { decrypt, encodeEncryptionKey } from "./crypto.js";
14
+ const request = async (config) => {
15
+ const headers = {};
16
+ if (config.requestCount !== undefined) {
17
+ headers["X-Request-Count"] = config.requestCount.toString();
18
+ }
19
+ const response = await fetch(config.url, { method: config.method || "GET", headers });
20
+ if (!response.ok)
21
+ throw new Error(`HTTP error! status: ${response.status}`);
22
+ const data = await response.json();
23
+ return { data };
24
+ };
25
+ export const fetchConfigs = async ({ connectUrl, sessionId, fetchInterval, fetchTimeout, }) => {
26
+ const sessionURL = withQuery(joinURL(connectUrl, ACCESS_KEY_SESSION_API), { sid: sessionId });
27
+ let requestCount = 0;
28
+ const condition = async () => {
29
+ const { data: session } = await request({ url: sessionURL, requestCount });
30
+ requestCount++;
31
+ return Boolean(session.accessKeyId && session.accessKeySecret);
32
+ };
33
+ await pWaitFor(condition, { interval: fetchInterval, timeout: fetchTimeout });
34
+ const { data: session } = await request({ url: sessionURL, requestCount });
35
+ await request({ url: sessionURL, method: "DELETE" });
36
+ return {
37
+ ...session,
38
+ accessKeyId: session.accessKeyId,
39
+ accessKeySecret: decrypt(session.accessKeySecret, session.accessKeyId, session.challenge),
40
+ };
41
+ };
42
+ function baseWrapSpinner(_, waiting) {
43
+ return Promise.resolve(waiting());
44
+ }
45
+ export async function createConnect({ connectUrl, openPage, fetchInterval = 3 * 1000, retry = 1500, source = "Blocklet CLI", connectAction = "connect-cli", wrapSpinner = baseWrapSpinner, closeOnSuccess, intervalFetchConfig, appName = "AIGNE CLI", appLogo = "https://www.aigne.io/favicon.ico?imageFilter=resize&w=32", }) {
46
+ const startSessionURL = joinURL(connectUrl, ACCESS_KEY_SESSION_API);
47
+ const { data: session } = await request({ url: startSessionURL, method: "POST" });
48
+ const token = session.id;
49
+ const pageUrl = withQuery(joinURL(connectUrl, connectAction), {
50
+ __token__: encodeEncryptionKey(token),
51
+ source,
52
+ closeOnSuccess,
53
+ cli: true,
54
+ appName: ` ${appName}`,
55
+ appLogo,
56
+ });
57
+ openPage?.(pageUrl);
58
+ return await wrapSpinner(`Waiting for connection: ${connectUrl}`, async () => {
59
+ const checkAuthorizeStatus = intervalFetchConfig ?? fetchConfigs;
60
+ const authorizeStatus = await checkAuthorizeStatus({
61
+ connectUrl,
62
+ sessionId: token,
63
+ fetchTimeout: retry * fetchInterval,
64
+ fetchInterval: retry,
65
+ });
66
+ return authorizeStatus;
67
+ });
68
+ }
69
+ export async function connectToAIGNEHub(url) {
70
+ const { origin, host } = new URL(url);
71
+ const connectUrl = joinURL(origin, WELLKNOWN_SERVICE_PATH_PREFIX);
72
+ const apiUrl = await getAIGNEHubMountPoint(url, AIGNE_HUB_BLOCKLET_DID);
73
+ try {
74
+ const openFn = isTest ? () => { } : open;
75
+ const result = await createConnect({
76
+ connectUrl: connectUrl,
77
+ connectAction: "gen-simple-access-key",
78
+ source: `@aigne/cli connect to AIGNE hub`,
79
+ closeOnSuccess: true,
80
+ openPage: (pageUrl) => openFn(pageUrl),
81
+ });
82
+ const accessKeyOptions = {
83
+ apiKey: result.accessKeySecret,
84
+ url: apiUrl,
85
+ };
86
+ // After redirection, write the AIGNE Hub access token
87
+ const aigneDir = join(homedir(), ".aigne");
88
+ if (!existsSync(aigneDir)) {
89
+ mkdirSync(aigneDir, { recursive: true });
90
+ }
91
+ const envs = parse(await readFile(AIGNE_ENV_FILE, "utf8").catch(() => stringify({})));
92
+ await writeFile(AIGNE_ENV_FILE, stringify({
93
+ ...envs,
94
+ [host]: {
95
+ AIGNE_HUB_API_KEY: accessKeyOptions.apiKey,
96
+ AIGNE_HUB_API_URL: accessKeyOptions.url,
97
+ },
98
+ default: {
99
+ AIGNE_HUB_API_URL: accessKeyOptions.url,
100
+ },
101
+ }));
102
+ return accessKeyOptions;
103
+ }
104
+ catch (error) {
105
+ logger.error("Failed to connect to AIGNE Hub", error.message);
106
+ return { apiKey: undefined, url: undefined };
107
+ }
108
+ }
109
+ export const checkConnectionStatus = async (host) => {
110
+ // aigne-hub access token
111
+ if (!existsSync(AIGNE_ENV_FILE)) {
112
+ throw new Error("AIGNE_HUB_API_KEY file not found, need to login first");
113
+ }
114
+ const data = await readFile(AIGNE_ENV_FILE, "utf8");
115
+ if (!data.includes("AIGNE_HUB_API_KEY")) {
116
+ throw new Error("AIGNE_HUB_API_KEY key not found, need to login first");
117
+ }
118
+ const envs = parse(data);
119
+ if (!envs[host]) {
120
+ throw new Error("AIGNE_HUB_API_KEY host not found, need to login first");
121
+ }
122
+ const env = envs[host];
123
+ if (!env.AIGNE_HUB_API_KEY) {
124
+ throw new Error("AIGNE_HUB_API_KEY key not found, need to login first");
125
+ }
126
+ return {
127
+ apiKey: env.AIGNE_HUB_API_KEY,
128
+ url: env.AIGNE_HUB_API_URL,
129
+ };
130
+ };
131
+ export async function loadAIGNEHubCredential(options) {
132
+ const isBlocklet = process.env.BLOCKLET_AIGNE_API_URL && process.env.BLOCKLET_AIGNE_API_PROVIDER;
133
+ if (isBlocklet)
134
+ return undefined;
135
+ const aigneDir = join(homedir(), ".aigne");
136
+ if (!existsSync(aigneDir)) {
137
+ mkdirSync(aigneDir, { recursive: true });
138
+ }
139
+ const envs = parse(await readFile(AIGNE_ENV_FILE, "utf8").catch(() => stringify({})));
140
+ const inquirerPrompt = (options?.inquirerPromptFn ?? inquirer.prompt);
141
+ const configUrl = options?.aigneHubUrl || process.env.AIGNE_HUB_API_URL;
142
+ const url = configUrl || envs?.default?.AIGNE_HUB_API_URL || AIGNE_HUB_URL;
143
+ const connectUrl = joinURL(new URL(url).origin, WELLKNOWN_SERVICE_PATH_PREFIX);
144
+ const { host } = new URL(url);
145
+ let credential = {};
146
+ try {
147
+ credential = await checkConnectionStatus(host);
148
+ }
149
+ catch (error) {
150
+ if (error instanceof Error && error.message.includes("login first")) {
151
+ let aigneHubUrl = connectUrl;
152
+ if (!configUrl) {
153
+ const { subscribe } = await inquirerPrompt({
154
+ type: "list",
155
+ name: "subscribe",
156
+ message: "No LLM API Keys or AIGNE Hub connections found. How would you like to proceed?",
157
+ choices: [
158
+ {
159
+ name: "Connect to the Arcblock official AIGNE Hub (recommended, free credits for new users)",
160
+ value: "official",
161
+ },
162
+ connectUrl.includes(AIGNE_HUB_URL)
163
+ ? {
164
+ name: "Connect to your own AIGNE Hub instance (self-hosted)",
165
+ value: "custom",
166
+ }
167
+ : null,
168
+ {
169
+ name: "Exit and configure my own LLM API Keys",
170
+ value: "manual",
171
+ },
172
+ ].filter(Boolean),
173
+ default: "official",
174
+ });
175
+ if (subscribe === "custom") {
176
+ const { customUrl } = await inquirerPrompt({
177
+ type: "input",
178
+ name: "customUrl",
179
+ message: "Enter the URL of your AIGNE Hub:",
180
+ validate(input) {
181
+ try {
182
+ const url = new URL(input);
183
+ return url.protocol.startsWith("http")
184
+ ? true
185
+ : "Must be a valid URL with http or https";
186
+ }
187
+ catch {
188
+ return "Invalid URL";
189
+ }
190
+ },
191
+ });
192
+ aigneHubUrl = customUrl;
193
+ }
194
+ else if (subscribe === "manual") {
195
+ console.log("You chose to configure your own LLM API Keys. Exiting...");
196
+ process.exit(0);
197
+ }
198
+ }
199
+ credential = await connectToAIGNEHub(aigneHubUrl);
200
+ }
201
+ else {
202
+ throw error;
203
+ }
204
+ }
205
+ return credential;
206
+ }
@@ -0,0 +1,4 @@
1
+ export declare const decrypt: (m: string, s: string, i: string) => string;
2
+ export declare const encrypt: (m: string, s: string, i: string) => string;
3
+ export declare const encodeEncryptionKey: (key: string) => string;
4
+ export declare const decodeEncryptionKey: (str: string) => Uint8Array<ArrayBuffer>;
@@ -0,0 +1,9 @@
1
+ import crypto from "node:crypto";
2
+ import { AesCrypter } from "@ocap/mcrypto/lib/crypter/aes-legacy.js";
3
+ const aes = new AesCrypter();
4
+ export const decrypt = (m, s, i) => aes.decrypt(m, crypto.pbkdf2Sync(i, s, 256, 32, "sha512").toString("hex"));
5
+ export const encrypt = (m, s, i) => aes.encrypt(m, crypto.pbkdf2Sync(i, s, 256, 32, "sha512").toString("hex"));
6
+ const escapeFn = (str) => str.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
7
+ const unescapeFn = (str) => (str + "===".slice((str.length + 3) % 4)).replace(/-/g, "+").replace(/_/g, "/");
8
+ export const encodeEncryptionKey = (key) => escapeFn(Buffer.from(key).toString("base64"));
9
+ export const decodeEncryptionKey = (str) => new Uint8Array(Buffer.from(unescapeFn(str), "base64"));
@@ -0,0 +1,12 @@
1
+ import type { ChatModel, ChatModelOptions } from "@aigne/core";
2
+ import type { LoadCredentialOptions } from "./type.js";
3
+ export declare function maskApiKey(apiKey?: string): string | undefined;
4
+ export declare const parseModelOption: (model: string) => {
5
+ provider: string | undefined;
6
+ model: string | undefined;
7
+ };
8
+ export declare const formatModelName: (model: string, inquirerPrompt: NonNullable<LoadCredentialOptions["inquirerPromptFn"]>) => Promise<{
9
+ provider: string;
10
+ model?: string;
11
+ }>;
12
+ export declare function loadChatModel(options?: ChatModelOptions & LoadCredentialOptions): Promise<ChatModel>;
@@ -0,0 +1,75 @@
1
+ import { AIGNE_HUB_DEFAULT_MODEL, findModel } from "@aigne/aigne-hub";
2
+ import { flat } from "@aigne/core/utils/type-utils.js";
3
+ import chalk from "chalk";
4
+ import inquirer from "inquirer";
5
+ import { AIGNE_HUB_PROVIDER } from "./constants.js";
6
+ import { loadAIGNEHubCredential } from "./credential.js";
7
+ export function maskApiKey(apiKey) {
8
+ if (!apiKey || apiKey.length <= 8)
9
+ return apiKey;
10
+ const start = apiKey.slice(0, 4);
11
+ const end = apiKey.slice(-4);
12
+ return `${start}${"*".repeat(8)}${end}`;
13
+ }
14
+ export const parseModelOption = (model) => {
15
+ const { provider, name } = model.match(/(?<provider>[^:]*)(:(?<name>.*))?/)?.groups ?? {};
16
+ return { provider: provider?.replace(/-/g, ""), model: name };
17
+ };
18
+ export const formatModelName = async (model, inquirerPrompt) => {
19
+ let { provider, model: name } = parseModelOption(model);
20
+ provider ||= AIGNE_HUB_PROVIDER;
21
+ const { match, all } = findModel(provider);
22
+ if (!match)
23
+ throw new Error(`Unsupported model: ${provider}/${name}, available providers: ${all.map((m) => m.name).join(", ")}`);
24
+ if (provider.includes(AIGNE_HUB_PROVIDER)) {
25
+ return { provider, model: name || AIGNE_HUB_DEFAULT_MODEL };
26
+ }
27
+ const requireEnvs = flat(match.apiKeyEnvName);
28
+ if (requireEnvs.some((name) => name && process.env[name])) {
29
+ return { provider, model: name };
30
+ }
31
+ const result = await inquirerPrompt({
32
+ type: "list",
33
+ name: "useAigneHub",
34
+ message: `Seems no API Key configured for ${provider}/${name}, select your preferred way to continue:`,
35
+ choices: [
36
+ {
37
+ name: `Connect to AIGNE Hub to use ${name} (Recommended since free credits available)`,
38
+ value: true,
39
+ },
40
+ {
41
+ name: `Exit and bring my owner API Key by set ${requireEnvs.join(", ")}`,
42
+ value: false,
43
+ },
44
+ ],
45
+ default: true,
46
+ });
47
+ if (!result.useAigneHub) {
48
+ console.log(chalk.yellow(`You can use command "export ${requireEnvs[0]}=xxx" to set API Key in your shell. Or you can set environment variables in .env file.`));
49
+ process.exit(0);
50
+ }
51
+ return { provider: AIGNE_HUB_PROVIDER, model: `${provider}/${name}` };
52
+ };
53
+ export async function loadChatModel(options) {
54
+ const { provider, model } = await formatModelName(options?.model || process.env.MODEL || "", options?.inquirerPromptFn ??
55
+ inquirer.prompt);
56
+ const params = {
57
+ model,
58
+ temperature: options?.temperature,
59
+ topP: options?.topP,
60
+ frequencyPenalty: options?.frequencyPenalty,
61
+ presencePenalty: options?.presencePenalty,
62
+ };
63
+ const { match, all } = findModel(provider);
64
+ if (!match) {
65
+ throw new Error(`Unsupported model provider ${provider}, available providers: ${all.map((m) => m.name).join(", ")}`);
66
+ }
67
+ const credential = provider.toLowerCase().includes(AIGNE_HUB_PROVIDER)
68
+ ? await loadAIGNEHubCredential(options)
69
+ : undefined;
70
+ return match.create({
71
+ ...credential,
72
+ model: params.model,
73
+ modelOptions: { ...params },
74
+ });
75
+ }
@@ -0,0 +1,38 @@
1
+ type InquirerPromptFn = (prompt: {
2
+ type: string;
3
+ name: string;
4
+ message: string;
5
+ choices: {
6
+ name: string;
7
+ value: any;
8
+ }[];
9
+ default: any;
10
+ }) => Promise<any>;
11
+ export type LoadCredentialOptions = {
12
+ aigneHubUrl?: string;
13
+ inquirerPromptFn?: InquirerPromptFn;
14
+ };
15
+ export type FetchResult = {
16
+ accessKeyId: string;
17
+ accessKeySecret: string;
18
+ };
19
+ export type BaseWrapSpinner = (_: string, waiting: () => Promise<FetchResult>) => Promise<FetchResult>;
20
+ export interface CreateConnectOptions {
21
+ connectUrl: string;
22
+ openPage?: (url: string) => void;
23
+ fetchInterval?: number;
24
+ retry?: number;
25
+ source?: string;
26
+ connectAction?: string;
27
+ appName?: string;
28
+ appLogo?: string;
29
+ wrapSpinner?: BaseWrapSpinner;
30
+ prettyUrl?: (url: string) => string;
31
+ closeOnSuccess?: boolean;
32
+ intervalFetchConfig?: (options: {
33
+ sessionId: string;
34
+ fetchInterval: number;
35
+ fetchTimeout: number;
36
+ }) => Promise<FetchResult>;
37
+ }
38
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -51,9 +51,11 @@ export class AIGNEListr extends Listr {
51
51
  try {
52
52
  this.ctx = {};
53
53
  this.spinner.start();
54
- const request = this.myOptions.formatRequest();
55
- if (request)
56
- console.log(request);
54
+ if (logger.enabled(LogLevel.INFO)) {
55
+ const request = this.myOptions.formatRequest();
56
+ if (request)
57
+ console.log(request);
58
+ }
57
59
  logger.logMessage = (...args) => this.logs.push(format(...args));
58
60
  for (const method of ["debug", "log", "info", "warn", "error"]) {
59
61
  console[method] = (...args) => {
@@ -1,5 +1,5 @@
1
- import type { LoadCredentialOptions, Model } from "@aigne/aigne-hub";
2
1
  import { AIGNE, type ChatModelOptions } from "@aigne/core";
2
+ import type { LoadCredentialOptions } from "./aigne-hub/type.js";
3
3
  import type { RunAIGNECommandOptions } from "./run-with-aigne.js";
4
4
  export interface RunOptions extends RunAIGNECommandOptions {
5
5
  path: string;
@@ -7,12 +7,7 @@ export interface RunOptions extends RunAIGNECommandOptions {
7
7
  cacheDir?: string;
8
8
  aigneHubUrl?: string;
9
9
  }
10
- export declare function loadAIGNE({ path, options, modelOptions, actionOptions, }: {
10
+ export declare function loadAIGNE({ path, modelOptions, }: {
11
11
  path?: string;
12
- options?: Model & Pick<RunOptions, "model" | "aigneHubUrl">;
13
- modelOptions?: ChatModelOptions;
14
- actionOptions?: {
15
- inquirerPromptFn?: LoadCredentialOptions["inquirerPromptFn"];
16
- runTest?: boolean;
17
- };
12
+ modelOptions?: ChatModelOptions & LoadCredentialOptions;
18
13
  }): Promise<AIGNE<import("@aigne/core").UserContext>>;
@@ -1,24 +1,17 @@
1
- import { existsSync, mkdirSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
3
- import { homedir } from "node:os";
4
- import { join } from "node:path";
5
- import { AIGNE_ENV_FILE, checkConnectionStatus, AIGNE_HUB_URL as DEFAULT_AIGNE_HUB_URL, formatModelName, loadModel, maskApiKey, parseModelOption, } from "@aigne/aigne-hub";
6
1
  import { AIGNE } from "@aigne/core";
7
- import { loadAIGNEFile } from "@aigne/core/loader/index.js";
2
+ import { isNil, omitBy } from "@aigne/core/utils/type-utils.js";
3
+ import { OpenAIImageModel } from "@aigne/openai";
8
4
  import boxen from "boxen";
9
5
  import chalk from "chalk";
10
- import inquirer from "inquirer";
11
- import { parse, stringify } from "yaml";
12
6
  import { availableMemories } from "../constants.js";
7
+ import { loadChatModel, maskApiKey } from "./aigne-hub/model.js";
13
8
  import { getUrlOrigin } from "./get-url-origin.js";
14
- const isTest = process.env.CI || process.env.NODE_ENV === "test";
15
- const mockInquirerPrompt = (() => Promise.resolve({ useAigneHub: true }));
16
9
  let printed = false;
17
10
  async function printChatModelInfoBox(model) {
18
11
  if (printed)
19
12
  return;
20
13
  printed = true;
21
- const credential = await model.getCredential();
14
+ const credential = await model.credential;
22
15
  const lines = [`${chalk.cyan("Provider")}: ${chalk.green(model.name.replace("ChatModel", ""))}`];
23
16
  if (credential?.model) {
24
17
  lines.push(`${chalk.cyan("Model")}: ${chalk.green(credential?.model)}`);
@@ -32,49 +25,22 @@ async function printChatModelInfoBox(model) {
32
25
  console.log(boxen(lines.join("\n"), { padding: 1, borderStyle: "classic", borderColor: "cyan" }));
33
26
  console.log("");
34
27
  }
35
- async function prepareAIGNEConfig(options, inquirerPromptFn) {
36
- const aigneDir = join(homedir(), ".aigne");
37
- if (!existsSync(aigneDir)) {
38
- mkdirSync(aigneDir, { recursive: true });
39
- }
40
- const envs = parse(await readFile(AIGNE_ENV_FILE, "utf8").catch(() => stringify({})));
41
- const inquirerPrompt = (inquirerPromptFn ?? inquirer.prompt);
42
- // get aigne hub url
43
- const configUrl = options?.aigneHubUrl || process.env.AIGNE_HUB_API_URL;
44
- const AIGNE_HUB_URL = configUrl || envs?.default?.AIGNE_HUB_API_URL || DEFAULT_AIGNE_HUB_URL;
45
- const { host } = new URL(AIGNE_HUB_URL);
46
- const result = await checkConnectionStatus(host).catch(() => null);
47
- const alreadyConnected = Boolean(result?.apiKey);
48
- return { AIGNE_HUB_URL, inquirerPrompt: alreadyConnected ? mockInquirerPrompt : inquirerPrompt };
49
- }
50
- export async function loadAIGNE({ path, options, modelOptions, actionOptions, }) {
51
- const { AIGNE_HUB_URL, inquirerPrompt } = await prepareAIGNEConfig(options, actionOptions?.inquirerPromptFn);
52
- const { temperature, topP, presencePenalty, frequencyPenalty } = options || {};
53
- let modelName = options?.model || "";
28
+ export async function loadAIGNE({ path, modelOptions, }) {
29
+ let aigne;
54
30
  if (path) {
55
- const { aigne } = await loadAIGNEFile(path).catch(() => ({ aigne: null }));
56
- const modelFromPath = `${aigne?.model?.provider ?? ""}:${aigne?.model?.name ?? ""}`;
57
- modelName = modelName || modelFromPath;
31
+ aigne = await AIGNE.load(path, {
32
+ memories: availableMemories,
33
+ model: (options) => loadChatModel({ ...options, ...omitBy(modelOptions ?? {}, (v) => isNil(v)) }),
34
+ imageModel: () => new OpenAIImageModel(),
35
+ });
58
36
  }
59
- // format model name
60
- const formattedModelName = isTest ? modelName : await formatModelName(modelName, inquirerPrompt);
61
- if (isTest && path && !actionOptions?.runTest) {
62
- const model = await loadModel(parseModelOption(formattedModelName));
63
- return await AIGNE.load(path, { loadModel, memories: availableMemories, model });
37
+ else {
38
+ const chatModel = await loadChatModel({ ...modelOptions });
39
+ aigne = new AIGNE({ model: chatModel });
64
40
  }
65
41
  console.log(`${chalk.grey("TIPS:")} run ${chalk.cyan("aigne observe")} to start the observability server.\n`);
66
- const model = await loadModel({
67
- ...parseModelOption(formattedModelName),
68
- temperature,
69
- topP,
70
- presencePenalty,
71
- frequencyPenalty,
72
- }, modelOptions, { aigneHubUrl: AIGNE_HUB_URL, inquirerPromptFn: actionOptions?.inquirerPromptFn });
73
- if (model) {
74
- await printChatModelInfoBox(model);
75
- }
76
- if (path) {
77
- return await AIGNE.load(path, { loadModel, memories: availableMemories, model });
42
+ if (aigne.model) {
43
+ await printChatModelInfoBox(aigne.model);
78
44
  }
79
- return new AIGNE({ model });
45
+ return aigne;
80
46
  }
@@ -63,4 +63,3 @@ export declare function runAgentWithAIGNE(aigne: AIGNE, agent: Agent, { outputKe
63
63
  } & Omit<RunAIGNECommandOptions, "input">): Promise<{
64
64
  result: Message;
65
65
  } | undefined>;
66
- export declare function stdinHasData(): Promise<boolean>;
@@ -1,21 +1,19 @@
1
- import { fstat } from "node:fs";
2
- import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
1
+ import { mkdir, stat, writeFile } from "node:fs/promises";
3
2
  import { dirname, isAbsolute, join } from "node:path";
4
3
  import { isatty } from "node:tty";
5
- import { promisify } from "node:util";
6
4
  import { exists } from "@aigne/agent-library/utils/fs.js";
7
- import { availableModels, parseModelOption } from "@aigne/aigne-hub";
8
- import { AIAgent, DEFAULT_OUTPUT_KEY, readAllString, UserAgent, } from "@aigne/core";
5
+ import { availableModels } from "@aigne/aigne-hub";
6
+ import { DEFAULT_OUTPUT_KEY, UserAgent, } from "@aigne/core";
9
7
  import { getLevelFromEnv, LogLevel, logger } from "@aigne/core/utils/logger.js";
10
- import { flat, isEmpty, tryOrThrow } from "@aigne/core/utils/type-utils.js";
8
+ import { isEmpty, isNil, omitBy, pick, tryOrThrow, } from "@aigne/core/utils/type-utils.js";
11
9
  import chalk from "chalk";
12
- import { parse } from "yaml";
13
10
  import yargs from "yargs";
14
11
  import { hideBin } from "yargs/helpers";
15
- import { ZodError, ZodObject, z } from "zod";
12
+ import { ZodError, z } from "zod";
16
13
  import { TerminalTracer } from "../tracer/terminal.js";
17
14
  import { loadAIGNE } from "./load-aigne.js";
18
15
  import { DEFAULT_CHAT_INPUT_KEY, runChatLoopInTerminal, } from "./run-chat-loop.js";
16
+ import { parseAgentInput, withAgentInputSchema } from "./yargs.js";
19
17
  export const createRunAIGNECommand = (yargs) => yargs
20
18
  .option("chat", {
21
19
  describe: "Run chat loop in terminal",
@@ -80,7 +78,7 @@ export const createRunAIGNECommand = (yargs) => yargs
80
78
  .option("log-level", {
81
79
  describe: `Log level for detailed debugging information. Values: ${Object.values(LogLevel).join(", ")}`,
82
80
  type: "string",
83
- default: getLevelFromEnv(logger.options.ns) || LogLevel.INFO,
81
+ default: getLevelFromEnv(logger.options.ns) || LogLevel.SILENT,
84
82
  coerce: customZodError("--log-level", (s) => z.nativeEnum(LogLevel).parse(s)),
85
83
  })
86
84
  .option("aigne-hub-url", {
@@ -88,38 +86,11 @@ export const createRunAIGNECommand = (yargs) => yargs
88
86
  type: "string",
89
87
  });
90
88
  export async function parseAgentInputByCommander(agent, options = {}) {
91
- const inputSchemaShape = flat(agent instanceof AIAgent ? agent.inputKey : undefined, agent.inputSchema instanceof ZodObject ? Object.keys(agent.inputSchema.shape) : []);
92
- const parsedInput = await yargs().parseAsync(options.argv ?? process.argv);
93
- const input = Object.fromEntries(await Promise.all(inputSchemaShape.map(async (key) => {
94
- const k = `input${key.charAt(0).toUpperCase()}${key.slice(1)}`;
95
- let value = parsedInput[k];
96
- if (typeof value === "string" && value.startsWith("@")) {
97
- value = await readFile(value.slice(1), "utf8");
98
- }
99
- return [key, value];
100
- })));
101
- const rawInput = options.input ||
102
- (isatty(process.stdin.fd) || !(await stdinHasData())
103
- ? null
104
- : [await readAllString(process.stdin)].filter(Boolean));
105
- if (rawInput?.length) {
106
- for (let raw of rawInput) {
107
- if (raw.startsWith("@")) {
108
- raw = await readFile(raw.slice(1), "utf8");
109
- }
110
- if (options.format === "json") {
111
- Object.assign(input, JSON.parse(raw));
112
- }
113
- else if (options.format === "yaml") {
114
- Object.assign(input, parse(raw));
115
- }
116
- else {
117
- Object.assign(input, typeof options.inputKey === "string"
118
- ? { [options.inputKey]: raw }
119
- : { [DEFAULT_CHAT_INPUT_KEY]: raw });
120
- }
121
- }
122
- }
89
+ const args = await withAgentInputSchema(yargs(), agent)
90
+ .showHelpOnFail(false)
91
+ .fail(() => { })
92
+ .parseAsync(options.argv ?? process.argv);
93
+ const input = await parseAgentInput({ ...args, input: options.input || args.input }, agent);
123
94
  if (isEmpty(input)) {
124
95
  const defaultInput = options.defaultInput || process.env.INITIAL_CALL;
125
96
  Object.assign(input, typeof defaultInput === "string"
@@ -135,14 +106,10 @@ export async function runWithAIGNE(agentCreator, { argv = process.argv, chatLoop
135
106
  logger.level = options.logLevel;
136
107
  }
137
108
  const aigne = await loadAIGNE({
138
- options: {
139
- ...parseModelOption(options.model),
140
- temperature: options.temperature,
141
- topP: options.topP,
142
- presencePenalty: options.presencePenalty,
143
- frequencyPenalty: options.frequencyPenalty,
109
+ modelOptions: {
110
+ ...modelOptions,
111
+ ...omitBy(pick(options, "model", "temperature", "topP", "presencePenalty", "frequencyPenalty"), (v) => isNil(v)),
144
112
  },
145
- modelOptions,
146
113
  });
147
114
  try {
148
115
  const agent = typeof agentCreator === "function" ? await agentCreator(aigne) : agentCreator;
@@ -214,7 +181,3 @@ export async function runAgentWithAIGNE(aigne, agent, { outputKey, chatLoopOptio
214
181
  }
215
182
  return { result };
216
183
  }
217
- export async function stdinHasData() {
218
- const stats = await promisify(fstat)(0);
219
- return stats.isFIFO() || stats.isFile();
220
- }
@@ -0,0 +1,20 @@
1
+ import { type Agent, type Message } from "@aigne/core";
2
+ import type { Argv } from "yargs";
3
+ import { ZodType } from "zod";
4
+ export declare function inferZodType(type: ZodType, opts?: {
5
+ array?: boolean;
6
+ optional?: boolean;
7
+ }): {
8
+ type: "string" | "number" | "boolean";
9
+ array?: boolean;
10
+ optional?: boolean;
11
+ };
12
+ export declare function withAgentInputSchema(yargs: Argv, agent: Agent): Argv<{
13
+ input?: string[];
14
+ format?: "json" | "yaml";
15
+ }>;
16
+ export declare function parseAgentInput(i: Message & {
17
+ input?: string[];
18
+ format?: "json" | "yaml";
19
+ }, agent: Agent): Promise<any>;
20
+ export declare function stdinHasData(): Promise<boolean>;
@@ -0,0 +1,107 @@
1
+ import { fstat } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { extname } from "node:path";
4
+ import { isatty } from "node:tty";
5
+ import { promisify } from "node:util";
6
+ import { AIAgent, readAllString } from "@aigne/core";
7
+ import { pick } from "@aigne/core/utils/type-utils.js";
8
+ import { parse } from "yaml";
9
+ import { ZodAny, ZodArray, ZodBoolean, ZodNumber, ZodObject, ZodString, ZodType, ZodUnknown, } from "zod";
10
+ export function inferZodType(type, opts = {}) {
11
+ if (type instanceof ZodUnknown || type instanceof ZodAny) {
12
+ return { type: "string", optional: true };
13
+ }
14
+ opts.optional ??= type.isNullable() || type.isOptional();
15
+ if ("innerType" in type._def && type._def.innerType instanceof ZodType) {
16
+ return inferZodType(type._def.innerType, opts);
17
+ }
18
+ if (type instanceof ZodArray) {
19
+ return inferZodType(type.element, { ...opts, array: true });
20
+ }
21
+ return {
22
+ ...opts,
23
+ array: opts.array || undefined,
24
+ optional: opts.optional || undefined,
25
+ type: type instanceof ZodBoolean ? "boolean" : type instanceof ZodNumber ? "number" : "string",
26
+ };
27
+ }
28
+ export function withAgentInputSchema(yargs, agent) {
29
+ const inputSchema = agent.inputSchema instanceof ZodObject ? agent.inputSchema.shape : {};
30
+ for (const [option, config] of Object.entries(inputSchema)) {
31
+ const type = inferZodType(config);
32
+ yargs.option(option, {
33
+ type: type.type,
34
+ description: config.description,
35
+ array: type.array,
36
+ });
37
+ if (!type.optional) {
38
+ yargs.demandOption(option);
39
+ }
40
+ }
41
+ return yargs
42
+ .option("input", {
43
+ type: "string",
44
+ array: true,
45
+ description: "Input to the agent, use @<file> to read from a file",
46
+ alias: ["i"],
47
+ })
48
+ .option("format", {
49
+ type: "string",
50
+ description: 'Input format, can be "json" or "yaml"',
51
+ choices: ["json", "yaml"],
52
+ });
53
+ }
54
+ export async function parseAgentInput(i, agent) {
55
+ const inputSchema = agent.inputSchema instanceof ZodObject ? agent.inputSchema.shape : {};
56
+ const input = Object.fromEntries(await Promise.all(Object.entries(pick(i, Object.keys(inputSchema))).map(async ([key, val]) => {
57
+ if (typeof val === "string" && val.startsWith("@")) {
58
+ const schema = inputSchema[key];
59
+ val = await readFileAsInput(val, {
60
+ format: schema instanceof ZodString ? "raw" : undefined,
61
+ });
62
+ }
63
+ return [key, val];
64
+ })));
65
+ const rawInput = i.input ||
66
+ (isatty(process.stdin.fd) || !(await stdinHasData())
67
+ ? null
68
+ : [await readAllString(process.stdin)].filter(Boolean));
69
+ if (rawInput) {
70
+ for (const raw of rawInput) {
71
+ const parsed = raw.startsWith("@") ? await readFileAsInput(raw, { format: i.format }) : raw;
72
+ if (typeof parsed !== "string") {
73
+ Object.assign(input, parsed);
74
+ }
75
+ else {
76
+ const inputKey = agent instanceof AIAgent ? agent.inputKey : undefined;
77
+ if (inputKey) {
78
+ Object.assign(input, { [inputKey]: parsed });
79
+ }
80
+ }
81
+ }
82
+ }
83
+ return input;
84
+ }
85
+ async function readFileAsInput(value, { format } = {}) {
86
+ if (value.startsWith("@")) {
87
+ const ext = extname(value);
88
+ value = await readFile(value.slice(1), "utf8");
89
+ if (!format) {
90
+ if (ext === ".json")
91
+ format = "json";
92
+ else if (ext === ".yaml" || ext === ".yml")
93
+ format = "yaml";
94
+ }
95
+ }
96
+ if (format === "json") {
97
+ return JSON.parse(value);
98
+ }
99
+ else if (format === "yaml") {
100
+ return parse(value);
101
+ }
102
+ return value;
103
+ }
104
+ export async function stdinHasData() {
105
+ const stats = await promisify(fstat)(0);
106
+ return stats.isFIFO() || stats.isFile();
107
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/cli",
3
- "version": "1.36.4",
3
+ "version": "1.38.0",
4
4
  "description": "Your command center for agent development",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -51,6 +51,7 @@
51
51
  "@inquirer/type": "^3.0.8",
52
52
  "@listr2/prompt-adapter-inquirer": "^3.0.1",
53
53
  "@modelcontextprotocol/sdk": "^1.15.0",
54
+ "@ocap/mcrypto": "^1.21.0",
54
55
  "@smithy/node-http-handler": "^4.1.0",
55
56
  "boxen": "^8.0.1",
56
57
  "chalk": "^5.4.1",
@@ -65,20 +66,22 @@
65
66
  "log-update": "^6.1.0",
66
67
  "marked": "^16.0.0",
67
68
  "nunjucks": "^3.2.4",
69
+ "open": "^10.2.0",
68
70
  "openai": "^5.8.3",
71
+ "p-wait-for": "^5.0.2",
69
72
  "prettier": "^3.6.2",
70
73
  "tar": "^7.4.3",
71
74
  "wrap-ansi": "^9.0.0",
72
75
  "yaml": "^2.8.0",
73
76
  "yargs": "^18.0.0",
74
77
  "zod": "^3.25.67",
75
- "@aigne/agent-library": "^1.21.21",
76
- "@aigne/aigne-hub": "^0.6.3",
77
- "@aigne/agentic-memory": "^1.0.21",
78
- "@aigne/core": "^1.50.1",
79
- "@aigne/default-memory": "^1.1.3",
80
- "@aigne/openai": "^0.11.3",
81
- "@aigne/observability-api": "^0.9.0"
78
+ "@aigne/agent-library": "^1.21.24",
79
+ "@aigne/agentic-memory": "^1.0.24",
80
+ "@aigne/aigne-hub": "^0.6.6",
81
+ "@aigne/core": "^1.53.0",
82
+ "@aigne/observability-api": "^0.9.1",
83
+ "@aigne/default-memory": "^1.1.6",
84
+ "@aigne/openai": "^0.12.0"
82
85
  },
83
86
  "devDependencies": {
84
87
  "@types/archiver": "^6.0.3",