gsd-pi 2.3.7 → 2.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/cli.js +32 -2
- package/dist/logo.d.ts +16 -0
- package/dist/logo.js +25 -0
- package/dist/onboarding.d.ts +43 -0
- package/dist/onboarding.js +425 -0
- package/dist/wizard.js +8 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +38 -9
- package/src/resources/GSD-WORKFLOW.md +2 -2
- package/src/resources/extensions/google-search/index.ts +1 -1
- package/src/resources/extensions/gsd/auto.ts +393 -168
- package/src/resources/extensions/gsd/files.ts +9 -7
- package/src/resources/extensions/gsd/index.ts +57 -2
- package/src/resources/extensions/gsd/metrics.ts +7 -5
- package/src/resources/extensions/gsd/migrate/command.ts +4 -1
- package/src/resources/extensions/gsd/migrate/validator.ts +5 -3
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +5 -5
- package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/parsers.test.ts +94 -0
- package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +23 -6
- package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +253 -0
- package/src/resources/extensions/gsd/tests/worktree.test.ts +116 -1
- package/src/resources/extensions/gsd/unit-runtime.ts +22 -1
- package/src/resources/extensions/gsd/workspace-index.ts +2 -2
- package/src/resources/extensions/gsd/worktree-command.ts +147 -41
- package/src/resources/extensions/gsd/worktree.ts +105 -8
- package/src/resources/extensions/mcporter/index.ts +21 -2
- package/src/resources/extensions/search-the-web/command-search-provider.ts +95 -0
- package/src/resources/extensions/search-the-web/http.ts +1 -1
- package/src/resources/extensions/search-the-web/index.ts +9 -3
- package/src/resources/extensions/search-the-web/provider.ts +118 -0
- package/src/resources/extensions/search-the-web/tavily.ts +116 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +265 -108
- package/src/resources/extensions/search-the-web/tool-search.ts +161 -88
- package/src/resources/extensions/subagent/index.ts +1 -1
|
@@ -20,6 +20,8 @@ import { LRUTTLCache } from "./cache";
|
|
|
20
20
|
import { fetchWithRetryTimed, fetchWithRetry, classifyError, type RateLimitInfo } from "./http";
|
|
21
21
|
import { normalizeQuery, toDedupeKey, detectFreshness } from "./url-utils";
|
|
22
22
|
import { formatSearchResults, type SearchResultFormatted, type FormatSearchOptions } from "./format";
|
|
23
|
+
import { getTavilyApiKey, resolveSearchProvider } from "./provider";
|
|
24
|
+
import { normalizeTavilyResult, mapFreshnessToTavily, type TavilySearchResponse } from "./tavily";
|
|
23
25
|
|
|
24
26
|
// =============================================================================
|
|
25
27
|
// Types
|
|
@@ -66,6 +68,7 @@ interface BraveSearchResponse {
|
|
|
66
68
|
interface CachedSearchResult {
|
|
67
69
|
results: SearchResultFormatted[];
|
|
68
70
|
summarizerKey?: string;
|
|
71
|
+
summaryText?: string;
|
|
69
72
|
queryCorrected?: boolean;
|
|
70
73
|
originalQuery?: string;
|
|
71
74
|
correctedQuery?: string;
|
|
@@ -90,6 +93,7 @@ interface SearchDetails {
|
|
|
90
93
|
errorKind?: string;
|
|
91
94
|
error?: string;
|
|
92
95
|
retryAfterMs?: number;
|
|
96
|
+
provider?: 'tavily' | 'brave';
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
// =============================================================================
|
|
@@ -184,6 +188,63 @@ async function fetchSummary(
|
|
|
184
188
|
}
|
|
185
189
|
}
|
|
186
190
|
|
|
191
|
+
// =============================================================================
|
|
192
|
+
// Tavily API execution
|
|
193
|
+
// =============================================================================
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Execute a search against the Tavily API.
|
|
197
|
+
* Returns a CachedSearchResult with normalized, deduplicated results.
|
|
198
|
+
*/
|
|
199
|
+
async function executeTavilySearch(
|
|
200
|
+
params: { query: string; freshness: string | null; domain?: string; wantSummary: boolean },
|
|
201
|
+
signal?: AbortSignal
|
|
202
|
+
): Promise<{ results: CachedSearchResult; latencyMs: number; rateLimit?: RateLimitInfo }> {
|
|
203
|
+
const requestBody: Record<string, unknown> = {
|
|
204
|
+
query: params.query,
|
|
205
|
+
max_results: 10,
|
|
206
|
+
search_depth: "basic",
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const tavilyTimeRange = mapFreshnessToTavily(params.freshness);
|
|
210
|
+
if (tavilyTimeRange) {
|
|
211
|
+
requestBody.time_range = tavilyTimeRange;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (params.domain) {
|
|
215
|
+
requestBody.include_domains = [params.domain];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (params.wantSummary) {
|
|
219
|
+
requestBody.include_answer = true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const timed = await fetchWithRetryTimed("https://api.tavily.com/search", {
|
|
223
|
+
method: "POST",
|
|
224
|
+
headers: {
|
|
225
|
+
"Content-Type": "application/json",
|
|
226
|
+
"Authorization": `Bearer ${getTavilyApiKey()}`,
|
|
227
|
+
},
|
|
228
|
+
body: JSON.stringify(requestBody),
|
|
229
|
+
signal,
|
|
230
|
+
}, 2);
|
|
231
|
+
|
|
232
|
+
const data: TavilySearchResponse = await timed.response.json();
|
|
233
|
+
const normalized = data.results.map(normalizeTavilyResult);
|
|
234
|
+
const deduplicated = deduplicateResults(normalized);
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
results: {
|
|
238
|
+
results: deduplicated,
|
|
239
|
+
summaryText: data.answer || undefined,
|
|
240
|
+
queryCorrected: false,
|
|
241
|
+
moreResultsAvailable: false,
|
|
242
|
+
},
|
|
243
|
+
latencyMs: timed.latencyMs,
|
|
244
|
+
rateLimit: timed.rateLimit,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
187
248
|
// =============================================================================
|
|
188
249
|
// Tool Registration
|
|
189
250
|
// =============================================================================
|
|
@@ -233,12 +294,15 @@ export function registerSearchTool(pi: ExtensionAPI) {
|
|
|
233
294
|
return { content: [{ type: "text", text: "Search cancelled." }] };
|
|
234
295
|
}
|
|
235
296
|
|
|
236
|
-
|
|
237
|
-
|
|
297
|
+
// ------------------------------------------------------------------
|
|
298
|
+
// Resolve search provider
|
|
299
|
+
// ------------------------------------------------------------------
|
|
300
|
+
const provider = resolveSearchProvider();
|
|
301
|
+
if (!provider) {
|
|
238
302
|
return {
|
|
239
|
-
content: [{ type: "text", text: "Web search unavailable:
|
|
303
|
+
content: [{ type: "text", text: "Web search unavailable: No search API key is set. Use secure_env_collect to set TAVILY_API_KEY or BRAVE_API_KEY." }],
|
|
240
304
|
isError: true,
|
|
241
|
-
details: { errorKind: "auth_error", error: "
|
|
305
|
+
details: { errorKind: "auth_error", error: "No search API key set" } satisfies Partial<SearchDetails>,
|
|
242
306
|
};
|
|
243
307
|
}
|
|
244
308
|
|
|
@@ -246,7 +310,7 @@ export function registerSearchTool(pi: ExtensionAPI) {
|
|
|
246
310
|
const wantSummary = params.summary ?? false;
|
|
247
311
|
|
|
248
312
|
// ------------------------------------------------------------------
|
|
249
|
-
// Resolve freshness
|
|
313
|
+
// Resolve freshness (shared — Brave format, converted for Tavily later)
|
|
250
314
|
// ------------------------------------------------------------------
|
|
251
315
|
let freshness: string | null = null;
|
|
252
316
|
if (params.freshness && params.freshness !== "auto") {
|
|
@@ -259,27 +323,32 @@ export function registerSearchTool(pi: ExtensionAPI) {
|
|
|
259
323
|
}
|
|
260
324
|
|
|
261
325
|
// ------------------------------------------------------------------
|
|
262
|
-
// Handle domain filter
|
|
326
|
+
// Handle domain filter (provider-specific)
|
|
263
327
|
// ------------------------------------------------------------------
|
|
264
328
|
let effectiveQuery = params.query;
|
|
265
|
-
if (params.domain) {
|
|
329
|
+
if (provider === "brave" && params.domain) {
|
|
266
330
|
if (!effectiveQuery.toLowerCase().includes("site:")) {
|
|
267
331
|
effectiveQuery = `site:${params.domain} ${effectiveQuery}`;
|
|
268
332
|
}
|
|
269
333
|
}
|
|
334
|
+
// Tavily uses include_domains in request body — no query modification
|
|
270
335
|
|
|
271
336
|
// ------------------------------------------------------------------
|
|
272
|
-
// Cache lookup
|
|
337
|
+
// Cache lookup (provider-prefixed key)
|
|
273
338
|
// ------------------------------------------------------------------
|
|
274
|
-
const cacheKey = normalizeQuery(effectiveQuery) + `|f:${freshness || ""}|s:${wantSummary}`;
|
|
339
|
+
const cacheKey = normalizeQuery(effectiveQuery) + `|f:${freshness || ""}|s:${wantSummary}|p:${provider}`;
|
|
275
340
|
const cached = searchCache.get(cacheKey);
|
|
276
341
|
|
|
277
342
|
if (cached) {
|
|
278
343
|
const limited = cached.results.slice(0, count);
|
|
279
344
|
|
|
280
345
|
let summaryText: string | undefined;
|
|
281
|
-
if (wantSummary
|
|
282
|
-
|
|
346
|
+
if (wantSummary) {
|
|
347
|
+
if (cached.summaryText) {
|
|
348
|
+
summaryText = cached.summaryText;
|
|
349
|
+
} else if (cached.summarizerKey) {
|
|
350
|
+
summaryText = (await fetchSummary(cached.summarizerKey, signal)) ?? undefined;
|
|
351
|
+
}
|
|
283
352
|
}
|
|
284
353
|
|
|
285
354
|
const formatOpts: FormatSearchOptions = {
|
|
@@ -312,6 +381,7 @@ export function registerSearchTool(pi: ExtensionAPI) {
|
|
|
312
381
|
originalQuery: cached.originalQuery,
|
|
313
382
|
correctedQuery: cached.correctedQuery,
|
|
314
383
|
moreResultsAvailable: cached.moreResultsAvailable,
|
|
384
|
+
provider,
|
|
315
385
|
};
|
|
316
386
|
|
|
317
387
|
return { content: [{ type: "text", text: content }], details };
|
|
@@ -321,92 +391,91 @@ export function registerSearchTool(pi: ExtensionAPI) {
|
|
|
321
391
|
|
|
322
392
|
try {
|
|
323
393
|
// ------------------------------------------------------------------
|
|
324
|
-
//
|
|
394
|
+
// Provider-specific fetch
|
|
325
395
|
// ------------------------------------------------------------------
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
396
|
+
let searchResult: CachedSearchResult;
|
|
397
|
+
let latencyMs: number | undefined;
|
|
398
|
+
let rateLimit: RateLimitInfo | undefined;
|
|
399
|
+
|
|
400
|
+
if (provider === "tavily") {
|
|
401
|
+
const tavilyResult = await executeTavilySearch(
|
|
402
|
+
{ query: params.query, freshness, domain: params.domain, wantSummary },
|
|
403
|
+
signal
|
|
404
|
+
);
|
|
405
|
+
searchResult = tavilyResult.results;
|
|
406
|
+
latencyMs = tavilyResult.latencyMs;
|
|
407
|
+
rateLimit = tavilyResult.rateLimit;
|
|
408
|
+
} else {
|
|
409
|
+
// ================================================================
|
|
410
|
+
// BRAVE PATH (unchanged API logic)
|
|
411
|
+
// ================================================================
|
|
412
|
+
const url = new URL("https://api.search.brave.com/res/v1/web/search");
|
|
413
|
+
url.searchParams.append("q", effectiveQuery);
|
|
414
|
+
url.searchParams.append("count", "10"); // Extra for dedup headroom
|
|
415
|
+
url.searchParams.append("extra_snippets", "true");
|
|
416
|
+
url.searchParams.append("text_decorations", "false");
|
|
417
|
+
|
|
418
|
+
if (freshness) {
|
|
419
|
+
url.searchParams.append("freshness", freshness);
|
|
420
|
+
}
|
|
421
|
+
if (wantSummary) {
|
|
422
|
+
url.searchParams.append("summary", "1");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const timed = await fetchWithRetryTimed(url.toString(), {
|
|
345
426
|
method: "GET",
|
|
346
427
|
headers: braveHeaders(),
|
|
347
428
|
signal,
|
|
348
429
|
}, 2);
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
430
|
+
|
|
431
|
+
const data: BraveSearchResponse = await timed.response.json();
|
|
432
|
+
const rawResults: BraveWebResult[] = data.web?.results ?? [];
|
|
433
|
+
const summarizerKey: string | undefined = data.summarizer?.key;
|
|
434
|
+
|
|
435
|
+
// Extract spellcheck/correction info
|
|
436
|
+
const queryInfo = data.query;
|
|
437
|
+
const queryCorrected = !!(queryInfo?.altered && queryInfo.altered !== queryInfo.original);
|
|
438
|
+
const originalQuery = queryCorrected ? (queryInfo?.original ?? params.query) : undefined;
|
|
439
|
+
const correctedQuery = queryCorrected ? queryInfo?.altered : undefined;
|
|
440
|
+
const moreResultsAvailable = queryInfo?.more_results_available ?? false;
|
|
441
|
+
|
|
442
|
+
// Normalize, deduplicate
|
|
443
|
+
const normalized = rawResults.map(normalizeBraveResult);
|
|
444
|
+
const deduplicated = deduplicateResults(normalized);
|
|
445
|
+
|
|
446
|
+
searchResult = {
|
|
447
|
+
results: deduplicated,
|
|
448
|
+
summarizerKey,
|
|
449
|
+
queryCorrected,
|
|
450
|
+
originalQuery,
|
|
451
|
+
correctedQuery,
|
|
452
|
+
moreResultsAvailable,
|
|
360
453
|
};
|
|
454
|
+
latencyMs = timed.latencyMs;
|
|
455
|
+
rateLimit = timed.rateLimit;
|
|
361
456
|
}
|
|
362
457
|
|
|
363
|
-
const data: BraveSearchResponse = await timed.response.json();
|
|
364
|
-
const rawResults: BraveWebResult[] = data.web?.results ?? [];
|
|
365
|
-
const summarizerKey: string | undefined = data.summarizer?.key;
|
|
366
|
-
|
|
367
458
|
// ------------------------------------------------------------------
|
|
368
|
-
//
|
|
459
|
+
// Shared post-fetch: cache, summary, format, return
|
|
369
460
|
// ------------------------------------------------------------------
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
const originalQuery = queryCorrected ? (queryInfo?.original ?? params.query) : undefined;
|
|
373
|
-
const correctedQuery = queryCorrected ? queryInfo?.altered : undefined;
|
|
374
|
-
const moreResultsAvailable = queryInfo?.more_results_available ?? false;
|
|
461
|
+
searchCache.set(cacheKey, searchResult);
|
|
462
|
+
const results = searchResult.results.slice(0, count);
|
|
375
463
|
|
|
376
|
-
// ------------------------------------------------------------------
|
|
377
|
-
// Normalize, deduplicate, cache
|
|
378
|
-
// ------------------------------------------------------------------
|
|
379
|
-
const normalized = rawResults.map(normalizeBraveResult);
|
|
380
|
-
const deduplicated = deduplicateResults(normalized);
|
|
381
|
-
|
|
382
|
-
searchCache.set(cacheKey, {
|
|
383
|
-
results: deduplicated,
|
|
384
|
-
summarizerKey,
|
|
385
|
-
queryCorrected,
|
|
386
|
-
originalQuery,
|
|
387
|
-
correctedQuery,
|
|
388
|
-
moreResultsAvailable,
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
const results = deduplicated.slice(0, count);
|
|
392
|
-
|
|
393
|
-
// ------------------------------------------------------------------
|
|
394
|
-
// Optionally fetch AI summary (best-effort)
|
|
395
|
-
// ------------------------------------------------------------------
|
|
396
464
|
let summaryText: string | undefined;
|
|
397
|
-
if (wantSummary
|
|
398
|
-
|
|
465
|
+
if (wantSummary) {
|
|
466
|
+
if (searchResult.summaryText) {
|
|
467
|
+
summaryText = searchResult.summaryText;
|
|
468
|
+
} else if (searchResult.summarizerKey) {
|
|
469
|
+
summaryText = (await fetchSummary(searchResult.summarizerKey, signal)) ?? undefined;
|
|
470
|
+
}
|
|
399
471
|
}
|
|
400
472
|
|
|
401
|
-
// ------------------------------------------------------------------
|
|
402
|
-
// Format output
|
|
403
|
-
// ------------------------------------------------------------------
|
|
404
473
|
const formatOpts: FormatSearchOptions = {
|
|
405
474
|
summary: summaryText,
|
|
406
|
-
queryCorrected,
|
|
407
|
-
originalQuery,
|
|
408
|
-
correctedQuery,
|
|
409
|
-
moreResultsAvailable,
|
|
475
|
+
queryCorrected: searchResult.queryCorrected,
|
|
476
|
+
originalQuery: searchResult.originalQuery,
|
|
477
|
+
correctedQuery: searchResult.correctedQuery,
|
|
478
|
+
moreResultsAvailable: searchResult.moreResultsAvailable,
|
|
410
479
|
};
|
|
411
480
|
|
|
412
481
|
const output = formatSearchResults(params.query, results, formatOpts);
|
|
@@ -427,12 +496,13 @@ export function registerSearchTool(pi: ExtensionAPI) {
|
|
|
427
496
|
cached: false,
|
|
428
497
|
freshness: freshness || "none",
|
|
429
498
|
hasSummary: !!summaryText,
|
|
430
|
-
latencyMs
|
|
431
|
-
rateLimit
|
|
432
|
-
queryCorrected,
|
|
433
|
-
originalQuery,
|
|
434
|
-
correctedQuery,
|
|
435
|
-
moreResultsAvailable,
|
|
499
|
+
latencyMs,
|
|
500
|
+
rateLimit,
|
|
501
|
+
queryCorrected: searchResult.queryCorrected,
|
|
502
|
+
originalQuery: searchResult.originalQuery,
|
|
503
|
+
correctedQuery: searchResult.correctedQuery,
|
|
504
|
+
moreResultsAvailable: searchResult.moreResultsAvailable,
|
|
505
|
+
provider,
|
|
436
506
|
};
|
|
437
507
|
|
|
438
508
|
return { content: [{ type: "text", text: content }], details };
|
|
@@ -443,7 +513,9 @@ export function registerSearchTool(pi: ExtensionAPI) {
|
|
|
443
513
|
details: {
|
|
444
514
|
errorKind: classified.kind,
|
|
445
515
|
error: classified.message,
|
|
516
|
+
retryAfterMs: classified.retryAfterMs,
|
|
446
517
|
query: params.query,
|
|
518
|
+
provider,
|
|
447
519
|
} satisfies Partial<SearchDetails>,
|
|
448
520
|
isError: true,
|
|
449
521
|
};
|
|
@@ -473,6 +545,7 @@ export function registerSearchTool(pi: ExtensionAPI) {
|
|
|
473
545
|
return new Text(theme.fg("error", `✗ ${details.error ?? "Search failed"}`) + kindTag, 0, 0);
|
|
474
546
|
}
|
|
475
547
|
|
|
548
|
+
const providerTag = details?.provider ? theme.fg("dim", ` [${details.provider}]`) : "";
|
|
476
549
|
const cacheTag = details?.cached ? theme.fg("dim", " [cached]") : "";
|
|
477
550
|
const freshTag = details?.freshness && details.freshness !== "none"
|
|
478
551
|
? theme.fg("dim", ` [${details.freshness}]`)
|
|
@@ -484,7 +557,7 @@ export function registerSearchTool(pi: ExtensionAPI) {
|
|
|
484
557
|
: "";
|
|
485
558
|
|
|
486
559
|
let text = theme.fg("success", `✓ ${details?.count ?? 0} results for "${details?.query}"`) +
|
|
487
|
-
cacheTag + freshTag + summaryTag + latencyTag + correctedTag;
|
|
560
|
+
providerTag + cacheTag + freshTag + summaryTag + latencyTag + correctedTag;
|
|
488
561
|
|
|
489
562
|
if (expanded && details?.results) {
|
|
490
563
|
text += "\n\n";
|
|
@@ -53,7 +53,7 @@ function formatUsageStats(
|
|
|
53
53
|
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
54
54
|
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
55
55
|
if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
|
|
56
|
-
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
56
|
+
if (usage.cost) parts.push(`$${(Number(usage.cost) || 0).toFixed(4)}`);
|
|
57
57
|
if (usage.contextTokens && usage.contextTokens > 0) {
|
|
58
58
|
parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
59
59
|
}
|