@philau2512/fast-context-mcp 1.5.3

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.
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Windsurf / Devin 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
+ * Lookup order (auto-detect):
8
+ * 1) Devin CLI credentials.toml on Linux/WSL
9
+ * 2) Windsurf / Devin state.vscdb candidates
10
+ * Prefer keys that start with `devin-session-token$`.
11
+ */
12
+
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { homedir, platform } from "node:os";
16
+ import initSqlJs from "sql.js";
17
+
18
+ const TOML_API_KEY_FIELDS = [
19
+ "api_key",
20
+ "apiKey",
21
+ "devin_api_key",
22
+ "devinApiKey",
23
+ "windsurf_api_key",
24
+ "windsurfApiKey",
25
+ "access_token",
26
+ "accessToken",
27
+ "token",
28
+ ];
29
+
30
+ /** Official app folder names only — Windsurf and Devin (never "Deviv"). */
31
+ const APP_NAMES_MAC_WIN = ["Windsurf", "Devin"];
32
+
33
+ /**
34
+ * Resolve APPDATA with injectable override.
35
+ * Explicit `deps.appdata` (including empty string) wins over process.env.
36
+ * @param {Object} [deps]
37
+ * @returns {string}
38
+ */
39
+ function resolveAppdata(deps = {}) {
40
+ if (Object.prototype.hasOwnProperty.call(deps, "appdata")) {
41
+ return deps.appdata || "";
42
+ }
43
+ return process.env.APPDATA || "";
44
+ }
45
+
46
+ /**
47
+ * Get all candidate paths to state.vscdb across Windsurf and Devin installs.
48
+ *
49
+ * Windsurf was succeeded by Devin (same team). DB schema is unchanged
50
+ * (`ItemTable`, `windsurfAuthStatus`); only the app directory name differs.
51
+ *
52
+ * @param {Object} [deps]
53
+ * @param {string} [deps.platform]
54
+ * @param {string} [deps.home]
55
+ * @param {string} [deps.xdgConfigHome]
56
+ * @param {string} [deps.appdata]
57
+ * @returns {string[]}
58
+ */
59
+ export function getDbPathCandidates(deps = {}) {
60
+ const plat = deps.platform ?? platform();
61
+ const home = deps.home ?? homedir();
62
+ const xdg = deps.xdgConfigHome ?? process.env.XDG_CONFIG_HOME;
63
+ const out = [];
64
+
65
+ if (plat === "darwin") {
66
+ const appSupp = join(home, "Library", "Application Support");
67
+ for (const appName of APP_NAMES_MAC_WIN) {
68
+ out.push(join(appSupp, appName, "User", "globalStorage", "state.vscdb"));
69
+ }
70
+ } else if (plat === "win32") {
71
+ const appdata = resolveAppdata(deps);
72
+ if (!appdata) {
73
+ throw new Error("Cannot determine APPDATA path");
74
+ }
75
+ for (const appName of APP_NAMES_MAC_WIN) {
76
+ out.push(join(appdata, appName, "User", "globalStorage", "state.vscdb"));
77
+ }
78
+ } else {
79
+ // Linux: Windsurf (Title Case) + devin (lowercase observed) + Devin (Title)
80
+ const config = xdg || join(home, ".config");
81
+ for (const appName of ["Windsurf", "devin", "Devin"]) {
82
+ out.push(join(config, appName, "User", "globalStorage", "state.vscdb"));
83
+ }
84
+ }
85
+ return out;
86
+ }
87
+
88
+ /**
89
+ * Backward-compatible: return the first candidate (legacy Windsurf path).
90
+ * @param {Object} [deps]
91
+ * @returns {string}
92
+ */
93
+ export function getDbPath(deps) {
94
+ return getDbPathCandidates(deps)[0];
95
+ }
96
+
97
+ /**
98
+ * Linux/WSL Devin CLI credential candidates.
99
+ * @param {Object} [deps]
100
+ * @param {string} [deps.platform]
101
+ * @param {string} [deps.home]
102
+ * @returns {string[]}
103
+ */
104
+ export function getCliCredentialPathCandidates(deps = {}) {
105
+ const plat = deps.platform ?? platform();
106
+ const home = deps.home ?? homedir();
107
+ if (plat !== "linux") return [];
108
+ return [join(home, ".local", "share", "devin", "credentials.toml")];
109
+ }
110
+
111
+ /**
112
+ * Credential sources in lookup order: CLI toml first, then SQLite DBs.
113
+ * @param {Object} [deps]
114
+ * @returns {{ type: "toml" | "sqlite", path: string }[]}
115
+ */
116
+ export function getCredentialSources(deps = {}) {
117
+ const tomlSources = getCliCredentialPathCandidates(deps).map((path) => ({
118
+ type: "toml",
119
+ path,
120
+ }));
121
+ const sqliteSources = getDbPathCandidates(deps).map((path) => ({
122
+ type: "sqlite",
123
+ path,
124
+ }));
125
+ return [...tomlSources, ...sqliteSources];
126
+ }
127
+
128
+ /**
129
+ * Extract an API key from Devin CLI credentials.toml content.
130
+ * @param {string} text
131
+ * @returns {string}
132
+ */
133
+ export function extractApiKeyFromToml(text) {
134
+ for (const field of TOML_API_KEY_FIELDS) {
135
+ const match = text.match(
136
+ new RegExp(`^\\s*${field}\\s*=\\s*(?:"([^"]+)"|'([^']+)'|([^\\s#]+))`, "m"),
137
+ );
138
+ const value = (match?.[1] || match?.[2] || match?.[3] || "").trim();
139
+ if (value) return value;
140
+ }
141
+
142
+ const fallback = text.match(/\bsk-[A-Za-z0-9_-]+\b/);
143
+ return fallback ? fallback[0] : "";
144
+ }
145
+
146
+ /**
147
+ * @param {string} key
148
+ * @returns {boolean}
149
+ */
150
+ function isPreferredApiKey(key) {
151
+ return typeof key === "string" && key.startsWith("devin-session-token$");
152
+ }
153
+
154
+ /**
155
+ * @param {string} credentialsPath
156
+ * @returns {{ api_key?: string, db_path: string, source_type: string, error?: string, hint?: string }}
157
+ */
158
+ function extractKeyFromToml(credentialsPath) {
159
+ if (!existsSync(credentialsPath)) {
160
+ return {
161
+ error: `Devin CLI credentials not found: ${credentialsPath}`,
162
+ hint: "Run `devin login` inside WSL/Linux, then retry.",
163
+ db_path: credentialsPath,
164
+ source_type: "devin_cli_credentials",
165
+ };
166
+ }
167
+
168
+ let text;
169
+ try {
170
+ text = readFileSync(credentialsPath, "utf8");
171
+ } catch (e) {
172
+ return {
173
+ error: `Failed to read Devin CLI credentials: ${e.message}`,
174
+ db_path: credentialsPath,
175
+ source_type: "devin_cli_credentials",
176
+ };
177
+ }
178
+
179
+ const apiKey = extractApiKeyFromToml(text);
180
+ if (!apiKey) {
181
+ return {
182
+ error: "Devin CLI credentials did not contain an API key",
183
+ hint: "Run `devin login` inside WSL/Linux, then retry.",
184
+ db_path: credentialsPath,
185
+ source_type: "devin_cli_credentials",
186
+ };
187
+ }
188
+
189
+ return {
190
+ api_key: apiKey,
191
+ db_path: credentialsPath,
192
+ source_type: "devin_cli_credentials",
193
+ };
194
+ }
195
+
196
+ /**
197
+ * @param {string} dbPath
198
+ * @returns {Promise<{ api_key?: string, db_path: string, source_type?: string, error?: string, hint?: string }>}
199
+ */
200
+ async function readApiKeyFromDb(dbPath) {
201
+ if (!existsSync(dbPath)) {
202
+ return {
203
+ error: `Windsurf / Devin database not found: ${dbPath}`,
204
+ hint: "Ensure Windsurf or Devin is installed and logged in.",
205
+ db_path: dbPath,
206
+ source_type: "sqlite",
207
+ };
208
+ }
209
+
210
+ let db;
211
+ try {
212
+ const SQL = await initSqlJs();
213
+ const buf = readFileSync(dbPath);
214
+ db = new SQL.Database(buf);
215
+ } catch (e) {
216
+ return {
217
+ error: `Failed to open database: ${e.message}`,
218
+ db_path: dbPath,
219
+ source_type: "sqlite",
220
+ };
221
+ }
222
+
223
+ try {
224
+ const stmt = db.prepare("SELECT value FROM ItemTable WHERE key = 'windsurfAuthStatus'");
225
+ if (!stmt.step()) {
226
+ stmt.free();
227
+ return {
228
+ error: "windsurfAuthStatus record not found",
229
+ hint: "Ensure Windsurf or Devin is logged in.",
230
+ db_path: dbPath,
231
+ source_type: "sqlite",
232
+ };
233
+ }
234
+
235
+ const row = stmt.getAsObject();
236
+ stmt.free();
237
+
238
+ let data;
239
+ try {
240
+ data = JSON.parse(row.value);
241
+ } catch {
242
+ return {
243
+ error: "windsurfAuthStatus data parse failed",
244
+ db_path: dbPath,
245
+ source_type: "sqlite",
246
+ };
247
+ }
248
+
249
+ const apiKey = data.apiKey || "";
250
+ if (!apiKey) {
251
+ return {
252
+ error: "apiKey field is empty",
253
+ db_path: dbPath,
254
+ source_type: "sqlite",
255
+ };
256
+ }
257
+
258
+ return {
259
+ api_key: apiKey,
260
+ db_path: dbPath,
261
+ source_type: "sqlite",
262
+ };
263
+ } catch (e) {
264
+ return {
265
+ error: `Extraction failed: ${e.message}`,
266
+ db_path: dbPath,
267
+ source_type: "sqlite",
268
+ };
269
+ } finally {
270
+ db.close();
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Extract API Key from Windsurf / Devin credential sources.
276
+ *
277
+ * Auto-detect scans CLI toml (Linux/WSL) then SQLite candidates and prefers
278
+ * `devin-session-token$` keys. Explicit `dbPath` skips auto order
279
+ * (`.toml` → toml reader, otherwise SQLite).
280
+ *
281
+ * @param {string} [dbPath]
282
+ * @param {Object} [deps]
283
+ * @returns {Promise<{ api_key?: string, db_path: string, source_type?: string, error?: string, hint?: string, tried_paths?: string[] }>}
284
+ */
285
+ export async function extractKey(dbPath, deps) {
286
+ // Explicit path: always hit that source (even if missing) for precise errors.
287
+ if (dbPath) {
288
+ const type = dbPath.endsWith(".toml") ? "toml" : "sqlite";
289
+ const result =
290
+ type === "toml" ? extractKeyFromToml(dbPath) : await readApiKeyFromDb(dbPath);
291
+ return { ...result, tried_paths: [dbPath] };
292
+ }
293
+
294
+ const sources = getCredentialSources(deps);
295
+ const tried_paths = sources.map((s) => s.path);
296
+ const existing = sources.filter((s) => existsSync(s.path));
297
+
298
+ if (!existing.length) {
299
+ return {
300
+ error: `Windsurf / Devin credential source not found. Tried:\n${tried_paths.map((p) => ` - ${p}`).join("\n")}`,
301
+ hint: "Ensure Windsurf/Devin is installed and logged in, or run `devin login` on Linux/WSL.",
302
+ db_path: sources[0]?.path || "",
303
+ tried_paths,
304
+ };
305
+ }
306
+
307
+ let firstUsable = null;
308
+ const failures = [];
309
+
310
+ for (const source of existing) {
311
+ const result =
312
+ source.type === "toml"
313
+ ? extractKeyFromToml(source.path)
314
+ : await readApiKeyFromDb(source.path);
315
+
316
+ if (result.api_key) {
317
+ if (isPreferredApiKey(result.api_key)) {
318
+ return { ...result, tried_paths };
319
+ }
320
+ if (!firstUsable) firstUsable = result;
321
+ } else {
322
+ failures.push(result);
323
+ }
324
+ }
325
+
326
+ if (firstUsable) return { ...firstUsable, tried_paths };
327
+
328
+ return {
329
+ error: `No usable apiKey found. Tried:\n${failures.map((r) => ` - ${r.db_path}: ${r.error}`).join("\n")}`,
330
+ hint: "Ensure Windsurf/Devin is logged in, or run `devin login` on Linux/WSL.",
331
+ db_path: existing[0]?.path || sources[0]?.path || "",
332
+ tried_paths,
333
+ };
334
+ }
@@ -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
+ }
@@ -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
+ }