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.
Files changed (37) hide show
  1. package/README.md +5 -2
  2. package/dist/cli.js +32 -2
  3. package/dist/logo.d.ts +16 -0
  4. package/dist/logo.js +25 -0
  5. package/dist/onboarding.d.ts +43 -0
  6. package/dist/onboarding.js +425 -0
  7. package/dist/wizard.js +8 -0
  8. package/package.json +1 -1
  9. package/scripts/postinstall.js +38 -9
  10. package/src/resources/GSD-WORKFLOW.md +2 -2
  11. package/src/resources/extensions/google-search/index.ts +1 -1
  12. package/src/resources/extensions/gsd/auto.ts +393 -168
  13. package/src/resources/extensions/gsd/files.ts +9 -7
  14. package/src/resources/extensions/gsd/index.ts +57 -2
  15. package/src/resources/extensions/gsd/metrics.ts +7 -5
  16. package/src/resources/extensions/gsd/migrate/command.ts +4 -1
  17. package/src/resources/extensions/gsd/migrate/validator.ts +5 -3
  18. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  19. package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +5 -5
  20. package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +3 -3
  21. package/src/resources/extensions/gsd/tests/parsers.test.ts +94 -0
  22. package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +23 -6
  23. package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +253 -0
  24. package/src/resources/extensions/gsd/tests/worktree.test.ts +116 -1
  25. package/src/resources/extensions/gsd/unit-runtime.ts +22 -1
  26. package/src/resources/extensions/gsd/workspace-index.ts +2 -2
  27. package/src/resources/extensions/gsd/worktree-command.ts +147 -41
  28. package/src/resources/extensions/gsd/worktree.ts +105 -8
  29. package/src/resources/extensions/mcporter/index.ts +21 -2
  30. package/src/resources/extensions/search-the-web/command-search-provider.ts +95 -0
  31. package/src/resources/extensions/search-the-web/http.ts +1 -1
  32. package/src/resources/extensions/search-the-web/index.ts +9 -3
  33. package/src/resources/extensions/search-the-web/provider.ts +118 -0
  34. package/src/resources/extensions/search-the-web/tavily.ts +116 -0
  35. package/src/resources/extensions/search-the-web/tool-llm-context.ts +265 -108
  36. package/src/resources/extensions/search-the-web/tool-search.ts +161 -88
  37. 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
- const apiKey = getBraveApiKey();
237
- if (!apiKey) {
297
+ // ------------------------------------------------------------------
298
+ // Resolve search provider
299
+ // ------------------------------------------------------------------
300
+ const provider = resolveSearchProvider();
301
+ if (!provider) {
238
302
  return {
239
- content: [{ type: "text", text: "Web search unavailable: BRAVE_API_KEY is not set. Use secure_env_collect to set BRAVE_API_KEY." }],
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: "BRAVE_API_KEY not set" } satisfies Partial<SearchDetails>,
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 && cached.summarizerKey) {
282
- summaryText = (await fetchSummary(cached.summarizerKey, signal)) ?? undefined;
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
- // Build Brave API request
394
+ // Provider-specific fetch
325
395
  // ------------------------------------------------------------------
326
- const url = new URL("https://api.search.brave.com/res/v1/web/search");
327
- url.searchParams.append("q", effectiveQuery);
328
- url.searchParams.append("count", "10"); // Extra for dedup headroom
329
- url.searchParams.append("extra_snippets", "true");
330
- url.searchParams.append("text_decorations", "false");
331
-
332
- if (freshness) {
333
- url.searchParams.append("freshness", freshness);
334
- }
335
- if (wantSummary) {
336
- url.searchParams.append("summary", "1");
337
- }
338
-
339
- // ------------------------------------------------------------------
340
- // Execute with timing
341
- // ------------------------------------------------------------------
342
- let timed;
343
- try {
344
- timed = await fetchWithRetryTimed(url.toString(), {
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
- } catch (fetchErr) {
350
- const classified = classifyError(fetchErr);
351
- return {
352
- content: [{ type: "text", text: `Search failed: ${classified.message}` }],
353
- details: {
354
- errorKind: classified.kind,
355
- error: classified.message,
356
- retryAfterMs: classified.retryAfterMs,
357
- query: params.query,
358
- } satisfies Partial<SearchDetails>,
359
- isError: true,
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
- // Extract spellcheck/correction info
459
+ // Shared post-fetch: cache, summary, format, return
369
460
  // ------------------------------------------------------------------
370
- const queryInfo = data.query;
371
- const queryCorrected = !!(queryInfo?.altered && queryInfo.altered !== queryInfo.original);
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 && summarizerKey) {
398
- summaryText = (await fetchSummary(summarizerKey, signal)) ?? undefined;
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: timed.latencyMs,
431
- rateLimit: timed.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
  }