feat: add Brave Search integration for researchers

- Add `websearch` command to gsd-tools.js for Brave API
- Detect BRAVE_API_KEY env var or ~/.gsd/brave_api_key file
- Persist brave_search setting to config.json on project init
- Update researcher agents to check config before calling

Graceful degradation: if brave_search is false, agents use
built-in WebSearch without wasted Bash calls.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Lex Christopherson
2026-02-08 10:36:26 -06:00
parent 9adb09fd29
commit 60ccba9446
3 changed files with 127 additions and 1 deletions

View File

@@ -97,6 +97,22 @@ When researching "best library for X": find what the ecosystem actually uses, do
**WebSearch tips:** Always include current year. Use multiple query variations. Cross-verify with authoritative sources.
## Enhanced Web Search (Brave API)
Check `brave_search` from init context. If `true`, use Brave Search for higher quality results:
```bash
node ~/.claude/get-shit-done/bin/gsd-tools.js websearch "your query" --limit 10
```
**Options:**
- `--limit N` — Number of results (default: 10)
- `--freshness day|week|month` — Restrict to recent content
If `brave_search: false` (or not set), use built-in WebSearch tool instead.
Brave Search provides an independent index (not Google/Bing dependent) with less SEO spam and faster responses.
## Verification Protocol
**WebSearch findings MUST be verified:**

View File

@@ -91,6 +91,22 @@ Problems: "[tech] common mistakes", "[tech] gotchas"
Always include current year. Use multiple query variations. Mark WebSearch-only findings as LOW confidence.
### Enhanced Web Search (Brave API)
Check `brave_search` from orchestrator context. If `true`, use Brave Search for higher quality results:
```bash
node ~/.claude/get-shit-done/bin/gsd-tools.js websearch "your query" --limit 10
```
**Options:**
- `--limit N` — Number of results (default: 10)
- `--freshness day|week|month` — Restrict to recent content
If `brave_search: false` (or not set), use built-in WebSearch tool instead.
Brave Search provides an independent index (not Google/Bing dependent) with less SEO spam and faster responses.
## Verification Protocol
**WebSearch findings must be verified:**

View File

@@ -26,6 +26,8 @@
* summary-extract <path> [--fields] Extract structured data from SUMMARY.md
* state-snapshot Structured parse of STATE.md
* phase-plan-index <phase> Index plans with waves and status
* websearch <query> Search web via Brave API (if configured)
* [--limit N] [--freshness day|week|month]
*
* Phase Operations:
* phase next-decimal <phase> Calculate next decimal phase number
@@ -165,6 +167,7 @@ function loadConfig(cwd) {
plan_checker: true,
verifier: true,
parallelization: true,
brave_search: false,
};
try {
@@ -197,6 +200,7 @@ function loadConfig(cwd) {
plan_checker: get('plan_checker', { section: 'workflow', field: 'plan_check' }) ?? defaults.plan_checker,
verifier: get('verifier', { section: 'workflow', field: 'verifier' }) ?? defaults.verifier,
parallelization,
brave_search: get('brave_search') ?? defaults.brave_search,
};
} catch {
return defaults;
@@ -584,6 +588,11 @@ function cmdConfigEnsureSection(cwd, raw) {
return;
}
// Detect Brave Search API key availability
const homedir = require('os').homedir();
const braveKeyFile = path.join(homedir, '.gsd', 'brave_api_key');
const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile));
// Create default config
const defaults = {
model_profile: 'balanced',
@@ -598,6 +607,7 @@ function cmdConfigEnsureSection(cwd, raw) {
verifier: true,
},
parallelization: true,
brave_search: hasBraveSearch,
};
try {
@@ -1987,6 +1997,70 @@ function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
output(fullResult, raw);
}
// ─── Web Search (Brave API) ──────────────────────────────────────────────────
async function cmdWebsearch(query, options, raw) {
const apiKey = process.env.BRAVE_API_KEY;
if (!apiKey) {
// No key = silent skip, agent falls back to built-in WebSearch
output({ available: false, reason: 'BRAVE_API_KEY not set' }, raw, '');
return;
}
if (!query) {
output({ available: false, error: 'Query required' }, raw, '');
return;
}
const params = new URLSearchParams({
q: query,
count: String(options.limit || 10),
country: 'us',
search_lang: 'en',
text_decorations: 'false'
});
if (options.freshness) {
params.set('freshness', options.freshness);
}
try {
const response = await fetch(
`https://api.search.brave.com/res/v1/web/search?${params}`,
{
headers: {
'Accept': 'application/json',
'X-Subscription-Token': apiKey
}
}
);
if (!response.ok) {
output({ available: false, error: `API error: ${response.status}` }, raw, '');
return;
}
const data = await response.json();
const results = (data.web?.results || []).map(r => ({
title: r.title,
url: r.url,
description: r.description,
age: r.age || null
}));
output({
available: true,
query,
count: results.length,
results
}, raw, results.map(r => `${r.title}\n${r.url}\n${r.description}`).join('\n\n'));
} catch (err) {
output({ available: false, error: err.message }, raw, '');
}
}
// ─── Frontmatter CRUD ────────────────────────────────────────────────────────
function cmdFrontmatterGet(cwd, filePath, field, raw) {
@@ -3659,6 +3733,11 @@ function cmdInitPlanPhase(cwd, phase, includes, raw) {
function cmdInitNewProject(cwd, raw) {
const config = loadConfig(cwd);
// Detect Brave Search API key availability
const homedir = require('os').homedir();
const braveKeyFile = path.join(homedir, '.gsd', 'brave_api_key');
const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile));
// Detect existing code
let hasCode = false;
let hasPackageFile = false;
@@ -3699,6 +3778,9 @@ function cmdInitNewProject(cwd, raw) {
// Git state
has_git: pathExistsInternal(cwd, '.git'),
// Enhanced search
brave_search_available: hasBraveSearch,
};
output(result, raw);
@@ -3841,6 +3923,7 @@ function cmdInitPhaseOp(cwd, phase, raw) {
const result = {
// Config
commit_docs: config.commit_docs,
brave_search: config.brave_search,
// Phase info
phase_found: !!phaseInfo,
@@ -4128,7 +4211,7 @@ function cmdInitProgress(cwd, includes, raw) {
// ─── CLI Router ───────────────────────────────────────────────────────────────
function main() {
async function main() {
const args = process.argv.slice(2);
const rawIndex = args.indexOf('--raw');
const raw = rawIndex !== -1;
@@ -4495,6 +4578,17 @@ function main() {
break;
}
case 'websearch': {
const query = args[1];
const limitIdx = args.indexOf('--limit');
const freshnessIdx = args.indexOf('--freshness');
await cmdWebsearch(query, {
limit: limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) : 10,
freshness: freshnessIdx !== -1 ? args[freshnessIdx + 1] : null,
}, raw);
break;
}
default:
error(`Unknown command: ${command}`);
}