fast-context-skill 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/src/cli.mjs ADDED
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { realpathSync, readFileSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ import { extractKeyInfo, searchWithContent } from "./core.mjs";
8
+ import { readRuntimeConfig } from "./config.mjs";
9
+ import { validateProjectPath } from "./project-path.mjs";
10
+
11
+ const PACKAGE_JSON = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
12
+
13
+ const GLOBAL_FLAGS = new Set(["--help", "-h", "--version", "-v"]);
14
+
15
+ export function isDirectRun(argvPath = process.argv[1], moduleUrl = import.meta.url) {
16
+ if (!argvPath) return false;
17
+ try {
18
+ return realpathSync(argvPath) === realpathSync(fileURLToPath(moduleUrl));
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ function usage() {
25
+ return `fast-context-skill ${PACKAGE_JSON.version}
26
+
27
+ Usage:
28
+ fast-context-skill search --query <text> --project <absolute-path> [options]
29
+ fast-context-skill --query <text> --project <absolute-path> [options]
30
+ fast-context-skill extract-key
31
+ fast-context-skill --check-key | --print-key | --key-env
32
+
33
+ Commands:
34
+ search 运行 AI 语义代码搜索,输出文件路径、行号范围和 grep keywords
35
+ extract-key 从本机 Windsurf 安装中提取 API Key
36
+
37
+ Search options:
38
+ -q, --query <text> 自然语言查询,必填
39
+ -p, --project <path> 项目根目录,必填,建议传绝对路径
40
+ --project-path <path> --project 的别名
41
+ --tree-depth <n> repo map 深度,默认 3,允许 0-6
42
+ --max-turns <n> 搜索轮数,默认来自 FC_MAX_TURNS 或 3,允许 1-5
43
+ --max-results <n> 返回文件数,默认 10,允许 1-30
44
+ --max-commands <n> 每轮最大本地命令数,默认来自 FC_MAX_COMMANDS 或 8
45
+ --timeout-ms <n> 请求超时毫秒数,默认来自 FC_TIMEOUT_MS 或 30000
46
+ --exclude <pattern> 排除路径/文件模式,可重复;逗号分隔也可
47
+ --include-code-snippets 附带代码片段,默认 false
48
+ --json 输出 JSON 包装,便于脚本消费
49
+ --progress 将进度日志输出到 stderr
50
+ --check-key 验证 Windsurf API Key 自动发现,只输出脱敏值
51
+ --print-key 输出完整 Windsurf API Key,仅限本机排查使用
52
+ --key-env 输出 export WINDSURF_API_KEY=... 命令
53
+ --db-path <path> 指定 Windsurf state.vscdb 路径,仅用于 key 命令
54
+
55
+ Environment:
56
+ WINDSURF_API_KEY Windsurf API Key;未设置时自动读取本机 Windsurf 数据库
57
+ FC_MAX_TURNS / FC_MAX_COMMANDS / FC_TIMEOUT_MS / FC_INCLUDE_SNIPPETS
58
+ FC_REPO_MAP_MODE / FC_BOOTSTRAP_* / FC_HOTSPOT_*
59
+ `;
60
+ }
61
+
62
+ function parseArgs(argv) {
63
+ const args = { _: [] };
64
+
65
+ for (let i = 0; i < argv.length; i++) {
66
+ const token = argv[i];
67
+ if (!token.startsWith("-")) {
68
+ args._.push(token);
69
+ continue;
70
+ }
71
+
72
+ const eq = token.indexOf("=");
73
+ const flag = eq === -1 ? token : token.slice(0, eq);
74
+ const inlineValue = eq === -1 ? null : token.slice(eq + 1);
75
+
76
+ if ([
77
+ "--help",
78
+ "-h",
79
+ "--version",
80
+ "-v",
81
+ "--json",
82
+ "--progress",
83
+ "--include-code-snippets",
84
+ "--check-key",
85
+ "--print-key",
86
+ "--key-env",
87
+ ].includes(flag)) {
88
+ args[flag] = true;
89
+ continue;
90
+ }
91
+
92
+ if (flag === "--no-include-code-snippets") {
93
+ args["--include-code-snippets"] = false;
94
+ continue;
95
+ }
96
+
97
+ const value = inlineValue ?? argv[++i];
98
+ if (value == null || value.startsWith("-")) {
99
+ throw new Error(`Missing value for ${flag}`);
100
+ }
101
+
102
+ if (flag === "--exclude") {
103
+ args[flag] ??= [];
104
+ args[flag].push(value);
105
+ } else {
106
+ args[flag] = value;
107
+ }
108
+ }
109
+
110
+ return args;
111
+ }
112
+
113
+ function pick(args, ...names) {
114
+ for (const name of names) {
115
+ if (args[name] !== undefined) return args[name];
116
+ }
117
+ return undefined;
118
+ }
119
+
120
+ function parseIntOption(value, name, { min, max }) {
121
+ if (value === undefined) return undefined;
122
+ const n = Number.parseInt(String(value), 10);
123
+ if (!Number.isInteger(n) || String(value).trim() === "") {
124
+ throw new Error(`${name} must be an integer`);
125
+ }
126
+ if (n < min || n > max) {
127
+ throw new Error(`${name} must be between ${min} and ${max}`);
128
+ }
129
+ return n;
130
+ }
131
+
132
+ function parseExclude(value) {
133
+ if (!value) return [];
134
+ const values = Array.isArray(value) ? value : [value];
135
+ return values
136
+ .flatMap((item) => String(item).split(","))
137
+ .map((item) => item.trim())
138
+ .filter(Boolean);
139
+ }
140
+
141
+ function maskKey(key) {
142
+ if (!key) return "";
143
+ if (key.length <= 12) return `${key.slice(0, 2)}...${key.slice(-2)}`;
144
+ return `${key.slice(0, 8)}...${key.slice(-6)}`;
145
+ }
146
+
147
+ function shellQuote(value) {
148
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
149
+ }
150
+
151
+ function toAbsoluteProjectPath(projectPath) {
152
+ if (!projectPath) return projectPath;
153
+ return resolve(projectPath);
154
+ }
155
+
156
+ function printJson(payload) {
157
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
158
+ }
159
+
160
+ async function runSearch(args, deps = {}) {
161
+ const config = readRuntimeConfig();
162
+ const query = pick(args, "--query", "-q");
163
+ const rawProjectPath = pick(args, "--project", "--project-path", "-p");
164
+ const projectRoot = toAbsoluteProjectPath(rawProjectPath);
165
+ const validationError = validateProjectPath(projectRoot);
166
+
167
+ if (!query || !String(query).trim()) {
168
+ throw new Error("search requires --query <text>");
169
+ }
170
+ if (validationError) {
171
+ throw new Error(validationError.replace(/^Error:\s*/, ""));
172
+ }
173
+
174
+ const maxTurns = parseIntOption(args["--max-turns"], "--max-turns", { min: 1, max: 5 }) ?? config.maxTurns;
175
+ const maxCommands = parseIntOption(args["--max-commands"], "--max-commands", { min: 1, max: 20 }) ?? config.maxCommands;
176
+ const maxResults = parseIntOption(args["--max-results"], "--max-results", { min: 1, max: 30 }) ?? 10;
177
+ const treeDepth = parseIntOption(args["--tree-depth"], "--tree-depth", { min: 0, max: 6 }) ?? 3;
178
+ const timeoutMs = parseIntOption(args["--timeout-ms"], "--timeout-ms", { min: 1000, max: 300000 }) ?? config.timeoutMs;
179
+ const includeSnippets = args["--include-code-snippets"] ?? config.includeSnippets;
180
+ const excludePaths = parseExclude(args["--exclude"]);
181
+ const showProgress = Boolean(args["--progress"]);
182
+ const searchFn = deps.searchWithContent || searchWithContent;
183
+
184
+ const text = await searchFn({
185
+ query: String(query).trim(),
186
+ projectRoot,
187
+ maxTurns,
188
+ maxCommands,
189
+ maxResults,
190
+ treeDepth,
191
+ timeoutMs,
192
+ excludePaths,
193
+ repoMapMode: config.repoMapMode,
194
+ bootstrapTreeDepth: config.bootstrapTreeDepth,
195
+ hotspotTopK: config.hotspotTopK,
196
+ hotspotTreeDepth: config.hotspotTreeDepth,
197
+ hotspotMaxBytes: config.hotspotMaxBytes,
198
+ bootstrapEnabled: config.bootstrapEnabled,
199
+ bootstrapMaxTurns: config.bootstrapMaxTurns,
200
+ bootstrapMaxCommands: config.bootstrapMaxCommands,
201
+ includeSnippets,
202
+ onProgress: showProgress ? (message) => process.stderr.write(`[fast-context] ${message}\n`) : null,
203
+ });
204
+
205
+ if (args["--json"]) {
206
+ printJson({ ok: !text.startsWith("Error:"), command: "search", result: text });
207
+ } else {
208
+ process.stdout.write(`${text}\n`);
209
+ }
210
+ }
211
+
212
+ async function getKeyResult(args, deps = {}) {
213
+ const extractFn = deps.extractKeyInfo || extractKeyInfo;
214
+ const dbPath = args["--db-path"] ? resolve(args["--db-path"]) : undefined;
215
+ return extractFn(dbPath);
216
+ }
217
+
218
+ function formatExtractKeyResult(result) {
219
+ if (result.error) {
220
+ return `Error: ${result.error}\n${result.hint || ""}\nDB path: ${result.db_path || "N/A"}`;
221
+ }
222
+
223
+ const key = result.api_key;
224
+ return [
225
+ "Windsurf API Key extracted successfully",
226
+ "",
227
+ ` Key: ${key.slice(0, 30)}...${key.slice(-10)}`,
228
+ ` Length: ${key.length}`,
229
+ ` Source: ${result.db_path}`,
230
+ "",
231
+ "Usage:",
232
+ ` export WINDSURF_API_KEY=${shellQuote(key)}`,
233
+ ].join("\n");
234
+ }
235
+
236
+ async function runExtractKey(args, deps = {}) {
237
+ const result = await getKeyResult(args, deps);
238
+ const ok = !result.error;
239
+ if (args["--json"]) {
240
+ printJson({ ok, command: "extract-key", result });
241
+ } else {
242
+ process.stdout.write(`${formatExtractKeyResult(result)}\n`);
243
+ }
244
+ if (!ok) process.exitCode = 1;
245
+ }
246
+
247
+ async function runKeyCommand(args, deps = {}) {
248
+ const keyFlags = ["--check-key", "--print-key", "--key-env"].filter((flag) => args[flag]);
249
+ if (keyFlags.length > 1) {
250
+ throw new Error("Choose only one key command: --check-key, --print-key, or --key-env");
251
+ }
252
+
253
+ const result = await getKeyResult(args, deps);
254
+ if (result.error) {
255
+ const text = `Error: ${result.error}\n${result.hint || ""}\nDB path: ${result.db_path || "N/A"}`;
256
+ if (args["--json"]) {
257
+ printJson({ ok: false, command: keyFlags[0].slice(2), result });
258
+ } else {
259
+ process.stdout.write(`${text}\n`);
260
+ }
261
+ process.exitCode = 1;
262
+ return;
263
+ }
264
+
265
+ if (args["--print-key"]) {
266
+ process.stderr.write(
267
+ "[fast-context] WARNING: printing the full Windsurf API key to stdout. " +
268
+ "Do not paste it into logs, issues, or repository files.\n"
269
+ );
270
+ process.stdout.write(`${result.api_key}\n`);
271
+ return;
272
+ }
273
+
274
+ if (args["--key-env"]) {
275
+ process.stdout.write(`export WINDSURF_API_KEY=${shellQuote(result.api_key)}\n`);
276
+ return;
277
+ }
278
+
279
+ if (args["--json"]) {
280
+ printJson({
281
+ ok: true,
282
+ command: "check-key",
283
+ result: {
284
+ api_key_masked: maskKey(result.api_key),
285
+ db_path: result.db_path,
286
+ },
287
+ });
288
+ return;
289
+ }
290
+
291
+ process.stdout.write("Windsurf API Key discovered.\n");
292
+ process.stdout.write(` Key: ${maskKey(result.api_key)}\n`);
293
+ process.stdout.write(` Source: ${result.db_path}\n`);
294
+ }
295
+
296
+ export async function main(argv = process.argv.slice(2), deps = {}) {
297
+ const args = parseArgs(argv);
298
+ let command = args._[0];
299
+
300
+ if (args["--version"] || args["-v"]) {
301
+ process.stdout.write(`${PACKAGE_JSON.version}\n`);
302
+ return;
303
+ }
304
+
305
+ if (args["--help"] || args["-h"]) {
306
+ process.stdout.write(usage());
307
+ return;
308
+ }
309
+
310
+ if (args["--check-key"] || args["--print-key"] || args["--key-env"]) {
311
+ await runKeyCommand(args, deps);
312
+ return;
313
+ }
314
+
315
+ if (!command && (args["--query"] || args["-q"])) {
316
+ command = "search";
317
+ }
318
+
319
+ if (!command) {
320
+ process.stdout.write(usage());
321
+ return;
322
+ }
323
+
324
+ if (GLOBAL_FLAGS.has(command)) {
325
+ process.stdout.write(command === "--version" || command === "-v" ? `${PACKAGE_JSON.version}\n` : usage());
326
+ return;
327
+ }
328
+
329
+ if (command === "search") {
330
+ await runSearch(args, deps);
331
+ return;
332
+ }
333
+
334
+ if (command === "extract-key" || command === "extract_windsurf_key") {
335
+ await runExtractKey(args, deps);
336
+ return;
337
+ }
338
+
339
+ throw new Error(`Unknown command: ${command}`);
340
+ }
341
+
342
+ if (isDirectRun()) {
343
+ main().catch((err) => {
344
+ process.stderr.write(`Error: ${err.message}\n\n`);
345
+ process.stderr.write(usage());
346
+ process.exit(1);
347
+ });
348
+ }
package/src/config.mjs ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * 读取 CLI 运行时配置。这里保留母项目的环境变量语义,但不依赖 MCP 层。
3
+ */
4
+ export function readIntEnv(name, defaultValue, opts = {}, env = process.env) {
5
+ const raw = env[name];
6
+ const parsed = Number.parseInt(raw ?? "", 10);
7
+ if (!Number.isFinite(parsed)) return defaultValue;
8
+ const min = typeof opts.min === "number" ? opts.min : null;
9
+ const max = typeof opts.max === "number" ? opts.max : null;
10
+ let value = parsed;
11
+ if (min !== null) value = Math.max(min, value);
12
+ if (max !== null) value = Math.min(max, value);
13
+ return value;
14
+ }
15
+
16
+ export function readBoolEnv(name, defaultValue, env = process.env) {
17
+ const raw = env[name];
18
+ if (raw == null) return defaultValue;
19
+ const v = String(raw).trim().toLowerCase();
20
+ if (["1", "true", "yes", "on"].includes(v)) return true;
21
+ if (["0", "false", "no", "off"].includes(v)) return false;
22
+ return defaultValue;
23
+ }
24
+
25
+ export function readRuntimeConfig(env = process.env) {
26
+ return {
27
+ maxTurns: readIntEnv("FC_MAX_TURNS", 3, { min: 1, max: 5 }, env),
28
+ maxCommands: readIntEnv("FC_MAX_COMMANDS", 8, { min: 1, max: 20 }, env),
29
+ timeoutMs: readIntEnv("FC_TIMEOUT_MS", 30000, { min: 1000, max: 300000 }, env),
30
+ repoMapMode: env.FC_REPO_MAP_MODE === "classic" ? "classic" : "bootstrap_hotspot",
31
+ bootstrapTreeDepth: readIntEnv("FC_BOOTSTRAP_TREE_DEPTH", 1, { min: 1, max: 3 }, env),
32
+ hotspotTopK: readIntEnv("FC_HOTSPOT_TOP_K", 4, { min: 0, max: 8 }, env),
33
+ hotspotTreeDepth: readIntEnv("FC_HOTSPOT_TREE_DEPTH", 2, { min: 1, max: 4 }, env),
34
+ hotspotMaxBytes: readIntEnv("FC_HOTSPOT_MAX_BYTES", 122880, { min: 16384, max: 262144 }, env),
35
+ bootstrapEnabled: readBoolEnv("FC_BOOTSTRAP_ENABLED", true, env),
36
+ bootstrapMaxTurns: readIntEnv("FC_BOOTSTRAP_MAX_TURNS", 2, { min: 1, max: 3 }, env),
37
+ bootstrapMaxCommands: readIntEnv("FC_BOOTSTRAP_MAX_COMMANDS", 6, { min: 1, max: 8 }, env),
38
+ includeSnippets: readBoolEnv("FC_INCLUDE_SNIPPETS", false, env),
39
+ };
40
+ }