
Fast Context
- 44 installs
- 20 repo stars
- Updated June 5, 2026
- oulkurt/fast-context-skill
Helps with ai & agent building tasks.
About
fast-context is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- fast-context
- AI & Agent Building
- AI-coding skill
Fast Context by the numbers
- 44 all-time installs (skills.sh)
- Ranked #7,851 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oulkurt/fast-context-skill --skill fast-contextAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 44 |
|---|---|
| repo stars | ★ 20 |
| Last updated | June 5, 2026 |
| Repository | oulkurt/fast-context-skill ↗ |
What it does
Helps with ai & agent building tasks.
Files
Fast Context
Overview
Use Fast Context as the first pass for codebase-aware work: locating implementation paths, tracing flows, finding tests, and collecting the complete definitions needed before producing code or project-specific advice.
This skill runs scripts/fast-context-search.mjs directly from the installed skill directory. The script uses vendored Fast Context core logic from the upstream v1.3.0-beta.2 source and Windsurf Devstral, but it does not require the Fast Context MCP server to be installed or enabled. It does not replace exact local inspection; use returned file paths and grep keywords to drive targeted rg, sed, and file reads.
When To Use
- Before code changes, architecture suggestions, bug fixes, test plans, or explanations that depend on project-specific behavior.
- When the relevant files are unknown or the feature crosses several modules.
- When you need full definitions, signatures, call sites, configuration, routes, schemas, or tests.
Skip this skill for purely conversational tasks, tiny known-file edits, or third-party library documentation lookups. Use a documentation retrieval skill such as Context7 for current external API docs.
Workflow
1. Build one natural-language query that names the user goal and asks for the exact context needed. Prefer "Where", "What", and "How" questions. 2. Resolve the installed skill directory and run node <skill-directory>/scripts/fast-context-search.mjs with --project set to the absolute project root and --query set to the natural-language search query. 3. Read the returned file ranges and use the suggested grep keywords with local tools to inspect exact definitions. 4. Run a completeness check: make sure all relevant classes, functions, variables, types, config, routes, call sites, and tests have been identified with full signatures. 5. If anything is missing, recursively query Fast Context again with the missing concept, symbol, or flow. 6. If ambiguity remains after retrieval, ask the user guiding questions before changing behavior.
Default Command
node /path/to/installed/fast-context/scripts/fast-context-search.mjs \
--project "/absolute/path/to/project" \
--query "Where is <feature or behavior> implemented, and what full definitions, signatures, call sites, config, and tests are relevant to <user goal>?" \
--max-results 10 \
--max-turns 3 \
--tree-depth 0 \
--exclude ".git,node_modules,dist,build,coverage"Use max_results 3-5 for focused symbol lookup, 10-20 for feature tracing, and 20-30 for broad exploration. Use max_turns 1 for quick lookup and 4-5 for complex cross-module tracing.
Query Patterns
- Locate implementation:
Where is <feature> implemented? Include entry points, core functions/classes, data types, config, and tests. - Trace flow:
How does <user action or API call> flow through the codebase from entry point to persistence/output? Include full definitions and call sites. - Prepare a change:
What code must change to <requirement>? Find current behavior, extension points, validation, tests, and adjacent risks. - Find tests:
What tests cover <behavior>, and where should new coverage be added? Include fixtures and helpers.
Tuning And Fallback
- If results are too shallow, increase
max_turnsormax_results, or query a narrower symbol or flow. - If the repo map is too large, reduce
tree_depth, addexclude_paths, or userepo_map_mode: "bootstrap_hotspot". - If authentication fails, run the same script with
--check-keyto verify local Windsurf key discovery, then retry after Windsurf is logged in orWINDSURF_API_KEYis set. - If the user explicitly needs the key, use
--print-keyto print it locally or--key-envto print anexport WINDSURF_API_KEY=...command for the current shell. Treat the output as a secret. - If Node dependencies are missing, run
npm installin the installed Fast Context skill directory. - If the script is unavailable or still fails, state that briefly and continue with local
rg --files,rg, and targeted file reads.
Reference
For the exact script parameter contract, response shape, and tuning notes, read references/script-contract.md only when needed.
name: Publish to npm
on:
workflow_dispatch:
push:
tags:
- "v*.*.*"
schedule:
- cron: "17 3 * * 1"
permissions:
contents: read
id-token: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
registry-url: "https://registry.npmjs.org"
- name: Install dependencies
run: npm ci
- name: Test
run: npm test
- name: Check package version
id: version
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
shell: bash
run: |
PKG="$(node -p "require('./package.json').name")"
VERSION="$(node -p "require('./package.json').version")"
echo "pkg=${PKG}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
if [[ -z "${NODE_AUTH_TOKEN}" ]]; then
echo "publish=false" >> "$GITHUB_OUTPUT"
echo "::warning::NPM_TOKEN is not configured; skipping npm publish."
elif npm view "${PKG}@${VERSION}" version >/dev/null 2>&1; then
echo "publish=false" >> "$GITHUB_OUTPUT"
echo "${PKG}@${VERSION} already exists on npm; skipping publish."
else
echo "publish=true" >> "$GITHUB_OUTPUT"
echo "${PKG}@${VERSION} is not on npm yet; publishing is allowed."
fi
- name: Publish
if: steps.version.outputs.publish == 'true'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
shell: bash
run: |
PKG="${{ steps.version.outputs.pkg }}"
EXTRA_ARGS=()
if [[ "${PKG}" == @*/* ]]; then
EXTRA_ARGS+=(--access public)
fi
npm publish --provenance "${EXTRA_ARGS[@]}"
node_modules/
.DS_Store
*.log
npm-debug.log*
.env
.env.*
!.env.example
coverage/
dist/
interface:
display_name: "Fast Context"
short_description: "Semantic code context retrieval"
default_prompt: "Use $fast-context to gather complete codebase context before planning this change."
policy:
allow_implicit_invocation: true
MIT License
Copyright (c) 2026 Mizu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Notice
This project is a Codex Skill and CLI adaptation of:
- Repository: https://github.com/SammySnake-d/fast-context-mcp
- Upstream version used for vendored core files:
v1.3.0-beta.2 - Upstream commit:
af65ce77a408656c815444397ef6892c47a96c0a - Upstream license: MIT, preserved at
scripts/lib/LICENSE.fast-context-mcp
The MCP server wrapper is not used by this package. The runtime entry point is scripts/fast-context-search.mjs, which calls the vendored core search code directly.
{
"name": "fast-context-skill",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "fast-context-skill",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@vscode/ripgrep": "^1.15.9",
"sql.js": "^1.14.0",
"tree-node-cli": "^1.6.0"
},
"bin": {
"fast-context-skill": "scripts/fast-context-search.mjs"
}
},
"node_modules/@vscode/ripgrep": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.18.0.tgz",
"integrity": "sha512-ns5lWe44tSfbTMbVUsyB+I1819PVSw4AdpgK0RNkzfWfwy6+3IUNSxwSrfTno1/oWaS/hERNz+XLWVyga2aJBQ==",
"license": "MIT",
"optionalDependencies": {
"@vscode/ripgrep-darwin-arm64": "1.18.0",
"@vscode/ripgrep-darwin-x64": "1.18.0",
"@vscode/ripgrep-linux-arm": "1.18.0",
"@vscode/ripgrep-linux-arm64": "1.18.0",
"@vscode/ripgrep-linux-ia32": "1.18.0",
"@vscode/ripgrep-linux-ppc64": "1.18.0",
"@vscode/ripgrep-linux-riscv64": "1.18.0",
"@vscode/ripgrep-linux-s390x": "1.18.0",
"@vscode/ripgrep-linux-x64": "1.18.0",
"@vscode/ripgrep-win32-arm64": "1.18.0",
"@vscode/ripgrep-win32-ia32": "1.18.0",
"@vscode/ripgrep-win32-x64": "1.18.0"
}
},
"node_modules/@vscode/ripgrep-darwin-arm64": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-darwin-arm64/-/ripgrep-darwin-arm64-1.18.0.tgz",
"integrity": "sha512-r3ktHSvbFycQNF6sl7sNDPocpsI7J+mEzh1IaZFkY0spm3k2Z9t8hPAeOK7+p0l6p6/swkQC14XWX01low+94Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@vscode/ripgrep-darwin-x64": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-darwin-x64/-/ripgrep-darwin-x64-1.18.0.tgz",
"integrity": "sha512-25b4gWbL138dGuQU244ebCKKc0q05ULBMoFSz9oAEUHNeqK/lOJViDS7DRvbDazzAzSEdan391Znks/R5mkaTQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@vscode/ripgrep-linux-arm": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-arm/-/ripgrep-linux-arm-1.18.0.tgz",
"integrity": "sha512-GDAvufNDHu8zqLEmXstalQF0Wh6wQvdsBi/Vg3Yi3CK4a8XoFXqqXVEHEZ9xQz3t0NfoSEc9JbvK9DDS6FxyxQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@vscode/ripgrep-linux-arm64": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-arm64/-/ripgrep-linux-arm64-1.18.0.tgz",
"integrity": "sha512-lQ/5zTG++U0E3IhVgS4EPTTn/U4okncaRMM5GOFfOYZywS4nuD31GhkHbNYlDk5CuDC68+hYJ0/eQeyCKJDA+g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@vscode/ripgrep-linux-ia32": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-ia32/-/ripgrep-linux-ia32-1.18.0.tgz",
"integrity": "sha512-YWLkSUtFd4Jh5EepIhA9RJSfv3uMAVMo+2rBIGHPBnvgLrZciIs2cDKei1/p6Wc/aCzUoHyMAg2R6tw4ZCBKGg==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@vscode/ripgrep-linux-ppc64": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-ppc64/-/ripgrep-linux-ppc64-1.18.0.tgz",
"integrity": "sha512-quXVY8fwQ8O/lvU1yrSqSl3jlUzysRSb+AfUfCL/tRtphxsKlFvPAejryZ6vg4Bgvn8XL74xb4qMCDmWgYrT5w==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@vscode/ripgrep-linux-riscv64": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-riscv64/-/ripgrep-linux-riscv64-1.18.0.tgz",
"integrity": "sha512-f5kBQBrWfQt8Q7OhSORuNDei5dkYagBj3y4jImSUXGMy8B/Ke7SltSRcUtjPv166FAFfHCAmWuZp3+cWnX2/Vw==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@vscode/ripgrep-linux-s390x": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-s390x/-/ripgrep-linux-s390x-1.18.0.tgz",
"integrity": "sha512-rTOcJFGGcl2c07RUOWUo4U1ndnemKhY6A9hnMB18uk7jSgJc0d/QLBGWMWpumdtoJtpizn/wIv5mXIisJukusQ==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@vscode/ripgrep-linux-x64": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-x64/-/ripgrep-linux-x64-1.18.0.tgz",
"integrity": "sha512-mQ3bVrUpnD2vs7QT0vX90Lt0cnUq467uFtEktIdsJJmW296RoSULRGqWgzG1AKxyBpNDD6l4ZO4qKf6SgyC23Q==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@vscode/ripgrep-win32-arm64": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-win32-arm64/-/ripgrep-win32-arm64-1.18.0.tgz",
"integrity": "sha512-vfTIjq1OHnzUjxZcHVQAMbnggp8dpGf+0QKFOZHwWPqFwXxQC8eCWM+5NUdoJ6yrElCeMzoUTXoK/LdZaniB+Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@vscode/ripgrep-win32-ia32": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-win32-ia32/-/ripgrep-win32-ia32-1.18.0.tgz",
"integrity": "sha512-//rfAE+BOw5AC2EMmepmiE36jUuevtQYNQqqlw1s3m9FlRxjxEut97RkRPHAu9BG4mSojatZx+kXZXNdyI9caQ==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@vscode/ripgrep-win32-x64": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-win32-x64/-/ripgrep-win32-x64-1.18.0.tgz",
"integrity": "sha512-KNPvtElldqILHdnAetujPaowkNbpqJy3ssIGGN6F6Kve9Qi+nNLI2DN01O83JjCEVQbCzl8Ov3QZ9Eov3BR8Dg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/big-integer": {
"version": "1.6.52",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
"integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
"license": "Unlicense",
"engines": {
"node": ">=0.6"
}
},
"node_modules/binary": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
"integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==",
"license": "MIT",
"dependencies": {
"buffers": "~0.1.1",
"chainsaw": "~0.1.0"
},
"engines": {
"node": "*"
}
},
"node_modules/bluebird": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==",
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/buffer-indexof-polyfill": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
"integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==",
"license": "MIT",
"engines": {
"node": ">=0.10"
}
},
"node_modules/buffers": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
"integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==",
"engines": {
"node": ">=0.2.0"
}
},
"node_modules/chainsaw": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
"integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==",
"license": "MIT/X11",
"dependencies": {
"traverse": ">=0.3.0 <0.4"
},
"engines": {
"node": "*"
}
},
"node_modules/commander": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"license": "MIT"
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/duplexer2": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
"integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==",
"license": "BSD-3-Clause",
"dependencies": {
"readable-stream": "^2.0.2"
}
},
"node_modules/fast-folder-size": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/fast-folder-size/-/fast-folder-size-1.6.1.tgz",
"integrity": "sha512-F3tRpfkAzb7TT2JNKaJUglyuRjRa+jelQD94s9OSqkfEeytLmupCqQiD+H2KoIXGtp4pB5m4zNmv5m2Ktcr+LA==",
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"unzipper": "^0.10.11"
},
"bin": {
"fast-folder-size": "cli.js"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"license": "ISC"
},
"node_modules/fstream": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
"integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"graceful-fs": "^4.1.2",
"inherits": "~2.0.0",
"mkdirp": ">=0.5 0",
"rimraf": "2"
},
"engines": {
"node": ">=0.6"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/listenercount": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
"integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==",
"license": "ISC"
},
"node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/pretty-bytes": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
"integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"license": "ISC",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/sql.js": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz",
"integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==",
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/traverse": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
"integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==",
"license": "MIT/X11",
"engines": {
"node": "*"
}
},
"node_modules/tree-node-cli": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/tree-node-cli/-/tree-node-cli-1.6.0.tgz",
"integrity": "sha512-M8um5Lbl76rWU5aC8oOeEhruiCM29lFCKnwpxrwMjpRicHXJx+bb9Cak11G3zYLrMb6Glsrhnn90rHIzDJrjvg==",
"license": "MIT",
"dependencies": {
"commander": "^5.0.0",
"fast-folder-size": "1.6.1",
"pretty-bytes": "^5.6.0"
},
"bin": {
"tree": "bin/tree.js",
"treee": "bin/tree.js"
}
},
"node_modules/unzipper": {
"version": "0.10.14",
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz",
"integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==",
"license": "MIT",
"dependencies": {
"big-integer": "^1.6.17",
"binary": "~0.3.0",
"bluebird": "~3.4.1",
"buffer-indexof-polyfill": "~1.0.0",
"duplexer2": "~0.1.4",
"fstream": "^1.0.12",
"graceful-fs": "^4.2.2",
"listenercount": "~1.0.1",
"readable-stream": "~2.3.6",
"setimmediate": "~1.0.4"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
}
}
}
{
"name": "fast-context-skill",
"version": "0.1.0",
"description": "Agent Skill and CLI adaptation of fast-context-mcp that runs Windsurf Devstral semantic code search without an MCP server.",
"license": "MIT",
"type": "module",
"bin": {
"fast-context-skill": "scripts/fast-context-search.mjs"
},
"files": [
"SKILL.md",
"agents/",
"references/",
"scripts/",
"LICENSE",
"NOTICE.md",
"README.md"
],
"scripts": {
"search": "node scripts/fast-context-search.mjs",
"test": "node --check scripts/fast-context-search.mjs && node --check scripts/lib/core.mjs && node scripts/fast-context-search.mjs --help >/dev/null && node test/cli.mjs"
},
"repository": {
"type": "git",
"url": "git+https://github.com/oulkurt/fast-context-skill.git"
},
"bugs": {
"url": "https://github.com/oulkurt/fast-context-skill/issues"
},
"homepage": "https://github.com/oulkurt/fast-context-skill#readme",
"keywords": [
"agent-skill",
"skills-cli",
"codex",
"skill",
"fast-context",
"semantic-search",
"windsurf",
"devstral",
"code-search"
],
"dependencies": {
"@vscode/ripgrep": "^1.15.9",
"sql.js": "^1.14.0",
"tree-node-cli": "^1.6.0"
}
}
Fast Context Skill

Fast Context Skill is an Agent Skill and CLI adaptation of `SammySnake-d/fast-context-mcp`.
It is a fork-style rewrite of the original MCP project: the Windsurf Devstral semantic search core is vendored from upstream v1.3.0-beta.2, while the MCP server wrapper is removed. Any Skills-compatible agent that can run local scripts can use this Skill; Codex is one supported install target, not a requirement.
Prerequisites
- Install Windsurf desktop and log in once.
That is the only setup. The Skill reads the API key directly from Windsurf's local state DB (state.vscdb), so you do not need to keep Windsurf running, copy a key, or set any environment variable. Manual key handling is only required if you do not have Windsurf desktop installed (for example on CI or a remote server) — see Windsurf API Key.
Quick Start
1. Install the Skill (interactive — picks up agent targets from your local skills CLI):
npx skills add oulkurt/fast-context-skillFor a fully non-interactive install to every supported agent target:
npx skills add oulkurt/fast-context-skill --skill fast-context -a '*' -y2. (Optional) Verify the Windsurf key is auto-discovered:
npx --yes github:oulkurt/fast-context-skill --check-key3. Use Fast Context:
Invoke $fast-context or your agent's skill invocation convention, or run the CLI directly:
npx --yes github:oulkurt/fast-context-skill \
--project "/absolute/path/to/project" \
--query "Where is authentication implemented?"What Changed From The MCP Repo
- Keeps the upstream semantic search loop that talks to Windsurf Devstral.
- Keeps local command execution helpers for
rg, file reads, tree output, and context gathering. - Removes the MCP server entry point from the runtime path.
- Adds an agent-friendly
SKILL.mdworkflow. - Adds
scripts/fast-context-search.mjsas the direct CLI entry point. - Adds npm packaging for direct CLI use.
Install With skills CLI
One-command install via the open skills CLI:
npx skills add oulkurt/fast-context-skill --skill fast-context -yTo install to every agent target supported by your local skills CLI:
npx skills add oulkurt/fast-context-skill --skill fast-context -a '*' -yUse the -a flag when you want to target a specific agent supported by your skills CLI version. For Codex:
npx skills add oulkurt/fast-context-skill --skill fast-context -a codex -yFor a global Codex install specifically:
npx skills add oulkurt/fast-context-skill --skill fast-context -a codex -g -yManual install is agent-specific: clone this repository into the skill directory your agent reads, then run npm install from that directory. For Codex:
git clone https://github.com/oulkurt/fast-context-skill.git ~/.codex/skills/fast-context
cd ~/.codex/skills/fast-context
npm installThen invoke $fast-context or your agent's skill invocation convention. The Skill instructs the agent to run the bundled script from the installed skill directory, for example:
node /path/to/installed/fast-context/scripts/fast-context-search.mjs \
--project "/absolute/path/to/project" \
--query "Where is authentication implemented?"Use As A CLI
From a clone:
npm install
node scripts/fast-context-search.mjs \
--project "/absolute/path/to/project" \
--query "Where is the database connection pool configured?"From GitHub without cloning:
npx --yes github:oulkurt/fast-context-skill \
--project "/absolute/path/to/project" \
--query "Where is the database connection pool configured?"Windsurf API Key
This project still depends on Windsurf's Devstral backend. It replaces the MCP layer, not the Windsurf backend.
Most users can skip this section. If you have Windsurf desktop installed and have logged in once, the script auto-discovers the key from state.vscdb on every run — no env var, no copy/paste, and Windsurf does not need to be running.
Manual key handling is only needed when:
- You do not have Windsurf desktop installed (CI, remote server, container).
- You want to override the auto-discovered key with a different one.
- Auto-discovery fails and you need to verify or inspect the value.
No-clone one-liners:
npx --yes github:oulkurt/fast-context-skill --check-key # verify, masked output
npx --yes github:oulkurt/fast-context-skill --print-key # print full key
eval "$(npx --yes github:oulkurt/fast-context-skill --key-env)" # export to current shellTo persist the key across shells (only do this if you cannot rely on Windsurf desktop), add the export to your shell rc file, for example:
echo "export WINDSURF_API_KEY=$(npx --yes github:oulkurt/fast-context-skill --print-key)" >> ~/.zshrcAfter installing the Skill, the same commands work against the installed script path:
node /path/to/installed/fast-context/scripts/fast-context-search.mjs --check-key
node /path/to/installed/fast-context/scripts/fast-context-search.mjs --print-key
eval "$(node /path/to/installed/fast-context/scripts/fast-context-search.mjs --key-env)"For a project install, that path is .agents/skills/fast-context/scripts/fast-context-search.mjs. For a global Codex install, it is usually ~/.codex/skills/fast-context/scripts/fast-context-search.mjs. From a repository clone, use scripts/fast-context-search.mjs directly.
Treat the value like any other API secret:
- Do not commit it.
- Do not paste it into GitHub issues, README files, workflows, or logs.
Development
npm install
npm test
npm pack --dry-runAttribution
This repository vendors and adapts code from `SammySnake-d/fast-context-mcp`, originally licensed under MIT. The vendored upstream license is preserved at scripts/lib/LICENSE.fast-context-mcp.
This project is not affiliated with Windsurf.
other
Thanks https://linux.do
Fast Context Script Contract
Command
Run semantic repository search without the Fast Context MCP server:
node /path/to/installed/fast-context/scripts/fast-context-search.mjs \
--project "/absolute/path/to/project" \
--query "Where is <feature> implemented?"The script imports vendored Fast Context core files from scripts/lib/ and calls searchWithContent() directly. It uses Windsurf Devstral over the network and obtains credentials from WINDSURF_API_KEY or Windsurf's local logged-in database.
Arguments
Required:
--query,-q: Natural-language search query.
Common:
--project,--project-path,-p: Absolute project root. Defaults to the current working directory.--max-results: Maximum returned files. Use 3-5 for focused lookup, 10-20 for feature tracing, 15-30 for broad exploration.--max-turns: Search rounds. Use 1 for quick lookup, 3 by default, 4-5 for deep tracing.--max-commands: Max local commands the remote search loop may request per round.--tree-depth: Initial repo map depth. Use 0 for auto, 1-2 for huge repos, 4-6 for small repos.--timeout-ms: Connect timeout for Devstral requests.--exclude: Directory or file patterns to omit from tree/search context. May be repeated or comma-separated.--repo-map-mode:classicorbootstrap_hotspot.
Hotspot and bootstrap:
--bootstrap-tree-depth--hotspot-top-k--hotspot-tree-depth--hotspot-max-bytes--bootstrap-enabled/--no-bootstrap--bootstrap-max-turns--bootstrap-max-commands
Key check:
--check-key: Verify Windsurf key discovery and print only a masked key plus source path. Never prints the full key.--print-key: Print the full discovered Windsurf key to stdout. Use only on the user's local machine.--key-env: Printexport WINDSURF_API_KEY='...'so users can runeval "$(node scripts/fast-context-search.mjs --key-env)".--db-path: Optional custom Windsurfstate.vscdbpath for key commands.
Response Expectations
- Relevant files and line ranges are pointers, not proof. Always inspect files locally before editing or making claims.
- Grep keywords are intended follow-up terms for
rg. - The
[config]line reports effectivetree_depth,max_turns,max_results, timeout, and excludes. Use it to tune retries.
Completeness Checklist
Before producing code or project-specific advice, confirm that you have the full definitions and signatures for:
- Entry points and public APIs.
- Core functions, classes, hooks, components, commands, or handlers.
- Types, interfaces, schemas, validators, and configuration.
- Data access, side effects, external calls, and persistence boundaries.
- Callers, consumers, and routing from user-visible behavior to implementation.
- Existing tests, fixtures, helpers, and snapshot or integration coverage.
If any item is unknown and relevant, run another focused Fast Context query or inspect with local search.
Failure Handling
If Node dependencies are missing, run npm install from the installed Fast Context skill directory. If the vendored core files are missing, repair or reinstall the skill. If the search fails because the repository is too large, retry with lower tree_depth and more exclude_paths. If results are incomplete, retry with a narrower query, higher max_turns, or repo_map_mode: "bootstrap_hotspot". If Windsurf authentication fails, run --check-key, ensure Windsurf is logged in, or set WINDSURF_API_KEY in the environment. If the user asks how to obtain the key, use --print-key or --key-env and remind them not to commit or share the output.
#!/usr/bin/env node
import { existsSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
const SKILL_DIR = resolve(SCRIPT_DIR, "..");
const CORE_PATH = join(SCRIPT_DIR, "lib", "core.mjs");
function usage() {
return `Usage:
fast-context-search --query <query> [--project <path>] [options]
fast-context-search --check-key [--db-path <path>]
fast-context-search --print-key [--db-path <path>]
fast-context-search --key-env [--db-path <path>]
Options:
-q, --query <text> Natural-language search query
-p, --project <path> Project root (default: current directory)
--project-path <path> Alias for --project
--max-results <n> Max files to return (default: 10)
--max-turns <n> Search rounds (default: 3)
--max-commands <n> Local commands per round (default: 8)
--tree-depth <n> Repo tree depth, 0 for auto (default: 0)
--timeout-ms <n> Request timeout (default: 30000)
--exclude <patterns> Comma-separated excludes; may be repeated
--repo-map-mode <mode> classic or bootstrap_hotspot
--bootstrap-tree-depth <n> Bootstrap tree depth
--hotspot-top-k <n> Hotspot directory count
--hotspot-tree-depth <n> Hotspot subtree depth
--hotspot-max-bytes <n> Hotspot repo-map byte budget
--bootstrap-enabled Enable bootstrap phase
--no-bootstrap Disable bootstrap phase
--bootstrap-max-turns <n> Bootstrap phase turns
--bootstrap-max-commands <n> Bootstrap commands per turn
--check-key Verify Windsurf key discovery without printing the full key
--print-key Print the full discovered Windsurf key to stdout
--key-env Print an export command for WINDSURF_API_KEY
--db-path <path> Custom Windsurf state.vscdb path for key commands
--help Show this help`;
}
function parseInteger(name, value) {
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed)) {
throw new Error(`${name} must be an integer, received: ${value}`);
}
return parsed;
}
function takeValue(args, index, name) {
const value = args[index + 1];
if (value == null || value.startsWith("--")) {
throw new Error(`${name} requires a value`);
}
return value;
}
function pushExclude(target, raw) {
for (const item of String(raw).split(",")) {
const trimmed = item.trim();
if (trimmed) target.push(trimmed);
}
}
function parseArgs(argv) {
const opts = {
projectRoot: process.cwd(),
maxResults: 10,
maxTurns: 3,
maxCommands: 8,
treeDepth: 0,
timeoutMs: 30000,
excludePaths: [],
repoMapMode: "bootstrap_hotspot",
bootstrapEnabled: true,
bootstrapTreeDepth: 1,
hotspotTopK: 4,
hotspotTreeDepth: 2,
hotspotMaxBytes: 120 * 1024,
bootstrapMaxTurns: 2,
bootstrapMaxCommands: 6,
checkKey: false,
printKey: false,
keyEnv: false,
dbPath: undefined,
help: false,
};
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
switch (arg) {
case "-q":
case "--query":
opts.query = takeValue(argv, i, arg);
i++;
break;
case "-p":
case "--project":
case "--project-path":
opts.projectRoot = takeValue(argv, i, arg);
i++;
break;
case "--max-results":
opts.maxResults = parseInteger(arg, takeValue(argv, i, arg));
i++;
break;
case "--max-turns":
opts.maxTurns = parseInteger(arg, takeValue(argv, i, arg));
i++;
break;
case "--max-commands":
opts.maxCommands = parseInteger(arg, takeValue(argv, i, arg));
i++;
break;
case "--tree-depth":
opts.treeDepth = parseInteger(arg, takeValue(argv, i, arg));
i++;
break;
case "--timeout-ms":
opts.timeoutMs = parseInteger(arg, takeValue(argv, i, arg));
i++;
break;
case "--exclude":
pushExclude(opts.excludePaths, takeValue(argv, i, arg));
i++;
break;
case "--repo-map-mode":
opts.repoMapMode = takeValue(argv, i, arg);
i++;
break;
case "--bootstrap-tree-depth":
opts.bootstrapTreeDepth = parseInteger(arg, takeValue(argv, i, arg));
i++;
break;
case "--hotspot-top-k":
opts.hotspotTopK = parseInteger(arg, takeValue(argv, i, arg));
i++;
break;
case "--hotspot-tree-depth":
opts.hotspotTreeDepth = parseInteger(arg, takeValue(argv, i, arg));
i++;
break;
case "--hotspot-max-bytes":
opts.hotspotMaxBytes = parseInteger(arg, takeValue(argv, i, arg));
i++;
break;
case "--bootstrap-enabled":
opts.bootstrapEnabled = true;
break;
case "--no-bootstrap":
opts.bootstrapEnabled = false;
break;
case "--bootstrap-max-turns":
opts.bootstrapMaxTurns = parseInteger(arg, takeValue(argv, i, arg));
i++;
break;
case "--bootstrap-max-commands":
opts.bootstrapMaxCommands = parseInteger(arg, takeValue(argv, i, arg));
i++;
break;
case "--check-key":
opts.checkKey = true;
break;
case "--print-key":
opts.printKey = true;
break;
case "--key-env":
opts.keyEnv = true;
break;
case "--db-path":
opts.dbPath = takeValue(argv, i, arg);
i++;
break;
case "-h":
case "--help":
opts.help = true;
break;
default:
throw new Error(`Unknown argument: ${arg}`);
}
}
opts.projectRoot = resolve(opts.projectRoot);
if (opts.dbPath) opts.dbPath = resolve(opts.dbPath);
opts.excludePaths = [...new Set(opts.excludePaths)];
const keyCommandCount = [opts.checkKey, opts.printKey, opts.keyEnv].filter(Boolean).length;
if (keyCommandCount > 1) {
throw new Error("Choose only one key command: --check-key, --print-key, or --key-env");
}
return opts;
}
function maskKey(key) {
if (!key) return "";
if (key.length <= 12) return `${key.slice(0, 2)}...${key.slice(-2)}`;
return `${key.slice(0, 8)}...${key.slice(-6)}`;
}
function shellQuote(value) {
return `'${String(value).replaceAll("'", "'\\''")}'`;
}
async function loadCore() {
if (!existsSync(CORE_PATH)) {
throw new Error(
`Fast Context vendored core is missing at ${CORE_PATH}\n` +
`Reinstall or repair the $fast-context skill.`
);
}
return import(pathToFileURL(CORE_PATH).href);
}
async function main() {
let opts;
try {
opts = parseArgs(process.argv.slice(2));
} catch (error) {
console.error(`Error: ${error.message}\n`);
console.error(usage());
process.exit(2);
}
if (opts.help) {
console.log(usage());
return;
}
try {
const { searchWithContent, extractKeyInfo } = await loadCore();
if (opts.checkKey || opts.printKey || opts.keyEnv) {
const result = await extractKeyInfo(opts.dbPath);
if (result.error) {
console.error(`Windsurf key discovery failed: ${result.error}`);
if (result.hint) console.error(result.hint);
if (result.db_path) console.error(`DB path: ${result.db_path}`);
process.exit(1);
}
if (opts.printKey) {
console.log(result.api_key);
return;
}
if (opts.keyEnv) {
console.log(`export WINDSURF_API_KEY=${shellQuote(result.api_key)}`);
return;
}
console.log("Windsurf key discovered.");
console.log(`Key: ${maskKey(result.api_key)}`);
console.log(`Source: ${result.db_path}`);
return;
}
if (!opts.query) {
console.error("Error: --query is required.\n");
console.error(usage());
process.exit(2);
}
const output = await searchWithContent({
query: opts.query,
projectRoot: opts.projectRoot,
maxTurns: opts.maxTurns,
maxCommands: opts.maxCommands,
maxResults: opts.maxResults,
treeDepth: opts.treeDepth,
timeoutMs: opts.timeoutMs,
excludePaths: opts.excludePaths,
repoMapMode: opts.repoMapMode,
bootstrapTreeDepth: opts.bootstrapTreeDepth,
hotspotTopK: opts.hotspotTopK,
hotspotTreeDepth: opts.hotspotTreeDepth,
hotspotMaxBytes: opts.hotspotMaxBytes,
bootstrapEnabled: opts.bootstrapEnabled,
bootstrapMaxTurns: opts.bootstrapMaxTurns,
bootstrapMaxCommands: opts.bootstrapMaxCommands,
});
console.log(output);
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
}
main();
/**
* Windsurf Fast Context — core protocol implementation (Node.js).
*
* Reverse-engineered Windsurf SWE-grep Connect-RPC/Protobuf protocol
* for standalone AI-driven semantic code search.
*
* Flow:
* query + tree → Windsurf Devstral API
* → Devstral returns tool_calls (rg/readfile/tree/ls/glob, up to 8 parallel)
* → execute locally → send results back → repeat for N rounds
* → ANSWER: file paths + line ranges + suggested rg patterns
*/
import { readdirSync, existsSync, statSync } from "node:fs";
import { resolve, join, relative, sep, isAbsolute } from "node:path";
import { gzipSync } from "node:zlib";
import { randomUUID } from "node:crypto";
import { platform, arch, release, version as osVersion, hostname, cpus, totalmem } from "node:os";
import treeNodeCli from "tree-node-cli";
import {
ProtobufEncoder,
extractStrings,
connectFrameEncode,
connectFrameDecode,
} from "./protobuf.mjs";
import { ToolExecutor } from "./executor.mjs";
import { extractKey } from "./extract-key.mjs";
import { scoreDirectories, tokenize as tokenizeBM25 } from "./directory-scorer.mjs";
// ─── Error Classification ──────────────────────────────────
/**
* Classified error for fetch failures with structured error codes.
*/
class FastContextError extends Error {
/**
* @param {string} message
* @param {string} code - TIMEOUT | PAYLOAD_TOO_LARGE | RATE_LIMITED | AUTH_ERROR | SERVER_ERROR | NETWORK_ERROR
* @param {Object} [details]
*/
constructor(message, code, details = {}) {
super(message);
this.name = "FastContextError";
this.code = code;
this.details = details;
}
}
/**
* Classify a raw fetch/HTTP error into a FastContextError.
* @param {Error} err
* @returns {FastContextError}
*/
function _classifyError(err) {
if (err instanceof FastContextError) return err;
// HTTP status-based classification
if (err.status) {
const s = err.status;
if (s === 413) return new FastContextError(err.message, "PAYLOAD_TOO_LARGE", { status: s });
if (s === 429) return new FastContextError(err.message, "RATE_LIMITED", { status: s });
if (s === 401 || s === 403) return new FastContextError(err.message, "AUTH_ERROR", { status: s });
return new FastContextError(err.message, "SERVER_ERROR", { status: s });
}
// Timeout (AbortSignal.timeout throws AbortError or TimeoutError)
if (err.name === "AbortError" || err.name === "TimeoutError" || /timeout/i.test(err.message)) {
return new FastContextError(err.message, "TIMEOUT");
}
// Everything else is a network-level issue
return new FastContextError(err.message, "NETWORK_ERROR");
}
// ─── Protocol Constants ────────────────────────────────────
const API_BASE = "https://server.self-serve.windsurf.com/exa.api_server_pb.ApiServerService";
const AUTH_BASE = "https://server.self-serve.windsurf.com/exa.auth_pb.AuthService";
const WS_APP = "windsurf";
const WS_APP_VER = process.env.WS_APP_VER || "1.48.2";
const WS_LS_VER = process.env.WS_LS_VER || "1.9544.35";
const WS_MODEL = process.env.WS_MODEL || "MODEL_SWE_1_6_FAST";
const DEBUG_MODE = process.env.FAST_CONTEXT_DEBUG === "1" || process.env.FAST_CONTEXT_DEBUG === "true";
// Default excludes aligned with Windsurf fast-search guidance.
// Minimal defaults — only dirs that are almost never source code.
// Users can add more via the exclude_paths parameter.
const DEFAULT_EXCLUDE_PATHS = [
"node_modules",
".git",
"__pycache__",
".venv",
"venv",
"dist",
"*.min.*",
];
// Repo-map optimization defaults (tunable via MCP params).
const REPO_MAP_OPTIMIZER_DEFAULTS = {
mode: "bootstrap_hotspot", // classic | bootstrap_hotspot
bootstrapTreeDepth: 1,
hotspotTopK: 4,
hotspotTreeDepth: 2,
maxBytes: 120 * 1024,
};
function _mergeExcludePaths(excludePaths = []) {
const merged = [...DEFAULT_EXCLUDE_PATHS];
for (const p of excludePaths || []) {
if (typeof p === "string" && p && !merged.includes(p)) {
merged.push(p);
}
}
return merged;
}
// ─── System Prompt Template ────────────────────────────────
const SYSTEM_PROMPT_TEMPLATE = `You are an expert software engineer, responsible for providing context \
to another engineer to solve a code issue in the current codebase. \
The user will present you with a description of the issue, and it is \
your job to provide a series of file paths with associated line ranges \
that contain ALL the information relevant to understand and correctly \
address the issue.
# IMPORTANT:
- A relevant file does not mean only the files that must be modified to \
solve the task. It means any file that contains information relevant to \
planning and implementing the fix, such as the definitions of classes \
and functions that are relevant to the pieces of code that will have to \
be modified.
- You should include enough context around the relevant lines to allow \
the engineer to understand the task correctly. You must include ENTIRE \
semantic blocks (functions, classes, definitions, etc). For example:
If addressing the issue requires modifying a method within a class, then \
you should include the entire class definition, not just the lines around \
the method we want to modify.
- NEVER truncate these blocks unless they are very large (hundreds of \
lines or more, in which case providing only a relevant portion of the \
block is acceptable).
- Your job is to essentially alleviate the job of the other engineer by \
giving them a clean starting context from which to start working. More \
precisely, you should minimize the number of files the engineer has to \
read to understand and solve the task correctly (while not providing \
irrelevant code snippets).
# ENVIRONMENT
- Working directory: /codebase. Make sure to run commands in this \
directory, not \`.
- Tool access: use the restricted_exec tool ONLY
- Allowed sub-commands (schema-enforced):
- rg: Search for patterns in files using ripgrep
- Required: pattern (string), path (string)
- Optional: include (array of globs), exclude (array of globs)
- readfile: Read contents of a file with optional line range
- Required: file (string)
- Optional: start_line (int), end_line (int) — 1-indexed, inclusive
- tree: Display directory structure as a tree
- Required: path (string)
- Optional: levels (int)
# THINKING RULES
- Think step-by-step. Plan, reason, and reflect before each tool call.
- Use tool calls liberally and purposefully to ground every conclusion \
in real code, not assumptions.
- If a command fails, rethink and try something different; do not \
complain to the user.
- AVOID REDUNDANT SEARCHES: Do not search for the same pattern multiple \
times with slightly different paths or excludes. One well-targeted search \
is better than multiple overlapping ones.
- PRIORITIZE READING over searching: Once you find a file path, read it \
directly instead of searching for more variations of the same pattern.
# FAST-SEARCH DEFAULTS (optimize rg/tree on large repos)
- Start NARROW, then widen only if needed. Prefer searching likely code \
roots first (e.g., \`src/\`, \`lib/\`, \`app/\`, \`packages/\`, \`services/\`) \
instead of \`/codebase\`.
- Prefer fixed-string search for literals: escape patterns or keep regex \
simple. Use smart case; avoid case-insensitive unless necessary.
- Prefer file-type filters and globs (in include) over full-repo scans.
- Default EXCLUDES for speed (apply via the exclude array): \
node_modules, .git, dist, build, coverage, .venv, venv, target, out, \
.cache, __pycache__, vendor, deps, third_party, logs, data, *.min.*
- Skip huge files where possible; when opening files, prefer reading \
only relevant ranges with readfile.
- Limit directory traversal with tree levels to quickly orient before \
deeper inspection.
# SOME EXAMPLES OF WORKFLOWS
- MAP – Use \`tree\` with small levels; \`rg\` on likely roots to grasp \
structure and hotspots.
- ANCHOR – \`rg\` for problem keywords and anchor symbols; restrict by \
language globs via include.
- TRACE – Follow imports with targeted \`rg\` in narrowed roots; open \
files with \`readfile\` scoped to entire semantic blocks.
- VERIFY – Confirm each candidate path exists by reading or additional \
searches; drop false positives (tests, vendored, generated) unless they \
must change.
# TOOL USE GUIDELINES
- You must use a SINGLE restricted_exec call in your answer, that lets \
you execute at most {max_commands} commands in a single turn. Each command must be \
an object with a \`type\` field of \`rg\`, \`readfile\`, or \`tree\` and the appropriate fields for that type.
- Example restricted_exec usage:
[TOOL_CALLS]restricted_exec[ARGS]{{
"command1": {{
"type": "rg",
"pattern": "Controller",
"path": "/codebase/slime",
"include": ["**/*.py"],
"exclude": ["**/node_modules/**", "**/.git/**", "**/dist/**", \
"**/build/**", "**/.venv/**", "**/__pycache__/**"]
}},
"command2": {{
"type": "readfile",
"file": "/codebase/slime/train.py",
"start_line": 1,
"end_line": 200
}},
"command3": {{
"type": "tree",
"path": "/codebase/slime/",
"levels": 2
}}
}}
- You have at most {max_turns} turns to interact with the environment by calling \
tools, so issuing multiple commands at once is necessary and encouraged \
to speed up your research.
- Each command result may be truncated to 50 lines; prefer multiple \
targeted reads/searches to build complete context.
- DO NOT EVER USE MORE THAN {max_commands} commands in a single turn, or you will \
be penalized.
# ANSWER FORMAT (strict format, including tags)
- You will output an XML structure with a root element "ANSWER" \
containing "file" elements. Each "file" element will have a "path" \
attribute and contain "range" elements.
- You will output this as your final response.
- The line ranges must be inclusive.
Output example inside the "answer" tool argument:
<ANSWER>
<file path="/codebase/info_theory/formulas/entropy.py">
<range>10-60</range>
<range>150-210</range>
</file>
<file path="/codebase/info_theory/data_structures/bits.py">
<range>1-40</range>
<range>110-170</range>
</file>
</ANSWER>
Remember: Prefer narrow, fixed-string, and type-filtered searches with \
aggressive excludes and size/depth limits. Widen scope only as needed. \
Use the restricted tools available to you, and output your answer in \
exactly the specified format.
# NO RESULTS POLICY
If after thorough searching you are confident that NO relevant files exist \
for the given query (e.g., the function/class/concept does not exist in the \
codebase), you MUST return an empty ANSWER:
<ANSWER></ANSWER>
Do NOT return irrelevant files (such as entry points or config files) just \
to provide some output. An empty answer is always better than a misleading one.
# RESULT COUNT
Aim to return at most {max_results} files in your answer. Focus on the most \
relevant files first. If fewer files are relevant, return fewer.
`;
const FINAL_FORCE_ANSWER =
"You have no turns left. Now you MUST provide your final ANSWER, even if it's not complete.";
const BOOTSTRAP_PROMPT_TEMPLATE = `You are a bootstrap planning agent for codebase hotspot discovery.
Your ONLY goal is to discover high-signal search keywords and hotspot directories for a later full search phase.
# OUTPUT CONTRACT
- Use the restricted_exec tool ONLY.
- Prefer rg + tree commands. Avoid deep readfile unless absolutely necessary.
- Do NOT output final <ANSWER> for code fixes in this phase.
- Keep commands focused and broad enough to identify likely relevant modules quickly.
# TOOL BUDGET
- You have at most {max_turns} turns.
- You may use up to {max_commands} commands per turn.
# STRATEGY
1) Start from the provided mini repo map.
2) Use targeted rg patterns derived from the user problem.
3) Use tree on likely top-level directories to identify hotspots.
4) Stop once you have enough keyword and hotspot coverage for phase-2.
`;
/**
* Smart trim accumulated messages to reduce payload size.
*
* Why this is needed:
* - Proto size grows quickly across turns (messages + tool results).
* - Keeping only the last N messages naively may drop the tool-call ↔ tool-result
* linkage (tool_call_id/ref_call_id) and remove useful progress context.
*
* Strategy:
* - Keep system prompt (index 0).
* - Keep user problem statement, but compact the repo map when trimming.
* - Keep the latest tool-call + tool-result pair (plus any trailing prompts).
* - Insert a compact progress summary so the model doesn't lose the thread.
*
* @param {Array} messages
* @param {Object} [state]
* @param {string} [state.query]
* @param {string[]} [state.recentFiles]
* @param {string[]} [state.recentPatterns]
* @param {Array<{type:string, desc:string}>} [state.recentCommands]
* @param {number} [state.turn]
* @returns {boolean} true if messages were actually trimmed/compacted
*/
function _trimMessages(messages, state = {}) {
if (!Array.isArray(messages) || messages.length < 2) return false;
const systemMsg = messages[0];
const userMsg = messages[1];
const truncateToolResultsPreserve = (text, maxPerBlock = 4000, maxTotal = 20000) => {
if (typeof text !== "string" || text.length <= maxTotal) return text;
const re = /<(command\d+)_result>\n([\s\S]*?)\n<\/\1_result>/g;
let m;
const parts = [];
let matched = false;
while ((m = re.exec(text)) !== null) {
matched = true;
const key = m[1];
let body = m[2] || "";
if (body.length > maxPerBlock) {
body = body.slice(0, maxPerBlock) + "\n...[truncated]...";
}
parts.push(`<${key}_result>\n${body}\n</${key}_result>`);
if (parts.join("").length > maxTotal) break;
}
if (!matched) {
return text.slice(0, maxTotal) + "\n...[tool results truncated]...";
}
const out = parts.join("");
return out.length <= maxTotal ? out : out.slice(0, maxTotal) + "\n...[tool results truncated]...";
};
// Find the most recent tool-result message and its matching tool-call message (if present).
let lastToolResultIdx = -1;
let refId = null;
for (let i = messages.length - 1; i >= 0; i--) {
const m = messages[i];
if (m && m.role === 4 && typeof m.ref_call_id === "string" && m.ref_call_id) {
lastToolResultIdx = i;
refId = m.ref_call_id;
break;
}
}
let lastToolCallIdx = -1;
if (refId) {
for (let i = lastToolResultIdx - 1; i >= 0; i--) {
const m = messages[i];
if (m && m.role === 2 && m.tool_call_id === refId) {
lastToolCallIdx = i;
break;
}
}
}
// Tail: keep tool-call + tool-result pair, plus anything after it (e.g., force-answer).
let tailStart = -1;
if (lastToolResultIdx !== -1) {
tailStart = lastToolCallIdx !== -1 ? lastToolCallIdx : Math.max(2, lastToolResultIdx - 1);
} else {
// No tool results yet: keep the last few messages only.
tailStart = Math.max(2, messages.length - 4);
}
const tail = messages.slice(tailStart);
// Compact the user message (repo map) when trimming, since it's usually the largest chunk.
let compactedUser = userMsg;
let didCompactUser = false;
if (userMsg && typeof userMsg.content === "string" && userMsg.content.includes("Repo Map")) {
const q =
(typeof state.query === "string" && state.query) ||
userMsg.content.match(/Problem Statement:\s*([^\n]+)/)?.[1]?.trim() ||
"";
const compact = `Problem Statement: ${q}\n\nRepo Map: (omitted to reduce payload). Use tree/rg to explore structure if needed.`;
if (compact.length < userMsg.content.length) {
compactedUser = { ...userMsg, content: compact };
didCompactUser = true;
}
}
// Build a compact progress summary to preserve important context across trims.
const recentCommands = Array.isArray(state.recentCommands) ? state.recentCommands : [];
const recentFiles = Array.isArray(state.recentFiles) ? state.recentFiles : [];
const recentPatterns = Array.isArray(state.recentPatterns) ? state.recentPatterns : [];
const turnNote = Number.isInteger(state.turn) ? ` turn=${state.turn}` : "";
const summaryLines = [
`[Context trimmed to reduce payload size.${turnNote}]`,
recentCommands.length ? `recent_commands: ${recentCommands.slice(-6).map((c) => c.desc).join(" | ")}` : "",
recentFiles.length ? `recent_files: ${recentFiles.slice(-12).join(", ")}` : "",
recentPatterns.length ? `rg_patterns: ${recentPatterns.slice(-20).join(", ")}` : "",
"Continue from the most recent tool results kept below.",
].filter(Boolean);
const summaryMsg = { role: 1, content: summaryLines.join("\n") };
// If trimming doesn't actually reduce anything, bail.
// We consider it "useful" if we either compact the user message or drop history.
const willDropHistory = tailStart > 2;
if (!didCompactUser && !willDropHistory) return false;
// Reduce oversized assistant/tool messages in the tail to avoid immediate re-overflow.
for (const m of tail) {
if (m && typeof m.content === "string") {
if (m.role === 2 && m.content.length > 8000) {
m.content = m.content.slice(0, 8000) + "\n...[assistant content truncated]...";
}
if (m.role === 4 && m.content.length > 20000) {
m.content = truncateToolResultsPreserve(m.content, 4000, 20000);
}
}
}
messages.length = 0;
messages.push(systemMsg);
// Avoid duplicating user message if it's already within the kept tail.
if (tailStart > 1) {
messages.push(compactedUser);
}
messages.push(summaryMsg, ...tail);
return true;
}
/**
* @param {number} maxTurns
* @param {number} maxCommands
* @param {number} maxResults
* @returns {string}
*/
function buildSystemPrompt(maxTurns = 3, maxCommands = 8, maxResults = 10) {
return SYSTEM_PROMPT_TEMPLATE
.replaceAll("{max_turns}", String(maxTurns))
.replaceAll("{max_commands}", String(maxCommands))
.replaceAll("{max_results}", String(maxResults));
}
function buildBootstrapPrompt(maxTurns = 2, maxCommands = 6) {
return BOOTSTRAP_PROMPT_TEMPLATE
.replaceAll("{max_turns}", String(maxTurns))
.replaceAll("{max_commands}", String(maxCommands));
}
function _extractTopDirFromCodebasePath(path = "") {
const p = String(path || "").replace(/\\/g, "/");
if (!p.startsWith("/codebase")) return null;
const rel = p.replace(/^\/codebase\/?/, "");
if (!rel) return null;
return rel.split("/")[0] || null;
}
async function _runBootstrapPhase({
query,
projectRoot,
apiKey,
jwt,
timeoutMs,
excludePaths,
bootstrapTreeDepth,
bootstrapMaxTurns,
bootstrapMaxCommands,
onProgress,
}) {
const log = (msg) => onProgress?.(`[bootstrap] ${msg}`);
const hints = { rgPatterns: [], hotDirs: [] };
try {
const { tree: miniMap, depth } = getRepoMap(projectRoot, bootstrapTreeDepth, excludePaths);
const systemPrompt = buildBootstrapPrompt(bootstrapMaxTurns, bootstrapMaxCommands);
const userContent = `Problem Statement: ${query}\n\nRepo Map (tree -L ${depth} /codebase):\n\`\`\`text\n${miniMap}\n\`\`\``;
const messages = [
{ role: 5, content: systemPrompt },
{ role: 1, content: userContent },
];
const toolDefs = getToolDefinitions(bootstrapMaxCommands);
const executor = new ToolExecutor(projectRoot);
for (let turn = 0; turn < bootstrapMaxTurns; turn++) {
log(`Turn ${turn + 1}/${bootstrapMaxTurns}`);
const proto = _buildRequest(apiKey, jwt, messages, toolDefs);
let respData;
try {
respData = await _streamingRequest(proto, timeoutMs);
} catch (e) {
log(`request failed: ${e.code || "UNKNOWN"}`);
break;
}
const [thinking, toolInfo] = _parseResponse(respData);
if (!toolInfo) break;
const [toolName, toolArgs] = toolInfo;
if (toolName !== "restricted_exec") break;
const callId = randomUUID();
const argsJson = JSON.stringify(toolArgs);
const cmds = Object.keys(toolArgs).filter((k) => k.startsWith("command"));
for (const cmdKey of cmds) {
const cmd = toolArgs[cmdKey];
if (!cmd || typeof cmd !== "object") continue;
if (cmd.type === "rg" && typeof cmd.pattern === "string" && cmd.pattern) {
hints.rgPatterns.push(cmd.pattern);
}
if (cmd.type === "tree" && typeof cmd.path === "string") {
const top = _extractTopDirFromCodebasePath(cmd.path);
if (top) hints.hotDirs.push(top);
}
}
const results = await executor.execToolCallAsync(toolArgs);
messages.push({
role: 2,
content: thinking,
tool_call_id: callId,
tool_name: "restricted_exec",
tool_args_json: argsJson,
});
messages.push({ role: 4, content: results, ref_call_id: callId });
}
} catch {
// Bootstrap is best-effort. Fall back silently.
}
return {
rgPatterns: [...new Set(hints.rgPatterns)].slice(-30),
hotDirs: [...new Set(hints.hotDirs)].slice(-12),
};
}
// ─── Tool Schema ───────────────────────────────────────────
function _buildCommandSchema(n) {
return {
type: "object",
description: `Command ${n} to execute. Must be one of: rg, readfile, or tree.`,
oneOf: [
{
properties: {
type: { type: "string", const: "rg", description: "Search for patterns in files using ripgrep." },
pattern: { type: "string", description: "The regex pattern to search for." },
path: { type: "string", description: "The path to search in." },
include: { type: "array", items: { type: "string" }, description: "File patterns to include." },
exclude: { type: "array", items: { type: "string" }, description: "File patterns to exclude." },
},
required: ["type", "pattern", "path"],
},
{
properties: {
type: { type: "string", const: "readfile", description: "Read contents of a file with optional line range." },
file: { type: "string", description: "Path to the file to read." },
start_line: { type: "integer", description: "Starting line number (1-indexed)." },
end_line: { type: "integer", description: "Ending line number (1-indexed)." },
},
required: ["type", "file"],
},
{
properties: {
type: { type: "string", const: "tree", description: "Display directory structure as a tree." },
path: { type: "string", description: "Path to the directory." },
levels: { type: "integer", description: "Number of directory levels." },
},
required: ["type", "path"],
},
{
properties: {
type: { type: "string", const: "ls", description: "List files in a directory." },
path: { type: "string", description: "Path to the directory." },
long_format: { type: "boolean" },
all: { type: "boolean" },
},
required: ["type", "path"],
},
{
properties: {
type: { type: "string", const: "glob", description: "Find files matching a glob pattern." },
pattern: { type: "string" },
path: { type: "string" },
type_filter: { type: "string", enum: ["file", "directory", "all"] },
},
required: ["type", "pattern", "path"],
},
],
};
}
/**
* @param {number} maxCommands
* @returns {string}
*/
function getToolDefinitions(maxCommands = 8) {
const props = {};
for (let i = 1; i <= maxCommands; i++) {
props[`command${i}`] = _buildCommandSchema(i);
}
const tools = [
{
type: "function",
function: {
name: "restricted_exec",
description: "Execute restricted commands (rg, readfile, tree, ls, glob) in parallel.",
parameters: { type: "object", properties: props, required: ["command1"] },
},
},
{
type: "function",
function: {
name: "answer",
description: "Final answer with relevant files and line ranges.",
parameters: {
type: "object",
properties: { answer: { type: "string", description: "The final answer in XML format." } },
required: ["answer"],
},
},
},
];
return JSON.stringify(tools);
}
// ─── Credentials ───────────────────────────────────────────
/**
* Auto-discover Windsurf API key from local installation.
* @returns {Promise<string|null>}
*/
async function autoDiscoverApiKey() {
try {
const result = await extractKey();
if (result.api_key && (result.api_key.startsWith("sk-") || result.api_key.startsWith("devin-"))) {
return result.api_key;
}
} catch {
// Extraction failed
}
return null;
}
/**
* Get API key from env var or auto-discovery.
* @returns {Promise<string>}
*/
async function getApiKey() {
const key = process.env.WINDSURF_API_KEY;
if (key) return key;
const discovered = await autoDiscoverApiKey();
if (discovered) return discovered;
throw new Error(
"Windsurf API Key not found. Set WINDSURF_API_KEY env var or ensure Windsurf is logged in. " +
"Run extract-key.mjs to see extraction methods."
);
}
// ─── JWT Cache ──────────────────────────────────────────────
/** @type {Map<string, { token: string, expiresAt: number }>} */
const _jwtCache = new Map();
/**
* Decode JWT payload and extract expiration time.
* @param {string} jwt
* @returns {number} expiration timestamp in seconds
*/
function _getJwtExp(jwt) {
try {
const parts = jwt.split(".");
if (parts.length < 2) return 0;
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
return payload.exp || 0;
} catch {
return 0;
}
}
/**
* Get a cached or fresh JWT token.
* Refreshes when token expires or is within 60s of expiration.
* @param {string} apiKey
* @returns {Promise<string>}
*/
async function getCachedJwt(apiKey) {
const now = Math.floor(Date.now() / 1000);
const cached = _jwtCache.get(apiKey);
if (cached && cached.expiresAt > now + 60) return cached.token;
const token = await fetchJwt(apiKey);
const exp = _getJwtExp(token);
_jwtCache.set(apiKey, { token, expiresAt: exp || now + 3600 });
return token;
}
// ─── TLS Fallback ──────────────────────────────────────────
// Match Python's SSL fallback: if NODE_TLS_REJECT_UNAUTHORIZED is not set
// and the first fetch fails with a TLS error, disable cert verification.
let _tlsFallbackApplied = false;
function _applyTlsFallback() {
if (!_tlsFallbackApplied && !process.env.NODE_TLS_REJECT_UNAUTHORIZED) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
_tlsFallbackApplied = true;
process.stderr.write(
"[fast-context] WARNING: TLS certificate verification disabled due to connection failure. " +
"Set NODE_TLS_REJECT_UNAUTHORIZED=0 explicitly to suppress this warning.\n"
);
}
}
// ─── Network Layer ─────────────────────────────────────────
/**
* Standard unary HTTP POST with proto content type.
* @param {string} url
* @param {Buffer} protoBytes
* @param {boolean} [compress=true]
* @returns {Promise<Buffer>}
*/
async function _unaryRequest(url, protoBytes, compress = true) {
const headers = {
"Content-Type": "application/proto",
"Connect-Protocol-Version": "1",
"User-Agent": "connect-go/1.18.1 (go1.25.5)",
"Accept-Encoding": "gzip",
};
let body;
if (compress) {
body = gzipSync(protoBytes);
headers["Content-Encoding"] = "gzip";
} else {
body = protoBytes;
}
const doFetch = () => fetch(url, {
method: "POST",
headers,
body,
signal: AbortSignal.timeout(30000),
});
let resp;
try {
resp = await doFetch();
} catch (e) {
// TLS or network error — try with cert verification disabled
_applyTlsFallback();
try {
resp = await doFetch();
} catch (e2) {
throw _classifyError(e2);
}
}
if (!resp.ok) {
const err = new Error(`HTTP ${resp.status}`);
err.status = resp.status;
throw _classifyError(err);
}
const arrayBuf = await resp.arrayBuffer();
return Buffer.from(arrayBuf);
}
/**
* Connect-RPC streaming POST to GetDevstralStream with retry.
* @param {Buffer} protoBytes
* @param {number} [timeoutMs=30000]
* @param {number} [maxRetries=2]
* @returns {Promise<Buffer>}
*/
async function _streamingRequest(protoBytes, timeoutMs = 30000, maxRetries = 2) {
const frame = connectFrameEncode(protoBytes);
const url = `${API_BASE}/GetDevstralStream`;
const traceId = randomUUID().replace(/-/g, "");
const spanId = randomUUID().replace(/-/g, "").slice(0, 16);
const baseTimeoutMs = Number.isFinite(timeoutMs) ? timeoutMs : 30000;
const abortMs = baseTimeoutMs + 5000;
const headers = {
"Content-Type": "application/connect+proto",
"Connect-Protocol-Version": "1",
"Connect-Accept-Encoding": "gzip",
"Connect-Content-Encoding": "gzip",
"Connect-Timeout-Ms": String(baseTimeoutMs),
"User-Agent": "connect-go/1.18.1 (go1.25.5)",
"Accept-Encoding": "identity",
"Baggage": `sentry-release=language-server-windsurf@${WS_LS_VER},` +
`sentry-environment=stable,sentry-sampled=false,` +
`sentry-trace_id=${traceId},` +
`sentry-public_key=b813f73488da69eedec534dba1029111`,
"Sentry-Trace": `${traceId}-${spanId}-0`,
};
const doFetch = () => fetch(url, {
method: "POST",
headers,
body: frame,
signal: AbortSignal.timeout(abortMs),
});
let lastErr;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
let resp;
try {
resp = await doFetch();
} catch (e) {
if (attempt === 0) {
_applyTlsFallback();
resp = await doFetch();
} else {
throw e;
}
}
if (!resp.ok) {
const err = new Error(`HTTP ${resp.status}`);
err.status = resp.status;
// Don't retry on 4xx client errors (except 429)
if (resp.status >= 400 && resp.status < 500 && resp.status !== 429) {
throw err;
}
lastErr = err;
if (attempt < maxRetries) {
await new Promise((r) => setTimeout(r, 1000 * (attempt + 1)));
continue;
}
throw err;
}
const arrayBuf = await resp.arrayBuffer();
return Buffer.from(arrayBuf);
} catch (e) {
lastErr = e;
// Don't retry on 4xx client errors (except 429)
if (e.status && e.status >= 400 && e.status < 500 && e.status !== 429) {
throw _classifyError(e);
}
if (attempt < maxRetries) {
await new Promise((r) => setTimeout(r, 1000 * (attempt + 1)));
continue;
}
}
}
throw _classifyError(lastErr);
}
/**
* Authenticate with API key to get JWT token.
* @param {string} apiKey
* @returns {Promise<string>}
*/
async function fetchJwt(apiKey) {
const meta = new ProtobufEncoder();
meta.writeString(1, WS_APP);
meta.writeString(2, WS_APP_VER);
meta.writeString(3, apiKey);
meta.writeString(4, "zh-cn");
meta.writeString(7, WS_LS_VER);
meta.writeString(12, WS_APP);
meta.writeBytes(30, Buffer.from([0x00, 0x01]));
const outer = new ProtobufEncoder();
outer.writeMessage(1, meta);
const resp = await _unaryRequest(`${AUTH_BASE}/GetUserJwt`, outer.toBuffer(), false);
for (const s of extractStrings(resp)) {
if (s.startsWith("eyJ") && s.includes(".")) {
return s;
}
}
throw new Error("Failed to extract JWT from GetUserJwt response");
}
/**
* Check rate limit. Returns true if OK, false if rate-limited.
* @param {string} apiKey
* @param {string} jwt
* @returns {Promise<boolean>}
*/
async function checkRateLimit(apiKey, jwt) {
const req = new ProtobufEncoder();
req.writeMessage(1, _buildMetadata(apiKey, jwt));
req.writeString(3, WS_MODEL);
try {
await _unaryRequest(`${API_BASE}/CheckUserMessageRateLimit`, req.toBuffer(), true);
return true;
} catch (e) {
if (e.status === 429) return false;
return true; // Don't block on network issues
}
}
// ─── Request Building ──────────────────────────────────────
/**
* Build protobuf metadata with app info, system info, JWT, etc.
* @param {string} apiKey
* @param {string} jwt
* @returns {ProtobufEncoder}
*/
function _buildMetadata(apiKey, jwt) {
const meta = new ProtobufEncoder();
meta.writeString(1, WS_APP);
meta.writeString(2, WS_APP_VER);
meta.writeString(3, apiKey);
meta.writeString(4, "zh-cn");
const plat = platform();
const sysInfo = {
Os: plat,
Arch: arch(),
Release: release(),
Version: osVersion(),
Machine: arch(),
Nodename: hostname(),
Sysname: plat === "darwin" ? "Darwin" : plat === "win32" ? "Windows_NT" : "Linux",
ProductVersion: "",
};
meta.writeString(5, JSON.stringify(sysInfo));
meta.writeString(7, WS_LS_VER);
const cpuList = cpus();
const ncpu = cpuList.length || 4;
const mem = totalmem();
const cpuInfo = {
NumSockets: 1,
NumCores: ncpu,
NumThreads: ncpu,
VendorID: "",
Family: "0",
Model: "0",
ModelName: cpuList[0]?.model || "Unknown",
Memory: mem,
};
meta.writeString(8, JSON.stringify(cpuInfo));
meta.writeString(12, WS_APP);
meta.writeString(21, jwt);
meta.writeBytes(30, Buffer.from([0x00, 0x01]));
return meta;
}
/**
* Build a chat message protobuf.
* @param {number} role - 1=user, 2=assistant, 4=tool_result, 5=system
* @param {string} content
* @param {Object} [opts]
* @param {string} [opts.toolCallId]
* @param {string} [opts.toolName]
* @param {string} [opts.toolArgsJson]
* @param {string} [opts.refCallId]
* @returns {ProtobufEncoder}
*/
function _buildChatMessage(role, content, opts = {}) {
const msg = new ProtobufEncoder();
msg.writeVarint(2, role);
msg.writeString(3, content);
if (opts.toolCallId && opts.toolName && opts.toolArgsJson) {
const tc = new ProtobufEncoder();
tc.writeString(1, opts.toolCallId);
tc.writeString(2, opts.toolName);
tc.writeString(3, opts.toolArgsJson);
msg.writeMessage(6, tc);
}
if (opts.refCallId) {
msg.writeString(7, opts.refCallId);
}
return msg;
}
/**
* Build a full request with metadata, messages, and tool definitions.
* @param {string} apiKey
* @param {string} jwt
* @param {Array} messages
* @param {string} toolDefs
* @returns {Buffer}
*/
function _buildRequest(apiKey, jwt, messages, toolDefs) {
const req = new ProtobufEncoder();
req.writeMessage(1, _buildMetadata(apiKey, jwt));
for (const m of messages) {
const msgEnc = _buildChatMessage(m.role, m.content, {
toolCallId: m.tool_call_id,
toolName: m.tool_name,
toolArgsJson: m.tool_args_json,
refCallId: m.ref_call_id,
});
req.writeMessage(2, msgEnc);
}
req.writeString(3, toolDefs);
return req.toBuffer();
}
// ─── Response Parsing ──────────────────────────────────────
/**
* Strip invalid UTF-8 bytes from a Buffer → clean string.
* Matches Python's bytes.decode("utf-8", errors="ignore").
* @param {Buffer} buf
* @returns {string}
*/
function stripInvalidUtf8(buf) {
return buf.toString("utf-8").replace(/\ufffd/g, "");
}
/**
* Parse tool call from [TOOL_CALLS]name[ARGS]{json} format.
* @param {string} text
* @returns {[string, string, Object]|null} [thinking, name, args] or null
*/
function _parseToolCall(text) {
text = text.replace(/<\/s>/g, "");
const m = text.match(/\[TOOL_CALLS\](\w+)\[ARGS\](\{.+)/s);
if (!m) return null;
const name = m[1];
const raw = m[2].trim();
// Find matching closing brace
let depth = 0;
let end = 0;
for (let i = 0; i < raw.length; i++) {
if (raw[i] === "{") depth++;
else if (raw[i] === "}") {
depth--;
if (depth === 0) {
end = i + 1;
break;
}
}
}
if (end === 0) end = raw.length;
let args;
const jsonCandidate = raw.slice(0, end);
try {
args = JSON.parse(jsonCandidate);
} catch {
// Attempt lenient fix: unquoted keys like exclude": → "exclude":
try {
const fixed = jsonCandidate.replace(/([{,]\s*)(\w+)\s*:/g, '$1"$2":');
args = JSON.parse(fixed);
} catch {
return null;
}
}
const thinking = text.slice(0, m.index).trim();
return [thinking, name, args];
}
/**
* Parse streaming response: decode frames, extract text, parse tool calls.
* @param {Buffer} data
* @returns {[string, [string, Object]|null]} [text, toolInfo]
*/
function _parseResponse(data) {
const frames = connectFrameDecode(data);
let allText = "";
for (const frameData of frames) {
// Check for error JSON
try {
const textCandidate = frameData.toString("utf-8");
if (textCandidate.startsWith("{")) {
const errObj = JSON.parse(textCandidate);
if (errObj.error) {
const code = errObj.error.code || "unknown";
const msg = errObj.error.message || "";
return [`[Error] ${code}: ${msg}`, null];
}
}
} catch {
// Not JSON, continue
}
// Extract text from frame — strip invalid UTF-8 (matches Python errors="ignore")
const rawText = stripInvalidUtf8(frameData);
if (rawText.includes("[TOOL_CALLS]")) {
allText = rawText;
break;
}
for (const s of extractStrings(frameData)) {
if (s.length > 10) {
allText += s;
}
}
}
const parsed = _parseToolCall(allText);
if (parsed) {
const [thinking, name, args] = parsed;
return [thinking, [name, args]];
}
return [allText, null];
}
// ─── Core Search ───────────────────────────────────────────
// Max safe tree size in bytes (server payload limit ~346KB, fixed overhead ~26KB,
// leave room for conversation accumulation across rounds)
const MAX_TREE_BYTES = 250 * 1024;
/**
* Convert an exclude pattern (directory/file name or simple glob) to RegExp
* for tree-node-cli's exclude option.
* @param {string} pattern - e.g. "node_modules", "dist", "*.min.*"
* @returns {RegExp}
*/
function _excludePatternToRegex(pattern) {
if (!/[*?]/.test(pattern)) {
// Simple name — exact match
return new RegExp("^" + pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "$");
}
// Glob → regex
let regex = "^";
for (const c of pattern) {
if (c === "*") regex += ".*";
else if (c === "?") regex += ".";
else if (".+^${}()|[]\\".includes(c)) regex += "\\" + c;
else regex += c;
}
regex += "$";
return new RegExp(regex);
}
/**
* Count files in a directory (non-recursive, fast estimate).
* @param {string} dir
* @returns {number}
*/
function _countFilesQuick(dir) {
try {
return readdirSync(dir).length;
} catch {
return 0;
}
}
/**
* Estimate project size and suggest optimal tree depth.
* - Small project (< 500 entries): depth 4
* - Medium project (500-5000 entries): depth 3
* - Large project (> 5000 entries): depth 2
* @param {string} projectRoot
* @returns {number}
*/
function _suggestTreeDepth(projectRoot) {
const count = _countFilesQuick(projectRoot);
if (count < 500) return 4;
if (count <= 5000) return 3;
return 2;
}
function _normalizeTreeRoot(treeStr, absRoot, virtualRoot = "/codebase") {
const rootPattern = new RegExp(absRoot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
let out = String(treeStr || "").replace(rootPattern, virtualRoot);
const lines = out.split("\n");
const dirName = absRoot.split("/").pop() || absRoot.split("\\").pop() || absRoot;
if (lines[0] === dirName) {
lines[0] = virtualRoot;
out = lines.join("\n");
}
return out;
}
/**
* Get a directory tree of the project with adaptive depth fallback.
*
* Tries the requested depth first. If the tree output exceeds MAX_TREE_BYTES,
* automatically falls back to lower depths until it fits.
*
* @param {string} projectRoot
* @param {number} [targetDepth=3] - Desired tree depth (0-6), 0 means auto
* @param {string[]} [excludePaths=[]] - Patterns to exclude from tree
* @returns {{ tree: string, depth: number, sizeBytes: number, fellBack: boolean, autoDepth: boolean }}
*/
function getRepoMap(projectRoot, targetDepth = 3, excludePaths = []) {
// Auto depth: if targetDepth is 0, use heuristic
const autoDepth = targetDepth === 0;
if (autoDepth) {
targetDepth = _suggestTreeDepth(projectRoot);
}
const excludeRegexes = excludePaths.length ? excludePaths.map(_excludePatternToRegex) : [];
for (let L = targetDepth; L >= 1; L--) {
try {
const opts = { maxDepth: L };
if (excludeRegexes.length) opts.exclude = excludeRegexes;
const stdout = treeNodeCli(projectRoot, opts);
// Normalize root to /codebase consistently.
let treeStr = _normalizeTreeRoot(stdout, projectRoot, "/codebase");
const sizeBytes = Buffer.byteLength(treeStr, "utf-8");
if (sizeBytes <= MAX_TREE_BYTES) {
return { tree: treeStr, depth: L, sizeBytes, fellBack: L < targetDepth, autoDepth };
}
// Too large, try lower depth
} catch {
// tree failed at this level, try lower
}
}
// Ultimate fallback: simple ls (also respects excludePaths)
try {
let entries = readdirSync(projectRoot).sort();
if (excludeRegexes.length) {
entries = entries.filter((e) => !excludeRegexes.some((rx) => rx.test(e)));
}
const treeStr = ["/codebase", ...entries.map((e) => `├── ${e}`)].join("\n");
return { tree: treeStr, depth: 0, sizeBytes: Buffer.byteLength(treeStr, "utf-8"), fellBack: true, autoDepth };
} catch {
const treeStr = "/codebase\n(empty or inaccessible)";
return { tree: treeStr, depth: 0, sizeBytes: treeStr.length, fellBack: true, autoDepth };
}
}
function _tokenizeQuery(query = "") {
return [...new Set(
String(query)
.toLowerCase()
.split(/[^a-z0-9_\-]+/)
.map((t) => t.trim())
.filter((t) => t.length >= 3)
)];
}
function _scoreTopLevelDir(dirName, queryTokens = []) {
const name = String(dirName || "").toLowerCase();
let score = 0;
const commonRoots = ["src", "app", "lib", "packages", "services", "server", "backend", "frontend", "api"];
if (commonRoots.includes(name)) score += 2;
for (const token of queryTokens) {
if (name.includes(token)) score += 4;
}
return score;
}
function _listTopLevelDirs(projectRoot, excludePaths = []) {
const excludeRegexes = excludePaths.length ? excludePaths.map(_excludePatternToRegex) : [];
const out = [];
let entries = [];
try {
entries = readdirSync(projectRoot).sort();
} catch {
return out;
}
for (const e of entries) {
if (excludeRegexes.some((rx) => rx.test(e))) continue;
const abs = join(projectRoot, e);
try {
if (statSync(abs).isDirectory()) out.push(e);
} catch {
// ignore
}
}
return out;
}
function _buildSubtreeForDir(projectRoot, dir, levels = 2) {
const abs = join(projectRoot, dir);
const vRoot = `/codebase/${dir}`;
try {
const stdout = treeNodeCli(abs, { maxDepth: levels });
return _normalizeTreeRoot(stdout, abs, vRoot);
} catch {
return `${vRoot}\n (failed to generate subtree)`;
}
}
function buildOptimizedRepoMap({
query,
projectRoot,
treeDepth,
excludePaths,
optimizer = {},
bootstrapHints = null,
onProgress = null,
}) {
const log = (msg) => onProgress?.(msg);
const cfg = { ...REPO_MAP_OPTIMIZER_DEFAULTS, ...(optimizer || {}) };
if (cfg.mode === "classic") {
const base = getRepoMap(projectRoot, treeDepth, excludePaths);
return {
...base,
strategy: "classic",
hotDirs: [],
};
}
const bootstrapDepth = Math.max(1, Math.min(3, Number(cfg.bootstrapTreeDepth) || 1));
const hotspotTopK = Math.max(0, Math.min(8, Number(cfg.hotspotTopK) || 4));
const hotspotTreeDepth = Math.max(1, Math.min(4, Number(cfg.hotspotTreeDepth) || 2));
const maxBytes = Math.max(16 * 1024, Number(cfg.maxBytes) || REPO_MAP_OPTIMIZER_DEFAULTS.maxBytes);
const bootstrap = getRepoMap(projectRoot, bootstrapDepth, excludePaths);
const topDirs = _listTopLevelDirs(projectRoot, excludePaths);
// Extract keywords from bootstrap hints (rgPatterns)
const keywords = bootstrapHints?.rgPatterns || [];
// Use BM25F + Probe + RRF for directory scoring
// This replaces the old token-based scoring + commonRoots approach
let hotDirs = [];
let pathSpines = [];
try {
const results = scoreDirectories(query, projectRoot, topDirs, excludePaths, {
topK: hotspotTopK,
useProbe: true, // Enable probe grep signal
keywords, // Bootstrap keywords
minReturn: 2, // Always return at least 2 directories for coverage
});
hotDirs = results.hotDirs;
pathSpines = results.pathSpines;
log(`BM25F scoring: hotDirs=[${hotDirs.join(",")}] pathSpines=${pathSpines.length} signals=${JSON.stringify(results.signals)}`);
} catch (e) {
// Lightweight fallback: use quick scoring without commonRoots
log(`BM25F failed, using quick token scoring: ${e.message}`);
const queryTerms = tokenizeBM25(query);
const scored = topDirs.map((d) => {
const dirTerms = tokenizeBM25(d);
let score = 0;
for (const qt of queryTerms) {
if (dirTerms.some(dt => dt.includes(qt) || qt.includes(dt))) score += 1;
}
return { dir: d, score };
}).sort((a, b) => b.score - a.score);
// Always return at least topK directories (no score > 0 filter)
hotDirs = scored.slice(0, hotspotTopK).map((x) => x.dir);
if (hotDirs.length === 0) hotDirs = topDirs.slice(0, hotspotTopK);
log(`Quick scoring fallback: ${hotDirs.join(",")}`);
}
const hotspotSections = [];
for (const d of hotDirs) {
hotspotSections.push(_buildSubtreeForDir(projectRoot, d, hotspotTreeDepth));
}
// Build path spines section for deep file visibility
const pathSpineSection = pathSpines.length > 0
? "# Relevant File Paths (from BM25F path spine extraction)\n" + pathSpines.map(p => `- /codebase/${p}`).join("\n")
: "";
let tree = bootstrap.tree;
const sections = [];
if (hotspotSections.length) {
sections.push("# Hotspot Subtrees\n" + hotspotSections.join("\n\n"));
}
if (pathSpineSection) {
sections.push(pathSpineSection);
}
if (sections.length) {
tree = `${bootstrap.tree}\n\n${sections.join("\n\n")}`;
}
// Keep map under configurable budget.
let sizeBytes = Buffer.byteLength(tree, "utf-8");
if (sizeBytes > maxBytes && (hotspotSections.length || pathSpineSection)) {
// First try removing path spines
if (pathSpineSection) {
const withoutSpines = sections.length > 1
? `${bootstrap.tree}\n\n${sections[0]}`
: bootstrap.tree;
sizeBytes = Buffer.byteLength(withoutSpines, "utf-8");
if (sizeBytes <= maxBytes) {
tree = withoutSpines;
}
}
// If still too large, progressively remove hotspot sections
if (sizeBytes > maxBytes && hotspotSections.length) {
let kept = [...hotspotSections];
while (kept.length > 0) {
kept.pop();
tree = kept.length
? `${bootstrap.tree}\n\n# Hotspot Subtrees\n${kept.join("\n\n")}`
: bootstrap.tree;
sizeBytes = Buffer.byteLength(tree, "utf-8");
if (sizeBytes <= maxBytes) break;
}
}
}
return {
tree,
depth: bootstrap.depth,
sizeBytes: Buffer.byteLength(tree, "utf-8"),
fellBack: bootstrap.fellBack,
autoDepth: bootstrap.autoDepth,
strategy: "bootstrap_hotspot",
hotDirs,
};
}
/**
* Parse answer XML into structured file + range data.
* @param {string} xmlText
* @param {string} projectRoot
* @returns {{ files: Array }}
*/
function _parseAnswer(xmlText, projectRoot) {
const files = [];
const resolvedRoot = resolve(projectRoot);
const fileRegex = /<file\s+path=(["'])([^"']+)\1>([\s\S]*?)<\/file>/g;
let fm;
while ((fm = fileRegex.exec(xmlText)) !== null) {
const vpath = fm[2];
let rel = vpath.replace(/^\/codebase[\/\\]?/, "");
rel = rel.replace(/^[\/\\]+/, "");
// Path safety: reject traversal attempts (../) and paths outside project root
const fullPath = resolve(projectRoot, rel);
const relToRoot = relative(resolvedRoot, fullPath);
if (relToRoot === ".." || relToRoot.startsWith(`..${sep}`) || isAbsolute(relToRoot)) {
continue;
}
const ranges = [];
const rangeRegex = /<range>(\d+)-(\d+)<\/range>/g;
let rm;
while ((rm = rangeRegex.exec(fm[3])) !== null) {
ranges.push([parseInt(rm[1], 10), parseInt(rm[2], 10)]);
}
files.push({ path: rel, full_path: fullPath, ranges });
}
return { files };
}
/**
* Execute Fast Context search.
*
* @param {Object} opts
* @param {string} opts.query - Natural language search query
* @param {string} opts.projectRoot - Project root directory
* @param {string} [opts.apiKey] - Windsurf API key (auto-discovered if not set)
* @param {string} [opts.jwt] - JWT token (auto-fetched if not set)
* @param {number} [opts.maxTurns=3] - Search rounds
* @param {number} [opts.maxCommands=8] - Max commands per round
* @param {number} [opts.maxResults=10] - Max number of files to return
* @param {number} [opts.treeDepth=3] - Directory tree depth for repo map (1-6, auto fallback)
* @param {number} [opts.timeoutMs=30000] - Connect-Timeout-Ms for streaming requests
* @param {string[]} [opts.excludePaths=[]] - Patterns to exclude from tree
* @param {function} [opts.onProgress] - Progress callback
* @returns {Promise<Object>}
*/
export async function search({
query,
projectRoot,
apiKey = null,
jwt = null,
maxTurns = 3,
maxCommands = 8,
maxResults = 10,
treeDepth = 3,
timeoutMs = 30000,
excludePaths = [],
repoMapMode = "bootstrap_hotspot",
bootstrapTreeDepth = 1,
hotspotTopK = 4,
hotspotTreeDepth = 2,
hotspotMaxBytes = 120 * 1024,
bootstrapEnabled = true,
bootstrapMaxTurns = 2,
bootstrapMaxCommands = 6,
onProgress = null,
}) {
const log = (msg) => onProgress?.(msg);
projectRoot = resolve(projectRoot);
const effectiveExcludePaths = _mergeExcludePaths(excludePaths);
// Get credentials
if (!apiKey) {
apiKey = await getApiKey();
}
if (!jwt) {
log("Fetching JWT...");
jwt = await getCachedJwt(apiKey);
}
// Check rate limit
log("Checking rate limit...");
if (!(await checkRateLimit(apiKey, jwt))) {
return { files: [], error: "Rate limited, please try again later" };
}
const executor = new ToolExecutor(projectRoot);
const toolDefs = getToolDefinitions(maxCommands);
const systemPrompt = buildSystemPrompt(maxTurns, maxCommands, maxResults);
let bootstrapHints = null;
if (bootstrapEnabled) {
bootstrapHints = await _runBootstrapPhase({
query,
projectRoot,
apiKey,
jwt,
timeoutMs,
excludePaths: effectiveExcludePaths,
bootstrapTreeDepth,
bootstrapMaxTurns,
bootstrapMaxCommands,
onProgress,
});
log(`Bootstrap hints: patterns=${bootstrapHints.rgPatterns.length}, hot_dirs=${bootstrapHints.hotDirs.length}`);
}
const { tree: repoMap, depth: actualDepth, sizeBytes: treeSizeBytes, fellBack, autoDepth, strategy: repoMapStrategy, hotDirs = [] } = buildOptimizedRepoMap({
query,
projectRoot,
treeDepth,
excludePaths: effectiveExcludePaths,
optimizer: {
mode: repoMapMode,
bootstrapTreeDepth,
hotspotTopK,
hotspotTreeDepth,
maxBytes: hotspotMaxBytes,
},
bootstrapHints,
onProgress,
});
log(`Repo map: tree -L ${actualDepth} (${(treeSizeBytes / 1024).toFixed(1)}KB)${fellBack ? ` [fell back from L=${treeDepth}]` : ""}${autoDepth ? " [auto]" : ""} [strategy=${repoMapStrategy}]${hotDirs.length ? ` [hot=${hotDirs.join(",")}]` : ""}`);
const userContent = `Problem Statement: ${query}\n\nRepo Map (tree -L ${actualDepth} /codebase):\n\`\`\`text\n${repoMap}\n\`\`\``;
const messages = [
{ role: 5, content: systemPrompt },
{ role: 1, content: userContent },
];
// Trim state for smart context trimming
const trimState = {
query,
turn: 0,
recentFiles: [],
recentPatterns: [],
recentCommands: [],
};
// Total API calls = maxTurns + 1 (last round for answer)
const totalApiCalls = maxTurns + 1;
let compensatedTurns = 0;
const MAX_COMPENSATIONS = 2;
let forceAnswerInjected = false;
for (let turn = 0; turn < totalApiCalls + compensatedTurns; turn++) {
log(`Turn ${turn + 1}/${totalApiCalls}`);
trimState.turn = turn + 1;
let proto = _buildRequest(apiKey, jwt, messages, toolDefs);
// Debug logging
if (DEBUG_MODE) {
console.error(`\n[DEBUG] ===== Turn ${turn + 1} Request =====`);
console.error(`[DEBUG] Messages count: ${messages.length}`);
console.error(`[DEBUG] Last message role: ${messages[messages.length - 1]?.role}`);
console.error(`[DEBUG] Proto size: ${proto.length} bytes`);
}
// Preflight trim: proactively reduce payload if proto is already large.
const MAX_PROTO_BYTES = 320 * 1024;
if (proto.length > MAX_PROTO_BYTES && messages.length > 1) {
log(`Proto size ${proto.length} bytes > ${MAX_PROTO_BYTES}. Trimming context before request...`);
if (_trimMessages(messages, trimState)) {
proto = _buildRequest(apiKey, jwt, messages, toolDefs);
if (DEBUG_MODE) console.error(`[DEBUG] Proto size after trim: ${proto.length} bytes`);
}
}
let respData;
try {
respData = await _streamingRequest(proto, timeoutMs);
} catch (e) {
const errCode = e.code || "UNKNOWN";
const baseMeta = {
treeDepth: actualDepth,
treeSizeKB: +(treeSizeBytes / 1024).toFixed(1),
fellBack,
projectRoot,
errorCode: errCode,
repoMapStrategy,
hotDirs,
};
// Auto-retry with trimmed context on payload/timeout errors
if ((errCode === "PAYLOAD_TOO_LARGE" || errCode === "TIMEOUT") && messages.length > 1) {
log(`${errCode} on turn ${turn + 1}: trimming context and retrying...`);
_trimMessages(messages, trimState);
const retryProto = _buildRequest(apiKey, jwt, messages, toolDefs);
try {
respData = await _streamingRequest(retryProto, timeoutMs);
} catch (retryErr) {
const retryCode = retryErr.code || errCode;
return {
files: [],
error: `${retryCode}: ${retryErr.message} (retry after context trim also failed)`,
_meta: { ...baseMeta, errorCode: retryCode, contextTrimmed: true },
};
}
} else {
return {
files: [],
error: `${errCode}: ${e.message}`,
_meta: baseMeta,
};
}
}
const [thinking, toolInfo] = _parseResponse(respData);
// Debug logging
if (DEBUG_MODE) {
console.error(`\n[DEBUG] ===== Turn ${turn + 1} Response =====`);
console.error(`[DEBUG] Response size: ${respData.length} bytes`);
console.error(`[DEBUG] Thinking: ${thinking.slice(0, 500)}${thinking.length > 500 ? '...' : ''}`);
console.error(`[DEBUG] Tool info: ${toolInfo ? `${toolInfo[0]}` : 'null'}`);
}
if (toolInfo === null) {
if (thinking.startsWith("[Error]")) {
return { files: [], error: thinking };
}
return { files: [], raw_response: thinking };
}
const [toolName, toolArgs] = toolInfo;
if (toolName === "answer") {
const answerXml = toolArgs.answer || "";
log("Received final answer");
const result = _parseAnswer(answerXml, projectRoot);
result.rg_patterns = [...new Set(executor.collectedRgPatterns)];
result._meta = {
treeDepth: actualDepth,
treeSizeKB: +(treeSizeBytes / 1024).toFixed(1),
fellBack,
repoMapStrategy,
hotDirs,
};
return result;
}
if (toolName === "restricted_exec") {
const callId = randomUUID();
const argsJson = JSON.stringify(toolArgs);
const cmds = Object.keys(toolArgs).filter((k) => k.startsWith("command"));
log(`Executing ${cmds.length} local commands`);
// Debug logging
if (DEBUG_MODE) {
console.error(`\n[DEBUG] ===== Tool Calls =====`);
for (const cmdKey of cmds) {
const cmd = toolArgs[cmdKey];
console.error(`[DEBUG] ${cmdKey}: ${JSON.stringify(cmd)}`);
}
}
// Check for valid commands (those with a type field)
const validCommands = cmds.filter((k) => {
const cmd = toolArgs[k];
return cmd && typeof cmd === "object" && cmd.type;
});
if (validCommands.length === 0 && compensatedTurns < MAX_COMPENSATIONS) {
compensatedTurns++;
log(`Turn compensation: no valid commands, extending search by 1 turn (${compensatedTurns}/${MAX_COMPENSATIONS})`);
} else if (validCommands.length === 0) {
log(`Turn compensation skipped: max compensations (${MAX_COMPENSATIONS}) reached, forcing turn advance`);
}
const results = await executor.execToolCallAsync(toolArgs);
// Update trim state with a compact summary of what we executed
try {
const tailUnique = (arr, n) => {
const out = [];
const seen = new Set();
for (let i = arr.length - 1; i >= 0 && out.length < n; i--) {
const v = arr[i];
if (typeof v !== "string" || !v) continue;
if (seen.has(v)) continue;
seen.add(v);
out.push(v);
}
return out.reverse();
};
const newCommands = [];
const newFiles = [];
const newPatterns = [];
for (const cmdKey of cmds) {
const cmd = toolArgs[cmdKey];
if (!cmd || typeof cmd !== "object") continue;
const t = cmd.type;
if (t === "rg" && cmd.pattern) {
newPatterns.push(cmd.pattern);
newCommands.push({ type: "rg", desc: `rg ${cmd.pattern}` });
} else if (t === "readfile" && cmd.file) {
const shortFile = cmd.file.replace(/^\/codebase\//, "");
newFiles.push(shortFile);
newCommands.push({ type: "readfile", desc: `read ${shortFile}` });
} else if (t === "tree" && cmd.path) {
newCommands.push({ type: "tree", desc: `tree ${cmd.path}` });
}
}
trimState.recentCommands = [...trimState.recentCommands, ...newCommands].slice(-12);
trimState.recentFiles = tailUnique([...trimState.recentFiles, ...newFiles], 20);
trimState.recentPatterns = tailUnique([...trimState.recentPatterns, ...newPatterns], 30);
} catch {
// Ignore errors in trim state update
}
messages.push({
role: 2,
content: thinking,
tool_call_id: callId,
tool_name: "restricted_exec",
tool_args_json: argsJson,
});
messages.push({ role: 4, content: results, ref_call_id: callId });
// Inject force-answer after last effective search round
const effectiveTurn = turn - compensatedTurns;
if (effectiveTurn >= maxTurns - 1 && !forceAnswerInjected) {
messages.push({ role: 1, content: FINAL_FORCE_ANSWER });
forceAnswerInjected = true;
log("Injected force-answer prompt");
}
}
}
return {
files: [],
error: "Max turns reached without getting an answer",
rg_patterns: [...new Set(executor.collectedRgPatterns)],
_meta: {
treeDepth: actualDepth,
treeSizeKB: +(treeSizeBytes / 1024).toFixed(1),
fellBack,
projectRoot,
repoMapStrategy,
hotDirs,
},
};
}
/**
* Search and return formatted result suitable for MCP tool response.
*
* @param {Object} opts
* @param {string} opts.query
* @param {string} opts.projectRoot
* @param {string} [opts.apiKey]
* @param {number} [opts.maxTurns=3]
* @param {number} [opts.maxCommands=8]
* @param {number} [opts.maxResults=10]
* @param {number} [opts.treeDepth=3]
* @param {number} [opts.timeoutMs=30000]
* @param {string[]} [opts.excludePaths=[]]
* @returns {Promise<string>}
*/
export async function searchWithContent({
query,
projectRoot,
apiKey = null,
maxTurns = 3,
maxCommands = 8,
maxResults = 10,
treeDepth = 3,
timeoutMs = 30000,
excludePaths = [],
repoMapMode = "bootstrap_hotspot",
bootstrapTreeDepth = 1,
hotspotTopK = 4,
hotspotTreeDepth = 2,
hotspotMaxBytes = 120 * 1024,
bootstrapEnabled = true,
bootstrapMaxTurns = 2,
bootstrapMaxCommands = 6,
}) {
const result = await search({
query,
projectRoot,
apiKey,
maxTurns,
maxCommands,
maxResults,
treeDepth,
timeoutMs,
excludePaths,
repoMapMode,
bootstrapTreeDepth,
hotspotTopK,
hotspotTreeDepth,
hotspotMaxBytes,
bootstrapEnabled,
bootstrapMaxTurns,
bootstrapMaxCommands,
});
if (result.error) {
const meta = result._meta;
let errMsg = `Error: ${result.error}`;
if (meta) {
errMsg += `\n\n[diagnostic] error_type=${meta.errorCode || "unknown"}, tree_depth_used=${meta.treeDepth}, tree_size=${meta.treeSizeKB}KB`;
if (meta.fellBack) errMsg += ` (auto fell back from requested depth)`;
if (meta.contextTrimmed) errMsg += `, context_trimmed=true`;
if (meta.projectRoot) errMsg += `\n[diagnostic] project_path=${meta.projectRoot}`;
errMsg += `\n[config] max_turns=${maxTurns}, max_results=${maxResults}, max_commands=${maxCommands}, timeout_ms=${timeoutMs}`;
if (excludePaths.length) errMsg += `, exclude_paths=[${excludePaths.join(", ")}]`;
// Targeted hints based on error type
if (meta.errorCode === "PAYLOAD_TOO_LARGE" || meta.errorCode === "TIMEOUT") {
errMsg += `\n[hint] Payload/timeout error. Try: reduce tree_depth, reduce max_turns, add exclude_paths, or narrow project_path to a subdirectory.`;
} else if (meta.errorCode === "AUTH_ERROR") {
errMsg += `\n[hint] Authentication error. The API key may be expired or revoked. Run fast-context-search.mjs --check-key, ensure Windsurf is logged in, or set a fresh WINDSURF_API_KEY.`;
} else if (meta.errorCode === "RATE_LIMITED") {
errMsg += `\n[hint] Rate limited. Wait a moment and retry.`;
} else {
errMsg += `\n[hint] If the error is payload-related, try a lower tree_depth value or add exclude_paths.`;
}
}
return errMsg;
}
const files = result.files || [];
const rgPatterns = result.rg_patterns || [];
// Deduplicate + filter short patterns
const uniquePatterns = [...new Set(rgPatterns)].filter((p) => p.length >= 3);
if (!files.length && !uniquePatterns.length) {
const raw = result.raw_response || "";
if (!raw) return "No relevant files found.";
const MAX_RAW = 500;
const truncated = raw.length > MAX_RAW ? raw.slice(0, MAX_RAW) + "\n...[raw_response truncated]..." : raw;
return `No relevant files found.\n\nRaw response:\n${truncated}`;
}
const parts = [];
const n = files.length;
if (files.length) {
parts.push(`Found ${n} relevant files.`);
parts.push("");
for (let i = 0; i < files.length; i++) {
const entry = files[i];
const rangesStr = entry.ranges.map(([s, e]) => `L${s}-${e}`).join(", ");
parts.push(` [${i + 1}/${n}] ${entry.full_path} (${rangesStr})`);
}
} else {
parts.push("No files found.");
}
if (uniquePatterns.length) {
parts.push("");
parts.push(`grep keywords: ${uniquePatterns.join(", ")}`);
}
// Append diagnostic metadata so the calling AI knows what happened
const meta = result._meta;
if (meta) {
const fbNote = meta.fellBack ? ` (fell back from requested depth)` : "";
parts.push("");
let configLine = `[config] tree_depth=${meta.treeDepth}${fbNote}, tree_size=${meta.treeSizeKB}KB, max_turns=${maxTurns}, max_results=${maxResults}, timeout_ms=${timeoutMs}`;
if (excludePaths.length) configLine += `, exclude_paths=[${excludePaths.join(", ")}]`;
parts.push(configLine);
}
return parts.join("\n");
}
/**
* Extract Windsurf API Key info (for MCP tool use).
* @returns {Promise<Object>}
*/
export async function extractKeyInfo(dbPath) {
return extractKey(dbPath);
}
/**
* Windsurf API Key extraction from local installation.
*
* Cross-platform: macOS / Windows / Linux.
* Uses sql.js (pure JS/WASM) to read state.vscdb — no native compilation needed.
*/
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { homedir, platform } from "node:os";
import initSqlJs from "sql.js";
/**
* Get the platform-specific path to Windsurf's state.vscdb.
* @returns {string}
*/
export function getDbPath() {
const plat = platform();
const home = homedir();
if (plat === "darwin") {
return join(home, "Library", "Application Support", "Windsurf", "User", "globalStorage", "state.vscdb");
} else if (plat === "win32") {
const appdata = process.env.APPDATA || "";
if (!appdata) throw new Error("Cannot determine APPDATA path");
return join(appdata, "Windsurf", "User", "globalStorage", "state.vscdb");
} else {
// Linux
const config = process.env.XDG_CONFIG_HOME || join(home, ".config");
return join(config, "Windsurf", "User", "globalStorage", "state.vscdb");
}
}
/**
* Extract API Key from Windsurf state.vscdb.
* @param {string} [dbPath]
* @returns {Promise<{ api_key?: string, db_path: string, error?: string, hint?: string }>}
*/
export async function extractKey(dbPath) {
if (!dbPath) {
dbPath = getDbPath();
}
if (!existsSync(dbPath)) {
return {
error: `Windsurf database not found: ${dbPath}`,
hint: "Ensure Windsurf is installed and logged in.",
db_path: dbPath,
};
}
let db;
try {
const SQL = await initSqlJs();
const buf = readFileSync(dbPath);
db = new SQL.Database(buf);
} catch (e) {
return { error: `Failed to open database: ${e.message}`, db_path: dbPath };
}
try {
const stmt = db.prepare("SELECT value FROM ItemTable WHERE key = 'windsurfAuthStatus'");
if (!stmt.step()) {
stmt.free();
return {
error: "windsurfAuthStatus record not found",
hint: "Ensure Windsurf is logged in.",
db_path: dbPath,
};
}
const row = stmt.getAsObject();
stmt.free();
let data;
try {
data = JSON.parse(row.value);
} catch {
return { error: "windsurfAuthStatus data parse failed", db_path: dbPath };
}
const apiKey = data.apiKey || "";
if (!apiKey) {
return { error: "apiKey field is empty", db_path: dbPath };
}
return { api_key: apiKey, db_path: dbPath };
} catch (e) {
return { error: `Extraction failed: ${e.message}`, db_path: dbPath };
} finally {
db.close();
}
}
MIT License
Copyright (c) 2025
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
/**
* Hand-written Protobuf encoder/decoder + Connect-RPC frame handling.
*
* Matches the Windsurf wire format exactly.
* Python bytearray → Node.js Buffer
* struct.pack(">I", len) → buf.writeUInt32BE
* gzip.compress/decompress → zlib.gzipSync/gunzipSync
*/
import { gzipSync, gunzipSync } from "node:zlib";
// ─── Protobuf Encoder ──────────────────────────────────────
export class ProtobufEncoder {
constructor() {
/** @type {Buffer[]} */
this._chunks = [];
}
/**
* Encode an unsigned varint into a Buffer.
* @param {number} value
* @returns {Buffer}
*/
_varint(value) {
const bytes = [];
while (value > 0x7f) {
bytes.push((value & 0x7f) | 0x80);
value >>>= 7;
}
bytes.push(value & 0x7f);
return Buffer.from(bytes);
}
/**
* Encode a field tag.
* @param {number} field
* @param {number} wire
* @returns {Buffer}
*/
_tag(field, wire) {
return this._varint((field << 3) | wire);
}
/**
* Write a varint field.
* @param {number} field
* @param {number} value
* @returns {ProtobufEncoder}
*/
writeVarint(field, value) {
this._chunks.push(this._tag(field, 0), this._varint(value));
return this;
}
/**
* Write a length-delimited string field.
* @param {number} field
* @param {string} value
* @returns {ProtobufEncoder}
*/
writeString(field, value) {
const data = Buffer.from(value, "utf-8");
this._chunks.push(this._tag(field, 2), this._varint(data.length), data);
return this;
}
/**
* Write a length-delimited bytes field.
* @param {number} field
* @param {Buffer|Uint8Array} value
* @returns {ProtobufEncoder}
*/
writeBytes(field, value) {
const buf = Buffer.isBuffer(value) ? value : Buffer.from(value);
this._chunks.push(this._tag(field, 2), this._varint(buf.length), buf);
return this;
}
/**
* Write a nested message field.
* @param {number} field
* @param {ProtobufEncoder} sub
* @returns {ProtobufEncoder}
*/
writeMessage(field, sub) {
const data = sub.toBuffer();
this._chunks.push(this._tag(field, 2), this._varint(data.length), data);
return this;
}
/**
* Return the encoded bytes as a Buffer.
* @returns {Buffer}
*/
toBuffer() {
return Buffer.concat(this._chunks);
}
}
// ─── Varint Decode ─────────────────────────────────────────
/**
* Decode a varint from a buffer at the given offset.
* @param {Buffer} buf
* @param {number} offset
* @returns {[number, number]} [value, newOffset]
*/
export function decodeVarint(buf, offset) {
let value = 0;
let shift = 0;
while (offset < buf.length) {
const b = buf[offset++];
value |= (b & 0x7f) << shift;
shift += 7;
if (!(b & 0x80)) break;
}
return [value, offset];
}
// ─── Protobuf String Extraction ────────────────────────────
/**
* Extract all UTF-8 strings (length > 5) from raw protobuf data
* by parsing wire types. Matches Python proto_extract_strings().
* @param {Buffer} data
* @returns {string[]}
*/
export function extractStrings(data) {
const strings = [];
let i = 0;
while (i < data.length) {
// Read tag varint
let tag = 0;
let shift = 0;
while (i < data.length) {
const b = data[i++];
tag |= (b & 0x7f) << shift;
shift += 7;
if (!(b & 0x80)) break;
}
const wire = tag & 0x7;
if (wire === 0) {
// Varint — skip
while (i < data.length) {
const b = data[i++];
if (!(b & 0x80)) break;
}
} else if (wire === 1) {
// 64-bit fixed
i += 8;
} else if (wire === 2) {
// Length-delimited
let length = 0;
shift = 0;
while (i < data.length) {
const b = data[i++];
length |= (b & 0x7f) << shift;
shift += 7;
if (!(b & 0x80)) break;
}
if (i + length <= data.length) {
const raw = data.subarray(i, i + length);
try {
const text = raw.toString("utf-8");
if (text.length > 5) {
strings.push(text);
}
} catch {
// Not valid UTF-8, skip
}
}
i += length;
} else if (wire === 5) {
// 32-bit fixed
i += 4;
} else {
// Unknown wire type — stop
break;
}
}
return strings;
}
// ─── Connect-RPC Frame Encode/Decode ───────────────────────
/**
* Encode protobuf bytes into a gzip-compressed Connect-RPC frame.
* Frame format: 1-byte flags + 4-byte big-endian length + payload
* @param {Buffer} protoBytes
* @param {boolean} [compress=true]
* @returns {Buffer}
*/
export function connectFrameEncode(protoBytes, compress = true) {
let payload;
let flags;
if (compress) {
payload = gzipSync(protoBytes);
flags = 1; // gzip compressed
} else {
payload = protoBytes;
flags = 0;
}
const header = Buffer.alloc(5);
header[0] = flags;
header.writeUInt32BE(payload.length, 1);
return Buffer.concat([header, payload]);
}
/**
* Decode Connect-RPC frames from raw response data.
* Handles gzip-compressed frames (flags 1 or 3).
* @param {Buffer} data
* @returns {Buffer[]}
*/
export function connectFrameDecode(data) {
const frames = [];
let i = 0;
while (i + 5 <= data.length) {
const flags = data[i];
const length = data.readUInt32BE(i + 1);
i += 5;
let payload = data.subarray(i, i + length);
i += length;
if (flags === 1 || flags === 3) {
try {
payload = gunzipSync(payload);
} catch {
// Decompression failed — use raw payload
}
}
frames.push(Buffer.from(payload));
}
return frames;
}
#!/usr/bin/env node
import { execFileSync, spawnSync } from "node:child_process";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import assert from "node:assert/strict";
import initSqlJs from "sql.js";
const scriptPath = resolve("scripts", "fast-context-search.mjs");
const tmpRoot = mkdtempSync(join(tmpdir(), "fast-context-skill-test-"));
const fakeKey = "sk-test-windsurf-key-1234567890";
function run(args) {
return execFileSync(process.execPath, [scriptPath, ...args], {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
}
try {
const SQL = await initSqlJs();
const db = new SQL.Database();
const dbPath = join(tmpRoot, "state.vscdb");
db.run("CREATE TABLE ItemTable (key TEXT PRIMARY KEY, value TEXT)");
db.run("INSERT INTO ItemTable (key, value) VALUES (?, ?)", [
"windsurfAuthStatus",
JSON.stringify({ apiKey: fakeKey }),
]);
writeFileSync(dbPath, Buffer.from(db.export()));
db.close();
const checkOutput = run(["--check-key", "--db-path", dbPath]);
assert.match(checkOutput, /Windsurf key discovered\./);
assert.match(checkOutput, /Key: sk-test-.*567890/);
assert.match(checkOutput, /Source: /);
assert.doesNotMatch(checkOutput, new RegExp(fakeKey));
const printOutput = run(["--print-key", "--db-path", dbPath]).trim();
assert.equal(printOutput, fakeKey);
const envOutput = run(["--key-env", "--db-path", dbPath]).trim();
assert.equal(envOutput, `export WINDSURF_API_KEY='${fakeKey}'`);
const conflict = spawnSync(
process.execPath,
[scriptPath, "--check-key", "--print-key", "--db-path", dbPath],
{ encoding: "utf8" }
);
assert.equal(conflict.status, 2);
assert.match(conflict.stderr, /Choose only one key command/);
} finally {
rmSync(tmpRoot, { recursive: true, force: true });
}