@aigne/cli 1.33.1 → 1.34.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,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.34.0](https://github.com/AIGNE-io/aigne-framework/compare/cli-v1.33.1...cli-v1.34.0) (2025-08-12)
4
+
5
+
6
+ ### Features
7
+
8
+ * **cli:** add retry functionality and improve error handling for AIGNE Hub ([#348](https://github.com/AIGNE-io/aigne-framework/issues/348)) ([672c93a](https://github.com/AIGNE-io/aigne-framework/commit/672c93abbba8b4b234f6d810536ff4b603a97e1e))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **core:** examples cases that failed when using aigne-hub ([#337](https://github.com/AIGNE-io/aigne-framework/issues/337)) ([0d4a31c](https://github.com/AIGNE-io/aigne-framework/commit/0d4a31c24d9e7d26f00d1accb80719d9ad79a4c6))
14
+
15
+
16
+ ### Dependencies
17
+
18
+ * The following workspace dependencies were updated
19
+ * dependencies
20
+ * @aigne/agent-library bumped to 1.21.18
21
+ * @aigne/agentic-memory bumped to 1.0.18
22
+ * @aigne/aigne-hub bumped to 0.4.9
23
+ * @aigne/core bumped to 1.49.0
24
+ * @aigne/default-memory bumped to 1.1.0
25
+ * @aigne/openai bumped to 0.11.0
26
+
3
27
  ## [1.33.1](https://github.com/AIGNE-io/aigne-framework/compare/cli-v1.33.0...cli-v1.33.1) (2025-08-12)
4
28
 
5
29
 
package/dist/cli.js CHANGED
@@ -4,6 +4,7 @@ import chalk from "chalk";
4
4
  import { config } from "dotenv-flow";
5
5
  import { hideBin } from "yargs/helpers";
6
6
  import { createAIGNECommand } from "./commands/aigne.js";
7
+ import { highlightUrl } from "./utils/string-utils.js";
7
8
  config({ silent: true });
8
9
  function getAIGNEFilePath() {
9
10
  let file = process.argv[2];
@@ -27,6 +28,6 @@ export default createAIGNECommand({ aigneFilePath })
27
28
  .parseAsync(hideBin([...process.argv.slice(0, 2), ...process.argv.slice(aigneFilePath ? 3 : 2)]))
28
29
  .catch((error) => {
29
30
  console.log(""); // Add an empty line for better readability
30
- console.error(`${chalk.red("Error:")} ${error.message.replace(/https?:\/\/[^\s]+/g, (url) => chalk.cyan(url))}`);
31
+ console.error(`${chalk.red("Error:")} ${highlightUrl(error.message)}`);
31
32
  process.exit(1);
32
33
  });
@@ -130,7 +130,10 @@ function innerZodType(type) {
130
130
  return type;
131
131
  }
132
132
  export async function invokeCLIAgentFromDir(options) {
133
- const aigne = await loadAIGNE(options.dir, { model: options.input.model });
133
+ const aigne = await loadAIGNE({
134
+ path: options.dir,
135
+ options: { model: options.input.model },
136
+ });
134
137
  try {
135
138
  const agent = aigne.cli.agents[options.agent];
136
139
  assert(agent, `Agent ${options.agent} not found in ${options.dir}`);
@@ -1,9 +1,9 @@
1
1
  import { existsSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { AIGNE_ENV_FILE, AIGNE_HUB_URL, connectToAIGNEHub } from "@aigne/aigne-hub";
3
4
  import chalk from "chalk";
4
- import { parse } from "yaml";
5
+ import { parse, stringify } from "yaml";
5
6
  import { getUserInfo } from "../utils/aigne-hub-user.js";
6
- import { AIGNE_ENV_FILE, connectToAIGNEHub, DEFAULT_URL } from "../utils/load-aigne.js";
7
7
  const formatNumber = (balance) => {
8
8
  const balanceNum = String(balance).split(".")[0];
9
9
  return chalk.yellow((balanceNum || "").replace(/\B(?=(\d{3})+(?!\d))/g, ","));
@@ -36,15 +36,12 @@ export async function displayStatus(statusList) {
36
36
  return;
37
37
  }
38
38
  console.log(chalk.cyan("AIGNE Hub Connection Status:\n"));
39
- const defaultStatus = statusList.find((status) => status.host === "default")?.apiUrl || DEFAULT_URL;
39
+ const defaultStatus = statusList.find((status) => status.host === "default")?.apiUrl || AIGNE_HUB_URL;
40
40
  for (const status of statusList.filter((status) => status.host !== "default")) {
41
- const userInfo = await getUserInfo({ baseUrl: status.apiUrl, apiKey: status.apiKey }).catch((e) => {
42
- console.error(e);
43
- return null;
44
- });
41
+ const userInfo = await getUserInfo({ baseUrl: status.apiUrl, apiKey: status.apiKey }).catch(() => null);
45
42
  const isConnected = new URL(status.apiUrl).origin === new URL(defaultStatus).origin;
46
- const statusIcon = isConnected ? chalk.green("✓") : chalk.red("");
47
- const statusText = isConnected ? "Connected" : "Disconnected";
43
+ const statusIcon = isConnected ? chalk.green("✓") : "";
44
+ const statusText = isConnected ? "Connected" : "Not connected";
48
45
  console.log(`${statusIcon} ${chalk.bold(status.host)}`);
49
46
  console.log(` Status: ${statusText}`);
50
47
  if (userInfo) {
@@ -56,9 +53,10 @@ export async function displayStatus(statusList) {
56
53
  if (userInfo?.creditBalance) {
57
54
  const balance = formatNumber(userInfo?.creditBalance?.balance);
58
55
  const total = formatNumber(userInfo?.creditBalance?.total);
59
- console.log(` Plan: ${balance} / ${total}`);
56
+ console.log(` Credits: ${balance} / ${total}`);
60
57
  }
61
- console.log(` Billing URL: ${userInfo?.paymentLink ? chalk.green(userInfo.paymentLink) : chalk.red("N/A")}`);
58
+ console.log(` Payment URL: ${userInfo?.paymentLink ? chalk.green(userInfo.paymentLink) : chalk.red("N/A")}`);
59
+ console.log(` Profile URL: ${userInfo?.profileLink ? chalk.green(userInfo.profileLink) : chalk.red("N/A")}`);
62
60
  }
63
61
  console.log("");
64
62
  }
@@ -81,6 +79,34 @@ export function createConnectCommand() {
81
79
  const statusList = await getConnectionStatus();
82
80
  await displayStatus(statusList);
83
81
  },
82
+ })
83
+ .command({
84
+ command: "switch",
85
+ describe: "Switch to a different AIGNE Hub",
86
+ handler: async (argv) => {
87
+ const url = argv.url;
88
+ console.log("Switching to AIGNE Hub: ", chalk.blue(url));
89
+ if (!url) {
90
+ console.error(chalk.red("✗ Please provide a URL to the AIGNE Hub server"));
91
+ process.exit(1);
92
+ }
93
+ const data = await readFile(AIGNE_ENV_FILE, "utf8");
94
+ const envs = parse(data);
95
+ let host = "";
96
+ try {
97
+ host = new URL(url).host;
98
+ }
99
+ catch {
100
+ console.error(chalk.red("✗ Invalid URL"));
101
+ process.exit(1);
102
+ }
103
+ if (!envs[host]) {
104
+ console.error(chalk.red("✗ AIGNE Hub already connected"));
105
+ process.exit(1);
106
+ }
107
+ await writeFile(AIGNE_ENV_FILE, stringify({ ...envs, default: { AIGNE_HUB_API_URL: envs[host]?.AIGNE_HUB_API_URL } }));
108
+ console.log(chalk.green("✓ Successfully connected to AIGNE Hub"));
109
+ },
84
110
  });
85
111
  },
86
112
  handler: async (argv) => {
@@ -44,6 +44,7 @@ export function createRunCommand({ aigneFilePath, } = {}) {
44
44
  if (options.logLevel)
45
45
  logger.level = options.logLevel;
46
46
  const { cacheDir, dir } = prepareDirs(path, options);
47
+ const originalLog = {};
47
48
  const { aigne, agent } = await new Listr([
48
49
  {
49
50
  title: "Prepare environment",
@@ -67,24 +68,37 @@ export function createRunCommand({ aigneFilePath, } = {}) {
67
68
  task: async (ctx, task) => {
68
69
  // Load env files in the aigne directory
69
70
  config({ path: dir, silent: true });
70
- const aigne = await loadAIGNE(dir, {
71
- ...options,
72
- model: options.model || process.env.MODEL,
73
- aigneHubUrl: options?.aigneHubUrl,
74
- }, {
75
- inquirerPromptFn: (prompt) => {
76
- if (prompt.type === "input") {
71
+ ctx.logs = [];
72
+ for (const method of ["debug", "log", "info", "warn", "error"]) {
73
+ originalLog[method] = console[method];
74
+ console[method] = (...args) => {
75
+ ctx.logs.push(...args);
76
+ task.output = args.join(" ");
77
+ };
78
+ }
79
+ const aigne = await loadAIGNE({
80
+ path: dir,
81
+ options: {
82
+ ...options,
83
+ model: options.model || process.env.MODEL,
84
+ aigneHubUrl: options?.aigneHubUrl,
85
+ },
86
+ actionOptions: {
87
+ inquirerPromptFn: (prompt) => {
88
+ if (prompt.type === "input") {
89
+ return task
90
+ .prompt(ListrInquirerPromptAdapter)
91
+ .run(inputInquirer, prompt)
92
+ .then((res) => ({ [prompt.name]: res }));
93
+ }
77
94
  return task
78
95
  .prompt(ListrInquirerPromptAdapter)
79
- .run(inputInquirer, prompt)
96
+ .run(selectInquirer, prompt)
80
97
  .then((res) => ({ [prompt.name]: res }));
81
- }
82
- return task
83
- .prompt(ListrInquirerPromptAdapter)
84
- .run(selectInquirer, prompt)
85
- .then((res) => ({ [prompt.name]: res }));
98
+ },
86
99
  },
87
100
  });
101
+ Object.assign(console, originalLog);
88
102
  ctx.aigne = aigne;
89
103
  },
90
104
  },
@@ -118,7 +132,12 @@ ${aigne.agents.map((agent) => ` - ${agent.name}`).join("\n")}
118
132
  showErrorMessage: false,
119
133
  timer: PRESET_TIMER,
120
134
  },
121
- }).run();
135
+ })
136
+ .run()
137
+ .then((ctx) => {
138
+ ctx.logs.forEach((log) => console.log(log));
139
+ return ctx;
140
+ });
122
141
  assert(aigne);
123
142
  assert(agent);
124
143
  const input = await parseAgentInputByCommander(agent, options);
@@ -51,7 +51,10 @@ export function createServeMCPCommand({ aigneFilePath, } = {}) {
51
51
  }
52
52
  export async function serveMCPServerFromDir(options) {
53
53
  const port = options.port || DEFAULT_PORT();
54
- const aigne = await loadAIGNE(options.dir, { aigneHubUrl: options.aigneHubUrl });
54
+ const aigne = await loadAIGNE({
55
+ path: options.dir,
56
+ options: { aigneHubUrl: options.aigneHubUrl },
57
+ });
55
58
  await serveMCPServer({
56
59
  aigne,
57
60
  host: options.host,
@@ -22,7 +22,10 @@ export function createTestCommand({ aigneFilePath, } = {}) {
22
22
  handler: async (options) => {
23
23
  const path = aigneFilePath || options.path;
24
24
  const absolutePath = isAbsolute(path) ? path : resolve(process.cwd(), path);
25
- const aigne = await loadAIGNE(absolutePath, { aigneHubUrl: options?.aigneHubUrl });
25
+ const aigne = await loadAIGNE({
26
+ path: absolutePath,
27
+ options: { aigneHubUrl: options?.aigneHubUrl },
28
+ });
26
29
  assert(aigne.rootDir);
27
30
  spawnSync("node", ["--test"], { cwd: aigne.rootDir, stdio: "inherit" });
28
31
  },
@@ -25,7 +25,7 @@ export declare class TerminalTracer {
25
25
  usage?: boolean;
26
26
  time?: boolean;
27
27
  input: Message;
28
- }): string;
28
+ }): Promise<string>;
29
29
  private marked;
30
30
  get outputKey(): string;
31
31
  formatRequest(agent: Agent, _context: Context, m?: Message, { running }?: {
@@ -10,6 +10,7 @@ import * as prompts from "@inquirer/prompts";
10
10
  import chalk from "chalk";
11
11
  import { Marked } from "marked";
12
12
  import { AIGNEListr, AIGNEListrRenderer } from "../utils/listr.js";
13
+ import { highlightUrl } from "../utils/string-utils.js";
13
14
  import { parseDuration } from "../utils/time.js";
14
15
  export class TerminalTracer {
15
16
  context;
@@ -28,6 +29,24 @@ export class TerminalTracer {
28
29
  : undefined,
29
30
  formatResult: (result, options) => [this.formatResult(agent, context, result, options)].filter(Boolean),
30
31
  }, [], { concurrent: true });
32
+ const proxiedPrompts = new Proxy({}, {
33
+ get: (_target, prop) => {
34
+ // biome-ignore lint/performance/noDynamicNamespaceImportAccess: we need to access prompts dynamically
35
+ const method = prompts[prop];
36
+ if (typeof method !== "function")
37
+ return undefined;
38
+ return async (config) => {
39
+ const renderer = listr["renderer"] instanceof AIGNEListrRenderer ? listr["renderer"] : undefined;
40
+ await renderer?.pause();
41
+ try {
42
+ return await method({ ...config });
43
+ }
44
+ finally {
45
+ await renderer?.resume();
46
+ }
47
+ };
48
+ },
49
+ });
31
50
  const onStart = async ({ context, agent, ...event }) => {
32
51
  if (agent instanceof UserAgent)
33
52
  return;
@@ -40,7 +59,7 @@ export class TerminalTracer {
40
59
  };
41
60
  this.tasks[contextId] = task;
42
61
  const listrTask = {
43
- title: this.formatTaskTitle(agent, { ...event }),
62
+ title: await this.formatTaskTitle(agent, { ...event }),
44
63
  task: (ctx, taskWrapper) => {
45
64
  const subtask = taskWrapper.newListr([{ task: () => task.promise }]);
46
65
  task.listr.resolve({ subtask, taskWrapper, ctx });
@@ -61,28 +80,7 @@ export class TerminalTracer {
61
80
  else {
62
81
  listr.add(listrTask);
63
82
  }
64
- return {
65
- options: {
66
- prompts: new Proxy({}, {
67
- get: (_target, prop) => {
68
- // biome-ignore lint/performance/noDynamicNamespaceImportAccess: we need to access prompts dynamically
69
- const method = prompts[prop];
70
- if (typeof method !== "function")
71
- return undefined;
72
- return async (config) => {
73
- const renderer = listr["renderer"] instanceof AIGNEListrRenderer ? listr["renderer"] : undefined;
74
- await renderer?.pause();
75
- try {
76
- return await method({ ...config });
77
- }
78
- finally {
79
- await renderer?.resume();
80
- }
81
- };
82
- },
83
- }),
84
- },
85
- };
83
+ return { options: { prompts: proxiedPrompts } };
86
84
  };
87
85
  const onSuccess = async ({ context, agent, output, ...event }) => {
88
86
  const contextId = context.id;
@@ -99,20 +97,49 @@ export class TerminalTracer {
99
97
  if (model)
100
98
  task.extraTitleMetadata.model = model;
101
99
  }
102
- taskWrapper.title = this.formatTaskTitle(agent, { ...event, task, usage: true, time: true });
100
+ taskWrapper.title = await this.formatTaskTitle(agent, {
101
+ ...event,
102
+ task,
103
+ usage: true,
104
+ time: true,
105
+ });
103
106
  if (!parentContextId || !this.tasks[parentContextId]) {
104
107
  Object.assign(ctx, output);
105
108
  }
106
109
  task.resolve();
107
110
  };
108
111
  const onError = async ({ context, agent, error, ...event }) => {
112
+ if ("type" in error && error.type === "NOT_ENOUGH") {
113
+ const retry = await proxiedPrompts.select({
114
+ message: highlightUrl(error.message),
115
+ choices: [
116
+ {
117
+ name: "I have bought some credits, try again",
118
+ value: "retry",
119
+ },
120
+ {
121
+ name: "Exit",
122
+ value: "exit",
123
+ },
124
+ ],
125
+ });
126
+ console.log("");
127
+ if (retry === "retry") {
128
+ return { retry: true };
129
+ }
130
+ }
109
131
  const contextId = context.id;
110
132
  const task = this.tasks[contextId];
111
133
  if (!task)
112
134
  return;
113
135
  task.endTime = Date.now();
114
136
  const { taskWrapper } = await task.listr.promise;
115
- taskWrapper.title = this.formatTaskTitle(agent, { ...event, task, usage: true, time: true });
137
+ taskWrapper.title = await this.formatTaskTitle(agent, {
138
+ ...event,
139
+ task,
140
+ usage: true,
141
+ time: true,
142
+ });
116
143
  task.reject(error);
117
144
  };
118
145
  const result = await listr.run(() => context.invoke(agent, input, {
@@ -145,10 +172,10 @@ export class TerminalTracer {
145
172
  const duration = endTime - startTime;
146
173
  return chalk.grey(`[${parseDuration(duration)}]`);
147
174
  }
148
- formatTaskTitle(agent, { task, usage, time, input }) {
175
+ async formatTaskTitle(agent, { task, usage, time, input }) {
149
176
  let title = agent.name;
150
177
  if (agent.taskTitle) {
151
- title += ` ${chalk.cyan(agent.renderTaskTitle(input))}`;
178
+ title += ` ${chalk.cyan(await agent.renderTaskTitle(input))}`;
152
179
  }
153
180
  if (usage && task?.usage)
154
181
  title += ` ${this.formatTokenUsage(task.usage, task.extraTitleMetadata)}`;
@@ -1,72 +1,18 @@
1
- import { availableModels } from "@aigne/aigne-hub";
2
- import { AIGNE } from "@aigne/core";
3
- import inquirer from "inquirer";
4
- import { type RunAIGNECommandOptions } from "./run-with-aigne.js";
5
- export declare const decrypt: (m: string, s: string, i: string) => string;
6
- export declare const encrypt: (m: string, s: string, i: string) => string;
7
- export declare const encodeEncryptionKey: (key: string) => string;
8
- export declare const decodeEncryptionKey: (str: string) => Uint8Array<ArrayBuffer>;
9
- export declare const AIGNE_ENV_FILE: string;
1
+ import type { LoadCredentialOptions, Model } from "@aigne/aigne-hub";
2
+ import { AIGNE, type ChatModelOptions } from "@aigne/core";
3
+ import type { RunAIGNECommandOptions } from "./run-with-aigne.js";
10
4
  export interface RunOptions extends RunAIGNECommandOptions {
11
5
  path: string;
12
6
  entryAgent?: string;
13
7
  cacheDir?: string;
14
8
  aigneHubUrl?: string;
15
9
  }
16
- type FetchResult = {
17
- accessKeyId: string;
18
- accessKeySecret: string;
19
- };
20
- export declare const fetchConfigs: ({ connectUrl, sessionId, fetchInterval, fetchTimeout, }: {
21
- connectUrl: string;
22
- sessionId: string;
23
- fetchInterval: number;
24
- fetchTimeout: number;
25
- }) => Promise<any>;
26
- declare function baseWrapSpinner(_: string, waiting: () => Promise<FetchResult>): Promise<FetchResult>;
27
- interface CreateConnectOptions {
28
- connectUrl: string;
29
- openPage?: (url: string) => void;
30
- fetchInterval?: number;
31
- retry?: number;
32
- source?: string;
33
- connectAction?: string;
34
- appName?: string;
35
- appLogo?: string;
36
- wrapSpinner?: typeof baseWrapSpinner;
37
- prettyUrl?: (url: string) => string;
38
- closeOnSuccess?: boolean;
39
- intervalFetchConfig?: (options: {
40
- sessionId: string;
41
- fetchInterval: number;
42
- fetchTimeout: number;
43
- }) => Promise<FetchResult>;
44
- }
45
- export declare function createConnect({ connectUrl, openPage, fetchInterval, retry, source, connectAction, wrapSpinner, closeOnSuccess, intervalFetchConfig, appName, appLogo, }: CreateConnectOptions): Promise<FetchResult>;
46
- export declare const DEFAULT_URL = "https://hub.aigne.io/";
47
- export declare const formatModelName: (models: ReturnType<typeof availableModels>, model: string, inquirerPrompt: typeof inquirer.prompt) => Promise<string>;
48
- export declare function connectToAIGNEHub(url: string): Promise<{
49
- apiKey: string;
50
- url: string;
51
- } | {
52
- apiKey: undefined;
53
- url: undefined;
54
- }>;
55
- export declare const checkConnectionStatus: (host: string) => Promise<{
56
- apiKey: any;
57
- url: string;
58
- }>;
59
- export declare function loadAIGNE(path: string, options?: Pick<RunOptions, "model" | "aigneHubUrl">, actionOptions?: {
60
- inquirerPromptFn?: (prompt: {
61
- type: string;
62
- name: string;
63
- message: string;
64
- choices: {
65
- name: string;
66
- value: any;
67
- }[];
68
- default: any;
69
- }) => Promise<any>;
70
- runTest?: boolean;
10
+ export declare function loadAIGNE({ path, options, modelOptions, actionOptions, }: {
11
+ path?: string;
12
+ options?: Model & Pick<RunOptions, "model" | "aigneHubUrl">;
13
+ modelOptions?: ChatModelOptions;
14
+ actionOptions?: {
15
+ inquirerPromptFn?: LoadCredentialOptions["inquirerPromptFn"];
16
+ runTest?: boolean;
17
+ };
71
18
  }): Promise<AIGNE<import("@aigne/core").UserContext>>;
72
- export {};
@@ -1,288 +1,54 @@
1
1
  import { existsSync, mkdirSync } from "node:fs";
2
- import { readFile, writeFile } from "node:fs/promises";
2
+ import { readFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { availableModels, findModel, loadModel } from "@aigne/aigne-hub";
5
+ import { AIGNE_ENV_FILE, checkConnectionStatus, AIGNE_HUB_URL as DEFAULT_AIGNE_HUB_URL, formatModelName, loadModel, parseModelOption, } from "@aigne/aigne-hub";
6
6
  import { AIGNE } from "@aigne/core";
7
7
  import { loadAIGNEFile } from "@aigne/core/loader/index.js";
8
- import { logger } from "@aigne/core/utils/logger.js";
9
- import { AesCrypter } from "@ocap/mcrypto/lib/crypter/aes-legacy.js";
10
- import crypto from "crypto";
11
8
  import inquirer from "inquirer";
12
- import open from "open";
13
- import pWaitFor from "p-wait-for";
14
- import { joinURL, withQuery } from "ufo";
15
9
  import { parse, stringify } from "yaml";
16
10
  import { availableMemories } from "../constants.js";
17
- import { parseModelOption } from "./run-with-aigne.js";
18
- const aes = new AesCrypter();
19
- export const decrypt = (m, s, i) => aes.decrypt(m, crypto.pbkdf2Sync(i, s, 256, 32, "sha512").toString("hex"));
20
- export const encrypt = (m, s, i) => aes.encrypt(m, crypto.pbkdf2Sync(i, s, 256, 32, "sha512").toString("hex"));
21
- const escapeFn = (str) => str.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
22
- const unescapeFn = (str) => (str + "===".slice((str.length + 3) % 4)).replace(/-/g, "+").replace(/_/g, "/");
23
- export const encodeEncryptionKey = (key) => escapeFn(Buffer.from(key).toString("base64"));
24
- export const decodeEncryptionKey = (str) => new Uint8Array(Buffer.from(unescapeFn(str), "base64"));
25
- const TEST_ENV = process.env.CI || process.env.NODE_ENV === "test";
26
- export const AIGNE_ENV_FILE = TEST_ENV
27
- ? join(homedir(), ".aigne", "test-aigne-hub-connected.yaml")
28
- : join(homedir(), ".aigne", "aigne-hub-connected.yaml");
29
- const request = async (config) => {
30
- const headers = {};
31
- if (config.requestCount !== undefined) {
32
- headers["X-Request-Count"] = config.requestCount.toString();
33
- }
34
- const response = await fetch(config.url, { method: config.method || "GET", headers });
35
- if (!response.ok)
36
- throw new Error(`HTTP error! status: ${response.status}`);
37
- const data = await response.json();
38
- return { data };
39
- };
40
- const WELLKNOWN_SERVICE_PATH_PREFIX = "/.well-known/service";
41
- const ACCESS_KEY_PREFIX = "/api/access-key";
42
- const ACCESS_KEY_SESSION_API = `${ACCESS_KEY_PREFIX}/session`;
43
- export const fetchConfigs = async ({ connectUrl, sessionId, fetchInterval, fetchTimeout, }) => {
44
- const sessionURL = withQuery(joinURL(connectUrl, ACCESS_KEY_SESSION_API), { sid: sessionId });
45
- let requestCount = 0;
46
- const condition = async () => {
47
- const { data: session } = await request({ url: sessionURL, requestCount });
48
- requestCount++;
49
- return Boolean(session.accessKeyId && session.accessKeySecret);
50
- };
51
- await pWaitFor(condition, { interval: fetchInterval, timeout: fetchTimeout });
52
- const { data: session } = await request({ url: sessionURL, requestCount });
53
- await request({ url: sessionURL, method: "DELETE" });
54
- return {
55
- ...session,
56
- accessKeyId: session.accessKeyId,
57
- accessKeySecret: decrypt(session.accessKeySecret, session.accessKeyId, session.challenge),
58
- };
59
- };
60
- function baseWrapSpinner(_, waiting) {
61
- return Promise.resolve(waiting());
62
- }
63
- 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", }) {
64
- try {
65
- const startSessionURL = joinURL(connectUrl, ACCESS_KEY_SESSION_API);
66
- const { data: session } = await request({ url: startSessionURL, method: "POST" });
67
- const token = session.id;
68
- const pageUrl = withQuery(joinURL(connectUrl, connectAction), {
69
- __token__: encodeEncryptionKey(token),
70
- source,
71
- closeOnSuccess,
72
- cli: true,
73
- appName: ` ${appName}`,
74
- appLogo,
75
- });
76
- openPage?.(pageUrl);
77
- return await wrapSpinner(`Waiting for connection: ${connectUrl}`, async () => {
78
- const checkAuthorizeStatus = intervalFetchConfig ?? fetchConfigs;
79
- const authorizeStatus = await checkAuthorizeStatus({
80
- connectUrl,
81
- sessionId: token,
82
- fetchTimeout: retry * fetchInterval,
83
- fetchInterval: retry,
84
- });
85
- return authorizeStatus;
86
- });
87
- }
88
- catch (e) {
89
- console.error(e);
90
- throw e;
91
- }
92
- }
93
- const AGENT_HUB_PROVIDER = "aignehub";
94
- const DEFAULT_AIGNE_HUB_MODEL = "openai/gpt-4o";
95
- const DEFAULT_AIGNE_HUB_PROVIDER_MODEL = `${AGENT_HUB_PROVIDER}:${DEFAULT_AIGNE_HUB_MODEL}`;
96
- export const DEFAULT_URL = "https://hub.aigne.io/";
97
- export const formatModelName = async (models, model, inquirerPrompt) => {
98
- if (!model)
99
- return DEFAULT_AIGNE_HUB_PROVIDER_MODEL;
100
- const { provider, name } = parseModelOption(model);
101
- if (!provider) {
102
- return DEFAULT_AIGNE_HUB_PROVIDER_MODEL;
103
- }
104
- const providerName = provider.replace(/-/g, "");
105
- if (providerName.includes(AGENT_HUB_PROVIDER)) {
106
- return model;
107
- }
108
- const m = findModel(models, providerName);
109
- if (!m)
110
- throw new Error(`Unsupported model: ${provider} ${name}`);
111
- const apiKeyEnvName = Array.isArray(m.apiKeyEnvName) ? m.apiKeyEnvName : [m.apiKeyEnvName];
112
- if (apiKeyEnvName.some((name) => name && process.env[name])) {
113
- return model;
114
- }
115
- if (TEST_ENV) {
116
- return `${AGENT_HUB_PROVIDER}:${provider}/${name}`;
117
- }
118
- const result = await inquirerPrompt({
119
- type: "list",
120
- name: "useAigneHub",
121
- message: `Seems no API Key configured for ${provider}/${name}, select your preferred way to continue:`,
122
- choices: [
123
- {
124
- name: `Connect to AIGNE Hub to use ${name} (Recommended since free credits available)`,
125
- value: true,
126
- },
127
- {
128
- name: `Exit and bring my owner API Key by set ${apiKeyEnvName.join(", ")}`,
129
- value: false,
130
- },
131
- ],
132
- default: true,
133
- });
134
- if (!result.useAigneHub)
135
- return model;
136
- return `${AGENT_HUB_PROVIDER}:${provider}/${name}`;
137
- };
138
- export async function connectToAIGNEHub(url) {
139
- const { origin, host } = new URL(url);
140
- const connectUrl = joinURL(origin, WELLKNOWN_SERVICE_PATH_PREFIX);
141
- const BLOCKLET_JSON_PATH = "__blocklet__.js?type=json";
142
- const blockletInfo = await fetch(joinURL(origin, BLOCKLET_JSON_PATH));
143
- const blocklet = await blockletInfo.json();
144
- const aigneHubMount = (blocklet?.componentMountPoints || []).find((m) => m.did === "z8ia3xzq2tMq8CRHfaXj1BTYJyYnEcHbqP8cJ");
145
- try {
146
- const result = await createConnect({
147
- connectUrl: connectUrl,
148
- connectAction: "gen-simple-access-key",
149
- source: `@aigne/cli connect to AIGNE hub`,
150
- closeOnSuccess: true,
151
- openPage: (pageUrl) => open(pageUrl),
152
- });
153
- const accessKeyOptions = {
154
- apiKey: result.accessKeySecret,
155
- url: joinURL(origin, aigneHubMount?.mountPoint || ""),
156
- };
157
- // After redirection, write the AIGNE Hub access token
158
- const aigneDir = join(homedir(), ".aigne");
159
- if (!existsSync(aigneDir)) {
160
- mkdirSync(aigneDir, { recursive: true });
161
- }
162
- const envs = parse(await readFile(AIGNE_ENV_FILE, "utf8").catch(() => stringify({})));
163
- await writeFile(AIGNE_ENV_FILE, stringify({
164
- ...envs,
165
- [host]: {
166
- AIGNE_HUB_API_KEY: accessKeyOptions.apiKey,
167
- AIGNE_HUB_API_URL: accessKeyOptions.url,
168
- },
169
- default: {
170
- AIGNE_HUB_API_URL: accessKeyOptions.url,
171
- },
172
- })).catch((err) => {
173
- logger.error("Failed to write AIGNE Hub access token to .aigne/aigne-hub-connected.yaml", err.message);
174
- throw err;
175
- });
176
- return accessKeyOptions;
177
- }
178
- catch (error) {
179
- logger.error("Failed to connect to AIGNE Hub", error.message);
180
- return { apiKey: undefined, url: undefined };
181
- }
182
- }
183
- export const checkConnectionStatus = async (host) => {
184
- // aigne-hub access token
185
- if (!existsSync(AIGNE_ENV_FILE)) {
186
- throw new Error("AIGNE_HUB_API_KEY file not found, need to login first");
187
- }
188
- const data = await readFile(AIGNE_ENV_FILE, "utf8");
189
- if (!data.includes("AIGNE_HUB_API_KEY")) {
190
- throw new Error("AIGNE_HUB_API_KEY key not found, need to login first");
191
- }
192
- const envs = parse(data);
193
- if (!envs[host]) {
194
- throw new Error("AIGNE_HUB_API_KEY host not found, need to login first");
195
- }
196
- const env = envs[host];
197
- if (!env.AIGNE_HUB_API_KEY) {
198
- throw new Error("AIGNE_HUB_API_KEY key not found, need to login first");
199
- }
200
- return {
201
- apiKey: env.AIGNE_HUB_API_KEY,
202
- url: joinURL(env.AIGNE_HUB_API_URL),
203
- };
204
- };
11
+ const isTest = process.env.CI || process.env.NODE_ENV === "test";
205
12
  const mockInquirerPrompt = (() => Promise.resolve({ useAigneHub: true }));
206
- export async function loadAIGNE(path, options, actionOptions) {
13
+ async function prepareAIGNEConfig(options, inquirerPromptFn) {
207
14
  const aigneDir = join(homedir(), ".aigne");
208
15
  if (!existsSync(aigneDir)) {
209
16
  mkdirSync(aigneDir, { recursive: true });
210
17
  }
211
18
  const envs = parse(await readFile(AIGNE_ENV_FILE, "utf8").catch(() => stringify({})));
212
- const inquirerPrompt = (actionOptions?.inquirerPromptFn ??
213
- inquirer.prompt);
214
- const models = availableModels();
19
+ const inquirerPrompt = (inquirerPromptFn ?? inquirer.prompt);
20
+ // get aigne hub url
215
21
  const configUrl = options?.aigneHubUrl || process.env.AIGNE_HUB_API_URL;
216
- const AIGNE_HUB_URL = configUrl || envs?.default?.AIGNE_HUB_API_URL || DEFAULT_URL;
217
- const connectUrl = joinURL(new URL(AIGNE_HUB_URL).origin, WELLKNOWN_SERVICE_PATH_PREFIX);
22
+ const AIGNE_HUB_URL = configUrl || envs?.default?.AIGNE_HUB_API_URL || DEFAULT_AIGNE_HUB_URL;
218
23
  const { host } = new URL(AIGNE_HUB_URL);
219
- const { aigne } = await loadAIGNEFile(path).catch(() => ({ aigne: null }));
220
24
  const result = await checkConnectionStatus(host).catch(() => null);
221
25
  const alreadyConnected = Boolean(result?.apiKey);
222
- const modelName = await formatModelName(models, options?.model || `${aigne?.model?.provider ?? ""}:${aigne?.model?.name ?? ""}`, alreadyConnected ? mockInquirerPrompt : inquirerPrompt);
223
- let credential = {};
224
- if (TEST_ENV && !actionOptions?.runTest) {
225
- const model = await loadModel(parseModelOption(modelName));
26
+ return { AIGNE_HUB_URL, inquirerPrompt: alreadyConnected ? mockInquirerPrompt : inquirerPrompt };
27
+ }
28
+ export async function loadAIGNE({ path, options, modelOptions, actionOptions, }) {
29
+ const { AIGNE_HUB_URL, inquirerPrompt } = await prepareAIGNEConfig(options, actionOptions?.inquirerPromptFn);
30
+ const { temperature, topP, presencePenalty, frequencyPenalty } = options || {};
31
+ let modelName = options?.model || "";
32
+ if (path) {
33
+ const { aigne } = await loadAIGNEFile(path).catch(() => ({ aigne: null }));
34
+ const modelFromPath = `${aigne?.model?.provider ?? ""}:${aigne?.model?.name ?? ""}`;
35
+ modelName = modelName || modelFromPath;
36
+ }
37
+ // format model name
38
+ const formattedModelName = isTest ? modelName : await formatModelName(modelName, inquirerPrompt);
39
+ if (isTest && path && !actionOptions?.runTest) {
40
+ const model = await loadModel(parseModelOption(formattedModelName));
226
41
  return await AIGNE.load(path, { loadModel, memories: availableMemories, model });
227
42
  }
228
- if ((modelName.toLocaleLowerCase() || "").includes(AGENT_HUB_PROVIDER)) {
229
- try {
230
- credential = await checkConnectionStatus(host);
231
- }
232
- catch (error) {
233
- if (error instanceof Error && error.message.includes("login first")) {
234
- let aigneHubUrl = connectUrl;
235
- if (!configUrl) {
236
- const { subscribe } = await inquirerPrompt({
237
- type: "list",
238
- name: "subscribe",
239
- message: "No LLM API Keys or AIGNE Hub connections found. How would you like to proceed?",
240
- choices: [
241
- {
242
- name: "Connect to the Arcblock official AIGNE Hub (recommended, free credits for new users)",
243
- value: "official",
244
- },
245
- connectUrl.includes(DEFAULT_URL)
246
- ? {
247
- name: "Connect to your own AIGNE Hub instance (self-hosted)",
248
- value: "custom",
249
- }
250
- : null,
251
- {
252
- name: "Exit and configure my own LLM API Keys",
253
- value: "manual",
254
- },
255
- ].filter(Boolean),
256
- default: "official",
257
- });
258
- if (subscribe === "custom") {
259
- const { customUrl } = await inquirerPrompt({
260
- type: "input",
261
- name: "customUrl",
262
- message: "Enter the URL of your AIGNE Hub:",
263
- validate(input) {
264
- try {
265
- const url = new URL(input);
266
- return url.protocol.startsWith("http")
267
- ? true
268
- : "Must be a valid URL with http or https";
269
- }
270
- catch {
271
- return "Invalid URL";
272
- }
273
- },
274
- });
275
- aigneHubUrl = customUrl;
276
- }
277
- else if (subscribe === "manual") {
278
- console.log("You chose to configure your own LLM API Keys. Exiting...");
279
- process.exit(0);
280
- }
281
- }
282
- credential = await connectToAIGNEHub(aigneHubUrl);
283
- }
284
- }
43
+ const model = await loadModel({
44
+ ...parseModelOption(formattedModelName),
45
+ temperature,
46
+ topP,
47
+ presencePenalty,
48
+ frequencyPenalty,
49
+ }, modelOptions, { aigneHubUrl: AIGNE_HUB_URL, inquirerPromptFn: actionOptions?.inquirerPromptFn });
50
+ if (path) {
51
+ return await AIGNE.load(path, { loadModel, memories: availableMemories, model });
285
52
  }
286
- const model = await loadModel(parseModelOption(modelName), undefined, credential);
287
- return await AIGNE.load(path, { loadModel, memories: availableMemories, model });
53
+ return new AIGNE({ model });
288
54
  }
@@ -1,4 +1,4 @@
1
- import { type Agent, AIGNE, type ChatModelOptions, type Message } from "@aigne/core";
1
+ import { type Agent, type AIGNE, type ChatModelOptions, type Message } from "@aigne/core";
2
2
  import { LogLevel } from "@aigne/core/utils/logger.js";
3
3
  import { type PromiseOrValue } from "@aigne/core/utils/type-utils.js";
4
4
  import type { Argv } from "yargs";
@@ -41,16 +41,14 @@ export declare const createRunAIGNECommand: (yargs: Argv) => Argv<{
41
41
  force: boolean;
42
42
  } & {
43
43
  "log-level": LogLevel;
44
+ } & {
45
+ "aigne-hub-url": string | undefined;
44
46
  }>;
45
47
  export declare function parseAgentInputByCommander(agent: Agent, options?: RunAIGNECommandOptions & {
46
48
  inputKey?: string;
47
49
  argv?: string[];
48
50
  defaultInput?: string | Message;
49
51
  }): Promise<Message>;
50
- export declare const parseModelOption: (model?: string) => {
51
- provider: string | undefined;
52
- name: string | undefined;
53
- };
54
52
  export declare function runWithAIGNE(agentCreator: ((aigne: AIGNE) => PromiseOrValue<Agent>) | Agent, { argv, chatLoopOptions, modelOptions, outputKey, }?: {
55
53
  argv?: typeof process.argv;
56
54
  chatLoopOptions?: ChatLoopOptions;
@@ -4,8 +4,8 @@ import { dirname, isAbsolute, join } from "node:path";
4
4
  import { isatty } from "node:tty";
5
5
  import { promisify } from "node:util";
6
6
  import { exists } from "@aigne/agent-library/utils/fs.js";
7
- import { availableModels, loadModel } from "@aigne/aigne-hub";
8
- import { AIAgent, AIGNE, DEFAULT_OUTPUT_KEY, readAllString, UserAgent, } from "@aigne/core";
7
+ import { availableModels, parseModelOption } from "@aigne/aigne-hub";
8
+ import { AIAgent, DEFAULT_OUTPUT_KEY, readAllString, UserAgent, } from "@aigne/core";
9
9
  import { getLevelFromEnv, LogLevel, logger } from "@aigne/core/utils/logger.js";
10
10
  import { flat, isEmpty, tryOrThrow } from "@aigne/core/utils/type-utils.js";
11
11
  import chalk from "chalk";
@@ -14,6 +14,7 @@ import yargs from "yargs";
14
14
  import { hideBin } from "yargs/helpers";
15
15
  import { ZodError, ZodObject, z } from "zod";
16
16
  import { TerminalTracer } from "../tracer/terminal.js";
17
+ import { loadAIGNE } from "./load-aigne.js";
17
18
  import { DEFAULT_CHAT_INPUT_KEY, runChatLoopInTerminal, } from "./run-chat-loop.js";
18
19
  export const createRunAIGNECommand = (yargs) => yargs
19
20
  .option("chat", {
@@ -81,6 +82,10 @@ export const createRunAIGNECommand = (yargs) => yargs
81
82
  type: "string",
82
83
  default: getLevelFromEnv(logger.options.ns) || LogLevel.INFO,
83
84
  coerce: customZodError("--log-level", (s) => z.nativeEnum(LogLevel).parse(s)),
85
+ })
86
+ .option("aigne-hub-url", {
87
+ describe: "Custom AIGNE Hub service URL. Used to fetch remote agent definitions or models. ",
88
+ type: "string",
84
89
  });
85
90
  export async function parseAgentInputByCommander(agent, options = {}) {
86
91
  const inputSchemaShape = flat(agent instanceof AIAgent ? agent.inputKey : undefined, agent.inputSchema instanceof ZodObject ? Object.keys(agent.inputSchema.shape) : []);
@@ -123,24 +128,22 @@ export async function parseAgentInputByCommander(agent, options = {}) {
123
128
  }
124
129
  return input;
125
130
  }
126
- export const parseModelOption = (model) => {
127
- const { provider, name } = (model || process.env.MODEL)?.match(/(?<provider>[^:]*)(:(?<name>(\S+)))?/)?.groups ?? {};
128
- return { provider, name };
129
- };
130
131
  export async function runWithAIGNE(agentCreator, { argv = process.argv, chatLoopOptions, modelOptions, outputKey, } = {}) {
131
132
  await yargs()
132
133
  .command("$0", "Run an agent with AIGNE", (yargs) => createRunAIGNECommand(yargs), async (options) => {
133
134
  if (options.logLevel) {
134
135
  logger.level = options.logLevel;
135
136
  }
136
- const model = await loadModel({
137
- ...parseModelOption(options.model),
138
- temperature: options.temperature,
139
- topP: options.topP,
140
- presencePenalty: options.presencePenalty,
141
- frequencyPenalty: options.frequencyPenalty,
142
- }, modelOptions);
143
- const aigne = new AIGNE({ model });
137
+ 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,
144
+ },
145
+ modelOptions,
146
+ });
144
147
  try {
145
148
  const agent = typeof agentCreator === "function" ? await agentCreator(aigne) : agentCreator;
146
149
  const input = await parseAgentInputByCommander(agent, {
@@ -0,0 +1 @@
1
+ export declare function highlightUrl(str: string): string;
@@ -0,0 +1,4 @@
1
+ import chalk from "chalk";
2
+ export function highlightUrl(str) {
3
+ return str.replace(/https?:\/\/[^\s]+/g, (url) => chalk.cyan(url));
4
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/cli",
3
- "version": "1.33.1",
3
+ "version": "1.34.0",
4
4
  "description": "Your command center for agent development",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -51,10 +51,8 @@
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",
55
54
  "@smithy/node-http-handler": "^4.1.0",
56
55
  "chalk": "^5.4.1",
57
- "crypto": "^1.0.1",
58
56
  "detect-port": "^2.1.0",
59
57
  "dotenv-flow": "^4.1.0",
60
58
  "express": "^5.1.0",
@@ -64,22 +62,22 @@
64
62
  "inquirer": "^12.7.0",
65
63
  "log-update": "^6.1.0",
66
64
  "marked": "^16.0.0",
65
+ "nunjucks": "^3.2.4",
67
66
  "open": "10.1.2",
68
67
  "openai": "^5.8.3",
69
- "p-wait-for": "^5.0.2",
70
68
  "prettier": "^3.6.2",
71
69
  "tar": "^7.4.3",
72
70
  "wrap-ansi": "^9.0.0",
73
71
  "yaml": "^2.8.0",
74
72
  "yargs": "^18.0.0",
75
73
  "zod": "^3.25.67",
76
- "@aigne/agent-library": "^1.21.17",
77
- "@aigne/agentic-memory": "^1.0.17",
78
- "@aigne/aigne-hub": "^0.4.8",
79
- "@aigne/core": "^1.48.0",
74
+ "@aigne/agent-library": "^1.21.18",
75
+ "@aigne/agentic-memory": "^1.0.18",
76
+ "@aigne/aigne-hub": "^0.4.9",
77
+ "@aigne/core": "^1.49.0",
78
+ "@aigne/default-memory": "^1.1.0",
80
79
  "@aigne/observability-api": "^0.9.0",
81
- "@aigne/default-memory": "^1.0.17",
82
- "@aigne/openai": "^0.10.17"
80
+ "@aigne/openai": "^0.11.0"
83
81
  },
84
82
  "devDependencies": {
85
83
  "@types/archiver": "^6.0.3",