@agentearth.ai/cli 0.1.0-beta.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/README.md +89 -0
- package/bin/ae.mjs +31 -0
- package/dist/commands/detail.d.ts +2 -0
- package/dist/commands/detail.js +71 -0
- package/dist/commands/detail.js.map +1 -0
- package/dist/commands/execute.d.ts +2 -0
- package/dist/commands/execute.js +118 -0
- package/dist/commands/execute.js.map +1 -0
- package/dist/commands/list.d.ts +2 -0
- package/dist/commands/list.js +94 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +96 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/recommend.d.ts +2 -0
- package/dist/commands/recommend.js +58 -0
- package/dist/commands/recommend.js.map +1 -0
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +67 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/updateCheck.d.ts +10 -0
- package/dist/commands/updateCheck.js +98 -0
- package/dist/commands/updateCheck.js.map +1 -0
- package/dist/commands/updateHint.d.ts +9 -0
- package/dist/commands/updateHint.js +25 -0
- package/dist/commands/updateHint.js.map +1 -0
- package/dist/commands/updateNpm.d.ts +13 -0
- package/dist/commands/updateNpm.js +64 -0
- package/dist/commands/updateNpm.js.map +1 -0
- package/dist/constants.d.ts +20 -0
- package/dist/constants.js +34 -0
- package/dist/constants.js.map +1 -0
- package/dist/core/args.d.ts +2 -0
- package/dist/core/args.js +110 -0
- package/dist/core/args.js.map +1 -0
- package/dist/core/backendErrors.d.ts +10 -0
- package/dist/core/backendErrors.js +69 -0
- package/dist/core/backendErrors.js.map +1 -0
- package/dist/core/errors.d.ts +20 -0
- package/dist/core/errors.js +44 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/hiddenInput.d.ts +10 -0
- package/dist/core/hiddenInput.js +53 -0
- package/dist/core/hiddenInput.js.map +1 -0
- package/dist/core/http.d.ts +9 -0
- package/dist/core/http.js +144 -0
- package/dist/core/http.js.map +1 -0
- package/dist/core/output.d.ts +11 -0
- package/dist/core/output.js +33 -0
- package/dist/core/output.js.map +1 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +300 -0
- package/dist/main.js.map +1 -0
- package/dist/storage/credentials.d.ts +9 -0
- package/dist/storage/credentials.js +97 -0
- package/dist/storage/credentials.js.map +1 -0
- package/dist/storage/fileProtection.d.ts +12 -0
- package/dist/storage/fileProtection.js +51 -0
- package/dist/storage/fileProtection.js.map +1 -0
- package/dist/storage/paths.d.ts +6 -0
- package/dist/storage/paths.js +55 -0
- package/dist/storage/paths.js.map +1 -0
- package/dist/storage/session.d.ts +23 -0
- package/dist/storage/session.js +175 -0
- package/dist/storage/session.js.map +1 -0
- package/dist/storage/telemetryQueue.d.ts +14 -0
- package/dist/storage/telemetryQueue.js +252 -0
- package/dist/storage/telemetryQueue.js.map +1 -0
- package/dist/storage/updateCache.d.ts +19 -0
- package/dist/storage/updateCache.js +80 -0
- package/dist/storage/updateCache.js.map +1 -0
- package/dist/telemetry/events.d.ts +42 -0
- package/dist/telemetry/events.js +80 -0
- package/dist/telemetry/events.js.map +1 -0
- package/dist/telemetry/uploader.d.ts +2 -0
- package/dist/telemetry/uploader.js +46 -0
- package/dist/telemetry/uploader.js.map +1 -0
- package/dist/types/api.d.ts +47 -0
- package/dist/types/api.js +9 -0
- package/dist/types/api.js.map +1 -0
- package/dist/types/cli.d.ts +42 -0
- package/dist/types/cli.js +9 -0
- package/dist/types/cli.js.map +1 -0
- package/package.json +29 -0
- package/skills/agentearth/SKILL.md +37 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 稳定错误模型。
|
|
3
|
+
*
|
|
4
|
+
* 命令模块不要直接把原生 Error 抛给 main.ts,而是尽量抛 CliError。
|
|
5
|
+
* 这样 agent 可以稳定读取 error.code,埋点也可以直接使用同一层 error_code。
|
|
6
|
+
* message 和 hint 是给人或 agent 看的安全文案,不应该包含后端原始敏感信息。
|
|
7
|
+
*/
|
|
8
|
+
export class CliError extends Error {
|
|
9
|
+
// 稳定错误码,给 agent、测试和埋点使用。
|
|
10
|
+
code;
|
|
11
|
+
// 下一步建议,通常告诉 agent 或用户该怎么恢复。
|
|
12
|
+
hint;
|
|
13
|
+
// 进程退出码;usage_error 用 2,其余大多数失败用 1。
|
|
14
|
+
exitCode;
|
|
15
|
+
// 是否允许作为 CLI 本地埋点上传;后端已经可见的错误会显式置为 false。
|
|
16
|
+
telemetryEligible;
|
|
17
|
+
constructor(code, message, hint, exitCode = 1, options = {}) {
|
|
18
|
+
// 调用父类 Error,让错误对象拥有 message 和 stack。
|
|
19
|
+
super(message);
|
|
20
|
+
// 设置 name,方便调试时看出这是项目自定义错误。
|
|
21
|
+
this.name = "CliError";
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.hint = hint;
|
|
24
|
+
this.exitCode = exitCode;
|
|
25
|
+
this.telemetryEligible = options.telemetryEligible ?? true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function usageError(message, hint = "Run ae --help.") {
|
|
29
|
+
// 命令行工具通常用退出码 2 表示“用法错误”。
|
|
30
|
+
return new CliError("usage_error", message, hint, 2);
|
|
31
|
+
}
|
|
32
|
+
export function normalizeUnknownError(error) {
|
|
33
|
+
// 如果本来就是 CliError,说明上游已经完成归因,直接返回即可。
|
|
34
|
+
if (error instanceof CliError) {
|
|
35
|
+
return error;
|
|
36
|
+
}
|
|
37
|
+
// 原生 Error 有 message,但没有稳定 code,所以统一归到 unknown。
|
|
38
|
+
if (error instanceof Error) {
|
|
39
|
+
return new CliError("unknown", error.message, "Retry or run ae --help.");
|
|
40
|
+
}
|
|
41
|
+
// JavaScript 允许抛字符串、数字等非 Error 值,这里兜底转成字符串。
|
|
42
|
+
return new CliError("unknown", String(error), "Retry or run ae --help.");
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAkDH,MAAM,OAAO,QAAS,SAAQ,KAAK;IACjC,yBAAyB;IAChB,IAAI,CAAY;IACzB,6BAA6B;IACpB,IAAI,CAAS;IACtB,oCAAoC;IAC3B,QAAQ,CAAS;IAC1B,0CAA0C;IACjC,iBAAiB,CAAU;IAEpC,YAAY,IAAe,EAAE,OAAe,EAAE,IAAY,EAAE,QAAQ,GAAG,CAAC,EAAE,UAA2B,EAAE;QACrG,sCAAsC;QACtC,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,4BAA4B;QAC5B,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,IAAI,IAAI,CAAC;IAC7D,CAAC;CACF;AAED,MAAM,UAAU,UAAU,CAAC,OAAe,EAAE,IAAI,GAAG,gBAAgB;IACjE,0BAA0B;IAC1B,OAAO,IAAI,QAAQ,CAAC,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,KAAc;IAClD,qCAAqC;IACrC,IAAI,KAAK,YAAY,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,gDAAgD;IAChD,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;QAC3B,OAAO,IAAI,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC,OAAO,EAAE,yBAAyB,CAAC,CAAC;IAC3E,CAAC;IAED,4CAA4C;IAC5C,OAAO,IAAI,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,yBAAyB,CAAC,CAAC;AAC3E,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type HiddenInputStream = NodeJS.ReadStream & {
|
|
2
|
+
isTTY?: boolean;
|
|
3
|
+
isRaw?: boolean;
|
|
4
|
+
setRawMode?: (enabled: boolean) => void;
|
|
5
|
+
};
|
|
6
|
+
type HiddenOutputStream = NodeJS.WriteStream & {
|
|
7
|
+
isTTY?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function readHiddenInput(promptText: string, input?: HiddenInputStream, output?: HiddenOutputStream): Promise<string>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TTY 隐藏输入工具。
|
|
3
|
+
*
|
|
4
|
+
* 这个模块只服务 `ae login` 的人工兜底场景:当用户直接在终端运行 login,
|
|
5
|
+
* 且没有通过 stdin、命令行参数或环境变量提供 API key 时,CLI 可以提示用户输入。
|
|
6
|
+
* 输入过程不回显字符,避免 API key 出现在终端日志或截图里。
|
|
7
|
+
*/
|
|
8
|
+
import { emitKeypressEvents } from "node:readline";
|
|
9
|
+
import { CliError } from "./errors.js";
|
|
10
|
+
export async function readHiddenInput(promptText, input = process.stdin, output = process.stderr) {
|
|
11
|
+
// 非 TTY 场景通常是 agent/脚本调用,不能阻塞等待人工输入。
|
|
12
|
+
if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") {
|
|
13
|
+
throw new CliError("auth_missing", "Interactive API key prompt requires a TTY.", "Pipe the API key into ae login --api-key-stdin.");
|
|
14
|
+
}
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
let value = "";
|
|
17
|
+
const previousRawMode = input.isRaw === true;
|
|
18
|
+
const cleanup = (writeNewline) => {
|
|
19
|
+
// 无论成功、取消还是异常,都恢复进入前的 raw mode。
|
|
20
|
+
input.off("keypress", onKeypress);
|
|
21
|
+
input.setRawMode?.(previousRawMode);
|
|
22
|
+
if (writeNewline) {
|
|
23
|
+
output.write("\n");
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const onKeypress = (character, key = {}) => {
|
|
27
|
+
if (key.ctrl && key.name === "c") {
|
|
28
|
+
cleanup(true);
|
|
29
|
+
reject(new CliError("auth_missing", "API key input cancelled.", "Pipe the API key into ae login --api-key-stdin."));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (key.name === "return" || key.name === "enter") {
|
|
33
|
+
cleanup(true);
|
|
34
|
+
resolve(value);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (key.name === "backspace") {
|
|
38
|
+
value = value.slice(0, -1);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// 只收集可见字符;控制键和方向键不进入 API key。
|
|
42
|
+
if (character && character >= " " && character !== "\u007f") {
|
|
43
|
+
value += character;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
output.write(promptText);
|
|
47
|
+
emitKeypressEvents(input);
|
|
48
|
+
input.setRawMode(true);
|
|
49
|
+
input.resume();
|
|
50
|
+
input.on("keypress", onKeypress);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=hiddenInput.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hiddenInput.js","sourceRoot":"","sources":["../../src/core/hiddenInput.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAsBvC,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,UAAkB,EAClB,QAAQ,OAAO,CAAC,KAA0B,EAC1C,SAAS,OAAO,CAAC,MAA4B;IAE7C,qCAAqC;IACrC,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,OAAO,KAAK,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;QAC5E,MAAM,IAAI,QAAQ,CAAC,cAAc,EAAE,4CAA4C,EAAE,iDAAiD,CAAC,CAAC;IACtI,CAAC;IAED,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,KAAK,GAAG,EAAE,CAAC;QACf,MAAM,eAAe,GAAG,KAAK,CAAC,KAAK,KAAK,IAAI,CAAC;QAE7C,MAAM,OAAO,GAAG,CAAC,YAAqB,EAAQ,EAAE;YAC9C,gCAAgC;YAChC,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;YAClC,KAAK,CAAC,UAAU,EAAE,CAAC,eAAe,CAAC,CAAC;YACpC,IAAI,YAAY,EAAE,CAAC;gBACjB,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACrB,CAAC;QACH,CAAC,CAAC;QAEF,MAAM,UAAU,GAAG,CAAC,SAAiB,EAAE,MAAoB,EAAE,EAAQ,EAAE;YACrE,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,KAAK,GAAG,EAAE,CAAC;gBACjC,OAAO,CAAC,IAAI,CAAC,CAAC;gBACd,MAAM,CAAC,IAAI,QAAQ,CAAC,cAAc,EAAE,0BAA0B,EAAE,iDAAiD,CAAC,CAAC,CAAC;gBACpH,OAAO;YACT,CAAC;YAED,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBAClD,OAAO,CAAC,IAAI,CAAC,CAAC;gBACd,OAAO,CAAC,KAAK,CAAC,CAAC;gBACf,OAAO;YACT,CAAC;YAED,IAAI,GAAG,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;gBAC7B,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;gBAC3B,OAAO;YACT,CAAC;YAED,8BAA8B;YAC9B,IAAI,SAAS,IAAI,SAAS,IAAI,GAAG,IAAI,SAAS,KAAK,QAAQ,EAAE,CAAC;gBAC5D,KAAK,IAAI,SAAS,CAAC;YACrB,CAAC;QACH,CAAC,CAAC;QAEF,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QACzB,kBAAkB,CAAC,KAAK,CAAC,CAAC;QAC1B,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QACvB,KAAK,CAAC,MAAM,EAAE,CAAC;QACf,KAAK,CAAC,EAAE,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface HttpOptions {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
timeoutMs: number;
|
|
5
|
+
}
|
|
6
|
+
export type QueryValue = string | number | boolean;
|
|
7
|
+
export declare function postJson<TResponse>(pathOrUrl: string, body: unknown, options: HttpOptions): Promise<TResponse>;
|
|
8
|
+
export declare function postJsonNoResponse(pathOrUrl: string, body: unknown, options: HttpOptions): Promise<void>;
|
|
9
|
+
export declare function getJson<TResponse>(pathOrUrl: string, query: Record<string, QueryValue>, options: HttpOptions): Promise<TResponse>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP 请求封装模块。
|
|
3
|
+
*
|
|
4
|
+
* 业务接口和埋点接口的响应规则不同:
|
|
5
|
+
* - recommend/list/detail/execute 这类业务接口需要返回 JSON,命令模块会读取 error_no。
|
|
6
|
+
* - CLI 埋点上传接口只要求 HTTP 200,不要求响应体。
|
|
7
|
+
*
|
|
8
|
+
* 因此这里提供两类 helper:`postJson/getJson` 用于业务接口,
|
|
9
|
+
* `postJsonNoResponse` 用于埋点上传。
|
|
10
|
+
*/
|
|
11
|
+
import { CliError } from "./errors.js";
|
|
12
|
+
export async function postJson(pathOrUrl, body, options) {
|
|
13
|
+
// POST JSON 时固定 Content-Type,并把 body 序列化成字符串。
|
|
14
|
+
return requestJson(pathOrUrl, options, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: { "Content-Type": "application/json" },
|
|
17
|
+
body: JSON.stringify(body)
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export async function postJsonNoResponse(pathOrUrl, body, options) {
|
|
21
|
+
// 埋点上传只看 HTTP 状态码,不读取响应体。
|
|
22
|
+
await requestNoResponse(pathOrUrl, options, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: { "Content-Type": "application/json" },
|
|
25
|
+
body: JSON.stringify(body)
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export async function getJson(pathOrUrl, query, options) {
|
|
29
|
+
// GET 请求没有 body,参数统一拼到 URL 的 query string 上。
|
|
30
|
+
return requestJson(appendQuery(pathOrUrl, query, options.baseUrl), options, {
|
|
31
|
+
method: "GET"
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async function requestJson(pathOrUrl, options, init) {
|
|
35
|
+
// 先发请求,再按 JSON 业务接口规则解析响应体。
|
|
36
|
+
const response = await request(pathOrUrl, options, init);
|
|
37
|
+
return parseJsonResponse(response);
|
|
38
|
+
}
|
|
39
|
+
async function requestNoResponse(pathOrUrl, options, init) {
|
|
40
|
+
// 先发请求,再只判断 HTTP 状态码。
|
|
41
|
+
const response = await request(pathOrUrl, options, init);
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
throw new CliError("backend_error", `Backend returned HTTP ${response.status}.`, "Retry later.", 1, { telemetryEligible: false });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function request(pathOrUrl, options, init) {
|
|
47
|
+
// AbortController 用来在 timeoutMs 后主动取消 fetch。
|
|
48
|
+
const controller = new AbortController();
|
|
49
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
50
|
+
try {
|
|
51
|
+
// resolveUrl 支持两种输入:相对路径和完整 URL。
|
|
52
|
+
const response = await fetch(resolveUrl(pathOrUrl, options.baseUrl), {
|
|
53
|
+
...init,
|
|
54
|
+
headers: {
|
|
55
|
+
...init.headers,
|
|
56
|
+
"X-Api-Key": options.apiKey
|
|
57
|
+
},
|
|
58
|
+
signal: controller.signal
|
|
59
|
+
});
|
|
60
|
+
// 401/403 表示鉴权失败,直接映射成 auth_invalid。
|
|
61
|
+
if (response.status === 401 || response.status === 403) {
|
|
62
|
+
throw new CliError("auth_invalid", "API key is invalid.", "Generate a valid API key and run ae login again.", 1, { telemetryEligible: false });
|
|
63
|
+
}
|
|
64
|
+
// 429 表示限流,agent 可以稍后重试。
|
|
65
|
+
if (response.status === 429) {
|
|
66
|
+
throw new CliError("rate_limited", "Request was rate limited.", "Retry later.", 1, { telemetryEligible: false });
|
|
67
|
+
}
|
|
68
|
+
// 其他状态码先返回给上层。业务接口可能在 400 时仍带 JSON error_no。
|
|
69
|
+
return response;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
// 已经归一化的 CliError 不要二次包装。
|
|
73
|
+
if (error instanceof CliError) {
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
// AbortController 触发的异常会叫 AbortError,归为 timeout。
|
|
77
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
78
|
+
throw new CliError("timeout", "Request timed out.", "Retry later.");
|
|
79
|
+
}
|
|
80
|
+
// 其他 fetch 失败通常是 DNS、代理、断网、连接失败等网络问题。
|
|
81
|
+
throw new CliError("network_error", "Unable to connect to AgentEarth backend.", "Check network and retry.");
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
// 请求结束后清理定时器,避免 Node 进程被无意义的 timer 挂住。
|
|
85
|
+
clearTimeout(timeout);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function parseJsonResponse(response) {
|
|
89
|
+
try {
|
|
90
|
+
// response.json() 会读取响应体并 JSON.parse。
|
|
91
|
+
const payload = await response.json();
|
|
92
|
+
// 业务接口的 HTTP 2xx 响应必须先符合 AgentAPI 顶层包络,后续命令模块再校验 tools/result 等业务字段。
|
|
93
|
+
if (response.ok && !isAgentApiEnvelope(payload)) {
|
|
94
|
+
throw new CliError("backend_response_invalid", "Backend response is missing AgentAPI envelope.", "Retry later.");
|
|
95
|
+
}
|
|
96
|
+
// HTTP 状态码不能被 body 里的成功标记覆盖;非 2xx 只有 error_no=-1 这类后端业务错误继续交给命令模块细分映射。
|
|
97
|
+
if (!response.ok && !isBackendBusinessError(payload)) {
|
|
98
|
+
throw new CliError("backend_error", `Backend returned HTTP ${response.status}.`, "Retry later.", 1, { telemetryEligible: false });
|
|
99
|
+
}
|
|
100
|
+
return payload;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
// 上面主动归一化出来的 CliError 需要原样透传,避免被误判为 JSON 解析失败。
|
|
104
|
+
if (error instanceof CliError) {
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
// 如果 HTTP 本身失败且响应体不是 JSON,归为普通 backend_error。
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
throw new CliError("backend_error", `Backend returned HTTP ${response.status}.`, "Retry later.", 1, { telemetryEligible: false });
|
|
110
|
+
}
|
|
111
|
+
// HTTP 成功但不是 JSON,说明后端响应不符合 CLI 契约。
|
|
112
|
+
throw new CliError("backend_response_invalid", `Backend returned invalid JSON with HTTP ${response.status}.`, "Retry later.");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function isBackendBusinessError(value) {
|
|
116
|
+
// AgentAPI 的业务失败响应使用 error_no=-1;这种情况保留给 mapBackendError 做更细错误码映射。
|
|
117
|
+
return isRecord(value) && value.error_no === -1;
|
|
118
|
+
}
|
|
119
|
+
function isAgentApiEnvelope(value) {
|
|
120
|
+
// AgentAPI 业务响应至少要有 error_no/error_msg,缺失时说明后端响应无法被 CLI 按契约解析。
|
|
121
|
+
return isRecord(value) && typeof value.error_no === "number" && typeof value.error_msg === "string";
|
|
122
|
+
}
|
|
123
|
+
function appendQuery(pathOrUrl, query, baseUrl) {
|
|
124
|
+
// URL 对象可以安全处理已有 query、转义字符和相对路径。
|
|
125
|
+
const url = new URL(resolveUrl(pathOrUrl, baseUrl));
|
|
126
|
+
for (const [key, value] of Object.entries(query)) {
|
|
127
|
+
// set 会覆盖同名参数;list 当前只支持一层标量,所以直接转字符串。
|
|
128
|
+
url.searchParams.set(key, String(value));
|
|
129
|
+
}
|
|
130
|
+
return url.toString();
|
|
131
|
+
}
|
|
132
|
+
function resolveUrl(pathOrUrl, baseUrl) {
|
|
133
|
+
// execute 使用后端返回的完整 tool_url,因此这里必须支持完整 URL。
|
|
134
|
+
if (/^https?:\/\//i.test(pathOrUrl)) {
|
|
135
|
+
return pathOrUrl;
|
|
136
|
+
}
|
|
137
|
+
// 其他接口通常传相对 path,这里和 baseUrl 拼起来,并处理多余斜杠。
|
|
138
|
+
return `${baseUrl.replace(/\/$/, "")}/${pathOrUrl.replace(/^\//, "")}`;
|
|
139
|
+
}
|
|
140
|
+
function isRecord(value) {
|
|
141
|
+
// object 会包含数组;响应 payload 的顶层必须是普通对象才可能有 error_no 字段。
|
|
142
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/core/http.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 输出模块。
|
|
3
|
+
*
|
|
4
|
+
* 这个文件只负责把命令结果写到 stdout 或 stderr,不做业务判断。
|
|
5
|
+
* 约定是:成功写 stdout,失败写 stderr;成功与失败主要靠进程退出码判断。
|
|
6
|
+
* 对 agent 来说,JSON 输出必须稳定,所以这里集中维护成功和失败的输出格式。
|
|
7
|
+
*/
|
|
8
|
+
import type { CommandSuccess } from "../types/cli.js";
|
|
9
|
+
import { CliError } from "./errors.js";
|
|
10
|
+
export declare function writeSuccess(result: CommandSuccess, json: boolean): void;
|
|
11
|
+
export declare function writeError(error: CliError, json: boolean): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function writeSuccess(result, json) {
|
|
2
|
+
// json=true 时,直接输出完整 JSON,方便 agent 解析字段。
|
|
3
|
+
if (json) {
|
|
4
|
+
// null, 2 表示格式化缩进 2 个空格;这不影响 JSON 可解析性。
|
|
5
|
+
console.log(JSON.stringify(result, null, 2));
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
// 如果未来支持人类可读模式,带 message 的成功结果可以退化为一行短文本。
|
|
9
|
+
if (typeof result.message === "string") {
|
|
10
|
+
console.log(result.message);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
// 没有 message 的成功结果,仍然输出 JSON,避免丢字段。
|
|
14
|
+
console.log(JSON.stringify(result, null, 2));
|
|
15
|
+
}
|
|
16
|
+
export function writeError(error, json) {
|
|
17
|
+
// 失败结构固定为 { error: { code, message, hint } }。
|
|
18
|
+
// 不再输出布尔 success 字段,失败由非 0 exit code 表示。
|
|
19
|
+
const payload = {
|
|
20
|
+
error: {
|
|
21
|
+
code: error.code,
|
|
22
|
+
message: error.message,
|
|
23
|
+
hint: error.hint
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
// agent 模式输出 JSON;人类模式输出更短的两行文本。
|
|
27
|
+
const output = json
|
|
28
|
+
? JSON.stringify(payload, null, 2)
|
|
29
|
+
: `${error.code}: ${error.message}\nHint: ${error.hint}`;
|
|
30
|
+
// 错误必须写 stderr,避免和 stdout 的业务结果混在一起。
|
|
31
|
+
console.error(output);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/core/output.ts"],"names":[],"mappings":"AAUA,MAAM,UAAU,YAAY,CAAC,MAAsB,EAAE,IAAa;IAChE,yCAAyC;IACzC,IAAI,IAAI,EAAE,CAAC;QACT,wCAAwC;QACxC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QAC7C,OAAO;IACT,CAAC;IAED,0CAA0C;IAC1C,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;QACvC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC5B,OAAO;IACT,CAAC;IAED,oCAAoC;IACpC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,KAAe,EAAE,IAAa;IACvD,8CAA8C;IAC9C,yCAAyC;IACzC,MAAM,OAAO,GAAmB;QAC9B,KAAK,EAAE;YACL,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,IAAI,EAAE,KAAK,CAAC,IAAI;SACjB;KACF,CAAC;IAEF,iCAAiC;IACjC,MAAM,MAAM,GAAG,IAAI;QACjB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAClC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,WAAW,KAAK,CAAC,IAAI,EAAE,CAAC;IAE3D,qCAAqC;IACrC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;AACxB,CAAC"}
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(argv: string[]): Promise<void>;
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 主调度模块。
|
|
3
|
+
*
|
|
4
|
+
* `bin/ae.mjs` 会把命令行参数交给这里的 main 函数。这个文件是阅读源码的主线:
|
|
5
|
+
* 1. 解析参数;
|
|
6
|
+
* 2. 判断 help/version;
|
|
7
|
+
* 3. 按命令名调用对应 command 模块;
|
|
8
|
+
* 4. 统一输出成功或失败;
|
|
9
|
+
* 5. 在命令结束后写入并尝试上传 CLI 埋点。
|
|
10
|
+
*
|
|
11
|
+
* 具体业务逻辑不写在这里,而是放在 commands 目录下,方便定位问题。
|
|
12
|
+
*/
|
|
13
|
+
import { CLI_VERSION, EXECUTE_PATH, RECOMMEND_PATH, TOOL_DETAIL_PATH, TOOLS_LIST_PATH } from "./constants.js";
|
|
14
|
+
import { runDetail } from "./commands/detail.js";
|
|
15
|
+
import { runExecute } from "./commands/execute.js";
|
|
16
|
+
import { runList } from "./commands/list.js";
|
|
17
|
+
import { runLogin } from "./commands/login.js";
|
|
18
|
+
import { runRecommend } from "./commands/recommend.js";
|
|
19
|
+
import { runUpdate } from "./commands/update.js";
|
|
20
|
+
import { parseArgs } from "./core/args.js";
|
|
21
|
+
import { normalizeUnknownError, usageError } from "./core/errors.js";
|
|
22
|
+
import { writeError, writeSuccess } from "./core/output.js";
|
|
23
|
+
import { enqueueTelemetry } from "./storage/telemetryQueue.js";
|
|
24
|
+
import { buildTelemetryParam, createTelemetryEvent } from "./telemetry/events.js";
|
|
25
|
+
import { flushTelemetry } from "./telemetry/uploader.js";
|
|
26
|
+
export async function main(argv) {
|
|
27
|
+
// 第一步:把原始字符串参数解析成 command、positionals 和 options。
|
|
28
|
+
// 参数解析本身也可能抛 usage_error,必须在 main.ts 内归一化,不能掉到 bin 入口的 unknown 兜底。
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = parseArgs(argv);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const normalized = normalizeUnknownError(error);
|
|
35
|
+
writeError(normalized, true);
|
|
36
|
+
process.exitCode = normalized.exitCode;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// --version 是最短路径:只打印版本号,不进入业务命令,也不写埋点。
|
|
40
|
+
if (parsed.options.version) {
|
|
41
|
+
console.log(CLI_VERSION);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// --help 或没有命令时,打印帮助文本后退出。
|
|
45
|
+
if (parsed.options.help || !parsed.command) {
|
|
46
|
+
console.log(helpText());
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// 记录命令开始时间,用于计算埋点 duration_ms。
|
|
50
|
+
const startedAt = Date.now();
|
|
51
|
+
// context 是传给每个 command 的公共上下文,目前主要包含 options。
|
|
52
|
+
const context = { options: parsed.options, telemetry: {} };
|
|
53
|
+
// agent 调用的命令默认 JSON;--json 仅作为兼容开关保留。
|
|
54
|
+
const jsonOutput = shouldOutputJson(parsed.command, parsed.options.json);
|
|
55
|
+
// telemetryEvent 先置空;命令结束后根据成功/失败决定是否生成。
|
|
56
|
+
let telemetryEvent = null;
|
|
57
|
+
try {
|
|
58
|
+
// 全局选项有些只适用于特定命令,这里结合 command 做二次校验。
|
|
59
|
+
validateCommandOptions(parsed.command, parsed.options);
|
|
60
|
+
// 部分命令没有位置参数,不能静默忽略 agent 误传的内容。
|
|
61
|
+
validateCommandArguments(parsed.command, parsed.positionals);
|
|
62
|
+
// dispatch 根据命令名调用具体模块,例如 runRecommend 或 runExecute。
|
|
63
|
+
const result = await dispatch(parsed.command, context, parsed.positionals);
|
|
64
|
+
// 成功输出写 stdout。
|
|
65
|
+
writeSuccess(result, jsonOutput);
|
|
66
|
+
// 后端能看到的业务成功不重复上报;login 成功需要记录本地 credentials 是否写入。
|
|
67
|
+
telemetryEvent = createSuccessTelemetry(parsed.command, parsed.positionals, context, Date.now() - startedAt);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
// 所有未知异常都先归一化成 CliError,确保输出结构稳定。
|
|
71
|
+
const normalized = normalizeUnknownError(error);
|
|
72
|
+
// 失败输出写 stderr。
|
|
73
|
+
writeError(normalized, jsonOutput);
|
|
74
|
+
// 非 0 退出码表示命令失败;usage_error 通常是 2。
|
|
75
|
+
process.exitCode = normalized.exitCode;
|
|
76
|
+
// 只把后端看不到的本地错误写入 CLI 埋点。
|
|
77
|
+
telemetryEvent = createErrorTelemetry(parsed.command, parsed.positionals, context, normalized, Date.now() - startedAt);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
// 埋点是 best-effort:写入或上传失败都不能影响主命令结果。
|
|
81
|
+
await recordAndFlushTelemetry(telemetryEvent, parsed.options);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function validateCommandOptions(command, options) {
|
|
85
|
+
// --api-key-stdin 是安装初始化时写入 credentials 的方式;业务命令不从 stdin 读取 API key。
|
|
86
|
+
if (options.apiKeyStdin && command !== "login") {
|
|
87
|
+
throw usageError("--api-key-stdin can only be used with ae login.", "Run ae login --api-key-stdin.");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function validateCommandArguments(command, args) {
|
|
91
|
+
// login/update 是零位置参数命令;多余参数通常说明 agent 误把业务内容拼进了命令。
|
|
92
|
+
if ((command === "login" || command === "update") && args.length > 0) {
|
|
93
|
+
throw usageError(`Unexpected arguments for ae ${command}.`, `Run ae ${command}.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function dispatch(command, context, args) {
|
|
97
|
+
// 每个 if 都对应一个命令模块;这里不做业务逻辑,只转发参数。
|
|
98
|
+
if (command === "login") {
|
|
99
|
+
return runLogin(context);
|
|
100
|
+
}
|
|
101
|
+
if (command === "recommend") {
|
|
102
|
+
return runRecommend(context, args);
|
|
103
|
+
}
|
|
104
|
+
if (command === "list") {
|
|
105
|
+
return runList(context, args);
|
|
106
|
+
}
|
|
107
|
+
if (command === "detail") {
|
|
108
|
+
return runDetail(context, args);
|
|
109
|
+
}
|
|
110
|
+
if (command === "execute") {
|
|
111
|
+
return runExecute(context, args);
|
|
112
|
+
}
|
|
113
|
+
if (command === "update") {
|
|
114
|
+
return runUpdate(context);
|
|
115
|
+
}
|
|
116
|
+
// 未知命令属于用法错误,不请求后端。
|
|
117
|
+
throw usageError(`Unknown command: ${command}`);
|
|
118
|
+
}
|
|
119
|
+
function helpText() {
|
|
120
|
+
// 帮助文本只展示当前支持命令,不展示已经移除的 schema/config。
|
|
121
|
+
return `AgentEarth CLI ${CLI_VERSION}
|
|
122
|
+
|
|
123
|
+
Usage:
|
|
124
|
+
ae login --api-key-stdin [--json]
|
|
125
|
+
ae login --api-key <key> [--json]
|
|
126
|
+
ae recommend "<query>"
|
|
127
|
+
ae list ['<json>']
|
|
128
|
+
ae detail <tool_name>
|
|
129
|
+
ae execute <tool_name> '<json>'
|
|
130
|
+
ae update
|
|
131
|
+
|
|
132
|
+
Global options:
|
|
133
|
+
--api-key <key> Temporarily override API key; not recommended for login secrets
|
|
134
|
+
--api-key-stdin Read login API key from stdin
|
|
135
|
+
--base-url <url> Override AgentEarth backend URL
|
|
136
|
+
--timeout <seconds> Override request timeout
|
|
137
|
+
--json Output JSON
|
|
138
|
+
--no-telemetry Disable telemetry for this run
|
|
139
|
+
--version Print version
|
|
140
|
+
--help Print help
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
function shouldOutputJson(command, explicitJson) {
|
|
144
|
+
// 用户显式传 --json 时,所有命令都输出 JSON。
|
|
145
|
+
if (explicitJson) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
// 没有 command 的场景已经在前面输出 help 文本,这里保留 false 作为防御性默认值。
|
|
149
|
+
if (!command) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
// 只要进入命令分发阶段,就默认给 agent 可解析的 JSON。
|
|
153
|
+
// 这包括未知命令和已移除命令;它们会统一变成 usage_error,方便 agent 纠正调用。
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
function createSuccessTelemetry(command, args, context, durationMs) {
|
|
157
|
+
// recommend/list/detail/execute 的成功请求后端本来就能看到,不重复上传 CLI 埋点。
|
|
158
|
+
// login 成功不同:后端只看到验证探针,看不到本地 credentials 是否写成功。
|
|
159
|
+
// update 成功或无更新也发生在本机,后端默认看不到 npm/update 的真实结果。
|
|
160
|
+
if (command !== "login" && command !== "update") {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
if (command === "update") {
|
|
164
|
+
return createTelemetryEvent("update", "", {
|
|
165
|
+
duration_ms: durationMs,
|
|
166
|
+
param: telemetryParamFor(command, args, context.telemetry)
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
// login 的后端 path 对应 recommend 探针,因为它复用了 recommend 接口验证 key。
|
|
170
|
+
return createTelemetryEvent("login", RECOMMEND_PATH, {
|
|
171
|
+
duration_ms: durationMs,
|
|
172
|
+
param: telemetryParamFor(command, args, context.telemetry)
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function createErrorTelemetry(command, args, context, error, durationMs) {
|
|
176
|
+
// 没有命令名,或者错误码不属于 CLI 本地归因,就不写埋点。
|
|
177
|
+
if (!command || !error.telemetryEligible || !isTelemetryErrorCode(error.code)) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
// 只有当前支持的命令才有稳定 method;未知命令不上传,避免污染日志维度。
|
|
181
|
+
const method = telemetryMethodFor(command);
|
|
182
|
+
if (!method) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return createTelemetryEvent(method, telemetryPathFor(command), {
|
|
186
|
+
duration_ms: durationMs,
|
|
187
|
+
param: telemetryParamFor(command, args, context.telemetry),
|
|
188
|
+
error_code: error.code,
|
|
189
|
+
error_msg: error.message
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
async function recordAndFlushTelemetry(event, options) {
|
|
193
|
+
// --no-telemetry 或 AE_NO_TELEMETRY=1 会完全跳过本次埋点。
|
|
194
|
+
if (options.noTelemetry) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
// 如果本次命令产生了需要上传的本地事件,先追加到本地 events.jsonl。
|
|
199
|
+
if (event) {
|
|
200
|
+
await enqueueTelemetry(event);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// 本地埋点写入失败不能影响用户命令,所以这里静默吞掉。
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
// 每次命令结束后尝试冲刷历史队列;失败也会在 uploader 内部静默保留。
|
|
208
|
+
await flushTelemetry(options);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// 这里再做一层兜底,保证 uploader 未来改动后也不会影响主命令。
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function telemetryMethodFor(command) {
|
|
215
|
+
// 只有这些命令能成为埋点 method。
|
|
216
|
+
if (command === "login"
|
|
217
|
+
|| command === "recommend"
|
|
218
|
+
|| command === "list"
|
|
219
|
+
|| command === "detail"
|
|
220
|
+
|| command === "execute"
|
|
221
|
+
|| command === "update") {
|
|
222
|
+
return command;
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
function telemetryPathFor(command) {
|
|
227
|
+
// path 按对应后端 API 路径记录,不写 CLI 命令文本。
|
|
228
|
+
if (command === "login" || command === "recommend")
|
|
229
|
+
return RECOMMEND_PATH;
|
|
230
|
+
if (command === "list")
|
|
231
|
+
return TOOLS_LIST_PATH;
|
|
232
|
+
if (command === "detail")
|
|
233
|
+
return TOOL_DETAIL_PATH;
|
|
234
|
+
if (command === "execute")
|
|
235
|
+
return EXECUTE_PATH;
|
|
236
|
+
return "";
|
|
237
|
+
}
|
|
238
|
+
function telemetryParamFor(command, args, telemetry = {}) {
|
|
239
|
+
// recommend 的业务参数是自然语言 query。
|
|
240
|
+
if (command === "recommend") {
|
|
241
|
+
return buildTelemetryParam({ query: args.join(" ").trim() });
|
|
242
|
+
}
|
|
243
|
+
// list 的可选参数是一个 JSON filter,当前整体放进 query 字段。
|
|
244
|
+
if (command === "list") {
|
|
245
|
+
return buildTelemetryParam({ query: args[0] ?? "" });
|
|
246
|
+
}
|
|
247
|
+
// detail 的业务参数是 tool_name。
|
|
248
|
+
if (command === "detail") {
|
|
249
|
+
return buildTelemetryParam({ toolName: args[0] ?? "" });
|
|
250
|
+
}
|
|
251
|
+
// execute 的业务参数包括 tool_name 和 params。
|
|
252
|
+
if (command === "execute") {
|
|
253
|
+
return buildTelemetryParam({
|
|
254
|
+
toolName: args[0] ?? "",
|
|
255
|
+
// execute.ts 已经读取过 @file/stdin/inline 参数,失败埋点优先复用解析结果。
|
|
256
|
+
params: telemetry.executeParams ?? parseInlineJsonObject(args[1])
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
// update 的本地结果写入 param.update_result;异常路径没有显式结果时按 failed 兜底。
|
|
260
|
+
if (command === "update") {
|
|
261
|
+
return buildTelemetryParam({ updateResult: telemetry.updateResult ?? "failed" });
|
|
262
|
+
}
|
|
263
|
+
// login 当前不上传 key 相关内容,所以 param 使用空对象字段。
|
|
264
|
+
return buildTelemetryParam();
|
|
265
|
+
}
|
|
266
|
+
function parseInlineJsonObject(raw) {
|
|
267
|
+
// @file 和 stdin 形式不在 main.ts 读取,避免为了埋点再读一次用户文件或 stdin。
|
|
268
|
+
if (!raw || raw.startsWith("@") || raw === "-") {
|
|
269
|
+
return {};
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
// 只有 inline JSON 能在这里安全解析进 param.params。
|
|
273
|
+
return JSON.parse(raw);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// 如果 JSON 本身坏了,具体错误由 execute.ts 抛 params_invalid。
|
|
277
|
+
// 埋点 param 里保留空对象,避免错误日志混入不可解析原文。
|
|
278
|
+
return {};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function isTelemetryErrorCode(code) {
|
|
282
|
+
// 这里列出的都是“后端不一定知道”的本地错误或后端响应契约错误。
|
|
283
|
+
return code === "usage_error"
|
|
284
|
+
|| code === "network_error"
|
|
285
|
+
|| code === "timeout"
|
|
286
|
+
|| code === "credentials_write_failed"
|
|
287
|
+
|| code === "session_write_failed"
|
|
288
|
+
|| code === "tool_name_parse_failed"
|
|
289
|
+
|| code === "schema_missing"
|
|
290
|
+
|| code === "tool_name_invalid"
|
|
291
|
+
|| code === "params_invalid"
|
|
292
|
+
|| code === "backend_response_invalid"
|
|
293
|
+
|| code === "version_check_failed"
|
|
294
|
+
|| code === "update_failed"
|
|
295
|
+
|| code === "update_verify_failed"
|
|
296
|
+
|| code === "update_not_applied"
|
|
297
|
+
// unknown 表示 CLI 本地异常未被更细错误码覆盖,后端默认看不到,也需要进入本地埋点。
|
|
298
|
+
|| code === "unknown";
|
|
299
|
+
}
|
|
300
|
+
//# sourceMappingURL=main.js.map
|