@aigne/cli 1.56.0 → 1.57.0-beta

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,43 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.57.0-beta](https://github.com/AIGNE-io/aigne-framework/compare/cli-v1.56.1-beta...cli-v1.57.0-beta) (2025-11-26)
4
+
5
+
6
+ ### Features
7
+
8
+ * **secure:** secure credential storage with keyring support ([#771](https://github.com/AIGNE-io/aigne-framework/issues/771)) ([023c202](https://github.com/AIGNE-io/aigne-framework/commit/023c202f75eddb37d003b1fad447b491e8e1a8c2))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **secrets:** support system keyring for secure credential storage ([#773](https://github.com/AIGNE-io/aigne-framework/issues/773)) ([859ac2d](https://github.com/AIGNE-io/aigne-framework/commit/859ac2d9eb6019d7a68726076d65841cd96bc9a4))
14
+
15
+
16
+ ### Dependencies
17
+
18
+ * The following workspace dependencies were updated
19
+ * dependencies
20
+ * @aigne/secrets bumped to 0.1.1-beta
21
+ * @aigne/aigne-hub bumped to 0.10.11-beta.1
22
+
23
+ ## [1.56.1-beta](https://github.com/AIGNE-io/aigne-framework/compare/cli-v1.56.0...cli-v1.56.1-beta) (2025-11-24)
24
+
25
+
26
+ ### Dependencies
27
+
28
+ * The following workspace dependencies were updated
29
+ * dependencies
30
+ * @aigne/afs-local-fs bumped to 1.2.1-beta
31
+ * @aigne/agent-library bumped to 1.22.1-beta
32
+ * @aigne/agentic-memory bumped to 1.1.1-beta
33
+ * @aigne/aigne-hub bumped to 0.10.11-beta
34
+ * @aigne/core bumped to 1.69.1-beta
35
+ * @aigne/default-memory bumped to 1.3.1-beta
36
+ * @aigne/observability-api bumped to 0.11.10-beta
37
+ * @aigne/openai bumped to 0.16.11-beta
38
+ * devDependencies
39
+ * @aigne/test-utils bumped to 0.5.64-beta
40
+
3
41
  ## [1.56.0](https://github.com/AIGNE-io/aigne-framework/compare/cli-v1.56.0-beta.4...cli-v1.56.0) (2025-11-21)
4
42
 
5
43
 
@@ -1,12 +1,10 @@
1
- import { existsSync } from "node:fs";
2
- import { readFile, writeFile } from "node:fs/promises";
3
1
  import { AIGNE_HUB_URL } from "@aigne/aigne-hub";
4
2
  import chalk from "chalk";
5
3
  import Table from "cli-table3";
6
4
  import inquirer from "inquirer";
7
- import { parse, stringify } from "yaml";
8
- import { AIGNE_ENV_FILE, isTest } from "../utils/aigne-hub/constants.js";
5
+ import { isTest } from "../utils/aigne-hub/constants.js";
9
6
  import { connectToAIGNEHub } from "../utils/aigne-hub/credential.js";
7
+ import getSecretStore from "../utils/aigne-hub/store/index.js";
10
8
  import { getUserInfo } from "../utils/aigne-hub-user.js";
11
9
  import { getUrlOrigin } from "../utils/get-url-origin.js";
12
10
  export const formatNumber = (balance) => {
@@ -53,21 +51,16 @@ function printHubStatus(data) {
53
51
  }
54
52
  }
55
53
  async function getHubs() {
56
- if (!existsSync(AIGNE_ENV_FILE)) {
57
- return [];
58
- }
59
54
  try {
60
- const data = await readFile(AIGNE_ENV_FILE, "utf8");
61
- const envs = parse(data);
55
+ const secretStore = await getSecretStore();
56
+ const hosts = await secretStore.listHosts();
62
57
  const statusList = [];
63
- for (const [host, config] of Object.entries(envs)) {
64
- if (host !== "default") {
65
- statusList.push({
66
- host,
67
- apiUrl: config.AIGNE_HUB_API_URL,
68
- apiKey: config.AIGNE_HUB_API_KEY,
69
- });
70
- }
58
+ for (const host of hosts) {
59
+ statusList.push({
60
+ host: new URL(host.AIGNE_HUB_API_URL).host,
61
+ apiUrl: host.AIGNE_HUB_API_URL,
62
+ apiKey: host.AIGNE_HUB_API_KEY || "",
63
+ });
71
64
  }
72
65
  return statusList;
73
66
  }
@@ -76,8 +69,14 @@ async function getHubs() {
76
69
  }
77
70
  }
78
71
  const getDefaultHub = async () => {
79
- const envs = parse(await readFile(AIGNE_ENV_FILE, "utf8").catch(() => stringify({})));
80
- return envs?.default?.AIGNE_HUB_API_URL || AIGNE_HUB_URL;
72
+ try {
73
+ const secretStore = await getSecretStore();
74
+ const defaultHost = await secretStore.getDefault();
75
+ return defaultHost?.AIGNE_HUB_API_URL || AIGNE_HUB_URL;
76
+ }
77
+ catch {
78
+ return AIGNE_HUB_URL;
79
+ }
81
80
  };
82
81
  async function formatHubsList(statusList) {
83
82
  if (statusList?.length === 0) {
@@ -211,7 +210,12 @@ async function showInfo() {
211
210
  value: h.apiUrl,
212
211
  })),
213
212
  });
214
- await printHubDetails(hubApiKey);
213
+ try {
214
+ await printHubDetails(hubApiKey);
215
+ }
216
+ catch (error) {
217
+ console.error(chalk.red("✗ Failed to print hub details:"), error.message);
218
+ }
215
219
  }
216
220
  function validateUrl(input) {
217
221
  try {
@@ -223,27 +227,18 @@ function validateUrl(input) {
223
227
  }
224
228
  }
225
229
  async function saveAndConnect(url) {
226
- const envs = parse(await readFile(AIGNE_ENV_FILE, "utf8").catch(() => stringify({})));
227
- const host = new URL(url).host;
228
- if (envs[host]) {
229
- const currentUrl = envs[host]?.AIGNE_HUB_API_URL;
230
- if (currentUrl) {
231
- await setDefaultHub(currentUrl);
232
- console.log(chalk.green(`✓ Hub ${getUrlOrigin(currentUrl)} connected successfully.`));
233
- return;
234
- }
230
+ const secretStore = await getSecretStore();
231
+ const currentKey = await secretStore.getKey(url);
232
+ if (currentKey?.AIGNE_HUB_API_URL) {
233
+ await setDefaultHub(currentKey.AIGNE_HUB_API_URL);
234
+ console.log(chalk.green(`✓ Hub ${getUrlOrigin(currentKey.AIGNE_HUB_API_URL)} connected successfully.`));
235
+ return;
235
236
  }
236
237
  try {
237
238
  if (isTest) {
238
- writeFile(AIGNE_ENV_FILE, stringify({
239
- "hub.aigne.io": {
240
- AIGNE_HUB_API_KEY: "test-key",
241
- AIGNE_HUB_API_URL: "https://hub.aigne.io/ai-kit",
242
- },
243
- default: {
244
- AIGNE_HUB_API_URL: "https://hub.aigne.io/ai-kit",
245
- },
246
- }));
239
+ await secretStore.setKey("https://hub.aigne.io/ai-kit", "test-key");
240
+ await secretStore.setDefault("https://hub.aigne.io/ai-kit");
241
+ console.log(chalk.green(`✓ Hub https://hub.aigne.io/ai-kit connected successfully.`));
247
242
  return;
248
243
  }
249
244
  await connectToAIGNEHub(url);
@@ -254,35 +249,46 @@ async function saveAndConnect(url) {
254
249
  }
255
250
  }
256
251
  async function setDefaultHub(url) {
257
- const envs = parse(await readFile(AIGNE_ENV_FILE, "utf8").catch(() => stringify({})));
258
- const host = new URL(url).host;
259
- if (!envs[host]) {
260
- console.error(chalk.red("✗ Hub not found"));
261
- return;
252
+ try {
253
+ const secretStore = await getSecretStore();
254
+ const key = await secretStore.getKey(url);
255
+ if (!key) {
256
+ console.error(chalk.red("✗ Hub not found"));
257
+ return;
258
+ }
259
+ await secretStore.setDefault(key.AIGNE_HUB_API_URL);
260
+ console.log(chalk.green(`✓ Switched active hub to ${getUrlOrigin(url)}`));
261
+ }
262
+ catch {
263
+ console.error(chalk.red("✗ Failed to set default hub"));
262
264
  }
263
- await writeFile(AIGNE_ENV_FILE, stringify({ ...envs, default: { AIGNE_HUB_API_URL: envs[host]?.AIGNE_HUB_API_URL } }));
264
- console.log(chalk.green(`✓ Switched active hub to ${getUrlOrigin(url)}`));
265
265
  }
266
266
  async function deleteHub(url) {
267
- const envs = parse(await readFile(AIGNE_ENV_FILE, "utf8").catch(() => stringify({})));
268
- const host = new URL(url).host;
269
- delete envs[host];
270
- if (envs.default?.AIGNE_HUB_API_URL && new URL(envs.default?.AIGNE_HUB_API_URL).host === host) {
271
- delete envs.default;
267
+ try {
268
+ const secretStore = await getSecretStore();
269
+ const key = await secretStore.getKey(url);
270
+ if (key) {
271
+ await secretStore.deleteKey(url);
272
+ }
273
+ await secretStore.deleteDefault();
274
+ console.log(chalk.green(`✓ Hub ${getUrlOrigin(url)} removed`));
275
+ }
276
+ catch {
277
+ console.error(chalk.red("✗ Failed to delete hub"));
272
278
  }
273
- await writeFile(AIGNE_ENV_FILE, stringify(envs));
274
- console.log(chalk.green(`✓ Hub ${getUrlOrigin(url)} removed`));
275
279
  }
276
280
  async function printHubDetails(url) {
277
- const envs = parse(await readFile(AIGNE_ENV_FILE, "utf8").catch(() => stringify({})));
278
- const host = new URL(url).host;
281
+ const secretStore = await getSecretStore();
282
+ const key = await secretStore.getKey(url);
283
+ const defaultHub = await getDefaultHub();
284
+ const isDefault = getUrlOrigin(url) === getUrlOrigin(defaultHub);
279
285
  const userInfo = await getUserInfo({
280
- baseUrl: envs[host]?.AIGNE_HUB_API_URL || "",
281
- apiKey: envs[host]?.AIGNE_HUB_API_KEY || "",
286
+ baseUrl: key?.AIGNE_HUB_API_URL || "",
287
+ apiKey: key?.AIGNE_HUB_API_KEY || "",
282
288
  }).catch(() => null);
283
289
  printHubStatus({
284
290
  hub: getUrlOrigin(url),
285
- status: userInfo ? "Connected" : "Not connected",
291
+ status: isDefault ? "Connected" : "Not connected",
286
292
  user: {
287
293
  name: userInfo?.user.fullName || "",
288
294
  did: userInfo?.user.did || "",
@@ -1,5 +1,4 @@
1
1
  import { existsSync, mkdirSync } from "node:fs";
2
- import { readFile, writeFile } from "node:fs/promises";
3
2
  import { homedir } from "node:os";
4
3
  import { join } from "node:path";
5
4
  import { AIGNE_HUB_BLOCKLET_DID, AIGNE_HUB_URL, getAIGNEHubMountPoint } from "@aigne/aigne-hub";
@@ -9,9 +8,9 @@ import inquirer from "inquirer";
9
8
  import open from "open";
10
9
  import pWaitFor from "p-wait-for";
11
10
  import { joinURL, withQuery } from "ufo";
12
- import { parse, stringify } from "yaml";
13
- import { ACCESS_KEY_SESSION_API, AIGNE_ENV_FILE, isTest, WELLKNOWN_SERVICE_PATH_PREFIX, } from "./constants.js";
11
+ import { ACCESS_KEY_SESSION_API, isTest, WELLKNOWN_SERVICE_PATH_PREFIX } from "./constants.js";
14
12
  import { decrypt, encodeEncryptionKey } from "./crypto.js";
13
+ import getSecretStore from "./store/index.js";
15
14
  const request = async (config) => {
16
15
  const headers = {};
17
16
  if (config.requestCount !== undefined) {
@@ -66,7 +65,7 @@ export async function createConnect({ connectUrl, openPage, fetchInterval = 3 *
66
65
  });
67
66
  }
68
67
  export async function connectToAIGNEHub(url) {
69
- const { origin, host } = new URL(url);
68
+ const { origin } = new URL(url);
70
69
  const connectUrl = joinURL(origin, WELLKNOWN_SERVICE_PATH_PREFIX);
71
70
  const apiUrl = await getAIGNEHubMountPoint(url, AIGNE_HUB_BLOCKLET_DID);
72
71
  try {
@@ -82,22 +81,9 @@ export async function connectToAIGNEHub(url) {
82
81
  apiKey: result.accessKeySecret,
83
82
  url: apiUrl,
84
83
  };
85
- // After redirection, write the AIGNE Hub access token
86
- const aigneDir = join(homedir(), ".aigne");
87
- if (!existsSync(aigneDir)) {
88
- mkdirSync(aigneDir, { recursive: true });
89
- }
90
- const envs = parse(await readFile(AIGNE_ENV_FILE, "utf8").catch(() => stringify({})));
91
- await writeFile(AIGNE_ENV_FILE, stringify({
92
- ...envs,
93
- [host]: {
94
- AIGNE_HUB_API_KEY: accessKeyOptions.apiKey,
95
- AIGNE_HUB_API_URL: accessKeyOptions.url,
96
- },
97
- default: {
98
- AIGNE_HUB_API_URL: accessKeyOptions.url,
99
- },
100
- }));
84
+ const secretStore = await getSecretStore();
85
+ await secretStore.setKey(accessKeyOptions.url, accessKeyOptions.apiKey);
86
+ await secretStore.setDefault(accessKeyOptions.url);
101
87
  return accessKeyOptions;
102
88
  }
103
89
  catch (error) {
@@ -106,20 +92,9 @@ export async function connectToAIGNEHub(url) {
106
92
  }
107
93
  }
108
94
  export const checkConnectionStatus = async (host) => {
109
- // aigne-hub access token
110
- if (!existsSync(AIGNE_ENV_FILE)) {
111
- throw new Error("AIGNE_HUB_API_KEY file not found, need to login first");
112
- }
113
- const data = await readFile(AIGNE_ENV_FILE, "utf8");
114
- if (!data.includes("AIGNE_HUB_API_KEY")) {
115
- throw new Error("AIGNE_HUB_API_KEY key not found, need to login first");
116
- }
117
- const envs = parse(data);
118
- if (!envs[host]) {
119
- throw new Error("AIGNE_HUB_API_KEY host not found, need to login first");
120
- }
121
- const env = envs[host];
122
- if (!env.AIGNE_HUB_API_KEY) {
95
+ const secretStore = await getSecretStore();
96
+ const env = await secretStore.getKey(host);
97
+ if (!env?.AIGNE_HUB_API_KEY) {
123
98
  throw new Error("AIGNE_HUB_API_KEY key not found, need to login first");
124
99
  }
125
100
  return {
@@ -135,10 +110,11 @@ export async function loadAIGNEHubCredential(options) {
135
110
  if (!existsSync(aigneDir)) {
136
111
  mkdirSync(aigneDir, { recursive: true });
137
112
  }
138
- const envs = parse(await readFile(AIGNE_ENV_FILE, "utf8").catch(() => stringify({})));
113
+ const secretStore = await getSecretStore();
114
+ const defaultHost = await secretStore.getDefault();
139
115
  const inquirerPrompt = (options?.inquirerPromptFn ?? inquirer.prompt);
140
116
  const configUrl = options?.aigneHubUrl || process.env.AIGNE_HUB_API_URL;
141
- const url = configUrl || envs?.default?.AIGNE_HUB_API_URL || AIGNE_HUB_URL;
117
+ const url = configUrl || defaultHost?.AIGNE_HUB_API_URL || AIGNE_HUB_URL;
142
118
  const connectUrl = joinURL(new URL(url).origin, WELLKNOWN_SERVICE_PATH_PREFIX);
143
119
  const { host } = new URL(url);
144
120
  let credential = {};
@@ -1,11 +1,10 @@
1
- import { readFile, writeFile } from "node:fs/promises";
2
1
  import { AIGNE_HUB_DEFAULT_MODEL, AIGNE_HUB_URL, findImageModel, findModel, getSupportedProviders, parseModel, resolveProviderModelId, } from "@aigne/aigne-hub";
3
2
  import { flat, omit } from "@aigne/core/utils/type-utils.js";
4
3
  import chalk from "chalk";
5
4
  import inquirer from "inquirer";
6
- import { parse, stringify } from "yaml";
7
- import { AIGNE_ENV_FILE, AIGNE_HUB_PROVIDER } from "./constants.js";
5
+ import { AIGNE_HUB_PROVIDER } from "./constants.js";
8
6
  import { loadAIGNEHubCredential } from "./credential.js";
7
+ import getSecretStore from "./store/index.js";
9
8
  export function maskApiKey(apiKey) {
10
9
  if (!apiKey || apiKey.length <= 8)
11
10
  return apiKey;
@@ -50,8 +49,9 @@ export const formatModelName = async (model, inquirerPrompt) => {
50
49
  if (requireEnvs.some((name) => name && process.env[name])) {
51
50
  return { provider, model: name };
52
51
  }
53
- const envs = parse(await readFile(AIGNE_ENV_FILE, "utf8").catch(() => stringify({})));
54
- if (process.env.AIGNE_HUB_API_KEY || envs?.default?.AIGNE_HUB_API_URL) {
52
+ const secretStore = await getSecretStore();
53
+ const defaultHost = await secretStore.getDefault();
54
+ if (process.env.AIGNE_HUB_API_KEY || defaultHost?.AIGNE_HUB_API_URL) {
55
55
  return { provider: AIGNE_HUB_PROVIDER, model: `${provider}/${name}` };
56
56
  }
57
57
  const result = await inquirerPrompt({
@@ -74,12 +74,13 @@ export const formatModelName = async (model, inquirerPrompt) => {
74
74
  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.`));
75
75
  process.exit(0);
76
76
  }
77
- if (envs && Object.keys(envs).length > 0 && !envs.default?.AIGNE_HUB_API_URL) {
77
+ const envs = await secretStore.listHostsMap();
78
+ if (envs && Object.keys(envs).length > 0 && !defaultHost?.AIGNE_HUB_API_URL) {
78
79
  const host = new URL(AIGNE_HUB_URL).host;
79
80
  const defaultEnv = envs[host]?.AIGNE_HUB_API_URL
80
81
  ? envs[host]
81
82
  : Object.values(envs)[0] || { AIGNE_HUB_API_URL: "" };
82
- await writeFile(AIGNE_ENV_FILE, stringify({ ...envs, default: { AIGNE_HUB_API_URL: defaultEnv?.AIGNE_HUB_API_URL } }));
83
+ await secretStore.setDefault(defaultEnv?.AIGNE_HUB_API_URL);
83
84
  }
84
85
  return { provider: AIGNE_HUB_PROVIDER, model: `${provider}/${name}` };
85
86
  };
@@ -0,0 +1,15 @@
1
+ import type { GetDefaultOptions, ItemInfo, StoreOptions } from "@aigne/secrets";
2
+ import { FileStore as BaseFileStore } from "@aigne/secrets";
3
+ declare class FileStore extends BaseFileStore {
4
+ private outputConfig;
5
+ constructor(options: Required<Pick<StoreOptions, "filepath">>);
6
+ setKey(url: string, apiKey: string): Promise<void>;
7
+ getKey(url: string): Promise<ItemInfo | null>;
8
+ deleteKey(url: string): Promise<boolean>;
9
+ listHosts(): Promise<ItemInfo[]>;
10
+ listHostsMap(): Promise<Record<string, ItemInfo>>;
11
+ setDefault(url: string): Promise<void>;
12
+ getDefault(options?: GetDefaultOptions): Promise<ItemInfo | null>;
13
+ deleteDefault(): Promise<void>;
14
+ }
15
+ export default FileStore;
@@ -0,0 +1,69 @@
1
+ import { FileStore as BaseFileStore } from "@aigne/secrets";
2
+ class FileStore extends BaseFileStore {
3
+ outputConfig;
4
+ constructor(options) {
5
+ super(options);
6
+ this.outputConfig = { url: "AIGNE_HUB_API_URL", key: "AIGNE_HUB_API_KEY" };
7
+ }
8
+ async setKey(url, apiKey) {
9
+ return this.setItem(this.normalizeHostFrom(url), {
10
+ [this.outputConfig.url]: url,
11
+ [this.outputConfig.key]: apiKey,
12
+ });
13
+ }
14
+ async getKey(url) {
15
+ const host = this.normalizeHostFrom(url);
16
+ return this.getItem(host);
17
+ }
18
+ async deleteKey(url) {
19
+ const host = this.normalizeHostFrom(url);
20
+ return this.deleteItem(host);
21
+ }
22
+ async listHosts() {
23
+ return this.listEntries();
24
+ }
25
+ async listHostsMap() {
26
+ return this.listMap();
27
+ }
28
+ async setDefault(url) {
29
+ return this.setDefaultItem({ [this.outputConfig.url]: url });
30
+ }
31
+ async getDefault(options = {}) {
32
+ const { fallbackToFirst = false, presetIfFallback = false } = options;
33
+ if (!(await this.available()))
34
+ return null;
35
+ try {
36
+ const value = await this.getDefaultItem();
37
+ const apiUrl = value?.[this.outputConfig.url];
38
+ if (apiUrl) {
39
+ const defaultInfo = await this.getKey(apiUrl);
40
+ if (defaultInfo)
41
+ return defaultInfo;
42
+ }
43
+ }
44
+ catch {
45
+ // ignore
46
+ }
47
+ if (!fallbackToFirst)
48
+ return null;
49
+ const hosts = await this.listHosts();
50
+ if (hosts.length === 0)
51
+ return null;
52
+ const firstHost = hosts[0];
53
+ if (!firstHost)
54
+ return null;
55
+ if (presetIfFallback && firstHost[this.outputConfig.url]) {
56
+ try {
57
+ await this.setDefault(firstHost[this.outputConfig.url]);
58
+ }
59
+ catch {
60
+ // ignore
61
+ }
62
+ }
63
+ return firstHost;
64
+ }
65
+ async deleteDefault() {
66
+ return this.deleteDefaultItem();
67
+ }
68
+ }
69
+ export default FileStore;
@@ -0,0 +1,4 @@
1
+ import FileStore from "./file.js";
2
+ import KeyringStore from "./keytar.js";
3
+ declare const getSecretStore: () => Promise<FileStore | KeyringStore>;
4
+ export default getSecretStore;
@@ -0,0 +1,39 @@
1
+ import { logger } from "@aigne/core/utils/logger.js";
2
+ import { AIGNE_ENV_FILE } from "../constants.js";
3
+ import FileStore from "./file.js";
4
+ import KeyringStore from "./keytar.js";
5
+ import { migrateFileToKeyring } from "./migrate.js";
6
+ async function createSecretStore(options) {
7
+ if (!options.serviceName) {
8
+ throw new Error("Secret store key is required");
9
+ }
10
+ const keyring = new KeyringStore(options);
11
+ if (await keyring.available()) {
12
+ if (options.filepath) {
13
+ try {
14
+ await migrateFileToKeyring(options);
15
+ logger.debug("Successfully migrated credentials from file to keyring");
16
+ }
17
+ catch (error) {
18
+ logger.warn("Failed to migrate credentials from file to keyring:", error.message);
19
+ }
20
+ }
21
+ return keyring;
22
+ }
23
+ const filepath = options.filepath;
24
+ if (!filepath) {
25
+ throw new Error("Filepath is required");
26
+ }
27
+ return new FileStore({ filepath });
28
+ }
29
+ let cachedSecretStore;
30
+ const getSecretStore = async () => {
31
+ if (!cachedSecretStore) {
32
+ cachedSecretStore = await createSecretStore({
33
+ filepath: AIGNE_ENV_FILE,
34
+ serviceName: "aigne-hub-keyring",
35
+ });
36
+ }
37
+ return cachedSecretStore;
38
+ };
39
+ export default getSecretStore;
@@ -0,0 +1,15 @@
1
+ import type { GetDefaultOptions, ItemInfo, StoreOptions } from "@aigne/secrets";
2
+ import { KeyringStore as BaseKeyringStore } from "@aigne/secrets";
3
+ declare class KeyringStore extends BaseKeyringStore {
4
+ private outputConfig;
5
+ constructor(options: StoreOptions);
6
+ setKey(url: string, apiKey: string): Promise<any>;
7
+ getKey(url: string): Promise<ItemInfo | null>;
8
+ deleteKey(url: string): Promise<boolean>;
9
+ listHosts(): Promise<ItemInfo[]>;
10
+ listHostsMap(): Promise<Record<string, ItemInfo>>;
11
+ setDefault(url: string): Promise<void>;
12
+ getDefault(options?: GetDefaultOptions): Promise<ItemInfo | null>;
13
+ deleteDefault(): Promise<void>;
14
+ }
15
+ export default KeyringStore;
@@ -0,0 +1,67 @@
1
+ import { KeyringStore as BaseKeyringStore } from "@aigne/secrets";
2
+ class KeyringStore extends BaseKeyringStore {
3
+ outputConfig;
4
+ constructor(options) {
5
+ super(options);
6
+ this.outputConfig = { url: "AIGNE_HUB_API_URL", key: "AIGNE_HUB_API_KEY" };
7
+ }
8
+ async setKey(url, apiKey) {
9
+ return this.setItem(this.normalizeHostFrom(url), {
10
+ [this.outputConfig.url]: url,
11
+ [this.outputConfig.key]: apiKey,
12
+ });
13
+ }
14
+ async getKey(url) {
15
+ const v = await this.getItem(this.normalizeHostFrom(url));
16
+ return v;
17
+ }
18
+ async deleteKey(url) {
19
+ const ok = await this.deleteItem(this.normalizeHostFrom(url));
20
+ return !!ok;
21
+ }
22
+ async listHosts() {
23
+ return this.listEntries();
24
+ }
25
+ async listHostsMap() {
26
+ return this.listMap();
27
+ }
28
+ async setDefault(url) {
29
+ return this.setDefaultItem({ [this.outputConfig.url]: url });
30
+ }
31
+ async getDefault(options = {}) {
32
+ const { fallbackToFirst = false, presetIfFallback = false } = options;
33
+ try {
34
+ const value = await this.getDefaultItem();
35
+ const apiUrl = value?.[this.outputConfig.url];
36
+ if (apiUrl) {
37
+ const defaultInfo = await this.getKey(apiUrl);
38
+ if (defaultInfo)
39
+ return defaultInfo;
40
+ }
41
+ }
42
+ catch {
43
+ // ignore
44
+ }
45
+ if (!fallbackToFirst)
46
+ return null;
47
+ const hosts = await this.listHosts();
48
+ if (hosts.length === 0)
49
+ return null;
50
+ const firstHost = hosts[0];
51
+ if (!firstHost)
52
+ return null;
53
+ if (presetIfFallback && firstHost[this.outputConfig.url]) {
54
+ try {
55
+ await this.setDefault(firstHost[this.outputConfig.url]);
56
+ }
57
+ catch {
58
+ // ignore
59
+ }
60
+ }
61
+ return firstHost;
62
+ }
63
+ async deleteDefault() {
64
+ return this.deleteDefaultItem();
65
+ }
66
+ }
67
+ export default KeyringStore;
@@ -0,0 +1,2 @@
1
+ import type { StoreOptions } from "@aigne/secrets";
2
+ export declare function migrateFileToKeyring(options: StoreOptions): Promise<boolean>;
@@ -0,0 +1,59 @@
1
+ import fs from "node:fs/promises";
2
+ import { logger } from "@aigne/core/utils/logger.js";
3
+ import FileStore from "./file.js";
4
+ import KeyringStore from "./keytar.js";
5
+ export async function migrateFileToKeyring(options) {
6
+ const { filepath } = options;
7
+ const outputConfig = {
8
+ url: "AIGNE_HUB_API_URL",
9
+ key: "AIGNE_HUB_API_KEY",
10
+ };
11
+ if (!filepath) {
12
+ throw new Error("Filepath is required for migration");
13
+ }
14
+ try {
15
+ await fs.access(filepath);
16
+ }
17
+ catch {
18
+ return true;
19
+ }
20
+ const keyring = new KeyringStore(options);
21
+ if (!(await keyring.available())) {
22
+ return false;
23
+ }
24
+ const fileStore = new FileStore({ filepath });
25
+ if (!(await fileStore.available())) {
26
+ return false;
27
+ }
28
+ const backupPath = `${filepath}.backup`;
29
+ try {
30
+ // Create backup before migration
31
+ await fs.copyFile(filepath, backupPath);
32
+ const hosts = await fileStore.listHosts();
33
+ const migrations = [];
34
+ for (const host of hosts) {
35
+ if (host[outputConfig.url] && host[outputConfig.key]) {
36
+ migrations.push(keyring.setKey(host[outputConfig.url], host[outputConfig.key]));
37
+ }
38
+ }
39
+ await Promise.all(migrations);
40
+ const defaultKey = await fileStore.getDefault();
41
+ if (defaultKey) {
42
+ await keyring.setDefault(defaultKey[outputConfig.url]);
43
+ }
44
+ await fs.rm(filepath);
45
+ await fs.rm(backupPath);
46
+ return true;
47
+ }
48
+ catch (error) {
49
+ try {
50
+ await fs.copyFile(backupPath, filepath);
51
+ await fs.rm(backupPath);
52
+ }
53
+ catch {
54
+ // If restore fails, at least backup exists
55
+ }
56
+ logger.error(`Migration failed: ${error instanceof Error ? error.message : String(error)}`);
57
+ return false;
58
+ }
59
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/cli",
3
- "version": "1.56.0",
3
+ "version": "1.57.0-beta",
4
4
  "description": "Your command center for agent development",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -90,15 +90,16 @@
90
90
  "zod": "^3.25.67",
91
91
  "zod-to-json-schema": "^3.24.6",
92
92
  "@aigne/afs": "^1.2.1",
93
+ "@aigne/agent-library": "^1.22.1-beta",
94
+ "@aigne/agentic-memory": "^1.1.1-beta",
95
+ "@aigne/afs-local-fs": "^1.2.1-beta",
96
+ "@aigne/secrets": "^0.1.1-beta",
93
97
  "@aigne/afs-history": "^1.1.0",
94
- "@aigne/agent-library": "^1.22.0",
95
- "@aigne/afs-local-fs": "^1.2.0",
96
- "@aigne/agentic-memory": "^1.1.0",
97
- "@aigne/core": "^1.69.0",
98
- "@aigne/aigne-hub": "^0.10.10",
99
- "@aigne/default-memory": "^1.3.0",
100
- "@aigne/observability-api": "^0.11.9",
101
- "@aigne/openai": "^0.16.10"
98
+ "@aigne/aigne-hub": "^0.10.11-beta.1",
99
+ "@aigne/core": "^1.69.1-beta",
100
+ "@aigne/observability-api": "^0.11.10-beta",
101
+ "@aigne/openai": "^0.16.11-beta",
102
+ "@aigne/default-memory": "^1.3.1-beta"
102
103
  },
103
104
  "devDependencies": {
104
105
  "@inquirer/testing": "^2.1.50",
@@ -115,7 +116,7 @@
115
116
  "rimraf": "^6.0.1",
116
117
  "typescript": "^5.9.2",
117
118
  "ufo": "^1.6.1",
118
- "@aigne/test-utils": "^0.5.63"
119
+ "@aigne/test-utils": "^0.5.64-beta"
119
120
  },
120
121
  "scripts": {
121
122
  "lint": "tsc --noEmit",