feiguazhitou-mcp-cli 0.1.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 +260 -0
- package/USER-GUIDE.md +139 -0
- package/dist/agent-guide.js +278 -0
- package/dist/agent-guide.js.map +1 -0
- package/dist/cli-error.js +30 -0
- package/dist/cli-error.js.map +1 -0
- package/dist/cli.js +952 -0
- package/dist/cli.js.map +1 -0
- package/dist/constants.js +42 -0
- package/dist/constants.js.map +1 -0
- package/dist/fs-utils.js +40 -0
- package/dist/fs-utils.js.map +1 -0
- package/dist/i18n.js +9 -0
- package/dist/i18n.js.map +1 -0
- package/dist/lease-lock.js +57 -0
- package/dist/lease-lock.js.map +1 -0
- package/dist/mcporter.js +249 -0
- package/dist/mcporter.js.map +1 -0
- package/dist/profile.js +262 -0
- package/dist/profile.js.map +1 -0
- package/dist/refresh.js +180 -0
- package/dist/refresh.js.map +1 -0
- package/dist/schema.js +39 -0
- package/dist/schema.js.map +1 -0
- package/dist/skills.js +139 -0
- package/dist/skills.js.map +1 -0
- package/dist/tool-catalog.js +126 -0
- package/dist/tool-catalog.js.map +1 -0
- package/package.json +42 -0
- package/skills/feiguazhitou-mcp-cli/SKILL.en.md +99 -0
- package/skills/feiguazhitou-mcp-cli/SKILL.md +130 -0
- package/skills/feiguazhitou-mcp-cli/skill.en.json +39 -0
- package/skills/feiguazhitou-mcp-cli/skill.json +39 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,952 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { describeDocument, describeMarkdown } from "./agent-guide.js";
|
|
6
|
+
import { CliError, errorDocument } from "./cli-error.js";
|
|
7
|
+
import { APP_NAME, COMMAND_NAME, DEFAULT_TIMEOUT_MS, ENV, GENERATED_FAILURE_EXIT_POLICY_VERSION, INTERNAL_STORAGE_PROFILE, LAUNCHER_VERSION, MAX_TIMEOUT_MS, } from "./constants.js";
|
|
8
|
+
import { outputLanguage, t } from "./i18n.js";
|
|
9
|
+
import { discoverWithMcporter, mcporterDetails } from "./mcporter.js";
|
|
10
|
+
import { createHeaderBindings, profilePaths, removeProfileCredentialHeader, removeProfileHeaderEnvironment, resolveProfile, setProfileCredentialHeader, setProfileEndpoint, setProfileHeaderEnvironment, setProfileTimeout, setProfileTokenEnvironment, } from "./profile.js";
|
|
11
|
+
import { currentArtifact, refreshProfile } from "./refresh.js";
|
|
12
|
+
import { listBundledSkills, showBundledSkill, skillProtocolDocument } from "./skills.js";
|
|
13
|
+
import { TOOL_CATALOG_PROTOCOL, TOOL_CATALOG_PROTOCOL_VERSION, catalogFreshness, findTool, toolCatalog, toolCatalogText, toolInvocationTemplate, toolMarkdown, } from "./tool-catalog.js";
|
|
14
|
+
const launcherCommands = new Set(["init", "refresh", "doctor", "config", "skills", "describe", "tools", "run", "help"]);
|
|
15
|
+
function usage() {
|
|
16
|
+
if (outputLanguage() === "zh") {
|
|
17
|
+
return `${APP_NAME} ${LAUNCHER_VERSION}
|
|
18
|
+
|
|
19
|
+
用法:
|
|
20
|
+
${COMMAND_NAME} help
|
|
21
|
+
${COMMAND_NAME} init [--endpoint URL]
|
|
22
|
+
${COMMAND_NAME} refresh [--force] [--output json|text]
|
|
23
|
+
${COMMAND_NAME} doctor [--output json|text]
|
|
24
|
+
${COMMAND_NAME} config
|
|
25
|
+
${COMMAND_NAME} config set endpoint URL
|
|
26
|
+
${COMMAND_NAME} config set header HEADER --value-stdin
|
|
27
|
+
${COMMAND_NAME} config unset header HEADER
|
|
28
|
+
${COMMAND_NAME} config set header-env HEADER ENV_VAR
|
|
29
|
+
${COMMAND_NAME} config unset header-env HEADER
|
|
30
|
+
${COMMAND_NAME} config set token-env ENV_VAR
|
|
31
|
+
${COMMAND_NAME} config set timeout MILLISECONDS
|
|
32
|
+
${COMMAND_NAME} skills protocol
|
|
33
|
+
${COMMAND_NAME} skills list [--output json]
|
|
34
|
+
${COMMAND_NAME} skills show NAME [--output json|markdown]
|
|
35
|
+
${COMMAND_NAME} describe [--output json|markdown]
|
|
36
|
+
${COMMAND_NAME} tools list [--refresh] [--force] [--output json|text]
|
|
37
|
+
${COMMAND_NAME} tools show NAME [--refresh] [--force] [--output json|markdown]
|
|
38
|
+
${COMMAND_NAME} run COMMAND [...generated-command-args]
|
|
39
|
+
|
|
40
|
+
运行时配置:
|
|
41
|
+
${ENV.endpoint} Streamable HTTP endpoint 覆盖值
|
|
42
|
+
${ENV.apiKey} api_key Header 的当前进程值
|
|
43
|
+
${ENV.token} Bearer Token 的当前进程值
|
|
44
|
+
${ENV.headers} 自定义请求 Header 的当前进程 JSON 对象
|
|
45
|
+
${ENV.autoApprove} 标记为自动批准的 MCP 工具或生成 command 的 JSON 数组
|
|
46
|
+
${ENV.timeout} MCP 超时毫秒数(默认:${DEFAULT_TIMEOUT_MS};范围:1-${MAX_TIMEOUT_MS})
|
|
47
|
+
${ENV.dataDirectory} 覆盖跨平台用户数据目录
|
|
48
|
+
${ENV.language} 设为 en 时输出英文(默认:中文)
|
|
49
|
+
|
|
50
|
+
调用约束:
|
|
51
|
+
Agent、脚本和其他调用方不得直接连接本机 MCP endpoint;所有工具调用必须使用
|
|
52
|
+
${COMMAND_NAME} run COMMAND ...。CLI 仅在内部连接 MCP。
|
|
53
|
+
飞瓜本机 MCP 按单会话使用:不要并发执行 run、refresh 或 doctor;必须等上一条
|
|
54
|
+
会连接 MCP 的命令完全结束后,再执行下一条。CLI 不会自动排队。
|
|
55
|
+
|
|
56
|
+
Agent 发现:
|
|
57
|
+
1. ${COMMAND_NAME} describe --output json
|
|
58
|
+
2. ${COMMAND_NAME} tools list --output json
|
|
59
|
+
3. ${COMMAND_NAME} refresh --output json(仅在目录缺失或需要最新 schema 时)
|
|
60
|
+
4. ${COMMAND_NAME} tools show NAME --output json
|
|
61
|
+
5. ${COMMAND_NAME} skills show feiguazhitou-mcp-cli
|
|
62
|
+
|
|
63
|
+
规范的 tools list/show/run 命令只读取本地生成状态,除非显式传入 --refresh
|
|
64
|
+
或执行 refresh。动态生成的 command 名位于目录中,不是启动器顶层命令。
|
|
65
|
+
|
|
66
|
+
默认超时为 ${DEFAULT_TIMEOUT_MS}ms;需要其他等待上限时,可使用 config set timeout MILLISECONDS,新值会在下一次 run 中生效。
|
|
67
|
+
|
|
68
|
+
使用 "${COMMAND_NAME} run COMMAND --help" 查看生成 command 的帮助。
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
71
|
+
return `${APP_NAME} ${LAUNCHER_VERSION}
|
|
72
|
+
|
|
73
|
+
Usage:
|
|
74
|
+
${COMMAND_NAME} help
|
|
75
|
+
${COMMAND_NAME} init [--endpoint URL]
|
|
76
|
+
${COMMAND_NAME} refresh [--force] [--output json|text]
|
|
77
|
+
${COMMAND_NAME} doctor [--output json|text]
|
|
78
|
+
${COMMAND_NAME} config
|
|
79
|
+
${COMMAND_NAME} config set endpoint URL
|
|
80
|
+
${COMMAND_NAME} config set header HEADER --value-stdin
|
|
81
|
+
${COMMAND_NAME} config unset header HEADER
|
|
82
|
+
${COMMAND_NAME} config set header-env HEADER ENV_VAR
|
|
83
|
+
${COMMAND_NAME} config unset header-env HEADER
|
|
84
|
+
${COMMAND_NAME} config set token-env ENV_VAR
|
|
85
|
+
${COMMAND_NAME} config set timeout MILLISECONDS
|
|
86
|
+
${COMMAND_NAME} skills protocol
|
|
87
|
+
${COMMAND_NAME} skills list [--output json]
|
|
88
|
+
${COMMAND_NAME} skills show NAME [--output json|markdown]
|
|
89
|
+
${COMMAND_NAME} describe [--output json|markdown]
|
|
90
|
+
${COMMAND_NAME} tools list [--refresh] [--force] [--output json|text]
|
|
91
|
+
${COMMAND_NAME} tools show NAME [--refresh] [--force] [--output json|markdown]
|
|
92
|
+
${COMMAND_NAME} run COMMAND [...generated-command-args]
|
|
93
|
+
|
|
94
|
+
Runtime configuration:
|
|
95
|
+
${ENV.endpoint} Streamable HTTP endpoint override
|
|
96
|
+
${ENV.apiKey} api_key header value for the current process
|
|
97
|
+
${ENV.token} Bearer token value for the current process
|
|
98
|
+
${ENV.headers} JSON object of custom request headers for the current process
|
|
99
|
+
${ENV.autoApprove} JSON array of MCP tool or generated command names marked auto-approved
|
|
100
|
+
${ENV.timeout} MCP timeout in milliseconds (default: ${DEFAULT_TIMEOUT_MS}; range: 1-${MAX_TIMEOUT_MS})
|
|
101
|
+
${ENV.dataDirectory} Override the cross-platform user data directory
|
|
102
|
+
${ENV.language} Set to en for English output (default: Chinese)
|
|
103
|
+
|
|
104
|
+
Transport boundary:
|
|
105
|
+
Agents, scripts, and other callers must not connect to the local MCP endpoint directly;
|
|
106
|
+
every tool call must use ${COMMAND_NAME} run COMMAND .... The CLI connects to MCP internally.
|
|
107
|
+
Use the local Feiguazhitou MCP as a single session: do not run run, refresh, or doctor
|
|
108
|
+
concurrently. Wait for each MCP-connecting command to finish before starting the next one.
|
|
109
|
+
The CLI does not queue concurrent calls automatically.
|
|
110
|
+
|
|
111
|
+
Agent discovery:
|
|
112
|
+
1. ${COMMAND_NAME} describe --output json
|
|
113
|
+
2. ${COMMAND_NAME} tools list --output json
|
|
114
|
+
3. ${COMMAND_NAME} refresh --output json (only when the catalog is missing or freshness is required)
|
|
115
|
+
4. ${COMMAND_NAME} tools show NAME --output json
|
|
116
|
+
5. ${COMMAND_NAME} skills show feiguazhitou-mcp-cli
|
|
117
|
+
|
|
118
|
+
Canonical tools list/show/run commands only read local generated state unless --refresh
|
|
119
|
+
or refresh is requested explicitly. Dynamic generated command names are catalog entries,
|
|
120
|
+
not launcher commands.
|
|
121
|
+
|
|
122
|
+
The default timeout is ${DEFAULT_TIMEOUT_MS}ms. To use a different limit, run config set timeout MILLISECONDS; it takes effect on the next run.
|
|
123
|
+
|
|
124
|
+
Use "${COMMAND_NAME} run COMMAND --help" for generated-command help.
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
function valueAfter(args, index, flag) {
|
|
128
|
+
const value = args[index + 1];
|
|
129
|
+
if (!value || value.startsWith("--")) {
|
|
130
|
+
throw new Error(t(`${flag} 需要一个值。`, `${flag} requires a value.`));
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
function parseGlobalOptions(args) {
|
|
135
|
+
const rest = [];
|
|
136
|
+
let dataDirectory;
|
|
137
|
+
let endpoint;
|
|
138
|
+
let force = false;
|
|
139
|
+
let command;
|
|
140
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
141
|
+
const token = args[index];
|
|
142
|
+
const parseLauncherOption = command === undefined || (launcherCommands.has(command) && !(command === "run" && rest.length >= 2));
|
|
143
|
+
if (parseLauncherOption && (token === "--profile" || token.startsWith("--profile="))) {
|
|
144
|
+
throw new CliError("INVALID_ARGUMENT", t("--profile 不受支持;feiguazhitou-mcp-cli 只管理一个本地飞瓜智投 MCP 连接。", "--profile is not supported; feiguazhitou-mcp-cli manages one local MCP connection."));
|
|
145
|
+
}
|
|
146
|
+
if (parseLauncherOption && token === "--data-dir") {
|
|
147
|
+
dataDirectory = valueAfter(args, index, token);
|
|
148
|
+
index += 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (parseLauncherOption && token.startsWith("--data-dir=")) {
|
|
152
|
+
dataDirectory = token.slice("--data-dir=".length);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (parseLauncherOption && token === "--endpoint") {
|
|
156
|
+
endpoint = valueAfter(args, index, token);
|
|
157
|
+
index += 1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (parseLauncherOption && token.startsWith("--endpoint=")) {
|
|
161
|
+
endpoint = token.slice("--endpoint=".length);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (parseLauncherOption && token === "--force") {
|
|
165
|
+
force = true;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
rest.push(token);
|
|
169
|
+
if (command === undefined && !token.startsWith("-")) {
|
|
170
|
+
command = token;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { dataDirectory, endpoint, force, rest };
|
|
174
|
+
}
|
|
175
|
+
function printJson(value) {
|
|
176
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
177
|
+
}
|
|
178
|
+
function rawInputUsage() {
|
|
179
|
+
return t(`复杂 JSON 输入:\n ${COMMAND_NAME} run COMMAND --raw @INPUT.json --output json\n cat INPUT.json | ${COMMAND_NAME} run COMMAND --raw - --output json\n`, `Complex JSON input:\n ${COMMAND_NAME} run COMMAND --raw @INPUT.json --output json\n cat INPUT.json | ${COMMAND_NAME} run COMMAND --raw - --output json\n`);
|
|
180
|
+
}
|
|
181
|
+
function rawInputError(message) {
|
|
182
|
+
return new CliError("INVALID_ARGUMENT", message, rawInputUsage().trim());
|
|
183
|
+
}
|
|
184
|
+
async function readRawStandardInput() {
|
|
185
|
+
const chunks = [];
|
|
186
|
+
for await (const chunk of process.stdin) {
|
|
187
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
188
|
+
}
|
|
189
|
+
const value = Buffer.concat(chunks).toString("utf8").replace(/^\uFEFF/, "");
|
|
190
|
+
if (!value.trim()) {
|
|
191
|
+
throw rawInputError(t("--raw - 需要从标准输入提供非空 JSON。", "--raw - requires non-empty JSON on standard input."));
|
|
192
|
+
}
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
async function readRawFile(filePath) {
|
|
196
|
+
if (!filePath) {
|
|
197
|
+
throw rawInputError(t("--raw @FILE 需要一个文件路径。", "--raw @FILE requires a file path."));
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const value = (await readFile(filePath, "utf8")).replace(/^\uFEFF/, "");
|
|
201
|
+
if (!value.trim()) {
|
|
202
|
+
throw rawInputError(t(`--raw 文件 '${filePath}' 不包含 JSON。`, `The --raw file '${filePath}' does not contain JSON.`));
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (error instanceof CliError) {
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
211
|
+
throw rawInputError(t(`无法读取 --raw 文件 '${filePath}':${detail}`, `Unable to read --raw file '${filePath}': ${detail}`));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function resolveRawInput(value) {
|
|
215
|
+
if (value === "-") {
|
|
216
|
+
return await readRawStandardInput();
|
|
217
|
+
}
|
|
218
|
+
if (value.startsWith("@")) {
|
|
219
|
+
return await readRawFile(value.slice(1));
|
|
220
|
+
}
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
async function normalizeGeneratedRawInput(args) {
|
|
224
|
+
const normalized = [];
|
|
225
|
+
let hasRawInput = false;
|
|
226
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
227
|
+
const token = args[index];
|
|
228
|
+
let rawValue;
|
|
229
|
+
if (token === "--raw") {
|
|
230
|
+
rawValue = valueAfter(args, index, token);
|
|
231
|
+
index += 1;
|
|
232
|
+
}
|
|
233
|
+
else if (token.startsWith("--raw=")) {
|
|
234
|
+
rawValue = token.slice("--raw=".length);
|
|
235
|
+
}
|
|
236
|
+
if (rawValue === undefined) {
|
|
237
|
+
normalized.push(token);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (hasRawInput) {
|
|
241
|
+
throw rawInputError(t("一次工具调用只能提供一个 --raw 输入。", "A tool invocation can provide only one --raw input."));
|
|
242
|
+
}
|
|
243
|
+
hasRawInput = true;
|
|
244
|
+
normalized.push("--raw", await resolveRawInput(rawValue));
|
|
245
|
+
}
|
|
246
|
+
return normalized;
|
|
247
|
+
}
|
|
248
|
+
async function runGeneratedCli(bundle, args, environment) {
|
|
249
|
+
return await new Promise((resolve, reject) => {
|
|
250
|
+
const child = spawn(process.execPath, [bundle, ...args], {
|
|
251
|
+
env: { ...process.env, ...environment, FORCE_COLOR: "0", NO_COLOR: "1" },
|
|
252
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
253
|
+
});
|
|
254
|
+
const stdout = [];
|
|
255
|
+
const stderr = [];
|
|
256
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
257
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
258
|
+
child.once("error", reject);
|
|
259
|
+
child.once("close", (code) => {
|
|
260
|
+
resolve({
|
|
261
|
+
code: code ?? 1,
|
|
262
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
263
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
function generatedCliFailureMessage(result) {
|
|
269
|
+
const message = result.stderr.trim();
|
|
270
|
+
if (message) {
|
|
271
|
+
return message;
|
|
272
|
+
}
|
|
273
|
+
return t(`生成的工具命令以退出码 ${result.code} 结束,且未输出错误信息。`, `Generated tool command exited with code ${result.code} without an error message.`);
|
|
274
|
+
}
|
|
275
|
+
function writeGeneratedCliResult(result, args) {
|
|
276
|
+
const hasOutput = result.stdout.trim().length > 0;
|
|
277
|
+
const jsonOutput = requestedJsonOutput(args);
|
|
278
|
+
if (hasOutput) {
|
|
279
|
+
process.stdout.write(result.stdout);
|
|
280
|
+
}
|
|
281
|
+
// A nonzero tool response can still be valid MCP JSON (for example ok:false).
|
|
282
|
+
// Preserve that output exactly; only synthesize an error document when nothing was returned.
|
|
283
|
+
if (result.code !== 0 && !hasOutput && jsonOutput) {
|
|
284
|
+
printJson(errorDocument(new CliError("COMMAND_FAILED", generatedCliFailureMessage(result), undefined, {
|
|
285
|
+
source: "generated-cli",
|
|
286
|
+
exitCode: result.code,
|
|
287
|
+
stderr: result.stderr,
|
|
288
|
+
})));
|
|
289
|
+
return result.code;
|
|
290
|
+
}
|
|
291
|
+
if (result.stderr) {
|
|
292
|
+
process.stderr.write(result.stderr);
|
|
293
|
+
}
|
|
294
|
+
return result.code;
|
|
295
|
+
}
|
|
296
|
+
function failureExitPolicyRemediation() {
|
|
297
|
+
return t(`执行 ${COMMAND_NAME} refresh --output json,生成包含当前失败退出码策略的本地 CLI。`, `Run ${COMMAND_NAME} refresh --output json to generate a local CLI with the current failure-exit policy.`);
|
|
298
|
+
}
|
|
299
|
+
function artifactCompatibility(manifest, currentTimeoutMs) {
|
|
300
|
+
if (!manifest) {
|
|
301
|
+
return {
|
|
302
|
+
status: "missing",
|
|
303
|
+
currentTimeoutMs,
|
|
304
|
+
artifactTimeoutMs: null,
|
|
305
|
+
remediation: t(`执行 ${COMMAND_NAME} refresh --output json 生成本地 CLI。`, `Run ${COMMAND_NAME} refresh --output json to generate a local CLI.`),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const failureExitPolicyMatches = manifest.failureExitPolicyVersion === GENERATED_FAILURE_EXIT_POLICY_VERSION;
|
|
309
|
+
if (!failureExitPolicyMatches) {
|
|
310
|
+
return {
|
|
311
|
+
status: "refresh-required",
|
|
312
|
+
currentTimeoutMs,
|
|
313
|
+
artifactTimeoutMs: manifest.timeoutMs,
|
|
314
|
+
failureExitPolicyVersion: manifest.failureExitPolicyVersion ?? null,
|
|
315
|
+
requiredFailureExitPolicyVersion: GENERATED_FAILURE_EXIT_POLICY_VERSION,
|
|
316
|
+
remediation: failureExitPolicyRemediation(),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
status: "ready",
|
|
321
|
+
currentTimeoutMs,
|
|
322
|
+
artifactTimeoutMs: manifest.timeoutMs,
|
|
323
|
+
runtimeTimeoutMs: currentTimeoutMs,
|
|
324
|
+
timeoutOverrideActive: manifest.timeoutMs !== currentTimeoutMs,
|
|
325
|
+
failureExitPolicyVersion: manifest.failureExitPolicyVersion,
|
|
326
|
+
requiredFailureExitPolicyVersion: GENERATED_FAILURE_EXIT_POLICY_VERSION,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
async function resolveCurrentProfile(options) {
|
|
330
|
+
return await resolveProfile(INTERNAL_STORAGE_PROFILE, {
|
|
331
|
+
dataDirectory: options.dataDirectory,
|
|
332
|
+
endpoint: options.endpoint,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
async function commandInit(options) {
|
|
336
|
+
const profile = await resolveCurrentProfile(options);
|
|
337
|
+
if (options.endpoint) {
|
|
338
|
+
await setProfileEndpoint(profile.paths, options.endpoint);
|
|
339
|
+
}
|
|
340
|
+
printJson({ endpoint: options.endpoint ?? profile.endpoint, dataDirectory: profile.paths.baseDirectory });
|
|
341
|
+
}
|
|
342
|
+
function configUsage() {
|
|
343
|
+
return t(`用法:
|
|
344
|
+
${COMMAND_NAME} config
|
|
345
|
+
${COMMAND_NAME} config set endpoint URL
|
|
346
|
+
${COMMAND_NAME} config set header HEADER --value-stdin
|
|
347
|
+
${COMMAND_NAME} config unset header HEADER
|
|
348
|
+
${COMMAND_NAME} config set header-env HEADER ENV_VAR
|
|
349
|
+
${COMMAND_NAME} config unset header-env HEADER
|
|
350
|
+
${COMMAND_NAME} config set token-env ENV_VAR
|
|
351
|
+
${COMMAND_NAME} config set timeout MILLISECONDS
|
|
352
|
+
`, `Usage:
|
|
353
|
+
${COMMAND_NAME} config
|
|
354
|
+
${COMMAND_NAME} config set endpoint URL
|
|
355
|
+
${COMMAND_NAME} config set header HEADER --value-stdin
|
|
356
|
+
${COMMAND_NAME} config unset header HEADER
|
|
357
|
+
${COMMAND_NAME} config set header-env HEADER ENV_VAR
|
|
358
|
+
${COMMAND_NAME} config unset header-env HEADER
|
|
359
|
+
${COMMAND_NAME} config set token-env ENV_VAR
|
|
360
|
+
${COMMAND_NAME} config set timeout MILLISECONDS
|
|
361
|
+
`);
|
|
362
|
+
}
|
|
363
|
+
function configInputError(message) {
|
|
364
|
+
return new CliError("INVALID_ARGUMENT", message, configUsage().trim());
|
|
365
|
+
}
|
|
366
|
+
async function readConfigValueFromStandardInput() {
|
|
367
|
+
const chunks = [];
|
|
368
|
+
for await (const chunk of process.stdin) {
|
|
369
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
370
|
+
}
|
|
371
|
+
const value = Buffer.concat(chunks).toString("utf8");
|
|
372
|
+
if (value.length === 0) {
|
|
373
|
+
throw configInputError(t("--value-stdin 需要从标准输入提供 Header 值。", "--value-stdin requires a Header value from standard input."));
|
|
374
|
+
}
|
|
375
|
+
return value.replace(/\r?\n$/, "");
|
|
376
|
+
}
|
|
377
|
+
async function commandConfig(options) {
|
|
378
|
+
const args = options.rest.slice(1);
|
|
379
|
+
if (args.length === 0) {
|
|
380
|
+
const profile = await resolveCurrentProfile(options);
|
|
381
|
+
printJson({
|
|
382
|
+
endpoint: profile.endpoint,
|
|
383
|
+
timeoutMs: profile.timeoutMs,
|
|
384
|
+
configuredTimeoutMs: profile.config.timeoutMs ?? null,
|
|
385
|
+
configuredHeaders: Object.fromEntries(Object.entries(profile.config.headers ?? {}).map(([name, reference]) => [name, { env: reference.env, prefix: reference.prefix ?? "" }])),
|
|
386
|
+
storedCredentialHeaderNames: profile.storedCredentialHeaderNames,
|
|
387
|
+
autoApprove: profile.autoApprove,
|
|
388
|
+
tokenEnv: profile.config.tokenEnv ?? ENV.token,
|
|
389
|
+
dataDirectory: profile.paths.baseDirectory,
|
|
390
|
+
});
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (args.length === 1 && args[0] === "--help") {
|
|
394
|
+
process.stdout.write(configUsage());
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const [verb, key, ...values] = args;
|
|
398
|
+
const paths = profilePaths(INTERNAL_STORAGE_PROFILE, options.dataDirectory);
|
|
399
|
+
if (verb === "set" && key === "endpoint" && values.length === 1) {
|
|
400
|
+
await setProfileEndpoint(paths, values[0]);
|
|
401
|
+
process.stderr.write(`${t("已保存 endpoint。", "Saved endpoint.")}\n`);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (verb === "set" && key === "header") {
|
|
405
|
+
const [header, source, ...extra] = values;
|
|
406
|
+
if (!header || source !== "--value-stdin" || extra.length > 0) {
|
|
407
|
+
throw configInputError(t(`用法:${COMMAND_NAME} config set header HEADER --value-stdin`, `Usage: ${COMMAND_NAME} config set header HEADER --value-stdin`));
|
|
408
|
+
}
|
|
409
|
+
const value = await readConfigValueFromStandardInput();
|
|
410
|
+
await setProfileCredentialHeader(paths, header, value);
|
|
411
|
+
const removedEnvironmentReference = await removeProfileHeaderEnvironment(paths, header);
|
|
412
|
+
process.stderr.write(`${t(`已将 ${header} 保存到本机未加密凭据文件。`, `Saved ${header} to the local unencrypted credential file.`)}\n`);
|
|
413
|
+
if (removedEnvironmentReference) {
|
|
414
|
+
process.stderr.write(`${t(`已移除 ${header} 原有的运行时 Header 引用。`, `Removed the existing runtime Header reference for ${header}.`)}\n`);
|
|
415
|
+
}
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (verb === "unset" && key === "header" && values.length === 1) {
|
|
419
|
+
const removed = await removeProfileCredentialHeader(paths, values[0]);
|
|
420
|
+
process.stderr.write(`${removed
|
|
421
|
+
? t(`已清除 ${values[0]} 的本机保存 Header 值。`, `Cleared the locally saved Header value for ${values[0]}.`)
|
|
422
|
+
: t(`${values[0]} 没有本机保存的 Header 值。`, `${values[0]} has no locally saved Header value.`)}\n`);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (verb === "set" && key === "header-env" && values.length === 2) {
|
|
426
|
+
const [header, env] = values;
|
|
427
|
+
await setProfileHeaderEnvironment(paths, header, env);
|
|
428
|
+
process.stderr.write(`${t(`已保存 ${header} 的运行时 Header 引用。`, `Saved runtime Header reference for ${header}.`)}\n`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (verb === "unset" && key === "header-env" && values.length === 1) {
|
|
432
|
+
const removed = await removeProfileHeaderEnvironment(paths, values[0]);
|
|
433
|
+
process.stderr.write(`${removed
|
|
434
|
+
? t(`已移除 ${values[0]} 的运行时 Header 引用。`, `Removed the runtime Header reference for ${values[0]}.`)
|
|
435
|
+
: t(`${values[0]} 没有运行时 Header 引用。`, `${values[0]} has no runtime Header reference.`)}\n`);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (verb === "set" && key === "token-env" && values.length === 1) {
|
|
439
|
+
await setProfileTokenEnvironment(paths, values[0]);
|
|
440
|
+
process.stderr.write(`${t("已保存 Token 环境变量引用。", "Saved token environment variable reference.")}\n`);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (verb === "set" && key === "timeout" && values.length === 1) {
|
|
444
|
+
const config = await setProfileTimeout(paths, values[0]);
|
|
445
|
+
process.stderr.write(`${t(`已保存超时值 ${config.timeoutMs}ms。`, `Saved timeout ${config.timeoutMs}ms.`)}\n`);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
throw configInputError(t("支持的 config 操作:endpoint、header、header-env、token-env、timeout。", "Supported config actions: endpoint, header, header-env, token-env, timeout."));
|
|
449
|
+
}
|
|
450
|
+
function doctorUsage() {
|
|
451
|
+
return t(`用法:${COMMAND_NAME} doctor [--output json|text]\n`, `Usage: ${COMMAND_NAME} doctor [--output json|text]\n`);
|
|
452
|
+
}
|
|
453
|
+
function parseDoctorOutput(args) {
|
|
454
|
+
let output = "json";
|
|
455
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
456
|
+
const token = args[index];
|
|
457
|
+
let value;
|
|
458
|
+
if (token === "--output") {
|
|
459
|
+
value = valueAfter(args, index, token);
|
|
460
|
+
index += 1;
|
|
461
|
+
}
|
|
462
|
+
else if (token.startsWith("--output=")) {
|
|
463
|
+
value = token.slice("--output=".length);
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
throw new CliError("INVALID_ARGUMENT", t(`不支持的 doctor 选项 '${token}'。`, `Unsupported doctor option '${token}'.`), doctorUsage().trim());
|
|
467
|
+
}
|
|
468
|
+
if (value !== "json" && value !== "text") {
|
|
469
|
+
throw new CliError("INVALID_ARGUMENT", t("doctor 的 output 必须为 json 或 text。", "doctor output must be json or text."), doctorUsage().trim());
|
|
470
|
+
}
|
|
471
|
+
output = value;
|
|
472
|
+
}
|
|
473
|
+
return output;
|
|
474
|
+
}
|
|
475
|
+
function doctorText(report) {
|
|
476
|
+
const connection = report.connection.status === "ok"
|
|
477
|
+
? t(`MCP 连接:正常(${report.connection.toolCount} 个工具)`, `MCP connection: ready (${report.connection.toolCount} tools)`)
|
|
478
|
+
: t(`MCP 连接:失败(${report.connection.error})`, `MCP connection: failed (${report.connection.error})`);
|
|
479
|
+
const compatibility = String(report.artifactCompatibility.status ?? "unknown");
|
|
480
|
+
const artifact = compatibility === "ready"
|
|
481
|
+
? t("生成 CLI:可执行", "Generated CLI: ready")
|
|
482
|
+
: compatibility === "missing"
|
|
483
|
+
? t("生成 CLI:未生成;请执行 refresh", "Generated CLI: missing; run refresh")
|
|
484
|
+
: t("生成 CLI:需要 refresh", "Generated CLI: refresh required");
|
|
485
|
+
const catalog = report.catalogFreshness.isStale
|
|
486
|
+
? t(`工具目录:已超过 7 天未检查(上次:${report.catalogFreshness.lastCheckedAt ?? "未知"});执行 tools list --refresh 可检查最新 schema。`, `Tool catalog: not checked for over 7 days (last check: ${report.catalogFreshness.lastCheckedAt ?? "unknown"}); run tools list --refresh to check the latest schema.`)
|
|
487
|
+
: t(`工具目录:近期已检查(上次:${report.catalogFreshness.lastCheckedAt ?? "未知"})`, `Tool catalog: checked recently (last check: ${report.catalogFreshness.lastCheckedAt ?? "unknown"})`);
|
|
488
|
+
return [
|
|
489
|
+
t("飞瓜智投 MCP CLI 诊断", "Feiguazhitou MCP CLI Doctor"),
|
|
490
|
+
t(`Endpoint:${report.endpoint}`, `Endpoint: ${report.endpoint}`),
|
|
491
|
+
connection,
|
|
492
|
+
artifact,
|
|
493
|
+
catalog,
|
|
494
|
+
t(`超时:${report.timeoutMs}ms。该值会在下一次 run 中立即生效;CLI 不会重试或改写第三方超时错误。`, `Timeout: ${report.timeoutMs}ms. This value takes effect on the next run; the CLI does not retry or rewrite third-party timeout errors.`),
|
|
495
|
+
].join("\n") + "\n";
|
|
496
|
+
}
|
|
497
|
+
async function commandDoctor(options) {
|
|
498
|
+
const args = options.rest.slice(1);
|
|
499
|
+
if (args.includes("--help")) {
|
|
500
|
+
if (args.length !== 1) {
|
|
501
|
+
throw new CliError("INVALID_ARGUMENT", doctorUsage().trim());
|
|
502
|
+
}
|
|
503
|
+
process.stdout.write(doctorUsage());
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const output = parseDoctorOutput(args);
|
|
507
|
+
const profile = await resolveCurrentProfile(options);
|
|
508
|
+
const current = await currentArtifact(profile);
|
|
509
|
+
const { profile: _internalStorageProfile, ...state } = current.state;
|
|
510
|
+
let connection;
|
|
511
|
+
try {
|
|
512
|
+
const discovery = await discoverWithMcporter(profile, createHeaderBindings(profile.profile, profile.headers));
|
|
513
|
+
connection = {
|
|
514
|
+
status: "ok",
|
|
515
|
+
toolCount: discovery.tools.length,
|
|
516
|
+
server: discovery.server,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
connection = { status: "error", error: error instanceof Error ? error.message : String(error) };
|
|
521
|
+
}
|
|
522
|
+
const report = {
|
|
523
|
+
launcherVersion: LAUNCHER_VERSION,
|
|
524
|
+
node: process.version,
|
|
525
|
+
mcporter: mcporterDetails(),
|
|
526
|
+
endpoint: profile.endpoint,
|
|
527
|
+
timeoutMs: profile.timeoutMs,
|
|
528
|
+
configuredHeaderNames: Object.keys(profile.headers),
|
|
529
|
+
autoApprove: profile.autoApprove,
|
|
530
|
+
connection,
|
|
531
|
+
state,
|
|
532
|
+
catalogFreshness: catalogFreshness(current.state.lastCheckedAt),
|
|
533
|
+
artifactCompatibility: artifactCompatibility(current.manifest, profile.timeoutMs),
|
|
534
|
+
generated: current.manifest
|
|
535
|
+
? {
|
|
536
|
+
artifactId: current.manifest.artifactId,
|
|
537
|
+
schemaHash: current.manifest.schemaHash,
|
|
538
|
+
timeoutMs: current.manifest.timeoutMs,
|
|
539
|
+
generatedAt: current.manifest.generatedAt,
|
|
540
|
+
server: current.manifest.server,
|
|
541
|
+
}
|
|
542
|
+
: null,
|
|
543
|
+
};
|
|
544
|
+
if (output === "json") {
|
|
545
|
+
printJson(report);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
process.stdout.write(doctorText(report));
|
|
549
|
+
}
|
|
550
|
+
function describeUsage() {
|
|
551
|
+
return t(`用法:${COMMAND_NAME} describe [--output json|markdown]\n`, `Usage: ${COMMAND_NAME} describe [--output json|markdown]\n`);
|
|
552
|
+
}
|
|
553
|
+
function toolsUsage() {
|
|
554
|
+
return t(`用法:
|
|
555
|
+
${COMMAND_NAME} tools list [--refresh] [--force] [--output json|text]
|
|
556
|
+
${COMMAND_NAME} tools show NAME [--refresh] [--force] [--output json|markdown]
|
|
557
|
+
`, `Usage:
|
|
558
|
+
${COMMAND_NAME} tools list [--refresh] [--force] [--output json|text]
|
|
559
|
+
${COMMAND_NAME} tools show NAME [--refresh] [--force] [--output json|markdown]
|
|
560
|
+
`);
|
|
561
|
+
}
|
|
562
|
+
function runUsage() {
|
|
563
|
+
return t(`用法:${COMMAND_NAME} run COMMAND [...generated-command-args]\n\n${rawInputUsage()}`, `Usage: ${COMMAND_NAME} run COMMAND [...generated-command-args]\n\n${rawInputUsage()}`);
|
|
564
|
+
}
|
|
565
|
+
function parseOutput(args, defaultOutput, command) {
|
|
566
|
+
let output = defaultOutput;
|
|
567
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
568
|
+
const token = args[index];
|
|
569
|
+
let value;
|
|
570
|
+
if (token === "--output") {
|
|
571
|
+
value = valueAfter(args, index, token);
|
|
572
|
+
index += 1;
|
|
573
|
+
}
|
|
574
|
+
else if (token.startsWith("--output=")) {
|
|
575
|
+
value = token.slice("--output=".length);
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
throw new CliError("INVALID_ARGUMENT", t(`不支持的 ${command} 选项 '${token}'。`, `Unsupported ${command} option '${token}'.`));
|
|
579
|
+
}
|
|
580
|
+
if (value !== "json" && value !== "markdown") {
|
|
581
|
+
throw new CliError("INVALID_ARGUMENT", t(`${command} 的 output 必须为 json 或 markdown。`, `${command} output must be json or markdown.`));
|
|
582
|
+
}
|
|
583
|
+
output = value;
|
|
584
|
+
}
|
|
585
|
+
return output;
|
|
586
|
+
}
|
|
587
|
+
function parseToolsOptions(args) {
|
|
588
|
+
let refresh = false;
|
|
589
|
+
let output = "text";
|
|
590
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
591
|
+
const token = args[index];
|
|
592
|
+
if (token === "--refresh") {
|
|
593
|
+
refresh = true;
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
let value;
|
|
597
|
+
if (token === "--output") {
|
|
598
|
+
value = valueAfter(args, index, token);
|
|
599
|
+
index += 1;
|
|
600
|
+
}
|
|
601
|
+
else if (token.startsWith("--output=")) {
|
|
602
|
+
value = token.slice("--output=".length);
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
throw new CliError("INVALID_ARGUMENT", t(`不支持的 tools 选项 '${token}'。`, `Unsupported tools option '${token}'.`));
|
|
606
|
+
}
|
|
607
|
+
if (value !== "json" && value !== "text") {
|
|
608
|
+
throw new CliError("INVALID_ARGUMENT", t("tools 的 output 必须为 json 或 text。", "tools output must be json or text."));
|
|
609
|
+
}
|
|
610
|
+
output = value;
|
|
611
|
+
}
|
|
612
|
+
return { refresh, output };
|
|
613
|
+
}
|
|
614
|
+
async function generatedToolCatalog(options, mode) {
|
|
615
|
+
const profile = await resolveCurrentProfile(options);
|
|
616
|
+
let artifact = await currentArtifact(profile);
|
|
617
|
+
if (mode === "refresh") {
|
|
618
|
+
try {
|
|
619
|
+
await refreshProfile(profile, options.force);
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
623
|
+
throw new CliError("DISCOVERY_FAILED", t(`工具发现失败:${detail}`, `Tool discovery failed: ${detail}`), t(`检查本地飞瓜智投客户端后,执行 ${COMMAND_NAME} refresh --output json。`, `Check the local Feiguazhitou client, then run ${COMMAND_NAME} refresh --output json.`));
|
|
624
|
+
}
|
|
625
|
+
artifact = await currentArtifact(profile);
|
|
626
|
+
}
|
|
627
|
+
if (!artifact.manifest) {
|
|
628
|
+
throw new CliError("ARTIFACT_MISSING", t("没有可用的生成工具目录。", "No generated tool catalog is available."), t(`执行 ${COMMAND_NAME} refresh --output json 后重试。`, `Run ${COMMAND_NAME} refresh --output json, then retry.`));
|
|
629
|
+
}
|
|
630
|
+
return { profile, artifact, catalog: toolCatalog(artifact.manifest, profile.timeoutMs, profile.autoApprove, artifact.state.lastCheckedAt) };
|
|
631
|
+
}
|
|
632
|
+
async function commandDescribe(options) {
|
|
633
|
+
const args = options.rest.slice(1);
|
|
634
|
+
if (args.includes("--help")) {
|
|
635
|
+
if (args.length !== 1) {
|
|
636
|
+
throw new CliError("INVALID_ARGUMENT", describeUsage().trim());
|
|
637
|
+
}
|
|
638
|
+
process.stdout.write(describeUsage());
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const output = parseOutput(args, "markdown", "describe");
|
|
642
|
+
if (output === "json") {
|
|
643
|
+
printJson(describeDocument());
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
process.stdout.write(describeMarkdown());
|
|
647
|
+
}
|
|
648
|
+
function parseToolDetailOptions(args, command) {
|
|
649
|
+
let refresh = false;
|
|
650
|
+
const outputArgs = [];
|
|
651
|
+
for (const token of args) {
|
|
652
|
+
if (token === "--refresh") {
|
|
653
|
+
refresh = true;
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
outputArgs.push(token);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return { refresh, output: parseOutput(outputArgs, "json", command) };
|
|
660
|
+
}
|
|
661
|
+
function writeToolDetail(catalog, name, output) {
|
|
662
|
+
const tool = findTool(catalog, name);
|
|
663
|
+
if (!tool) {
|
|
664
|
+
throw new CliError("TOOL_NOT_FOUND", t(`未知的生成工具 '${name}'。`, `Unknown generated tool '${name}'.`), t(`执行 ${COMMAND_NAME} tools list --output json,并选择其中一个 command 值。`, `Run ${COMMAND_NAME} tools list --output json and choose one of its command values.`));
|
|
665
|
+
}
|
|
666
|
+
if (output === "markdown") {
|
|
667
|
+
process.stdout.write(toolMarkdown(catalog, tool));
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
printJson({
|
|
671
|
+
protocol: TOOL_CATALOG_PROTOCOL,
|
|
672
|
+
protocolVersion: TOOL_CATALOG_PROTOCOL_VERSION,
|
|
673
|
+
artifact: catalog.artifact,
|
|
674
|
+
tool,
|
|
675
|
+
invocationTemplate: toolInvocationTemplate(tool),
|
|
676
|
+
workflowSkill: "feiguazhitou-mcp-cli",
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
async function commandTools(options) {
|
|
680
|
+
const [action, ...args] = options.rest.slice(1);
|
|
681
|
+
if (action === "--help") {
|
|
682
|
+
process.stdout.write(toolsUsage());
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (action === "list") {
|
|
686
|
+
if (args.includes("--help")) {
|
|
687
|
+
if (args.length !== 1) {
|
|
688
|
+
throw new CliError("INVALID_ARGUMENT", toolsUsage().trim());
|
|
689
|
+
}
|
|
690
|
+
process.stdout.write(toolsUsage());
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const parsed = parseToolsOptions(args);
|
|
694
|
+
const result = await generatedToolCatalog(options, parsed.refresh ? "refresh" : "cached");
|
|
695
|
+
if (parsed.output === "json") {
|
|
696
|
+
printJson(result.catalog);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
process.stdout.write(toolCatalogText(result.catalog));
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (action === "show") {
|
|
703
|
+
const [name, ...flags] = args;
|
|
704
|
+
if (!name || name === "--help") {
|
|
705
|
+
if (name === "--help" && flags.length === 0) {
|
|
706
|
+
process.stdout.write(toolsUsage());
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
throw new CliError("INVALID_ARGUMENT", toolsUsage().trim());
|
|
710
|
+
}
|
|
711
|
+
if (flags.includes("--help")) {
|
|
712
|
+
if (flags.length !== 1) {
|
|
713
|
+
throw new CliError("INVALID_ARGUMENT", toolsUsage().trim());
|
|
714
|
+
}
|
|
715
|
+
process.stdout.write(toolsUsage());
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const parsed = parseToolDetailOptions(flags, "tools show");
|
|
719
|
+
const result = await generatedToolCatalog(options, parsed.refresh ? "refresh" : "cached");
|
|
720
|
+
writeToolDetail(result.catalog, name, parsed.output);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
throw new CliError("INVALID_ARGUMENT", action ? t(`不支持的 tools 操作 '${action}'。`, `Unsupported tools action '${action}'.`) : t("tools 需要 list 或 show 操作。", "tools requires either the list or show action."), toolsUsage().trim());
|
|
724
|
+
}
|
|
725
|
+
function parseSkillsOutput(args, defaultOutput) {
|
|
726
|
+
let output = defaultOutput;
|
|
727
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
728
|
+
const token = args[index];
|
|
729
|
+
let value;
|
|
730
|
+
if (token === "--output") {
|
|
731
|
+
value = valueAfter(args, index, token);
|
|
732
|
+
index += 1;
|
|
733
|
+
}
|
|
734
|
+
else if (token.startsWith("--output=")) {
|
|
735
|
+
value = token.slice("--output=".length);
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
throw new Error(t(`不支持的 skills 选项 '${token}'。`, `Unsupported skills option '${token}'.`));
|
|
739
|
+
}
|
|
740
|
+
if (value !== "json" && value !== "markdown") {
|
|
741
|
+
throw new Error(t("Skill 的 output 必须为 json 或 markdown。", "Skill output must be json or markdown."));
|
|
742
|
+
}
|
|
743
|
+
output = value;
|
|
744
|
+
}
|
|
745
|
+
return output;
|
|
746
|
+
}
|
|
747
|
+
async function commandSkills(options) {
|
|
748
|
+
const [action, ...args] = options.rest.slice(1);
|
|
749
|
+
if (action === "protocol") {
|
|
750
|
+
if (args.length > 0) {
|
|
751
|
+
throw new Error(t(`用法:${COMMAND_NAME} skills protocol`, `Usage: ${COMMAND_NAME} skills protocol`));
|
|
752
|
+
}
|
|
753
|
+
printJson(skillProtocolDocument());
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (action === "list") {
|
|
757
|
+
const output = parseSkillsOutput(args, "json");
|
|
758
|
+
if (output !== "json") {
|
|
759
|
+
throw new Error(t(`${COMMAND_NAME} skills list 仅支持 --output json。`, `${COMMAND_NAME} skills list supports only --output json.`));
|
|
760
|
+
}
|
|
761
|
+
printJson({ ...skillProtocolDocument(), skills: await listBundledSkills() });
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (action === "show") {
|
|
765
|
+
const [name, ...flags] = args;
|
|
766
|
+
if (!name || name.startsWith("-")) {
|
|
767
|
+
throw new Error(t(`用法:${COMMAND_NAME} skills show NAME [--output json|markdown]`, `Usage: ${COMMAND_NAME} skills show NAME [--output json|markdown]`));
|
|
768
|
+
}
|
|
769
|
+
const output = parseSkillsOutput(flags, "markdown");
|
|
770
|
+
const skill = await showBundledSkill(name);
|
|
771
|
+
if (output === "markdown") {
|
|
772
|
+
process.stdout.write(skill.markdown.endsWith("\n") ? skill.markdown : `${skill.markdown}\n`);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
printJson({ ...skillProtocolDocument(), skill });
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
throw new Error(t("支持的 skill 操作:protocol、list、show。", "Supported skill actions: protocol, list, show."));
|
|
779
|
+
}
|
|
780
|
+
async function dispatchGeneratedCommand(options, commandArgs) {
|
|
781
|
+
const profile = await resolveCurrentProfile(options);
|
|
782
|
+
let artifact = await currentArtifact(profile);
|
|
783
|
+
const artifactMissing = !artifact.bundle || !artifact.manifest;
|
|
784
|
+
const failureExitPolicyRefreshRequired = artifact.manifest !== undefined && artifact.manifest.failureExitPolicyVersion !== GENERATED_FAILURE_EXIT_POLICY_VERSION;
|
|
785
|
+
if (artifactMissing || failureExitPolicyRefreshRequired) {
|
|
786
|
+
if (artifactMissing) {
|
|
787
|
+
throw new CliError("ARTIFACT_MISSING", t("没有可用的生成 CLI。", "No generated CLI is available."), t(`执行 ${COMMAND_NAME} refresh --output json 后重试。`, `Run ${COMMAND_NAME} refresh --output json, then retry.`));
|
|
788
|
+
}
|
|
789
|
+
if (failureExitPolicyRefreshRequired) {
|
|
790
|
+
throw new CliError("ARTIFACT_REFRESH_REQUIRED", t("生成的 CLI 不包含当前失败退出码策略。", "The generated CLI does not include the current failure-exit policy."), failureExitPolicyRemediation());
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (!artifact.bundle || !artifact.manifest) {
|
|
794
|
+
throw new CliError("ARTIFACT_MISSING", t("refresh 后仍没有可用的生成 CLI。", "No generated CLI is available after refresh."));
|
|
795
|
+
}
|
|
796
|
+
const catalog = toolCatalog(artifact.manifest, profile.timeoutMs, profile.autoApprove, artifact.state.lastCheckedAt);
|
|
797
|
+
const requestedTool = commandArgs[0];
|
|
798
|
+
const tool = requestedTool ? findTool(catalog, requestedTool) : undefined;
|
|
799
|
+
if (!tool) {
|
|
800
|
+
throw new CliError("TOOL_NOT_FOUND", requestedTool
|
|
801
|
+
? t(`未知的生成工具 '${requestedTool}'。`, `Unknown generated tool '${requestedTool}'.`)
|
|
802
|
+
: t("需要提供一个生成工具 command。", "A generated tool command is required."), t(`执行 ${COMMAND_NAME} tools list --output json,并选择其中一个 command 值。`, `Run ${COMMAND_NAME} tools list --output json and choose one of its command values.`));
|
|
803
|
+
}
|
|
804
|
+
// MCPorter exposes a root --timeout flag. Supplying it at launch keeps the
|
|
805
|
+
// generated catalog reusable while making the current CLI timeout effective.
|
|
806
|
+
const generatedArgs = ["--timeout", String(profile.timeoutMs), tool.command, ...(await normalizeGeneratedRawInput(commandArgs.slice(1)))];
|
|
807
|
+
const bindings = createHeaderBindings(profile.profile, profile.headers);
|
|
808
|
+
const missingHeaders = Object.keys(artifact.manifest.headerBindings).filter((name) => !Object.keys(bindings.bindings).some((current) => current.toLowerCase() === name.toLowerCase()));
|
|
809
|
+
const addedHeaders = Object.keys(bindings.bindings).filter((name) => !Object.keys(artifact.manifest.headerBindings).some((current) => current.toLowerCase() === name.toLowerCase()));
|
|
810
|
+
if (missingHeaders.length > 0 || addedHeaders.length > 0) {
|
|
811
|
+
throw new CliError("ARTIFACT_REFRESH_REQUIRED", t("HTTP Header 名称已变更。", "HTTP header names changed."), t(`执行一次 ${COMMAND_NAME} refresh --force --output json;仅 Header 值变更不需要重新生成。`, `Run ${COMMAND_NAME} refresh --force --output json once; header values themselves never require regeneration.`));
|
|
812
|
+
}
|
|
813
|
+
const environment = { [ENV.runtimeEndpoint]: profile.endpoint };
|
|
814
|
+
for (const [name, generatedEnv] of Object.entries(artifact.manifest.headerBindings)) {
|
|
815
|
+
const currentName = Object.keys(profile.headers).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
|
|
816
|
+
if (!currentName) {
|
|
817
|
+
throw new CliError("COMMAND_FAILED", t(`缺少生成 Header '${name}' 的运行时值。`, `Missing runtime value for generated header '${name}'.`));
|
|
818
|
+
}
|
|
819
|
+
environment[generatedEnv] = profile.headers[currentName];
|
|
820
|
+
}
|
|
821
|
+
return writeGeneratedCliResult(await runGeneratedCli(artifact.bundle, generatedArgs, environment), generatedArgs);
|
|
822
|
+
}
|
|
823
|
+
function refreshUsage() {
|
|
824
|
+
return t(`用法:${COMMAND_NAME} refresh [--force] [--output json|text]\n`, `Usage: ${COMMAND_NAME} refresh [--force] [--output json|text]\n`);
|
|
825
|
+
}
|
|
826
|
+
function parseRefreshOutput(args) {
|
|
827
|
+
let output = "text";
|
|
828
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
829
|
+
const token = args[index];
|
|
830
|
+
let value;
|
|
831
|
+
if (token === "--output") {
|
|
832
|
+
value = valueAfter(args, index, token);
|
|
833
|
+
index += 1;
|
|
834
|
+
}
|
|
835
|
+
else if (token.startsWith("--output=")) {
|
|
836
|
+
value = token.slice("--output=".length);
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
throw new CliError("INVALID_ARGUMENT", t(`不支持的 refresh 选项 '${token}'。`, `Unsupported refresh option '${token}'.`), refreshUsage().trim());
|
|
840
|
+
}
|
|
841
|
+
if (value !== "json" && value !== "text") {
|
|
842
|
+
throw new CliError("INVALID_ARGUMENT", t("refresh 的 output 必须为 json 或 text。", "refresh output must be json or text."), refreshUsage().trim());
|
|
843
|
+
}
|
|
844
|
+
output = value;
|
|
845
|
+
}
|
|
846
|
+
return output;
|
|
847
|
+
}
|
|
848
|
+
async function commandRefresh(options) {
|
|
849
|
+
const args = options.rest.slice(1);
|
|
850
|
+
if (args.includes("--help")) {
|
|
851
|
+
if (args.length !== 1) {
|
|
852
|
+
throw new CliError("INVALID_ARGUMENT", refreshUsage().trim());
|
|
853
|
+
}
|
|
854
|
+
process.stdout.write(refreshUsage());
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const output = parseRefreshOutput(args);
|
|
858
|
+
const profile = await resolveCurrentProfile(options);
|
|
859
|
+
const result = await refreshProfile(profile, options.force);
|
|
860
|
+
if (output === "json") {
|
|
861
|
+
printJson(result);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
process.stderr.write(`${result.message}\n`);
|
|
865
|
+
}
|
|
866
|
+
async function commandRun(options) {
|
|
867
|
+
const [name, ...args] = options.rest.slice(1);
|
|
868
|
+
if (!name || name === "--help") {
|
|
869
|
+
if (name === "--help" && args.length === 0) {
|
|
870
|
+
process.stdout.write(runUsage());
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
throw new CliError("INVALID_ARGUMENT", runUsage().trim());
|
|
874
|
+
}
|
|
875
|
+
const code = await dispatchGeneratedCommand(options, [name, ...args]);
|
|
876
|
+
process.exitCode = code;
|
|
877
|
+
}
|
|
878
|
+
async function main() {
|
|
879
|
+
const options = parseGlobalOptions(process.argv.slice(2));
|
|
880
|
+
const first = options.rest[0];
|
|
881
|
+
if (first === "--version" || first === "-V") {
|
|
882
|
+
process.stdout.write(`${LAUNCHER_VERSION}\n`);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (!first) {
|
|
886
|
+
process.stdout.write(usage());
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (first === "init") {
|
|
890
|
+
await commandInit(options);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
if (first === "refresh") {
|
|
894
|
+
await commandRefresh(options);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
if (first === "doctor") {
|
|
898
|
+
await commandDoctor(options);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
if (first === "config") {
|
|
902
|
+
await commandConfig(options);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
if (first === "skills") {
|
|
906
|
+
await commandSkills(options);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
if (first === "describe") {
|
|
910
|
+
await commandDescribe(options);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
if (first === "tools") {
|
|
914
|
+
await commandTools(options);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
if (first === "run") {
|
|
918
|
+
await commandRun(options);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
if (first === "help") {
|
|
922
|
+
process.stdout.write(usage());
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
if (first === "--help" || first === "-h") {
|
|
926
|
+
process.stdout.write(usage());
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
throw new CliError("INVALID_ARGUMENT", t(`未知的启动器命令 '${first}'。`, `Unknown launcher command '${first}'.`), usage().trim());
|
|
930
|
+
}
|
|
931
|
+
function requestedJsonOutput(args) {
|
|
932
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
933
|
+
const token = args[index];
|
|
934
|
+
if (token === "--output" && args[index + 1] === "json") {
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
937
|
+
if (token === "--output=json") {
|
|
938
|
+
return true;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return false;
|
|
942
|
+
}
|
|
943
|
+
main().catch((error) => {
|
|
944
|
+
if (requestedJsonOutput(process.argv.slice(2))) {
|
|
945
|
+
printJson(errorDocument(error));
|
|
946
|
+
}
|
|
947
|
+
else {
|
|
948
|
+
process.stderr.write(`${COMMAND_NAME}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
949
|
+
}
|
|
950
|
+
process.exitCode = 1;
|
|
951
|
+
});
|
|
952
|
+
//# sourceMappingURL=cli.js.map
|