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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windsurf API Key extraction from local installation.
|
|
3
|
+
*
|
|
4
|
+
* Cross-platform: macOS / Windows / Linux.
|
|
5
|
+
* Uses sql.js (pure JS/WASM) to read state.vscdb — no native compilation needed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { homedir, platform } from "node:os";
|
|
11
|
+
import initSqlJs from "sql.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the platform-specific path to Windsurf's state.vscdb.
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
export function getDbPath() {
|
|
18
|
+
const plat = platform();
|
|
19
|
+
const home = homedir();
|
|
20
|
+
|
|
21
|
+
if (plat === "darwin") {
|
|
22
|
+
return join(home, "Library", "Application Support", "Windsurf", "User", "globalStorage", "state.vscdb");
|
|
23
|
+
} else if (plat === "win32") {
|
|
24
|
+
const appdata = process.env.APPDATA || "";
|
|
25
|
+
if (!appdata) throw new Error("Cannot determine APPDATA path");
|
|
26
|
+
return join(appdata, "Windsurf", "User", "globalStorage", "state.vscdb");
|
|
27
|
+
} else {
|
|
28
|
+
// Linux
|
|
29
|
+
const config = process.env.XDG_CONFIG_HOME || join(home, ".config");
|
|
30
|
+
return join(config, "Windsurf", "User", "globalStorage", "state.vscdb");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extract API Key from Windsurf state.vscdb.
|
|
36
|
+
* @param {string} [dbPath]
|
|
37
|
+
* @returns {Promise<{ api_key?: string, db_path: string, error?: string, hint?: string }>}
|
|
38
|
+
*/
|
|
39
|
+
export async function extractKey(dbPath) {
|
|
40
|
+
if (!dbPath) {
|
|
41
|
+
dbPath = getDbPath();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!existsSync(dbPath)) {
|
|
45
|
+
return {
|
|
46
|
+
error: `Windsurf database not found: ${dbPath}`,
|
|
47
|
+
hint: "Ensure Windsurf is installed and logged in.",
|
|
48
|
+
db_path: dbPath,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let db;
|
|
53
|
+
try {
|
|
54
|
+
const SQL = await initSqlJs();
|
|
55
|
+
const buf = readFileSync(dbPath);
|
|
56
|
+
db = new SQL.Database(buf);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
return { error: `Failed to open database: ${e.message}`, db_path: dbPath };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const stmt = db.prepare("SELECT value FROM ItemTable WHERE key = 'windsurfAuthStatus'");
|
|
63
|
+
if (!stmt.step()) {
|
|
64
|
+
stmt.free();
|
|
65
|
+
return {
|
|
66
|
+
error: "windsurfAuthStatus record not found",
|
|
67
|
+
hint: "Ensure Windsurf is logged in.",
|
|
68
|
+
db_path: dbPath,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const row = stmt.getAsObject();
|
|
73
|
+
stmt.free();
|
|
74
|
+
|
|
75
|
+
let data;
|
|
76
|
+
try {
|
|
77
|
+
data = JSON.parse(row.value);
|
|
78
|
+
} catch {
|
|
79
|
+
return { error: "windsurfAuthStatus data parse failed", db_path: dbPath };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const apiKey = data.apiKey || "";
|
|
83
|
+
if (!apiKey) {
|
|
84
|
+
return { error: "apiKey field is empty", db_path: dbPath };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { api_key: apiKey, db_path: dbPath };
|
|
88
|
+
} catch (e) {
|
|
89
|
+
return { error: `Extraction failed: ${e.message}`, db_path: dbPath };
|
|
90
|
+
} finally {
|
|
91
|
+
db.close();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
export const PROJECT_PATH_REQUIRED_MESSAGE =
|
|
6
|
+
"project_path is required. Pass the absolute path to the project root directory.";
|
|
7
|
+
|
|
8
|
+
export const projectPathSchema = z
|
|
9
|
+
.string()
|
|
10
|
+
.trim()
|
|
11
|
+
.min(1, PROJECT_PATH_REQUIRED_MESSAGE);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validate the project root path provided to fast_context_search.
|
|
15
|
+
* Returns null when valid, otherwise an MCP-friendly error string.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} projectPath
|
|
18
|
+
* @param {(path: string) => import("node:fs").Stats} [statFn]
|
|
19
|
+
* @returns {string|null}
|
|
20
|
+
*/
|
|
21
|
+
export function validateProjectPath(projectPath, statFn = statSync) {
|
|
22
|
+
if (!projectPath) {
|
|
23
|
+
return `Error: ${PROJECT_PATH_REQUIRED_MESSAGE}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!isAbsolute(projectPath)) {
|
|
27
|
+
return `Error: project_path must be an absolute path, got: ${projectPath}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const st = statFn(projectPath);
|
|
32
|
+
if (!st.isDirectory()) {
|
|
33
|
+
return `Error: project_path is not a directory: ${projectPath}`;
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error?.code === "ENOENT") {
|
|
37
|
+
return `Error: project_path does not exist: ${projectPath}`;
|
|
38
|
+
}
|
|
39
|
+
if (error?.code === "EACCES" || error?.code === "EPERM") {
|
|
40
|
+
return `Error: cannot access project_path (${error.code}): ${projectPath}`;
|
|
41
|
+
}
|
|
42
|
+
const reason = error?.message ? `${error.code || "UNKNOWN"}: ${error.message}` : String(error);
|
|
43
|
+
return `Error: failed to validate project_path: ${reason}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return null;
|
|
47
|
+
}
|
package/src/protobuf.mjs
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-written Protobuf encoder/decoder + Connect-RPC frame handling.
|
|
3
|
+
*
|
|
4
|
+
* Matches the Windsurf wire format exactly.
|
|
5
|
+
* Python bytearray → Node.js Buffer
|
|
6
|
+
* struct.pack(">I", len) → buf.writeUInt32BE
|
|
7
|
+
* gzip.compress/decompress → zlib.gzipSync/gunzipSync
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { gzipSync, gunzipSync } from "node:zlib";
|
|
11
|
+
|
|
12
|
+
// ─── Protobuf Encoder ──────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export class ProtobufEncoder {
|
|
15
|
+
constructor() {
|
|
16
|
+
/** @type {Buffer[]} */
|
|
17
|
+
this._chunks = [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Encode an unsigned varint into a Buffer.
|
|
22
|
+
* @param {number} value
|
|
23
|
+
* @returns {Buffer}
|
|
24
|
+
*/
|
|
25
|
+
_varint(value) {
|
|
26
|
+
const bytes = [];
|
|
27
|
+
while (value > 0x7f) {
|
|
28
|
+
bytes.push((value & 0x7f) | 0x80);
|
|
29
|
+
value >>>= 7;
|
|
30
|
+
}
|
|
31
|
+
bytes.push(value & 0x7f);
|
|
32
|
+
return Buffer.from(bytes);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Encode a field tag.
|
|
37
|
+
* @param {number} field
|
|
38
|
+
* @param {number} wire
|
|
39
|
+
* @returns {Buffer}
|
|
40
|
+
*/
|
|
41
|
+
_tag(field, wire) {
|
|
42
|
+
return this._varint((field << 3) | wire);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Write a varint field.
|
|
47
|
+
* @param {number} field
|
|
48
|
+
* @param {number} value
|
|
49
|
+
* @returns {ProtobufEncoder}
|
|
50
|
+
*/
|
|
51
|
+
writeVarint(field, value) {
|
|
52
|
+
this._chunks.push(this._tag(field, 0), this._varint(value));
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Write a length-delimited string field.
|
|
58
|
+
* @param {number} field
|
|
59
|
+
* @param {string} value
|
|
60
|
+
* @returns {ProtobufEncoder}
|
|
61
|
+
*/
|
|
62
|
+
writeString(field, value) {
|
|
63
|
+
const data = Buffer.from(value, "utf-8");
|
|
64
|
+
this._chunks.push(this._tag(field, 2), this._varint(data.length), data);
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Write a length-delimited bytes field.
|
|
70
|
+
* @param {number} field
|
|
71
|
+
* @param {Buffer|Uint8Array} value
|
|
72
|
+
* @returns {ProtobufEncoder}
|
|
73
|
+
*/
|
|
74
|
+
writeBytes(field, value) {
|
|
75
|
+
const buf = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
76
|
+
this._chunks.push(this._tag(field, 2), this._varint(buf.length), buf);
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Write a nested message field.
|
|
82
|
+
* @param {number} field
|
|
83
|
+
* @param {ProtobufEncoder} sub
|
|
84
|
+
* @returns {ProtobufEncoder}
|
|
85
|
+
*/
|
|
86
|
+
writeMessage(field, sub) {
|
|
87
|
+
const data = sub.toBuffer();
|
|
88
|
+
this._chunks.push(this._tag(field, 2), this._varint(data.length), data);
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Return the encoded bytes as a Buffer.
|
|
94
|
+
* @returns {Buffer}
|
|
95
|
+
*/
|
|
96
|
+
toBuffer() {
|
|
97
|
+
return Buffer.concat(this._chunks);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Varint Decode ─────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Decode a varint from a buffer at the given offset.
|
|
105
|
+
* @param {Buffer} buf
|
|
106
|
+
* @param {number} offset
|
|
107
|
+
* @returns {[number, number]} [value, newOffset]
|
|
108
|
+
*/
|
|
109
|
+
export function decodeVarint(buf, offset) {
|
|
110
|
+
let value = 0;
|
|
111
|
+
let shift = 0;
|
|
112
|
+
while (offset < buf.length) {
|
|
113
|
+
const b = buf[offset++];
|
|
114
|
+
value |= (b & 0x7f) << shift;
|
|
115
|
+
shift += 7;
|
|
116
|
+
if (!(b & 0x80)) break;
|
|
117
|
+
}
|
|
118
|
+
return [value, offset];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── Protobuf String Extraction ────────────────────────────
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Extract all UTF-8 strings (length > 5) from raw protobuf data
|
|
125
|
+
* by parsing wire types. Matches Python proto_extract_strings().
|
|
126
|
+
* @param {Buffer} data
|
|
127
|
+
* @returns {string[]}
|
|
128
|
+
*/
|
|
129
|
+
export function extractStrings(data) {
|
|
130
|
+
const strings = [];
|
|
131
|
+
let i = 0;
|
|
132
|
+
while (i < data.length) {
|
|
133
|
+
// Read tag varint
|
|
134
|
+
let tag = 0;
|
|
135
|
+
let shift = 0;
|
|
136
|
+
while (i < data.length) {
|
|
137
|
+
const b = data[i++];
|
|
138
|
+
tag |= (b & 0x7f) << shift;
|
|
139
|
+
shift += 7;
|
|
140
|
+
if (!(b & 0x80)) break;
|
|
141
|
+
}
|
|
142
|
+
const wire = tag & 0x7;
|
|
143
|
+
if (wire === 0) {
|
|
144
|
+
// Varint — skip
|
|
145
|
+
while (i < data.length) {
|
|
146
|
+
const b = data[i++];
|
|
147
|
+
if (!(b & 0x80)) break;
|
|
148
|
+
}
|
|
149
|
+
} else if (wire === 1) {
|
|
150
|
+
// 64-bit fixed
|
|
151
|
+
i += 8;
|
|
152
|
+
} else if (wire === 2) {
|
|
153
|
+
// Length-delimited
|
|
154
|
+
let length = 0;
|
|
155
|
+
shift = 0;
|
|
156
|
+
while (i < data.length) {
|
|
157
|
+
const b = data[i++];
|
|
158
|
+
length |= (b & 0x7f) << shift;
|
|
159
|
+
shift += 7;
|
|
160
|
+
if (!(b & 0x80)) break;
|
|
161
|
+
}
|
|
162
|
+
if (i + length <= data.length) {
|
|
163
|
+
const raw = data.subarray(i, i + length);
|
|
164
|
+
try {
|
|
165
|
+
const text = raw.toString("utf-8");
|
|
166
|
+
if (text.length > 5) {
|
|
167
|
+
strings.push(text);
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// Not valid UTF-8, skip
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
i += length;
|
|
174
|
+
} else if (wire === 5) {
|
|
175
|
+
// 32-bit fixed
|
|
176
|
+
i += 4;
|
|
177
|
+
} else {
|
|
178
|
+
// Unknown wire type — stop
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return strings;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── Connect-RPC Frame Encode/Decode ───────────────────────
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Encode protobuf bytes into a gzip-compressed Connect-RPC frame.
|
|
189
|
+
* Frame format: 1-byte flags + 4-byte big-endian length + payload
|
|
190
|
+
* @param {Buffer} protoBytes
|
|
191
|
+
* @param {boolean} [compress=true]
|
|
192
|
+
* @returns {Buffer}
|
|
193
|
+
*/
|
|
194
|
+
export function connectFrameEncode(protoBytes, compress = true) {
|
|
195
|
+
let payload;
|
|
196
|
+
let flags;
|
|
197
|
+
if (compress) {
|
|
198
|
+
payload = gzipSync(protoBytes);
|
|
199
|
+
flags = 1; // gzip compressed
|
|
200
|
+
} else {
|
|
201
|
+
payload = protoBytes;
|
|
202
|
+
flags = 0;
|
|
203
|
+
}
|
|
204
|
+
const header = Buffer.alloc(5);
|
|
205
|
+
header[0] = flags;
|
|
206
|
+
header.writeUInt32BE(payload.length, 1);
|
|
207
|
+
return Buffer.concat([header, payload]);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Decode Connect-RPC frames from raw response data.
|
|
212
|
+
* Handles gzip-compressed frames (flags 1 or 3).
|
|
213
|
+
* @param {Buffer} data
|
|
214
|
+
* @returns {Buffer[]}
|
|
215
|
+
*/
|
|
216
|
+
export function connectFrameDecode(data) {
|
|
217
|
+
const frames = [];
|
|
218
|
+
let i = 0;
|
|
219
|
+
while (i + 5 <= data.length) {
|
|
220
|
+
const flags = data[i];
|
|
221
|
+
const length = data.readUInt32BE(i + 1);
|
|
222
|
+
i += 5;
|
|
223
|
+
let payload = data.subarray(i, i + length);
|
|
224
|
+
i += length;
|
|
225
|
+
if (flags === 1 || flags === 3) {
|
|
226
|
+
try {
|
|
227
|
+
payload = gunzipSync(payload);
|
|
228
|
+
} catch {
|
|
229
|
+
// Decompression failed — use raw payload
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
frames.push(Buffer.from(payload));
|
|
233
|
+
}
|
|
234
|
+
return frames;
|
|
235
|
+
}
|