@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 +59 -0
- package/dist/commands/app.js +9 -95
- package/dist/commands/hub.js +3 -1
- package/dist/commands/run.js +1 -3
- package/dist/commands/serve-mcp.js +1 -1
- package/dist/commands/test.js +1 -1
- package/dist/tracer/terminal.d.ts +2 -0
- package/dist/tracer/terminal.js +58 -1
- package/dist/utils/aigne-hub/constants.d.ts +6 -0
- package/dist/utils/aigne-hub/constants.js +12 -0
- package/dist/utils/aigne-hub/credential.d.ts +23 -0
- package/dist/utils/aigne-hub/credential.js +206 -0
- package/dist/utils/aigne-hub/crypto.d.ts +4 -0
- package/dist/utils/aigne-hub/crypto.js +9 -0
- package/dist/utils/aigne-hub/model.d.ts +12 -0
- package/dist/utils/aigne-hub/model.js +75 -0
- package/dist/utils/aigne-hub/type.d.ts +38 -0
- package/dist/utils/aigne-hub/type.js +1 -0
- package/dist/utils/listr.js +5 -3
- package/dist/utils/load-aigne.d.ts +3 -8
- package/dist/utils/load-aigne.js +17 -51
- package/dist/utils/run-with-aigne.d.ts +0 -1
- package/dist/utils/run-with-aigne.js +15 -52
- package/dist/utils/yargs.d.ts +20 -0
- package/dist/utils/yargs.js +107 -0
- package/package.json +11 -8
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
|
|
package/dist/commands/app.js
CHANGED
|
@@ -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 {
|
|
6
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
158
|
+
aigne: await AIGNE.load(dir),
|
|
245
159
|
dir,
|
|
246
160
|
version: result.version,
|
|
247
161
|
};
|
package/dist/commands/hub.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import {
|
|
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) => {
|
package/dist/commands/run.js
CHANGED
|
@@ -78,12 +78,10 @@ export function createRunCommand({ aigneFilePath, } = {}) {
|
|
|
78
78
|
}
|
|
79
79
|
const aigne = await loadAIGNE({
|
|
80
80
|
path: dir,
|
|
81
|
-
|
|
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
|
-
|
|
56
|
+
modelOptions: { aigneHubUrl: options.aigneHubUrl },
|
|
57
57
|
});
|
|
58
58
|
await serveMCPServer({
|
|
59
59
|
aigne,
|
package/dist/commands/test.js
CHANGED
|
@@ -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
|
-
|
|
27
|
+
modelOptions: { aigneHubUrl: options?.aigneHubUrl },
|
|
28
28
|
});
|
|
29
29
|
assert(aigne.rootDir);
|
|
30
30
|
spawnSync("node", ["--test"], { cwd: aigne.rootDir, stdio: "inherit" });
|
package/dist/tracer/terminal.js
CHANGED
|
@@ -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 {};
|
package/dist/utils/listr.js
CHANGED
|
@@ -51,9 +51,11 @@ export class AIGNEListr extends Listr {
|
|
|
51
51
|
try {
|
|
52
52
|
this.ctx = {};
|
|
53
53
|
this.spinner.start();
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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,
|
|
10
|
+
export declare function loadAIGNE({ path, modelOptions, }: {
|
|
11
11
|
path?: string;
|
|
12
|
-
|
|
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>>;
|
package/dist/utils/load-aigne.js
CHANGED
|
@@ -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 {
|
|
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.
|
|
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
|
|
36
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
|
45
|
+
return aigne;
|
|
80
46
|
}
|
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
import {
|
|
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
|
|
8
|
-
import {
|
|
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 {
|
|
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,
|
|
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.
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
139
|
-
...
|
|
140
|
-
|
|
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.
|
|
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.
|
|
76
|
-
"@aigne/
|
|
77
|
-
"@aigne/
|
|
78
|
-
"@aigne/core": "^1.
|
|
79
|
-
"@aigne/
|
|
80
|
-
"@aigne/
|
|
81
|
-
"@aigne/
|
|
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",
|