yingmi-skill-cli 0.0.1

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 (3) hide show
  1. package/README.md +194 -0
  2. package/bin/index.js +1349 -0
  3. package/package.json +39 -0
package/bin/index.js ADDED
@@ -0,0 +1,1349 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ var commander = require('commander');
6
+ var fs = require('fs');
7
+ var os = require('os');
8
+ var path = require('path');
9
+ var axios = require('axios');
10
+ var puppeteer = require('puppeteer');
11
+ var crypto = require('crypto');
12
+ var child_process = require('child_process');
13
+
14
+ var name = "yingmi-cli";
15
+ var version = "0.0.10";
16
+
17
+ function fail(message) {
18
+ console.error(message);
19
+ process.exit(1);
20
+ }
21
+
22
+ function resolveCommand(root, pathSegments) {
23
+ let current = root;
24
+ pathSegments.forEach((segment) => {
25
+ current = current?.commands.find((command) => command.name() === segment);
26
+ });
27
+ return current;
28
+ }
29
+ function registerHelpCommand(program) {
30
+ program.command("help").description("\u663E\u793A CLI \u6216\u6307\u5B9A\u5B50\u547D\u4EE4\u7684\u5E2E\u52A9\u4FE1\u606F").argument("[commandPath...]", "\u547D\u4EE4\u8DEF\u5F84\uFF0C\u4F8B\u5982: mcp call").action((commandPath) => {
31
+ if (!commandPath || commandPath.length === 0) {
32
+ program.outputHelp();
33
+ return;
34
+ }
35
+ const targetCommand = resolveCommand(program, commandPath);
36
+ if (!targetCommand) {
37
+ fail(`\u672A\u627E\u5230\u547D\u4EE4: ${commandPath.join(" ")}`);
38
+ }
39
+ targetCommand.outputHelp();
40
+ });
41
+ }
42
+
43
+ const CONFIG_DIR = path.join(os.homedir(), ".yingmi-cli");
44
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
45
+ const SKILLS_DIR = path.join(CONFIG_DIR, "skills");
46
+ const DEFAULT_QIEMAN_BASE_URL = "https://qieman.com";
47
+ const DEFAULT_REMOTE_SKILL_SERVER_BASE_URL = "https://stargate-staging.yingmi-inc.com/";
48
+ function ensureConfigDir() {
49
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
50
+ }
51
+ function readConfig() {
52
+ ensureConfigDir();
53
+ if (!fs.existsSync(CONFIG_FILE)) {
54
+ return {};
55
+ }
56
+ const content = fs.readFileSync(CONFIG_FILE, "utf8").trim();
57
+ if (!content) {
58
+ return {};
59
+ }
60
+ const parsed = JSON.parse(content);
61
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
62
+ throw new Error("\u914D\u7F6E\u6587\u4EF6\u683C\u5F0F\u975E\u6CD5\uFF0C\u8BF7\u68C0\u67E5 ~/.yingmi-cli/config.json");
63
+ }
64
+ return parsed;
65
+ }
66
+ function writeConfig(config) {
67
+ ensureConfigDir();
68
+ fs.writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}
69
+ `, "utf8");
70
+ }
71
+ function setApiKey(apiKey) {
72
+ const nextConfig = {
73
+ ...readConfig(),
74
+ apiKey
75
+ };
76
+ writeConfig(nextConfig);
77
+ }
78
+ function getApiKeyApplySession() {
79
+ return readConfig().apiKeyApplySession;
80
+ }
81
+ function setApiKeyApplySession(session) {
82
+ const nextConfig = {
83
+ ...readConfig(),
84
+ apiKeyApplySession: session
85
+ };
86
+ writeConfig(nextConfig);
87
+ }
88
+ function clearApiKeyApplySession() {
89
+ const nextConfig = {
90
+ ...readConfig()
91
+ };
92
+ delete nextConfig.apiKeyApplySession;
93
+ writeConfig(nextConfig);
94
+ }
95
+ function getCurrentRemoteSkillSession() {
96
+ return readConfig().currentRemoteSkillSession;
97
+ }
98
+ function setCurrentRemoteSkillSession(session) {
99
+ const nextConfig = {
100
+ ...readConfig(),
101
+ currentRemoteSkillSession: session
102
+ };
103
+ writeConfig(nextConfig);
104
+ }
105
+ function maskSecret(value) {
106
+ if (!value) {
107
+ return "(\u672A\u8BBE\u7F6E)";
108
+ }
109
+ if (value.length <= 8) {
110
+ return `${value.slice(0, 2)}***${value.slice(-2)}`;
111
+ }
112
+ return `${value.slice(0, 4)}***${value.slice(-4)}`;
113
+ }
114
+ function getConfigFilePath() {
115
+ return CONFIG_FILE;
116
+ }
117
+ function getSkillsDirPath() {
118
+ ensureConfigDir();
119
+ fs.mkdirSync(SKILLS_DIR, { recursive: true });
120
+ return SKILLS_DIR;
121
+ }
122
+ function getRemoteSkillServerBaseUrl() {
123
+ const configuredBaseUrl = readConfig().remoteSkillServerBaseUrl?.trim();
124
+ return configuredBaseUrl || DEFAULT_REMOTE_SKILL_SERVER_BASE_URL;
125
+ }
126
+ function getQiemanBaseUrl() {
127
+ const configuredBaseUrl = readConfig().qiemanBaseUrl?.trim();
128
+ return configuredBaseUrl || DEFAULT_QIEMAN_BASE_URL;
129
+ }
130
+
131
+ function validatePhone(phone) {
132
+ if (!/^1\d{10}$/.test(phone)) {
133
+ throw new Error("\u624B\u673A\u53F7\u683C\u5F0F\u975E\u6CD5\uFF0C\u8BF7\u4F20\u5165 11 \u4F4D\u4E2D\u56FD\u5927\u9646\u624B\u673A\u53F7");
134
+ }
135
+ }
136
+ function validateXSign(xSign) {
137
+ if (!/^\d+[A-Fa-f0-9]{40}$/.test(xSign)) {
138
+ throw new Error("x-sign \u683C\u5F0F\u975E\u6CD5\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C\u521D\u59CB\u5316\u6D41\u7A0B");
139
+ }
140
+ }
141
+ function validateVerifyCode(verifyCode) {
142
+ if (!/^\d{4,8}$/.test(verifyCode)) {
143
+ throw new Error("\u9A8C\u8BC1\u7801\u683C\u5F0F\u975E\u6CD5\uFF0C\u8BF7\u4F20\u5165 4 \u5230 8 \u4F4D\u6570\u5B57\u9A8C\u8BC1\u7801");
144
+ }
145
+ }
146
+ function maskPhone(phone) {
147
+ return phone.replace(/^(\d{3})\d{4}(\d{4})$/, "$1****$2");
148
+ }
149
+ function extractApiKey(response) {
150
+ const apiKey = response.key ?? response.apiKey;
151
+ return typeof apiKey === "string" && apiKey.trim() ? apiKey.trim() : void 0;
152
+ }
153
+
154
+ function getInitStatusSummary() {
155
+ const config = readConfig();
156
+ const session = getApiKeyApplySession();
157
+ return {
158
+ configFile: getConfigFilePath(),
159
+ hasApiKey: Boolean(config.apiKey),
160
+ apiKey: maskSecret(config.apiKey),
161
+ hasPendingSession: Boolean(session),
162
+ pendingPhone: session?.phone ? maskPhone(session.phone) : null
163
+ };
164
+ }
165
+
166
+ function getErrorMessage(error) {
167
+ if (error instanceof axios.AxiosError) {
168
+ const status = error.response?.status;
169
+ const data = error.response?.data;
170
+ const detail = data?.msg?.trim() || data?.detail?.msg?.trim() || error.message || "\u672A\u77E5\u9519\u8BEF";
171
+ return `\u8BF7\u6C42\u5931\u8D25${status ? ` (${status})` : ""}: ${detail}`;
172
+ }
173
+ return error instanceof Error ? error.message : String(error);
174
+ }
175
+ function isInvalidVerifyCodeError(error) {
176
+ if (!(error instanceof axios.AxiosError)) {
177
+ return false;
178
+ }
179
+ const data = error.response?.data;
180
+ const errorCode = data?.code ?? data?.detail?.code;
181
+ const errorMessage = data?.msg ?? data?.detail?.msg ?? "";
182
+ return errorCode === "8602" || errorMessage.includes("\u77ED\u4FE1\u9A8C\u8BC1\u7801\u6709\u8BEF");
183
+ }
184
+
185
+ function runInitDoctorChecks() {
186
+ const checks = [
187
+ {
188
+ name: "node",
189
+ status: "ok",
190
+ message: `\u5F53\u524D Node.js \u7248\u672C: ${process.version}`
191
+ }
192
+ ];
193
+ try {
194
+ const status = getInitStatusSummary();
195
+ checks.push({
196
+ name: "config-file",
197
+ status: "ok",
198
+ message: `\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84\u5DF2\u89E3\u6790: ${status.configFile}`
199
+ });
200
+ checks.push(
201
+ status.hasApiKey ? {
202
+ name: "api-key",
203
+ status: "ok",
204
+ message: `\u5DF2\u914D\u7F6E apiKey: ${status.apiKey}`
205
+ } : {
206
+ name: "api-key",
207
+ status: "warning",
208
+ message: "\u5C1A\u672A\u914D\u7F6E apiKey",
209
+ nextStep: "\u6267\u884C yingmi-cli init setup --api-key <your-api-key> \u6216\u9A8C\u8BC1\u7801\u521D\u59CB\u5316\u6D41\u7A0B"
210
+ }
211
+ );
212
+ if (status.hasPendingSession && status.pendingPhone) {
213
+ checks.push({
214
+ name: "pending-session",
215
+ status: "warning",
216
+ message: `\u5B58\u5728\u5F85\u5B8C\u6210\u7684\u9A8C\u8BC1\u7801\u767B\u5F55\u4E0A\u4E0B\u6587: ${status.pendingPhone}`,
217
+ nextStep: "\u6267\u884C yingmi-cli init setup --verify-code <\u9A8C\u8BC1\u7801> \u5B8C\u6210\u521D\u59CB\u5316"
218
+ });
219
+ } else {
220
+ checks.push({
221
+ name: "pending-session",
222
+ status: "ok",
223
+ message: "\u5F53\u524D\u6CA1\u6709\u5F85\u5B8C\u6210\u7684\u9A8C\u8BC1\u7801\u767B\u5F55\u4E0A\u4E0B\u6587"
224
+ });
225
+ }
226
+ } catch (error) {
227
+ checks.push({
228
+ name: "config-read",
229
+ status: "warning",
230
+ message: `\u8BFB\u53D6\u914D\u7F6E\u5931\u8D25: ${getErrorMessage(error)}`,
231
+ nextStep: "\u68C0\u67E5 ~/.yingmi-cli/config.json \u683C\u5F0F\uFF0C\u5FC5\u8981\u65F6\u4FEE\u590D\u540E\u91CD\u8BD5"
232
+ });
233
+ }
234
+ return {
235
+ status: checks.some((check) => check.status === "warning") ? "warning" : "ok",
236
+ nodeVersion: process.version,
237
+ configFile: getConfigFilePath(),
238
+ checks
239
+ };
240
+ }
241
+
242
+ function getPendingSetupResult() {
243
+ return {
244
+ status: "pending",
245
+ setupMode: "guided",
246
+ configFile: getConfigFilePath(),
247
+ availableActions: ["--api-key <value>", "--phone <value>", "--verify-code <value>"],
248
+ nextSteps: [
249
+ "yingmi-cli init setup --api-key <your-api-key>",
250
+ "yingmi-cli init setup --phone <your-phone>",
251
+ "yingmi-cli init setup --verify-code <\u9A8C\u8BC1\u7801>"
252
+ ]
253
+ };
254
+ }
255
+
256
+ function getHeaderValue(headers, headerName) {
257
+ const expectedHeaderName = headerName.toLowerCase();
258
+ const matchedEntry = Object.entries(headers).find(
259
+ ([currentHeaderName]) => currentHeaderName.toLowerCase() === expectedHeaderName
260
+ );
261
+ return matchedEntry?.[1];
262
+ }
263
+ function waitForFirstFetchRequest(page, timeoutMs) {
264
+ return new Promise((resolve, reject) => {
265
+ let settled = false;
266
+ const cleanup = () => {
267
+ clearTimeout(timer);
268
+ page.off("request", handleRequest);
269
+ };
270
+ const settle = (callback) => {
271
+ if (settled) {
272
+ return;
273
+ }
274
+ settled = true;
275
+ cleanup();
276
+ callback();
277
+ };
278
+ const handleRequest = (request) => {
279
+ if (request.resourceType() !== "fetch") {
280
+ return;
281
+ }
282
+ settle(() => resolve(request));
283
+ };
284
+ const timer = setTimeout(() => {
285
+ settle(() => reject(new Error(`\u5728 ${timeoutMs}ms \u5185\u672A\u6355\u83B7\u5230 fetch \u8BF7\u6C42`)));
286
+ }, timeoutMs);
287
+ page.on("request", handleRequest);
288
+ });
289
+ }
290
+
291
+ function getCandidateExecutablePaths() {
292
+ const homeDirectory = os.homedir();
293
+ switch (process.platform) {
294
+ case "darwin":
295
+ return [
296
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
297
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
298
+ "/Applications/Chromium.app/Contents/MacOS/Chromium"
299
+ ];
300
+ case "win32":
301
+ return [
302
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
303
+ "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
304
+ "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
305
+ "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"
306
+ ];
307
+ default:
308
+ return [
309
+ "/usr/bin/google-chrome",
310
+ "/usr/bin/google-chrome-stable",
311
+ "/usr/bin/chromium",
312
+ "/usr/bin/chromium-browser",
313
+ `${homeDirectory}/.cache/puppeteer/chrome/linux-*/chrome-linux64/chrome`
314
+ ];
315
+ }
316
+ }
317
+ function resolveExecutablePath(executablePath) {
318
+ const customExecutablePath = executablePath?.trim() || process.env.PUPPETEER_EXECUTABLE_PATH?.trim();
319
+ if (customExecutablePath) {
320
+ if (!fs.existsSync(customExecutablePath)) {
321
+ throw new Error(`\u6D4F\u89C8\u5668\u4E0D\u5B58\u5728: ${customExecutablePath}`);
322
+ }
323
+ return customExecutablePath;
324
+ }
325
+ const matchedPath = getCandidateExecutablePaths().find(
326
+ (candidatePath) => fs.existsSync(candidatePath)
327
+ );
328
+ if (!matchedPath) {
329
+ throw new Error(
330
+ "\u672A\u627E\u5230\u53EF\u7528\u7684 Chrome/Chromium\uFF0C\u53EF\u901A\u8FC7 --executable-path \u6216 PUPPETEER_EXECUTABLE_PATH \u6307\u5B9A\u6D4F\u89C8\u5668\u8DEF\u5F84"
331
+ );
332
+ }
333
+ return matchedPath;
334
+ }
335
+
336
+ const DEFAULT_TIMEOUT_MS = 2e4;
337
+ async function getXSign(options = {}) {
338
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
339
+ const browser = await puppeteer.launch({
340
+ executablePath: resolveExecutablePath(options.executablePath),
341
+ headless: options.headless ?? true,
342
+ args: ["--no-sandbox", "--disable-setuid-sandbox"]
343
+ });
344
+ try {
345
+ const page = await browser.newPage();
346
+ const firstFetchRequestPromise = waitForFirstFetchRequest(page, timeoutMs);
347
+ const url = options.url ?? getQiemanBaseUrl();
348
+ await page.goto(url, {
349
+ waitUntil: "domcontentloaded",
350
+ timeout: timeoutMs
351
+ });
352
+ const firstFetchRequest = await firstFetchRequestPromise;
353
+ const xSign = getHeaderValue(firstFetchRequest.headers(), "x-sign")?.trim();
354
+ if (!xSign) {
355
+ throw new Error(`\u9996\u4E2A fetch \u8BF7\u6C42\u672A\u643A\u5E26 x-sign: ${firstFetchRequest.url()}`);
356
+ }
357
+ return xSign;
358
+ } finally {
359
+ await browser.close();
360
+ }
361
+ }
362
+
363
+ function trimTrailingSlash(value) {
364
+ return value.replace(/\/+$/, "");
365
+ }
366
+ function getQiemanPmdjBaseUrl() {
367
+ return `${trimTrailingSlash(getQiemanBaseUrl())}/pmdj/v1`;
368
+ }
369
+ const API_KEY_APPLICATION_PAYLOAD = {
370
+ name: "CLI\u7533\u8BF7\u7684\u5360\u4F4D\u7B26",
371
+ organization: "CLI\u7533\u8BF7\u7684\u5360\u4F4D\u7B26",
372
+ email: "",
373
+ usagePurposes: ["\u63A2\u7D22\u66F4\u591A\u5408\u4F5C\u53EF\u80FD"],
374
+ usageDescription: "",
375
+ agreementIds: ["AGREEMENT34-V20250425"],
376
+ positionId: "user_position_other",
377
+ position: "CLI\u7533\u8BF7\u7684\u5360\u4F4D\u7B26",
378
+ organizationType: "\u5176\u4ED6"
379
+ };
380
+
381
+ function createRequestId() {
382
+ const hexTimestamp = Date.now().toString(16).toUpperCase();
383
+ const randomSuffix = crypto.randomBytes(8).toString("hex").toUpperCase();
384
+ return `yingmi-cli.${hexTimestamp}${randomSuffix}`;
385
+ }
386
+ function getHeaders(xSign, accessToken) {
387
+ return {
388
+ accept: "application/json",
389
+ "content-type": "application/json",
390
+ "x-request-id": createRequestId(),
391
+ "x-sign": xSign,
392
+ ...accessToken ? { authorization: `Bearer ${accessToken}` } : {}
393
+ };
394
+ }
395
+
396
+ async function prepareSmsSetup(phone) {
397
+ validatePhone(phone);
398
+ const qiemanPmdjBaseUrl = getQiemanPmdjBaseUrl();
399
+ const xSign = await getXSign();
400
+ validateXSign(xSign);
401
+ const response = await axios.post(
402
+ `${qiemanPmdjBaseUrl}/user/register/phone/prepare`,
403
+ { phone },
404
+ {
405
+ headers: getHeaders(xSign),
406
+ timeout: 2e4
407
+ }
408
+ );
409
+ if (!response.data.token) {
410
+ throw new Error("\u83B7\u53D6\u9A8C\u8BC1\u7801\u6210\u529F\uFF0C\u4F46\u54CD\u5E94\u4E2D\u7F3A\u5C11 token\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
411
+ }
412
+ setApiKeyApplySession({
413
+ phone,
414
+ xSign,
415
+ prepareToken: response.data.token,
416
+ requestedAt: Date.now()
417
+ });
418
+ return {
419
+ status: "code_sent",
420
+ setupMode: "sms_prepare",
421
+ configFile: getConfigFilePath(),
422
+ pendingPhone: maskPhone(phone),
423
+ cooldownSeconds: response.data.nextInSec ?? null,
424
+ nextStep: "yingmi-cli init setup --verify-code <\u9A8C\u8BC1\u7801>"
425
+ };
426
+ }
427
+
428
+ async function setupWithApiKey(apiKey) {
429
+ setApiKey(apiKey);
430
+ return {
431
+ status: "configured",
432
+ setupMode: "direct_api_key",
433
+ apiKey: maskSecret(apiKey),
434
+ configFile: getConfigFilePath()
435
+ };
436
+ }
437
+
438
+ async function refreshSessionXSignOnInvalidVerifyCode(session) {
439
+ if (!session) {
440
+ return;
441
+ }
442
+ const nextXSign = await getXSign();
443
+ validateXSign(nextXSign);
444
+ setApiKeyApplySession({
445
+ ...session,
446
+ xSign: nextXSign
447
+ });
448
+ }
449
+
450
+ async function confirmSmsSetup(verifyCode) {
451
+ validateVerifyCode(verifyCode);
452
+ const session = getApiKeyApplySession();
453
+ if (!session) {
454
+ throw new Error("\u672A\u627E\u5230\u9A8C\u8BC1\u7801\u767B\u5F55\u4E0A\u4E0B\u6587\uFF0C\u8BF7\u5148\u6267\u884C: yingmi-cli init setup --phone <\u624B\u673A\u53F7>");
455
+ }
456
+ if (!session.accessToken && !session.prepareToken) {
457
+ throw new Error("\u7533\u8BF7\u4E0A\u4E0B\u6587\u7F3A\u5C11\u767B\u5F55 token\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C: yingmi-cli init setup --phone <\u624B\u673A\u53F7>");
458
+ }
459
+ try {
460
+ const qiemanPmdjBaseUrl = getQiemanPmdjBaseUrl();
461
+ let accessToken = session.accessToken;
462
+ if (!accessToken) {
463
+ const confirmResponse = await axios.post(
464
+ `${qiemanPmdjBaseUrl}/user/register/phone/confirm`,
465
+ {
466
+ phone: session.phone,
467
+ poManagerName: null,
468
+ token: session.prepareToken,
469
+ verifyCode,
470
+ autoLogin: true
471
+ },
472
+ {
473
+ headers: getHeaders(session.xSign),
474
+ timeout: 2e4
475
+ }
476
+ );
477
+ accessToken = confirmResponse.data.accessToken?.trim();
478
+ if (!accessToken) {
479
+ throw new Error("\u786E\u8BA4\u767B\u5F55\u6210\u529F\uFF0C\u4F46\u54CD\u5E94\u4E2D\u7F3A\u5C11 accessToken\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
480
+ }
481
+ setApiKeyApplySession({
482
+ ...session,
483
+ accessToken
484
+ });
485
+ }
486
+ const currentApplicationResponse = await axios.get(
487
+ `${qiemanPmdjBaseUrl}/stargate/api-key-application`,
488
+ {
489
+ headers: getHeaders(session.xSign, accessToken),
490
+ timeout: 2e4
491
+ }
492
+ );
493
+ let apiKey = extractApiKey(currentApplicationResponse.data);
494
+ if (!apiKey) {
495
+ const hasExistingApplication = Object.keys(currentApplicationResponse.data ?? {}).length > 0;
496
+ if (hasExistingApplication) {
497
+ throw new Error(
498
+ `\u5DF2\u5B58\u5728 API Key \u7533\u8BF7\u8BB0\u5F55\uFF0C\u4F46\u54CD\u5E94\u4E2D\u672A\u8FD4\u56DE key\uFF0C\u5F53\u524D\u72B6\u6001: ${currentApplicationResponse.data.status ?? "unknown"}`
499
+ );
500
+ }
501
+ const applyResponse = await axios.post(
502
+ `${qiemanPmdjBaseUrl}/stargate/api-key-application`,
503
+ API_KEY_APPLICATION_PAYLOAD,
504
+ {
505
+ headers: getHeaders(session.xSign, accessToken),
506
+ timeout: 2e4
507
+ }
508
+ );
509
+ apiKey = extractApiKey(applyResponse.data);
510
+ }
511
+ if (!apiKey) {
512
+ throw new Error("API Key \u7533\u8BF7\u6210\u529F\uFF0C\u4F46\u54CD\u5E94\u4E2D\u7F3A\u5C11 key\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
513
+ }
514
+ setApiKey(apiKey);
515
+ clearApiKeyApplySession();
516
+ return {
517
+ status: "configured",
518
+ setupMode: "sms_confirm",
519
+ apiKey: maskSecret(apiKey),
520
+ configFile: getConfigFilePath()
521
+ };
522
+ } catch (error) {
523
+ if (isInvalidVerifyCodeError(error)) {
524
+ try {
525
+ await refreshSessionXSignOnInvalidVerifyCode(session);
526
+ } catch (refreshError) {
527
+ throw new Error(
528
+ `\u9A8C\u8BC1\u7801\u6821\u9A8C\u5931\u8D25\uFF0C\u5237\u65B0 x-sign \u4E5F\u5931\u8D25\u4E86: ${getErrorMessage(refreshError)}\u3002\u8BF7\u91CD\u65B0\u6267\u884C: yingmi-cli init setup --phone <\u624B\u673A\u53F7>`
529
+ );
530
+ }
531
+ }
532
+ throw new Error(getErrorMessage(error));
533
+ }
534
+ }
535
+
536
+ function countSetupModes(options) {
537
+ return [options.apiKey, options.phone, options.verifyCode].filter(Boolean).length;
538
+ }
539
+ function persistHiddenConfigOptions(options) {
540
+ const remoteSkillServerBaseUrl = options.remoteSkillServerBaseUrl?.trim();
541
+ const qiemanBaseUrl = options.qiemanBaseUrl?.trim();
542
+ if (!remoteSkillServerBaseUrl && !qiemanBaseUrl) {
543
+ return;
544
+ }
545
+ writeConfig({
546
+ ...readConfig(),
547
+ ...remoteSkillServerBaseUrl ? { remoteSkillServerBaseUrl } : {},
548
+ ...qiemanBaseUrl ? { qiemanBaseUrl } : {}
549
+ });
550
+ }
551
+ async function runInitSetup(options) {
552
+ const apiKey = options.apiKey?.trim();
553
+ const phone = options.phone?.trim();
554
+ const qiemanBaseUrl = options.qiemanBaseUrl?.trim();
555
+ const remoteSkillServerBaseUrl = options.remoteSkillServerBaseUrl?.trim();
556
+ const verifyCode = options.verifyCode?.trim();
557
+ persistHiddenConfigOptions({
558
+ remoteSkillServerBaseUrl,
559
+ qiemanBaseUrl
560
+ });
561
+ if (countSetupModes({ apiKey, phone, verifyCode }) > 1) {
562
+ throw new Error("`--api-key`\u3001`--phone`\u3001`--verify-code` \u4E00\u6B21\u53EA\u80FD\u4F7F\u7528\u4E00\u79CD\uFF0C\u8BF7\u6309\u521D\u59CB\u5316\u9636\u6BB5\u5206\u522B\u6267\u884C");
563
+ }
564
+ if (apiKey) {
565
+ return setupWithApiKey(apiKey);
566
+ }
567
+ if (phone) {
568
+ return prepareSmsSetup(phone);
569
+ }
570
+ if (verifyCode) {
571
+ return confirmSmsSetup(verifyCode);
572
+ }
573
+ return getPendingSetupResult();
574
+ }
575
+
576
+ function registerDoctorCommand(initCommand) {
577
+ initCommand.command("doctor").description("\u68C0\u67E5\u672C\u5730\u914D\u7F6E\u4E0E\u8FD0\u884C\u73AF\u5883").action(() => {
578
+ console.log(JSON.stringify(runInitDoctorChecks(), null, 2));
579
+ });
580
+ }
581
+
582
+ function registerSetupCommand(initCommand) {
583
+ initCommand.command("setup").description("\u6267\u884C\u9996\u6B21\u521D\u59CB\u5316\uFF0C\u652F\u6301\u76F4\u63A5\u5199\u5165 API Key \u6216\u9A8C\u8BC1\u7801\u7533\u8BF7 API Key").option("--api-key <value>", "\u76F4\u63A5\u5199\u5165 StarGate API Key").option("--phone <value>", "\u901A\u8FC7\u624B\u673A\u53F7\u53D1\u8D77\u9A8C\u8BC1\u7801\u521D\u59CB\u5316").option("--verify-code <value>", "\u786E\u8BA4\u77ED\u4FE1\u9A8C\u8BC1\u7801\u5E76\u5B8C\u6210 API Key \u521D\u59CB\u5316").addOption(
584
+ new commander.Option("--remote-skill-server-base-url <value>", "\u8986\u76D6 remote-skill \u670D\u52A1\u57FA\u5730\u5740").hideHelp()
585
+ ).addOption(new commander.Option("--qieman-base-url <value>", "\u8986\u76D6\u4E14\u6162\u670D\u52A1\u57FA\u5730\u5740").hideHelp()).action(async (options) => {
586
+ try {
587
+ const result = await runInitSetup(options);
588
+ console.log(JSON.stringify(result, null, 2));
589
+ } catch (error) {
590
+ fail(error.message);
591
+ }
592
+ });
593
+ }
594
+
595
+ function registerStatusCommand(initCommand) {
596
+ initCommand.command("status").description("\u67E5\u770B\u521D\u59CB\u5316\u72B6\u6001\u548C\u914D\u7F6E\u6458\u8981").action(() => {
597
+ console.log(JSON.stringify(getInitStatusSummary(), null, 2));
598
+ });
599
+ }
600
+
601
+ function registerInitCommand(program) {
602
+ const initCommand = program.command("init").description("\u521D\u59CB\u5316 CLI \u5E76\u67E5\u770B\u5F53\u524D\u72B6\u6001");
603
+ registerSetupCommand(initCommand);
604
+ registerStatusCommand(initCommand);
605
+ registerDoctorCommand(initCommand);
606
+ }
607
+
608
+ function buildRequestPath(pathTemplate, pathValues) {
609
+ return pathTemplate.replace(/\{([^}]+)\}/g, (_, key) => {
610
+ const value = pathValues[key];
611
+ if (value === void 0) {
612
+ throw new Error(`\u7F3A\u5C11 path \u53C2\u6570: ${key}`);
613
+ }
614
+ return encodeURIComponent(String(value));
615
+ });
616
+ }
617
+
618
+ function isPlainObject(value) {
619
+ return typeof value === "object" && value !== null && !Array.isArray(value);
620
+ }
621
+
622
+ function readJsonInput(input, inputFile) {
623
+ if (input && inputFile) {
624
+ fail("--input \u548C --input-file \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528");
625
+ }
626
+ if (!input && !inputFile) {
627
+ return {};
628
+ }
629
+ const raw = inputFile ? fs.readFileSync(inputFile, "utf8") : input;
630
+ try {
631
+ return JSON.parse(raw ?? "{}");
632
+ } catch (error) {
633
+ fail(`\u8F93\u5165\u4E0D\u662F\u5408\u6CD5 JSON: ${error.message}`);
634
+ }
635
+ }
636
+
637
+ function requireApiKey() {
638
+ const { apiKey } = readConfig();
639
+ if (!apiKey) {
640
+ fail(
641
+ "\u672A\u914D\u7F6E apiKey\uFF0C\u8BF7\u5148\u6267\u884C: yingmi-cli init setup --api-key <your-api-key>\uFF0C\u6216\u5148\u7528 yingmi-cli init setup --phone <\u624B\u673A\u53F7> \u5B8C\u6210\u9A8C\u8BC1\u7801\u521D\u59CB\u5316"
642
+ );
643
+ }
644
+ return apiKey;
645
+ }
646
+
647
+ function getParametersByLocation(operation, location) {
648
+ return operation.parameters.filter((parameter) => parameter.in === location);
649
+ }
650
+ function splitInput(operation, rawInput) {
651
+ if (!isPlainObject(rawInput)) {
652
+ if (operation.requestBody && operation.parameters.length === 0) {
653
+ return {
654
+ path: {},
655
+ query: {},
656
+ header: {},
657
+ body: rawInput
658
+ };
659
+ }
660
+ fail("\u8C03\u7528\u8F93\u5165\u5FC5\u987B\u662F JSON \u5BF9\u8C61");
661
+ }
662
+ const reservedKeys = /* @__PURE__ */ new Set(["path", "query", "header", "body"]);
663
+ const pathParameters = getParametersByLocation(operation, "path");
664
+ const queryParameters = getParametersByLocation(operation, "query");
665
+ const headerParameters = getParametersByLocation(operation, "header");
666
+ const pathValues = {
667
+ ...Object.fromEntries(pathParameters.map((parameter) => [parameter.name, rawInput[parameter.name]])),
668
+ ...isPlainObject(rawInput.path) ? rawInput.path : {}
669
+ };
670
+ const queryValues = {
671
+ ...Object.fromEntries(queryParameters.map((parameter) => [parameter.name, rawInput[parameter.name]])),
672
+ ...isPlainObject(rawInput.query) ? rawInput.query : {}
673
+ };
674
+ const headerValues = {
675
+ ...Object.fromEntries(
676
+ headerParameters.map((parameter) => [parameter.name, rawInput[parameter.name]])
677
+ ),
678
+ ...isPlainObject(rawInput.header) ? rawInput.header : {}
679
+ };
680
+ const explicitBody = Object.prototype.hasOwnProperty.call(rawInput, "body") ? rawInput.body : void 0;
681
+ const parameterNames = /* @__PURE__ */ new Set([
682
+ ...pathParameters.map((parameter) => parameter.name),
683
+ ...queryParameters.map((parameter) => parameter.name),
684
+ ...headerParameters.map((parameter) => parameter.name)
685
+ ]);
686
+ const remainingTopLevelEntries = Object.entries(rawInput).filter(
687
+ ([key]) => !reservedKeys.has(key) && !parameterNames.has(key)
688
+ );
689
+ let body = explicitBody;
690
+ if (body === void 0 && operation.requestBody) {
691
+ if (operation.parameters.length === 0) {
692
+ body = rawInput;
693
+ } else if (remainingTopLevelEntries.length > 0) {
694
+ body = Object.fromEntries(remainingTopLevelEntries);
695
+ }
696
+ }
697
+ return {
698
+ path: pathValues,
699
+ query: queryValues,
700
+ header: headerValues,
701
+ body
702
+ };
703
+ }
704
+ function getParametersByIn(operation, location) {
705
+ return getParametersByLocation(operation, location);
706
+ }
707
+
708
+ function inferSchemaType(schema) {
709
+ if (schema.type) {
710
+ return schema.type;
711
+ }
712
+ if (schema.properties || schema.required) {
713
+ return "object";
714
+ }
715
+ if (schema.items) {
716
+ return "array";
717
+ }
718
+ return void 0;
719
+ }
720
+ function validateSchema(schema, value, label) {
721
+ if (!schema || value === void 0) {
722
+ return;
723
+ }
724
+ if (value === null) {
725
+ if (schema.nullable) {
726
+ return;
727
+ }
728
+ throw new Error(`${label} \u4E0D\u80FD\u4E3A null`);
729
+ }
730
+ if (schema.oneOf?.length) {
731
+ const matches = schema.oneOf.some((candidate) => {
732
+ try {
733
+ validateSchema(candidate, value, label);
734
+ return true;
735
+ } catch {
736
+ return false;
737
+ }
738
+ });
739
+ if (!matches) {
740
+ throw new Error(`${label} \u4E0D\u7B26\u5408 oneOf \u7EA6\u675F`);
741
+ }
742
+ return;
743
+ }
744
+ if (schema.anyOf?.length) {
745
+ const matches = schema.anyOf.some((candidate) => {
746
+ try {
747
+ validateSchema(candidate, value, label);
748
+ return true;
749
+ } catch {
750
+ return false;
751
+ }
752
+ });
753
+ if (!matches) {
754
+ throw new Error(`${label} \u4E0D\u7B26\u5408 anyOf \u7EA6\u675F`);
755
+ }
756
+ return;
757
+ }
758
+ if (schema.enum && !schema.enum.some((item) => item === value)) {
759
+ throw new Error(`${label} \u4E0D\u5728\u5141\u8BB8\u7684\u679A\u4E3E\u503C\u4E2D: ${schema.enum.join(", ")}`);
760
+ }
761
+ const type = inferSchemaType(schema);
762
+ switch (type) {
763
+ case "object": {
764
+ if (!isPlainObject(value)) {
765
+ throw new Error(`${label} \u5FC5\u987B\u662F\u5BF9\u8C61`);
766
+ }
767
+ for (const requiredKey of schema.required ?? []) {
768
+ if (value[requiredKey] === void 0) {
769
+ throw new Error(`${label}.${requiredKey} \u4E3A\u5FC5\u586B\u9879`);
770
+ }
771
+ }
772
+ Object.entries(schema.properties ?? {}).forEach(([propertyName, propertySchema]) => {
773
+ validateSchema(propertySchema, value[propertyName], `${label}.${propertyName}`);
774
+ });
775
+ return;
776
+ }
777
+ case "array": {
778
+ if (!Array.isArray(value)) {
779
+ throw new Error(`${label} \u5FC5\u987B\u662F\u6570\u7EC4`);
780
+ }
781
+ value.forEach((item, index) => validateSchema(schema.items, item, `${label}[${index}]`));
782
+ return;
783
+ }
784
+ case "string": {
785
+ if (typeof value !== "string") {
786
+ throw new Error(`${label} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
787
+ }
788
+ return;
789
+ }
790
+ case "integer": {
791
+ if (typeof value !== "number" || !Number.isInteger(value)) {
792
+ throw new Error(`${label} \u5FC5\u987B\u662F\u6574\u6570`);
793
+ }
794
+ return;
795
+ }
796
+ case "number": {
797
+ if (typeof value !== "number" || Number.isNaN(value)) {
798
+ throw new Error(`${label} \u5FC5\u987B\u662F\u6570\u5B57`);
799
+ }
800
+ return;
801
+ }
802
+ case "boolean": {
803
+ if (typeof value !== "boolean") {
804
+ throw new Error(`${label} \u5FC5\u987B\u662F\u5E03\u5C14\u503C`);
805
+ }
806
+ return;
807
+ }
808
+ default:
809
+ return;
810
+ }
811
+ }
812
+ function validateParameters(parameters, values, locationLabel) {
813
+ parameters.forEach((parameter) => {
814
+ const value = values[parameter.name];
815
+ if (parameter.required && value === void 0) {
816
+ throw new Error(`${locationLabel} \u53C2\u6570 ${parameter.name} \u4E3A\u5FC5\u586B\u9879`);
817
+ }
818
+ validateSchema(parameter.schema, value, `${locationLabel}.${parameter.name}`);
819
+ });
820
+ }
821
+
822
+ const DOCS_URL = "https://stargate.yingmi.com/api/docs.json";
823
+ const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
824
+ async function fetchOpenApiDocument(apiKey) {
825
+ const response = await axios.get(DOCS_URL, {
826
+ params: {
827
+ apiKey
828
+ },
829
+ timeout: 15e3
830
+ });
831
+ return response.data;
832
+ }
833
+ function getServerBaseUrl(document) {
834
+ return document.servers?.[0]?.url ?? "https://stargate.yingmi.com/api";
835
+ }
836
+ function listOperations(document) {
837
+ const operations = [];
838
+ Object.entries(document.paths).forEach(([path, pathItem]) => {
839
+ const sharedParameters = pathItem.parameters ?? [];
840
+ HTTP_METHODS.forEach((method) => {
841
+ const operation = pathItem[method];
842
+ if (!operation?.operationId) {
843
+ return;
844
+ }
845
+ operations.push({
846
+ name: operation.operationId,
847
+ summary: operation.summary,
848
+ description: operation.description,
849
+ method: method.toUpperCase(),
850
+ path,
851
+ parameters: [...sharedParameters, ...operation.parameters ?? []],
852
+ requestBody: operation.requestBody,
853
+ responses: operation.responses ?? {}
854
+ });
855
+ });
856
+ });
857
+ return operations.sort((left, right) => left.name.localeCompare(right.name));
858
+ }
859
+ function getOperationByName(document, toolName) {
860
+ return listOperations(document).find((operation) => operation.name === toolName);
861
+ }
862
+ function getPrimaryRequestSchema(operation) {
863
+ const content = operation.requestBody?.content;
864
+ if (!content) {
865
+ return void 0;
866
+ }
867
+ return content["application/json"]?.schema ?? content["*/*"]?.schema ?? Object.values(content)[0]?.schema;
868
+ }
869
+ function getPrimaryRequestContentType(operation) {
870
+ const content = operation.requestBody?.content;
871
+ if (!content) {
872
+ return void 0;
873
+ }
874
+ if (content["application/json"]) {
875
+ return "application/json";
876
+ }
877
+ if (content["*/*"]) {
878
+ return "application/json";
879
+ }
880
+ return Object.keys(content)[0];
881
+ }
882
+
883
+ function registerCallCommand(mcpCommand) {
884
+ mcpCommand.command("call").description("\u8C03\u7528\u6307\u5B9A MCP \u5DE5\u5177").argument("<toolName>", "\u5DE5\u5177\u540D").option("--input <json>", "\u76F4\u63A5\u4F20\u5165 JSON \u5B57\u7B26\u4E32").option("--input-file <path>", "\u4ECE\u6587\u4EF6\u8BFB\u53D6 JSON \u8F93\u5165").action(async (toolName, options) => {
885
+ const apiKey = requireApiKey();
886
+ const document = await fetchOpenApiDocument(apiKey);
887
+ const operation = getOperationByName(document, toolName);
888
+ if (!operation) {
889
+ fail(`Tool not found: ${toolName}`);
890
+ }
891
+ const rawInput = readJsonInput(options.input, options.inputFile);
892
+ const sections = splitInput(operation, rawInput);
893
+ try {
894
+ validateParameters(getParametersByIn(operation, "path"), sections.path, "path");
895
+ validateParameters(getParametersByIn(operation, "query"), sections.query, "query");
896
+ validateParameters(getParametersByIn(operation, "header"), sections.header, "header");
897
+ if (operation.requestBody?.required && sections.body === void 0) {
898
+ throw new Error("body \u4E3A\u5FC5\u586B\u9879");
899
+ }
900
+ validateSchema(getPrimaryRequestSchema(operation), sections.body, "body");
901
+ } catch (error) {
902
+ fail(`\u8F93\u5165\u6821\u9A8C\u5931\u8D25: ${error.message}`);
903
+ }
904
+ const url = buildRequestPath(operation.path, sections.path);
905
+ const contentType = getPrimaryRequestContentType(operation);
906
+ try {
907
+ const response = await axios.request({
908
+ method: operation.method,
909
+ baseURL: getServerBaseUrl(document),
910
+ url,
911
+ params: sections.query,
912
+ data: sections.body,
913
+ headers: {
914
+ Authorization: `Bearer ${apiKey}`,
915
+ ...contentType ? { "Content-Type": contentType } : {},
916
+ ...Object.fromEntries(
917
+ Object.entries(sections.header).map(([key, value]) => [key, String(value)])
918
+ )
919
+ },
920
+ timeout: 2e4
921
+ });
922
+ console.log(JSON.stringify(response.data, null, 2));
923
+ } catch (error) {
924
+ if (error instanceof axios.AxiosError) {
925
+ const status = error.response?.status;
926
+ const data = error.response?.data;
927
+ const detail = data ? JSON.stringify(data, null, 2) : error.message;
928
+ fail(`\u8C03\u7528\u5931\u8D25${status ? ` (${status})` : ""}: ${detail}`);
929
+ }
930
+ fail(`\u8C03\u7528\u5931\u8D25: ${error.message}`);
931
+ }
932
+ });
933
+ }
934
+
935
+ function registerListCommand(mcpCommand) {
936
+ mcpCommand.command("list").description("\u5217\u51FA\u6240\u6709 MCP \u5DE5\u5177\u6458\u8981").action(async () => {
937
+ const apiKey = requireApiKey();
938
+ const document = await fetchOpenApiDocument(apiKey);
939
+ const operations = listOperations(document);
940
+ console.log(
941
+ JSON.stringify(
942
+ operations.map((operation) => ({
943
+ name: operation.name,
944
+ summary: operation.summary,
945
+ description: operation.description,
946
+ method: operation.method,
947
+ path: operation.path
948
+ })),
949
+ null,
950
+ 2
951
+ )
952
+ );
953
+ });
954
+ }
955
+
956
+ function registerSchemaCommand(mcpCommand) {
957
+ mcpCommand.command("schema").description("\u67E5\u770B\u67D0\u4E2A MCP \u5DE5\u5177\u7684\u5B8C\u6574 Schema").argument("<toolName>", "\u5DE5\u5177\u540D").action(async (toolName) => {
958
+ const apiKey = requireApiKey();
959
+ const document = await fetchOpenApiDocument(apiKey);
960
+ const operation = getOperationByName(document, toolName);
961
+ if (!operation) {
962
+ fail(`Tool not found: ${toolName}`);
963
+ }
964
+ console.log(
965
+ JSON.stringify(
966
+ {
967
+ name: operation.name,
968
+ summary: operation.summary,
969
+ description: operation.description,
970
+ method: operation.method,
971
+ path: operation.path,
972
+ parameters: operation.parameters,
973
+ requestBody: operation.requestBody,
974
+ responses: operation.responses
975
+ },
976
+ null,
977
+ 2
978
+ )
979
+ );
980
+ });
981
+ }
982
+
983
+ function registerMcpCommand(program) {
984
+ const mcpCommand = program.command("mcp").description("\u6D4F\u89C8 MCP \u6458\u8981\u3001\u67E5\u770B Schema \u5E76\u8C03\u7528\u5DE5\u5177");
985
+ registerListCommand(mcpCommand);
986
+ registerSchemaCommand(mcpCommand);
987
+ registerCallCommand(mcpCommand);
988
+ }
989
+
990
+ async function runScriptInDirectory(script, workingDirectory) {
991
+ const shellCommand = process.platform === "win32" ? "cmd.exe" : "sh";
992
+ const shellArgs = process.platform === "win32" ? ["/d", "/s", "/c", script] : ["-lc", script];
993
+ return new Promise((resolve, reject) => {
994
+ const child = child_process.spawn(shellCommand, shellArgs, {
995
+ cwd: workingDirectory,
996
+ env: process.env
997
+ });
998
+ let stdout = "";
999
+ let stderr = "";
1000
+ child.stdout?.on("data", (chunk) => {
1001
+ stdout += chunk.toString();
1002
+ });
1003
+ child.stderr?.on("data", (chunk) => {
1004
+ stderr += chunk.toString();
1005
+ });
1006
+ child.on("error", reject);
1007
+ child.on("close", (code) => {
1008
+ const exitCode = code ?? 1;
1009
+ resolve({
1010
+ exitCode,
1011
+ status: exitCode === 0 ? "success" : "failed",
1012
+ stdout,
1013
+ stderr
1014
+ });
1015
+ });
1016
+ });
1017
+ }
1018
+
1019
+ async function extractZipArchive(archivePath, targetDirectory) {
1020
+ fs.mkdirSync(targetDirectory, { recursive: true });
1021
+ await new Promise((resolve, reject) => {
1022
+ const child = child_process.spawn("unzip", ["-oq", archivePath, "-d", targetDirectory], {
1023
+ stdio: "inherit"
1024
+ });
1025
+ child.on("error", reject);
1026
+ child.on("exit", (code) => {
1027
+ if (code === 0) {
1028
+ resolve();
1029
+ return;
1030
+ }
1031
+ reject(new Error(`unzip \u89E3\u538B\u5931\u8D25\uFF0C\u9000\u51FA\u7801: ${code ?? "null"}`));
1032
+ });
1033
+ });
1034
+ }
1035
+
1036
+ const REQUEST_TIMEOUT = 15e3;
1037
+ const DOWNLOAD_TIMEOUT = 12e4;
1038
+ const REMOTE_SKILL_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
1039
+ function normalizeServerBaseUrl(serverBaseUrl) {
1040
+ return serverBaseUrl.replace(/\/+$/, "");
1041
+ }
1042
+ function buildRequestHeaders(apiKey) {
1043
+ return {
1044
+ apiKey
1045
+ };
1046
+ }
1047
+ function listDirectoryEntries(rootDirectory, baseDirectory = rootDirectory) {
1048
+ const entries = fs.readdirSync(rootDirectory, { withFileTypes: true }).sort((left, right) => {
1049
+ if (left.isDirectory() !== right.isDirectory()) {
1050
+ return left.isDirectory() ? -1 : 1;
1051
+ }
1052
+ return left.name.localeCompare(right.name);
1053
+ });
1054
+ return entries.flatMap((entry) => {
1055
+ const absolutePath = path.join(rootDirectory, entry.name);
1056
+ const relativePath = path.relative(baseDirectory, absolutePath);
1057
+ if (entry.isDirectory()) {
1058
+ return [
1059
+ { path: relativePath, type: "directory" },
1060
+ ...listDirectoryEntries(absolutePath, baseDirectory)
1061
+ ];
1062
+ }
1063
+ return [{ path: relativePath, type: "file" }];
1064
+ });
1065
+ }
1066
+ function resolveSkillRootDirectory(extractedDirectory) {
1067
+ const entries = fs.readdirSync(extractedDirectory, { withFileTypes: true });
1068
+ if (entries.length !== 1 || !entries[0]?.isDirectory()) {
1069
+ return extractedDirectory;
1070
+ }
1071
+ return path.join(extractedDirectory, entries[0].name);
1072
+ }
1073
+ function assertRemoteSkillDetail(data) {
1074
+ if (typeof data !== "object" || data === null || typeof data.name !== "string" || typeof data.description !== "string" || typeof data.packageUrl !== "string") {
1075
+ throw new Error("\u8FDC\u7AEF skill \u8BE6\u60C5\u683C\u5F0F\u975E\u6CD5\uFF0C\u8BF7\u68C0\u67E5\u670D\u52A1\u7AEF\u8FD4\u56DE");
1076
+ }
1077
+ }
1078
+ function validateRemoteSkillName(skillName) {
1079
+ const normalizedName = skillName.trim();
1080
+ if (!REMOTE_SKILL_NAME_PATTERN.test(normalizedName)) {
1081
+ throw new Error("skill \u540D\u79F0\u975E\u6CD5\uFF0C\u4EC5\u652F\u6301\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u70B9\u3001\u4E0B\u5212\u7EBF\u548C\u4E2D\u5212\u7EBF");
1082
+ }
1083
+ return normalizedName;
1084
+ }
1085
+ async function fetchRemoteSkillSummaries(apiKey) {
1086
+ const serverBaseUrl = normalizeServerBaseUrl(getRemoteSkillServerBaseUrl());
1087
+ const response = await axios.get(`${serverBaseUrl}/api/get-skills`, {
1088
+ headers: buildRequestHeaders(apiKey),
1089
+ timeout: REQUEST_TIMEOUT
1090
+ });
1091
+ return response.data;
1092
+ }
1093
+ async function prepareRemoteSkillContext(skillName, apiKey) {
1094
+ const normalizedSkillName = validateRemoteSkillName(skillName);
1095
+ const serverBaseUrl = normalizeServerBaseUrl(getRemoteSkillServerBaseUrl());
1096
+ const detailResponse = await axios.get(
1097
+ `${serverBaseUrl}/api/get-skill-by-name`,
1098
+ {
1099
+ headers: buildRequestHeaders(apiKey),
1100
+ params: {
1101
+ name: normalizedSkillName
1102
+ },
1103
+ timeout: REQUEST_TIMEOUT
1104
+ }
1105
+ );
1106
+ assertRemoteSkillDetail(detailResponse.data);
1107
+ const skillsDirectory = getSkillsDirPath();
1108
+ const archivePath = path.join(skillsDirectory, `${normalizedSkillName}.zip`);
1109
+ const extractionDirectory = path.join(skillsDirectory, normalizedSkillName);
1110
+ const url = detailResponse.data.packageUrl.replace("-internal.", ".");
1111
+ const packageResponse = await axios.get(url, {
1112
+ responseType: "arraybuffer",
1113
+ timeout: DOWNLOAD_TIMEOUT
1114
+ });
1115
+ fs.rmSync(extractionDirectory, { recursive: true, force: true });
1116
+ fs.mkdirSync(extractionDirectory, { recursive: true });
1117
+ fs.writeFileSync(archivePath, Buffer.from(packageResponse.data));
1118
+ try {
1119
+ await extractZipArchive(archivePath, extractionDirectory);
1120
+ } finally {
1121
+ fs.rmSync(archivePath, { force: true });
1122
+ }
1123
+ const workingDirectory = resolveSkillRootDirectory(extractionDirectory);
1124
+ return {
1125
+ skill: detailResponse.data,
1126
+ workingDirectory,
1127
+ files: listDirectoryEntries(workingDirectory)
1128
+ };
1129
+ }
1130
+
1131
+ function formatWorkingDirectoryTree(rootDir) {
1132
+ const lines = ["."];
1133
+ const walk = (currentDir, prefix) => {
1134
+ let entries;
1135
+ try {
1136
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
1137
+ } catch {
1138
+ return;
1139
+ }
1140
+ entries.sort((left, right) => {
1141
+ const leftRank = left.isDirectory() ? 0 : 1;
1142
+ const rightRank = right.isDirectory() ? 0 : 1;
1143
+ if (leftRank !== rightRank) {
1144
+ return leftRank - rightRank;
1145
+ }
1146
+ return left.name.localeCompare(right.name);
1147
+ });
1148
+ entries.forEach((entry, index) => {
1149
+ const isLast = index === entries.length - 1;
1150
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
1151
+ lines.push(`${prefix}${connector}${entry.name}`);
1152
+ if (entry.isDirectory()) {
1153
+ const nextPrefix = prefix + (isLast ? " " : "\u2502 ");
1154
+ walk(path.join(currentDir, entry.name), nextPrefix);
1155
+ }
1156
+ });
1157
+ };
1158
+ walk(rootDir, "");
1159
+ return lines.join("\n");
1160
+ }
1161
+ function readScriptInput(options) {
1162
+ if (!options.scriptFile) {
1163
+ return options.script;
1164
+ }
1165
+ try {
1166
+ return fs.readFileSync(options.scriptFile, "utf8");
1167
+ } catch {
1168
+ throw new Error("\u8BFB\u53D6\u811A\u672C\u6587\u4EF6\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u8DEF\u5F84\u662F\u5426\u5B58\u5728\u4E14\u5F53\u524D\u7528\u6237\u6709\u8BFB\u53D6\u6743\u9650");
1169
+ }
1170
+ }
1171
+ function registerRemoteSkillCommand(program) {
1172
+ const remoteSkillCommand = program.command("remote-skill").description("\u67E5\u770B skill \u6458\u8981\uFF0C\u5E76\u8FDB\u5165\u67D0\u4E2A skill \u4E0A\u4E0B\u6587");
1173
+ remoteSkillCommand.command("list").description("\u5C55\u793A\u5F53\u524D\u652F\u6301\u7684 skill \u6458\u8981").action(async () => {
1174
+ try {
1175
+ const apiKey = requireApiKey();
1176
+ const skills = await fetchRemoteSkillSummaries(apiKey);
1177
+ console.log(JSON.stringify(skills, null, 2));
1178
+ } catch (error) {
1179
+ fail(error.message);
1180
+ }
1181
+ });
1182
+ remoteSkillCommand.command("enter").description("\u8FDB\u5165\u67D0\u4E2A skill \u4E0A\u4E0B\u6587\uFF0C\u5E76\u5C55\u793A\u76EE\u5F55\u548C\u6587\u4EF6\u7ED3\u6784").argument("<skillName>", "skill \u540D\u79F0").action(async (skillName) => {
1183
+ try {
1184
+ const apiKey = requireApiKey();
1185
+ const context = await prepareRemoteSkillContext(skillName, apiKey);
1186
+ setCurrentRemoteSkillSession({
1187
+ skillName,
1188
+ workingDirectory: context.workingDirectory
1189
+ });
1190
+ console.log(formatWorkingDirectoryTree(context.workingDirectory));
1191
+ } catch (error) {
1192
+ fail(error.message);
1193
+ }
1194
+ });
1195
+ remoteSkillCommand.command("exec").description("\u5728\u6700\u8FD1\u4E00\u6B21\u8FDB\u5165\u7684 skill \u4E0A\u4E0B\u6587\u4E2D\u6267\u884C\u811A\u672C").option("--script <content>", "\u5728\u5F53\u524D skill \u4E0A\u4E0B\u6587\u4E2D\u6267\u884C\u811A\u672C").option("--script-file <path>", "\u4ECE\u6587\u4EF6\u8BFB\u53D6\u811A\u672C\u5185\u5BB9\u540E\u6267\u884C").action(async (options) => {
1196
+ try {
1197
+ if (options.script && options.scriptFile) {
1198
+ fail("--script \u548C --script-file \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528");
1199
+ }
1200
+ const script = readScriptInput(options);
1201
+ if (!script) {
1202
+ fail("\u8BF7\u901A\u8FC7 --script \u6216 --script-file \u63D0\u4F9B\u8981\u6267\u884C\u7684\u811A\u672C");
1203
+ }
1204
+ const currentSession = getCurrentRemoteSkillSession();
1205
+ if (!currentSession) {
1206
+ fail("\u8BF7\u5148\u6267\u884C remote-skill enter <skillName> \u8FDB\u5165\u67D0\u4E2A skill \u4E0A\u4E0B\u6587");
1207
+ }
1208
+ if (!fs.existsSync(currentSession.workingDirectory)) {
1209
+ fail(
1210
+ `\u5F53\u524D skill \u4E0A\u4E0B\u6587\u76EE\u5F55\u4E0D\u5B58\u5728\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C remote-skill enter ${currentSession.skillName}`
1211
+ );
1212
+ }
1213
+ const execution = await runScriptInDirectory(script, currentSession.workingDirectory);
1214
+ console.log(
1215
+ JSON.stringify(
1216
+ {
1217
+ skillName: currentSession.skillName,
1218
+ workingDirectory: currentSession.workingDirectory,
1219
+ execution
1220
+ },
1221
+ null,
1222
+ 2
1223
+ )
1224
+ );
1225
+ } catch (error) {
1226
+ fail(error.message);
1227
+ }
1228
+ });
1229
+ }
1230
+
1231
+ const REGISTRY_URL$1 = "https://registry.npmmirror.com";
1232
+ async function fetchLatestVersion() {
1233
+ const response = await axios.get(
1234
+ `${REGISTRY_URL$1}/${encodeURIComponent(name)}`,
1235
+ {
1236
+ timeout: 1e4
1237
+ }
1238
+ );
1239
+ const latestVersion = response.data?.["dist-tags"]?.latest?.trim();
1240
+ if (!latestVersion) {
1241
+ throw new Error("\u672A\u83B7\u53D6\u5230\u6700\u65B0\u7248\u672C\u53F7");
1242
+ }
1243
+ return latestVersion;
1244
+ }
1245
+
1246
+ async function installLatestVersion() {
1247
+ const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
1248
+ await new Promise((resolve, reject) => {
1249
+ const child = child_process.spawn(
1250
+ npmCommand,
1251
+ [
1252
+ "install",
1253
+ "-g",
1254
+ `${name}@latest`,
1255
+ "--registry=https://registry.npmmirror.com",
1256
+ "--prefer-online"
1257
+ ],
1258
+ {
1259
+ env: {
1260
+ ...process.env,
1261
+ PUPPETEER_SKIP_DOWNLOAD: "true"
1262
+ },
1263
+ stdio: "inherit"
1264
+ }
1265
+ );
1266
+ child.on("error", reject);
1267
+ child.on("exit", (code) => {
1268
+ if (code === 0) {
1269
+ resolve();
1270
+ return;
1271
+ }
1272
+ reject(new Error(`npm install \u9000\u51FA\u7801\u5F02\u5E38: ${code ?? "null"}`));
1273
+ });
1274
+ });
1275
+ }
1276
+
1277
+ function normalizeVersion(input) {
1278
+ return input.trim().replace(/^v/i, "").split(/[.-]/);
1279
+ }
1280
+ function parseVersionPart(part) {
1281
+ return /^\d+$/.test(part) ? Number(part) : part;
1282
+ }
1283
+
1284
+ function isNewerVersion(latestVersion, currentVersion) {
1285
+ const latestParts = normalizeVersion(latestVersion).map(parseVersionPart);
1286
+ const currentParts = normalizeVersion(currentVersion).map(parseVersionPart);
1287
+ const maxLength = Math.max(latestParts.length, currentParts.length);
1288
+ for (let index = 0; index < maxLength; index += 1) {
1289
+ const latestPart = latestParts[index] ?? 0;
1290
+ const currentPart = currentParts[index] ?? 0;
1291
+ if (latestPart === currentPart) {
1292
+ continue;
1293
+ }
1294
+ if (typeof latestPart === "number" && typeof currentPart === "number") {
1295
+ return latestPart > currentPart;
1296
+ }
1297
+ return String(latestPart) > String(currentPart);
1298
+ }
1299
+ return false;
1300
+ }
1301
+
1302
+ const REGISTRY_URL = "https://registry.npmmirror.com";
1303
+ async function checkAndUpdate(checkOnly = false) {
1304
+ console.log("\u6B63\u5728\u68C0\u67E5\u66F4\u65B0...");
1305
+ const latestVersion = await fetchLatestVersion();
1306
+ if (!isNewerVersion(latestVersion, version)) {
1307
+ console.log(`\u5F53\u524D\u5DF2\u662F\u6700\u65B0\u7248\u672C: ${version}`);
1308
+ return;
1309
+ }
1310
+ console.log(`\u53D1\u73B0\u65B0\u7248\u672C: ${version} -> ${latestVersion}`);
1311
+ if (checkOnly) {
1312
+ console.log(
1313
+ `\u53EF\u6267\u884C PUPPETEER_SKIP_DOWNLOAD=true npm install -g ${name}@latest --registry=${REGISTRY_URL} --prefer-online \u8FDB\u884C\u5347\u7EA7`
1314
+ );
1315
+ return;
1316
+ }
1317
+ await installLatestVersion();
1318
+ console.log(`\u5347\u7EA7\u5B8C\u6210\uFF0C\u5F53\u524D\u6700\u65B0\u7248\u672C: ${latestVersion}`);
1319
+ }
1320
+
1321
+ function registerUpgradeCommand(program) {
1322
+ program.command("upgrade").description("\u68C0\u67E5 CLI \u66F4\u65B0\uFF0C\u5E76\u5728\u6709\u65B0\u7248\u672C\u65F6\u6267\u884C\u5347\u7EA7").option("--check-only", "\u4EC5\u68C0\u67E5\u6700\u65B0\u7248\u672C\uFF0C\u4E0D\u6267\u884C\u5B89\u88C5").action(async (options) => {
1323
+ try {
1324
+ await checkAndUpdate(Boolean(options.checkOnly));
1325
+ } catch (error) {
1326
+ console.error(
1327
+ `\u68C0\u67E5\u66F4\u65B0\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}\u3002\u8BF7\u68C0\u67E5\u7F51\u7EDC\u6216\u7A0D\u540E\u91CD\u8BD5\u3002`
1328
+ );
1329
+ process.exit(1);
1330
+ }
1331
+ });
1332
+ }
1333
+
1334
+ function createProgram() {
1335
+ const program = new commander.Command();
1336
+ program.name("yingmi-cli").version(version);
1337
+ registerInitCommand(program);
1338
+ registerMcpCommand(program);
1339
+ registerRemoteSkillCommand(program);
1340
+ registerHelpCommand(program);
1341
+ registerUpgradeCommand(program);
1342
+ program.showHelpAfterError("(\u4F7F\u7528 --help \u67E5\u770B\u547D\u4EE4\u5E2E\u52A9)");
1343
+ return program;
1344
+ }
1345
+
1346
+ createProgram().parseAsync(process.argv).catch((error) => {
1347
+ console.error(error instanceof Error ? error.message : String(error));
1348
+ process.exit(1);
1349
+ });