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/LICENSE +21 -0
- package/NOTICE.md +12 -0
- package/README.md +172 -0
- package/SKILL.md +116 -0
- package/package.json +34 -0
- package/references/script-contract.md +70 -0
- package/src/cli.mjs +348 -0
- package/src/config.mjs +40 -0
- package/src/core.mjs +2246 -0
- package/src/directory-scorer.mjs +1086 -0
- package/src/executor.mjs +659 -0
- package/src/extract-key.mjs +93 -0
- package/src/project-path.mjs +47 -0
- package/src/protobuf.mjs +235 -0
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
|
+
}
|