@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.
Files changed (85) hide show
  1. package/README.md +89 -0
  2. package/bin/ae.mjs +31 -0
  3. package/dist/commands/detail.d.ts +2 -0
  4. package/dist/commands/detail.js +71 -0
  5. package/dist/commands/detail.js.map +1 -0
  6. package/dist/commands/execute.d.ts +2 -0
  7. package/dist/commands/execute.js +118 -0
  8. package/dist/commands/execute.js.map +1 -0
  9. package/dist/commands/list.d.ts +2 -0
  10. package/dist/commands/list.js +94 -0
  11. package/dist/commands/list.js.map +1 -0
  12. package/dist/commands/login.d.ts +2 -0
  13. package/dist/commands/login.js +96 -0
  14. package/dist/commands/login.js.map +1 -0
  15. package/dist/commands/recommend.d.ts +2 -0
  16. package/dist/commands/recommend.js +58 -0
  17. package/dist/commands/recommend.js.map +1 -0
  18. package/dist/commands/update.d.ts +2 -0
  19. package/dist/commands/update.js +67 -0
  20. package/dist/commands/update.js.map +1 -0
  21. package/dist/commands/updateCheck.d.ts +10 -0
  22. package/dist/commands/updateCheck.js +98 -0
  23. package/dist/commands/updateCheck.js.map +1 -0
  24. package/dist/commands/updateHint.d.ts +9 -0
  25. package/dist/commands/updateHint.js +25 -0
  26. package/dist/commands/updateHint.js.map +1 -0
  27. package/dist/commands/updateNpm.d.ts +13 -0
  28. package/dist/commands/updateNpm.js +64 -0
  29. package/dist/commands/updateNpm.js.map +1 -0
  30. package/dist/constants.d.ts +20 -0
  31. package/dist/constants.js +34 -0
  32. package/dist/constants.js.map +1 -0
  33. package/dist/core/args.d.ts +2 -0
  34. package/dist/core/args.js +110 -0
  35. package/dist/core/args.js.map +1 -0
  36. package/dist/core/backendErrors.d.ts +10 -0
  37. package/dist/core/backendErrors.js +69 -0
  38. package/dist/core/backendErrors.js.map +1 -0
  39. package/dist/core/errors.d.ts +20 -0
  40. package/dist/core/errors.js +44 -0
  41. package/dist/core/errors.js.map +1 -0
  42. package/dist/core/hiddenInput.d.ts +10 -0
  43. package/dist/core/hiddenInput.js +53 -0
  44. package/dist/core/hiddenInput.js.map +1 -0
  45. package/dist/core/http.d.ts +9 -0
  46. package/dist/core/http.js +144 -0
  47. package/dist/core/http.js.map +1 -0
  48. package/dist/core/output.d.ts +11 -0
  49. package/dist/core/output.js +33 -0
  50. package/dist/core/output.js.map +1 -0
  51. package/dist/main.d.ts +1 -0
  52. package/dist/main.js +300 -0
  53. package/dist/main.js.map +1 -0
  54. package/dist/storage/credentials.d.ts +9 -0
  55. package/dist/storage/credentials.js +97 -0
  56. package/dist/storage/credentials.js.map +1 -0
  57. package/dist/storage/fileProtection.d.ts +12 -0
  58. package/dist/storage/fileProtection.js +51 -0
  59. package/dist/storage/fileProtection.js.map +1 -0
  60. package/dist/storage/paths.d.ts +6 -0
  61. package/dist/storage/paths.js +55 -0
  62. package/dist/storage/paths.js.map +1 -0
  63. package/dist/storage/session.d.ts +23 -0
  64. package/dist/storage/session.js +175 -0
  65. package/dist/storage/session.js.map +1 -0
  66. package/dist/storage/telemetryQueue.d.ts +14 -0
  67. package/dist/storage/telemetryQueue.js +252 -0
  68. package/dist/storage/telemetryQueue.js.map +1 -0
  69. package/dist/storage/updateCache.d.ts +19 -0
  70. package/dist/storage/updateCache.js +80 -0
  71. package/dist/storage/updateCache.js.map +1 -0
  72. package/dist/telemetry/events.d.ts +42 -0
  73. package/dist/telemetry/events.js +80 -0
  74. package/dist/telemetry/events.js.map +1 -0
  75. package/dist/telemetry/uploader.d.ts +2 -0
  76. package/dist/telemetry/uploader.js +46 -0
  77. package/dist/telemetry/uploader.js.map +1 -0
  78. package/dist/types/api.d.ts +47 -0
  79. package/dist/types/api.js +9 -0
  80. package/dist/types/api.js.map +1 -0
  81. package/dist/types/cli.d.ts +42 -0
  82. package/dist/types/cli.js +9 -0
  83. package/dist/types/cli.js.map +1 -0
  84. package/package.json +29 -0
  85. package/skills/agentearth/SKILL.md +37 -0
@@ -0,0 +1,252 @@
1
+ /**
2
+ * 本地埋点队列模块。
3
+ *
4
+ * CLI 埋点不能影响主命令,所以事件会先写入本地 events.jsonl。
5
+ * 上传成功后,再从文件头部删除已经上传的事件。jsonl 的意思是:
6
+ * 一行一个 JSON 对象,适合追加写入,也适合按行读取前 N 条。
7
+ */
8
+ import { appendFile, readFile, writeFile } from "node:fs/promises";
9
+ import { EXECUTE_PATH, RECOMMEND_PATH, TELEMETRY_QUEUE_MAX_BYTES, TELEMETRY_QUEUE_MAX_LINES, TOOL_DETAIL_PATH, TOOLS_LIST_PATH } from "../constants.js";
10
+ import { TELEMETRY_LOG_TYPE, TELEMETRY_MSG } from "../telemetry/events.js";
11
+ import { pathExists, protectSensitiveFile } from "./fileProtection.js";
12
+ import { ensureAgentEarthDir, getTelemetryQueuePath } from "./paths.js";
13
+ export async function enqueueTelemetry(event, options = {}) {
14
+ // 写队列前确保本地数据目录存在。
15
+ await ensureAgentEarthDir();
16
+ // 每条事件单独一行,后续读取时可以按换行切分。
17
+ const queuePath = getTelemetryQueuePath();
18
+ // events.jsonl 高频追加写入;已存在时复用首次创建时收紧的权限。
19
+ const queueFileExists = await pathExists(queuePath);
20
+ await appendFile(queuePath, `${JSON.stringify(event)}\n`, { encoding: "utf8", mode: 0o600 });
21
+ // 入队后立即裁剪,避免埋点后端长期不可用时本地队列无限增长。
22
+ await trimTelemetryQueue(queuePath, options.fileProtection);
23
+ // events.jsonl 可能包含 query/params,和 credentials 一样按敏感文件保护。
24
+ if (!queueFileExists) {
25
+ await protectTelemetryQueueFile(queuePath, options.fileProtection);
26
+ }
27
+ }
28
+ async function trimTelemetryQueue(queuePath, fileProtection) {
29
+ try {
30
+ const raw = await readFile(queuePath, "utf8");
31
+ const lines = raw.split(/\r?\n/).filter(Boolean);
32
+ const trimmedLines = keepLatestTelemetryLines(lines);
33
+ if (trimmedLines.length === lines.length && Buffer.byteLength(raw, "utf8") <= TELEMETRY_QUEUE_MAX_BYTES) {
34
+ return;
35
+ }
36
+ await writeTelemetryQueueLines(queuePath, trimmedLines, fileProtection);
37
+ }
38
+ catch {
39
+ // 裁剪失败也不能影响主命令;后续上传或下一次入队会再尝试处理。
40
+ }
41
+ }
42
+ function keepLatestTelemetryLines(lines) {
43
+ // 先按行数保留最新队尾,旧事件可丢,因为埋点是 best-effort 数据。
44
+ let latestLines = lines.slice(-TELEMETRY_QUEUE_MAX_LINES);
45
+ // 再按字节数裁剪;如果单条最新事件本身超过上限,仍保留这一条,避免本次事件被静默丢弃。
46
+ while (latestLines.length > 1 && telemetryLinesByteLength(latestLines) > TELEMETRY_QUEUE_MAX_BYTES) {
47
+ latestLines = latestLines.slice(1);
48
+ }
49
+ return latestLines;
50
+ }
51
+ function telemetryLinesByteLength(lines) {
52
+ return Buffer.byteLength(lines.length > 0 ? `${lines.join("\n")}\n` : "", "utf8");
53
+ }
54
+ async function writeTelemetryQueueLines(queuePath, lines, fileProtection) {
55
+ // 如果裁剪重写时文件被外部删除,重建后仍需要补一次保护。
56
+ const queueFileExists = await pathExists(queuePath);
57
+ await writeFile(queuePath, lines.length > 0 ? `${lines.join("\n")}\n` : "", { encoding: "utf8", mode: 0o600 });
58
+ if (!queueFileExists) {
59
+ await protectTelemetryQueueFile(queuePath, fileProtection);
60
+ }
61
+ }
62
+ export async function protectTelemetryQueueFile(queuePath, options) {
63
+ // 统一复用敏感文件保护逻辑:Windows 收紧 ACL,macOS/Linux chmod 600。
64
+ await protectSensitiveFile(queuePath, options);
65
+ }
66
+ export async function readTelemetryQueue(limit = 20) {
67
+ return (await readTelemetryQueueBatch(limit)).events;
68
+ }
69
+ export async function readTelemetryQueueBatch(limit = 20) {
70
+ try {
71
+ // 一次最多读取前 limit 条,避免单次上传过大。
72
+ const raw = await readFile(getTelemetryQueuePath(), "utf8");
73
+ const lines = raw
74
+ // 同时兼容 Windows 的 CRLF 和 macOS/Linux 的 LF。
75
+ .split(/\r?\n/)
76
+ // 文件末尾通常会有空行,这里去掉空行。
77
+ .filter(Boolean);
78
+ const events = [];
79
+ let consumedLineCount = 0;
80
+ for (const line of lines) {
81
+ if (events.length >= limit) {
82
+ break;
83
+ }
84
+ consumedLineCount += 1;
85
+ try {
86
+ // 每一行都应该是一个 TelemetryEvent JSON;结构不对的本地坏数据也要跳过。
87
+ const event = JSON.parse(line);
88
+ if (isTelemetryEvent(event)) {
89
+ events.push(event);
90
+ }
91
+ }
92
+ catch {
93
+ // 损坏行不能上传,但要计入 consumedLineCount,上传成功后一起从队头清理。
94
+ }
95
+ }
96
+ return { events, consumedLineCount };
97
+ }
98
+ catch {
99
+ // 队列文件不存在、不可读或内容损坏时,都返回空队列,避免影响主命令。
100
+ return { events: [], consumedLineCount: 0 };
101
+ }
102
+ }
103
+ export async function dropTelemetryQueueHead(count) {
104
+ // count<=0 没有删除意义,直接返回。
105
+ if (count <= 0) {
106
+ return;
107
+ }
108
+ await ensureAgentEarthDir();
109
+ try {
110
+ // 重新读取完整文件,然后删掉前 count 行。
111
+ const raw = await readFile(getTelemetryQueuePath(), "utf8");
112
+ const lines = raw.split(/\r?\n/).filter(Boolean);
113
+ // 上传成功删除队头后也要继续执行容量保护,避免历史超限队列只靠下一次入队才被修复。
114
+ const remaining = keepLatestTelemetryLines(lines.slice(count));
115
+ // 如果还有剩余事件,保留换行;如果没有,写成空文件。
116
+ const queuePath = getTelemetryQueuePath();
117
+ // 重写队列文件后继续保持私有权限。
118
+ await writeTelemetryQueueLines(queuePath, remaining);
119
+ }
120
+ catch {
121
+ // 删除队头失败时静默处理;下次上传可能会重复发送,但不会影响用户命令。
122
+ }
123
+ }
124
+ function isTelemetryEvent(value) {
125
+ // events.jsonl 是本地可变文件,不能只相信 JSON.parse 成功;字段结构也要符合日志规范。
126
+ if (!isRecord(value)) {
127
+ return false;
128
+ }
129
+ return value.log_type === TELEMETRY_LOG_TYPE
130
+ && (value.level === "info" || value.level === "error")
131
+ && typeof value.request_id === "string"
132
+ && typeof value.path === "string"
133
+ && typeof value.param === "string"
134
+ // param 在日志规范中是 JSON string;本地队列被篡改或损坏时,非法 param 不能上传到后端。
135
+ && isTelemetryParamString(value.param)
136
+ && isTelemetryMethod(value.method)
137
+ && (value.status_code === 0 || value.status_code === 1)
138
+ // duration_ms 必须符合日志规范的 int 语义:非负整数。
139
+ && typeof value.duration_ms === "number"
140
+ && Number.isInteger(value.duration_ms)
141
+ && value.duration_ms >= 0
142
+ && value.msg === TELEMETRY_MSG
143
+ && typeof value.error_msg === "string"
144
+ && isIsoDateTimeString(value.occurred_at)
145
+ && typeof value.cli_version === "string"
146
+ && isTelemetryPlatform(value.platform)
147
+ && isTelemetryShellFamily(value.shell_family)
148
+ && isTelemetryErrorCode(value.error_code)
149
+ && hasExpectedTelemetryPath(value)
150
+ && hasConsistentTelemetryStatus(value);
151
+ }
152
+ function hasExpectedTelemetryPath(value) {
153
+ // method/path 必须符合日志规范表,避免本地队列坏数据把命令和后端路径错配。
154
+ if (value.method === "login" || value.method === "recommend") {
155
+ return value.path === RECOMMEND_PATH;
156
+ }
157
+ if (value.method === "list")
158
+ return value.path === TOOLS_LIST_PATH;
159
+ if (value.method === "detail")
160
+ return value.path === TOOL_DETAIL_PATH;
161
+ if (value.method === "execute")
162
+ return value.path === EXECUTE_PATH;
163
+ if (value.method === "update")
164
+ return value.path === "";
165
+ return false;
166
+ }
167
+ function hasConsistentTelemetryStatus(value) {
168
+ if (value.error_code === "") {
169
+ // 成功或无本地错误时,状态字段必须全部落在成功语义上。
170
+ return value.level === "info"
171
+ && value.status_code === 0
172
+ && value.error_msg === "";
173
+ }
174
+ // 失败事件必须同时具备 error 级别、失败状态码、非空错误码和安全错误文案。
175
+ return value.level === "error"
176
+ && value.status_code === 1
177
+ && typeof value.error_code === "string"
178
+ && value.error_code.length > 0
179
+ && typeof value.error_msg === "string"
180
+ && value.error_msg.length > 0;
181
+ }
182
+ function isTelemetryMethod(value) {
183
+ return value === "login"
184
+ || value === "recommend"
185
+ || value === "list"
186
+ || value === "detail"
187
+ || value === "execute"
188
+ || value === "update";
189
+ }
190
+ function isTelemetryPlatform(value) {
191
+ return value === "windows"
192
+ || value === "macos"
193
+ || value === "linux"
194
+ || value === "unknown";
195
+ }
196
+ function isTelemetryShellFamily(value) {
197
+ return value === "powershell"
198
+ || value === "cmd"
199
+ || value === "bash"
200
+ || value === "zsh"
201
+ || value === "unknown";
202
+ }
203
+ function isTelemetryErrorCode(value) {
204
+ // 成功或无本地错误时 error_code 必须为空字符串;失败时必须是日志规范允许值。
205
+ return value === ""
206
+ || value === "usage_error"
207
+ || value === "network_error"
208
+ || value === "timeout"
209
+ || value === "credentials_write_failed"
210
+ || value === "session_write_failed"
211
+ || value === "tool_name_parse_failed"
212
+ || value === "schema_missing"
213
+ || value === "tool_name_invalid"
214
+ || value === "params_invalid"
215
+ || value === "backend_response_invalid"
216
+ || value === "version_check_failed"
217
+ || value === "update_failed"
218
+ || value === "update_verify_failed"
219
+ || value === "update_not_applied"
220
+ // unknown 是 CLI 本地兜底错误码,合法事件不能在上传前被当作坏数据丢弃。
221
+ || value === "unknown";
222
+ }
223
+ function isIsoDateTimeString(value) {
224
+ if (typeof value !== "string") {
225
+ return false;
226
+ }
227
+ // createTelemetryEvent 使用 Date.toISOString(),格式固定为 YYYY-MM-DDTHH:mm:ss.sssZ。
228
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)) {
229
+ return false;
230
+ }
231
+ const timestamp = Date.parse(value);
232
+ return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value;
233
+ }
234
+ function isTelemetryParamString(value) {
235
+ try {
236
+ // param 反序列化后必须符合日志规范里的内部字段类型,避免本地坏数据进入后端。
237
+ const parsed = JSON.parse(value);
238
+ return isRecord(parsed)
239
+ && typeof parsed.query === "string"
240
+ && typeof parsed.tool_name === "string"
241
+ && isRecord(parsed.params)
242
+ && typeof parsed.update_result === "string";
243
+ }
244
+ catch {
245
+ return false;
246
+ }
247
+ }
248
+ function isRecord(value) {
249
+ // object 会包含数组,所以这里排除 Array,避免数组被当成事件对象。
250
+ return typeof value === "object" && value !== null && !Array.isArray(value);
251
+ }
252
+ //# sourceMappingURL=telemetryQueue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telemetryQueue.js","sourceRoot":"","sources":["../../src/storage/telemetryQueue.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { type ProtectSensitiveFileOptions } from "./fileProtection.js";
2
+ export declare const UPDATE_CACHE_TTL_MS: number;
3
+ export interface UpdateCache {
4
+ last_checked_at: string;
5
+ current_version: string;
6
+ latest_version: string;
7
+ has_update: boolean;
8
+ message?: string;
9
+ update_command?: string;
10
+ release_notes?: unknown[];
11
+ }
12
+ export type WritableUpdateCache = Omit<UpdateCache, "last_checked_at">;
13
+ export interface WriteUpdateCacheOptions {
14
+ fileProtection?: ProtectSensitiveFileOptions;
15
+ }
16
+ export declare function readUpdateCache(now?: Date): Promise<UpdateCache | null>;
17
+ export declare function writeUpdateCache(value: WritableUpdateCache, checkedAt?: Date, options?: WriteUpdateCacheOptions): Promise<UpdateCache>;
18
+ export declare function protectUpdateCacheFile(filePath: string, options?: ProtectSensitiveFileOptions): Promise<void>;
19
+ export declare function isUpdateCacheFresh(cache: UpdateCache, now?: Date): boolean;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * 更新检查缓存模块。
3
+ *
4
+ * list/recommend 会读取这个文件展示 fresh 更新提示。
5
+ * 缓存失效时,CLI 通过 npm view 查询 npm registry,并把检查结果写回这里,
6
+ * 避免每次工具发现都请求 registry。
7
+ */
8
+ import { readFile, writeFile } from "node:fs/promises";
9
+ import { pathExists, protectSensitiveFile } from "./fileProtection.js";
10
+ import { ensureAgentEarthDir, getUpdateCachePath } from "./paths.js";
11
+ export const UPDATE_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
12
+ export async function readUpdateCache(now = new Date()) {
13
+ try {
14
+ // update-cache 不可信,读取后必须做结构校验和过期判断。
15
+ const raw = await readFile(getUpdateCachePath(), "utf8");
16
+ const cache = normalizeUpdateCache(JSON.parse(raw));
17
+ if (!cache || !isUpdateCacheFresh(cache, now)) {
18
+ return null;
19
+ }
20
+ return cache;
21
+ }
22
+ catch {
23
+ // 文件不存在、JSON 损坏或权限异常,都视为没有可用缓存。
24
+ return null;
25
+ }
26
+ }
27
+ export async function writeUpdateCache(value, checkedAt = new Date(), options = {}) {
28
+ // 写缓存前先确保 AgentEarth 本地数据目录存在。
29
+ await ensureAgentEarthDir();
30
+ const cache = {
31
+ ...value,
32
+ last_checked_at: checkedAt.toISOString()
33
+ };
34
+ const cachePath = getUpdateCachePath();
35
+ // 已存在的 update-cache.json 复用首次创建时的文件权限,避免每次缓存刷新都跑 ACL 命令。
36
+ const cacheFileExists = await pathExists(cachePath);
37
+ // update-cache 不保存 API key,但会影响更新提示,也按当前用户私有文件处理。
38
+ await writeFile(cachePath, `${JSON.stringify(cache, null, 2)}\n`, { mode: 0o600 });
39
+ if (!cacheFileExists) {
40
+ await protectUpdateCacheFile(cachePath, options.fileProtection);
41
+ }
42
+ return cache;
43
+ }
44
+ export async function protectUpdateCacheFile(filePath, options) {
45
+ // update-cache 会影响更新提示和本地更新流程,按当前用户私有文件处理。
46
+ await protectSensitiveFile(filePath, options);
47
+ }
48
+ export function isUpdateCacheFresh(cache, now = new Date()) {
49
+ const checkedAt = Date.parse(cache.last_checked_at);
50
+ if (!Number.isFinite(checkedAt)) {
51
+ return false;
52
+ }
53
+ const ageMs = now.getTime() - checkedAt;
54
+ // last_checked_at 晚于当前时间时,说明本地时间或缓存内容异常,不能长期跳过版本检查。
55
+ return ageMs >= 0 && ageMs < UPDATE_CACHE_TTL_MS;
56
+ }
57
+ function normalizeUpdateCache(value) {
58
+ // 只接受后续更新流程必须依赖的核心字段,其它字段做保守透传。
59
+ if (!isRecord(value)
60
+ || typeof value.last_checked_at !== "string"
61
+ || typeof value.current_version !== "string"
62
+ || typeof value.latest_version !== "string"
63
+ || typeof value.has_update !== "boolean") {
64
+ return null;
65
+ }
66
+ return {
67
+ last_checked_at: value.last_checked_at,
68
+ current_version: value.current_version,
69
+ latest_version: value.latest_version,
70
+ has_update: value.has_update,
71
+ message: typeof value.message === "string" ? value.message : undefined,
72
+ update_command: typeof value.update_command === "string" ? value.update_command : undefined,
73
+ release_notes: Array.isArray(value.release_notes) ? value.release_notes : undefined
74
+ };
75
+ }
76
+ function isRecord(value) {
77
+ // object 会包含数组,所以这里排除 Array。
78
+ return typeof value === "object" && value !== null && !Array.isArray(value);
79
+ }
80
+ //# sourceMappingURL=updateCache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"updateCache.js","sourceRoot":"","sources":["../../src/storage/updateCache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ export declare const TELEMETRY_LOG_TYPE = "agentearth_cli_telemetry";
2
+ export declare const TELEMETRY_MSG = "cli telemetry event";
3
+ export type TelemetryMethod = "login" | "recommend" | "list" | "detail" | "execute" | "update";
4
+ export type TelemetryLevel = "info" | "error";
5
+ export type TelemetryPlatform = "windows" | "macos" | "linux" | "unknown";
6
+ export type TelemetryShellFamily = "powershell" | "cmd" | "bash" | "zsh" | "unknown";
7
+ export interface TelemetryEvent {
8
+ log_type: typeof TELEMETRY_LOG_TYPE;
9
+ level: TelemetryLevel;
10
+ request_id: string;
11
+ path: string;
12
+ param: string;
13
+ method: TelemetryMethod;
14
+ status_code: 0 | 1;
15
+ duration_ms: number;
16
+ msg: typeof TELEMETRY_MSG;
17
+ error_msg: string;
18
+ occurred_at: string;
19
+ cli_version: string;
20
+ platform: TelemetryPlatform;
21
+ shell_family: TelemetryShellFamily;
22
+ error_code: string;
23
+ }
24
+ export interface TelemetryParamFields {
25
+ query?: string;
26
+ toolName?: string;
27
+ params?: unknown;
28
+ updateResult?: string;
29
+ }
30
+ export interface CreateTelemetryEventFields {
31
+ level?: TelemetryLevel;
32
+ request_id?: string;
33
+ param?: string;
34
+ status_code?: 0 | 1;
35
+ duration_ms?: number;
36
+ error_msg?: string;
37
+ error_code?: string;
38
+ platform?: TelemetryPlatform;
39
+ shell_family?: TelemetryShellFamily;
40
+ }
41
+ export declare function buildTelemetryParam(fields?: TelemetryParamFields): string;
42
+ export declare function createTelemetryEvent(method: TelemetryMethod, path: string, fields?: CreateTelemetryEventFields): TelemetryEvent;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * CLI 埋点日志构造模块。
3
+ *
4
+ * 这里把 CLI 内部信息整理成后端日志接口要求的字段。注意当前模型不是旧的
5
+ * “event_name + 二级状态字段”事件模型,而是飞书日志规范风格:
6
+ * log_type、level、request_id、path、param、method、status_code、error_code 等。
7
+ *
8
+ * main.ts 只调用这里的函数生成事件,不应该自己拼字段,避免口径分散。
9
+ */
10
+ import { randomUUID } from "node:crypto";
11
+ import { CLI_VERSION } from "../constants.js";
12
+ // log_type 是固定值,用于后端区分 CLI 埋点日志。
13
+ export const TELEMETRY_LOG_TYPE = "agentearth_cli_telemetry";
14
+ // msg 是固定摘要,不写具体错误原因;具体失败原因放 error_code/error_msg。
15
+ export const TELEMETRY_MSG = "cli telemetry event";
16
+ export function buildTelemetryParam(fields = {}) {
17
+ // 后端要求 param 是 string,所以这里把内部对象 JSON.stringify。
18
+ return JSON.stringify({
19
+ // 没有值的 string 字段必须是空字符串,不使用 null。
20
+ query: fields.query ?? "",
21
+ tool_name: fields.toolName ?? "",
22
+ // params 必须是对象;如果传入字符串/数组/null,就按空对象处理。
23
+ params: isRecord(fields.params) ? fields.params : {},
24
+ update_result: fields.updateResult ?? ""
25
+ });
26
+ }
27
+ export function createTelemetryEvent(method, path, fields = {}) {
28
+ // error_code 为空字符串表示本地成功或无本地错误。
29
+ const errorCode = fields.error_code ?? "";
30
+ return {
31
+ log_type: TELEMETRY_LOG_TYPE,
32
+ // 如果有 error_code,默认 level=error;否则默认 info。
33
+ level: fields.level ?? (errorCode ? "error" : "info"),
34
+ // request_id 用于后端去重;同一条本地事件重试上传时不会重新生成。
35
+ request_id: fields.request_id ?? `req_${randomUUID()}`,
36
+ path,
37
+ param: fields.param ?? buildTelemetryParam(),
38
+ method,
39
+ status_code: fields.status_code ?? (errorCode ? 1 : 0),
40
+ // duration_ms 要求是非负整数。
41
+ duration_ms: Math.max(0, Math.trunc(fields.duration_ms ?? 0)),
42
+ msg: TELEMETRY_MSG,
43
+ error_msg: fields.error_msg ?? "",
44
+ occurred_at: new Date().toISOString(),
45
+ cli_version: CLI_VERSION,
46
+ platform: fields.platform ?? detectPlatform(),
47
+ shell_family: fields.shell_family ?? detectShellFamily(),
48
+ error_code: errorCode
49
+ };
50
+ }
51
+ function detectPlatform() {
52
+ // Node.js 在 Windows 上的 process.platform 是 win32。
53
+ if (process.platform === "win32")
54
+ return "windows";
55
+ // macOS 对应 darwin。
56
+ if (process.platform === "darwin")
57
+ return "macos";
58
+ // Linux 对应 linux。
59
+ if (process.platform === "linux")
60
+ return "linux";
61
+ return "unknown";
62
+ }
63
+ function detectShellFamily() {
64
+ // 不同系统把 shell 信息放在不同环境变量里,所以拼成一个字符串做粗略判断。
65
+ const shell = `${process.env.SHELL ?? ""} ${process.env.ComSpec ?? ""} ${process.env.PSModulePath ?? ""}`.toLowerCase();
66
+ if (shell.includes("powershell"))
67
+ return "powershell";
68
+ if (shell.includes("cmd.exe"))
69
+ return "cmd";
70
+ if (shell.includes("zsh"))
71
+ return "zsh";
72
+ if (shell.includes("bash"))
73
+ return "bash";
74
+ return "unknown";
75
+ }
76
+ function isRecord(value) {
77
+ // param.params 只接受普通对象;数组虽然也是 object,但业务语义不是对象参数。
78
+ return typeof value === "object" && value !== null && !Array.isArray(value);
79
+ }
80
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/telemetry/events.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { GlobalOptions } from "../types/cli.js";
2
+ export declare function flushTelemetry(options: GlobalOptions): Promise<void>;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * CLI 埋点上传模块。
3
+ *
4
+ * 本模块从本地 events.jsonl 读取待上传日志,调用后端 CLI 埋点接口。
5
+ * 上传成功只以 HTTP 200 为准,不解析响应体。上传失败时保留本地队列,
6
+ * 等下一次 CLI 命令结束后再尝试。
7
+ *
8
+ * 重要原则:埋点失败绝不能改变当前业务命令的输出和退出码。
9
+ */
10
+ import { TELEMETRY_PATH } from "../constants.js";
11
+ import { postJsonNoResponse } from "../core/http.js";
12
+ import { resolveApiKey } from "../storage/credentials.js";
13
+ import { dropTelemetryQueueHead, readTelemetryQueueBatch } from "../storage/telemetryQueue.js";
14
+ export async function flushTelemetry(options) {
15
+ // 用户或测试可以关闭埋点。
16
+ if (options.noTelemetry) {
17
+ return;
18
+ }
19
+ // 默认最多读取队头 20 条,具体限制在 readTelemetryQueueBatch 内部。
20
+ const batch = await readTelemetryQueueBatch();
21
+ const events = batch.events;
22
+ if (events.length === 0) {
23
+ if (batch.consumedLineCount > 0) {
24
+ // 队头只有损坏行时,不请求后端,直接清理这些无法上传的本地坏数据。
25
+ await dropTelemetryQueueHead(batch.consumedLineCount);
26
+ }
27
+ // 没有待上传事件时不请求后端。
28
+ return;
29
+ }
30
+ try {
31
+ // 埋点接口也需要 API key,后端用它识别用户。
32
+ const apiKey = await resolveApiKey(options);
33
+ // 后端契约是 POST { events },成功只要求 HTTP 200。
34
+ await postJsonNoResponse(TELEMETRY_PATH, { events }, {
35
+ apiKey,
36
+ baseUrl: options.baseUrl,
37
+ timeoutMs: options.timeoutMs
38
+ });
39
+ // 上传成功后,删除本次实际消费过的队头原始行;其中可能包含被跳过的损坏行。
40
+ await dropTelemetryQueueHead(batch.consumedLineCount);
41
+ }
42
+ catch {
43
+ // 任何上传失败都静默处理,队列保留,等待后续重试。
44
+ }
45
+ }
46
+ //# sourceMappingURL=uploader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uploader.js","sourceRoot":"","sources":["../../src/telemetry/uploader.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAG/F,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAAsB;IACzD,eAAe;IACf,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;QACxB,OAAO;IACT,CAAC;IAED,kDAAkD;IAClD,MAAM,KAAK,GAAG,MAAM,uBAAuB,EAAE,CAAC;IAC9C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAC5B,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,IAAI,KAAK,CAAC,iBAAiB,GAAG,CAAC,EAAE,CAAC;YAChC,mCAAmC;YACnC,MAAM,sBAAsB,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;QACxD,CAAC;QACD,iBAAiB;QACjB,OAAO;IACT,CAAC;IAED,IAAI,CAAC;QACH,4BAA4B;QAC5B,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,CAAC;QAE5C,wCAAwC;QACxC,MAAM,kBAAkB,CACtB,cAAc,EACd,EAAE,MAAM,EAAE,EACV;YACE,MAAM;YACN,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,SAAS,EAAE,OAAO,CAAC,SAAS;SAC7B,CACF,CAAC;QAEF,uCAAuC;QACvC,MAAM,sBAAsB,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACxD,CAAC;IAAC,MAAM,CAAC;QACP,2BAA2B;IAC7B,CAAC;AACH,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * 后端 AgentAPI 契约类型。
3
+ *
4
+ * 这些 interface 描述 CLI 和后端之间传输的数据形状。它们只提供 TypeScript
5
+ * 编译期提示,不能证明运行时数据一定正确,所以命令模块里仍会做必要的空值
6
+ * 和字段检查。
7
+ */
8
+ export interface RecommendRequest {
9
+ query: string;
10
+ limit?: number;
11
+ }
12
+ export interface ApiTool {
13
+ tool_url?: string;
14
+ description?: string;
15
+ when_to_use?: string;
16
+ input_schema?: unknown;
17
+ credit?: number;
18
+ }
19
+ export interface RecommendResponse {
20
+ error_no: number;
21
+ error_msg: string;
22
+ total?: number;
23
+ tools?: ApiTool[];
24
+ }
25
+ export interface ToolListResponse {
26
+ error_no: number;
27
+ error_msg: string;
28
+ total?: number;
29
+ page?: number;
30
+ page_size?: number;
31
+ tools?: ApiTool[];
32
+ }
33
+ export interface ToolDetailResponse {
34
+ error_no: number;
35
+ error_msg: string;
36
+ total?: number;
37
+ tools?: ApiTool[];
38
+ }
39
+ export interface ExecuteRequest {
40
+ params: unknown;
41
+ }
42
+ export interface ExecuteResponse {
43
+ error_no: number;
44
+ error_msg: string;
45
+ result: unknown;
46
+ agentearth_suggestion?: string;
47
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 后端 AgentAPI 契约类型。
3
+ *
4
+ * 这些 interface 描述 CLI 和后端之间传输的数据形状。它们只提供 TypeScript
5
+ * 编译期提示,不能证明运行时数据一定正确,所以命令模块里仍会做必要的空值
6
+ * 和字段检查。
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/types/api.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}