gsd-pi 2.3.8 → 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 +353 -144
  13. package/src/resources/extensions/gsd/files.ts +9 -7
  14. package/src/resources/extensions/gsd/index.ts +3 -1
  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
package/README.md CHANGED
@@ -48,6 +48,8 @@ GSD v2 solves all of these because it's not a prompt framework anymore — it's
48
48
 
49
49
  ### Migrating from v1
50
50
 
51
+ > **Note:** Migration works best with a `ROADMAP.md` file for milestone structure. Without one, milestones are inferred from the `phases/` directory.
52
+
51
53
  If you have projects with `.planning` directories from the original Get Shit Done, you can migrate them to GSD-2's `.gsd` format:
52
54
 
53
55
  ```bash
@@ -198,7 +200,7 @@ Both terminals read and write the same `.gsd/` files on disk. Your decisions in
198
200
 
199
201
  ### First launch
200
202
 
201
- On first run, GSD prompts for optional API keys (Brave Search, Google Gemini, Context7, Jina) for web research and documentation tools. All optional — press Enter to skip any.
203
+ On first run, GSD prompts for optional API keys (Brave Search, Google Gemini, Context7, Jina, Tavily) for web research and documentation tools. All optional — press Enter to skip any.
202
204
 
203
205
  ### Commands
204
206
 
@@ -326,7 +328,7 @@ GSD ships with 13 extensions, all loaded automatically:
326
328
  |-----------|-----------------|
327
329
  | **GSD** | Core workflow engine, auto mode, commands, dashboard |
328
330
  | **Browser Tools** | Playwright-based browser for UI verification |
329
- | **Search the Web** | Brave Search + Jina page extraction |
331
+ | **Search the Web** | Brave Search, Tavily, or Jina page extraction |
330
332
  | **Google Search** | Gemini-powered web search with AI-synthesized answers |
331
333
  | **Context7** | Up-to-date library/framework documentation |
332
334
  | **Background Shell** | Long-running process management with readiness detection |
@@ -386,6 +388,7 @@ gsd (CLI binary)
386
388
 
387
389
  Optional:
388
390
  - Brave Search API key (web research)
391
+ - Tavily API key (web research — alternative to Brave)
389
392
  - Google Gemini API key (web research via Gemini Search grounding)
390
393
  - Context7 API key (library docs)
391
394
  - Jina API key (page extraction)
package/dist/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AuthStorage, DefaultResourceLoader, ModelRegistry, SettingsManager, SessionManager, createAgentSession, InteractiveMode, runPrintMode, } from '@mariozechner/pi-coding-agent';
1
+ import { AuthStorage, DefaultResourceLoader, ModelRegistry, SettingsManager, SessionManager, createAgentSession, InteractiveMode, runPrintMode, runRpcMode, } from '@mariozechner/pi-coding-agent';
2
2
  import { existsSync, readdirSync, renameSync, readFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { agentDir, sessionsDir, authFilePath } from './app-paths.js';
@@ -33,6 +33,24 @@ function parseCliArgs(argv) {
33
33
  else if (arg === '--tools' && i + 1 < args.length) {
34
34
  flags.tools = args[++i].split(',');
35
35
  }
36
+ else if (arg === '--version' || arg === '-v') {
37
+ process.stdout.write((process.env.GSD_VERSION || '0.0.0') + '\n');
38
+ process.exit(0);
39
+ }
40
+ else if (arg === '--help' || arg === '-h') {
41
+ process.stdout.write(`GSD v${process.env.GSD_VERSION || '0.0.0'} — Get Shit Done\n\n`);
42
+ process.stdout.write('Usage: gsd [options] [message...]\n\n');
43
+ process.stdout.write('Options:\n');
44
+ process.stdout.write(' --mode <text|json|rpc> Output mode (default: interactive)\n');
45
+ process.stdout.write(' --print, -p Single-shot print mode\n');
46
+ process.stdout.write(' --model <id> Override model (e.g. claude-opus-4-6)\n');
47
+ process.stdout.write(' --no-session Disable session persistence\n');
48
+ process.stdout.write(' --extension <path> Load additional extension\n');
49
+ process.stdout.write(' --tools <a,b,c> Restrict available tools\n');
50
+ process.stdout.write(' --version, -v Print version and exit\n');
51
+ process.stdout.write(' --help, -h Print this help and exit\n');
52
+ process.exit(0);
53
+ }
36
54
  else if (!arg.startsWith('--') && !arg.startsWith('-')) {
37
55
  flags.messages.push(arg);
38
56
  }
@@ -130,8 +148,12 @@ if (isPrintMode) {
130
148
  }
131
149
  }
132
150
  const mode = cliFlags.mode || 'text';
151
+ if (mode === 'rpc') {
152
+ await runRpcMode(session);
153
+ process.exit(0);
154
+ }
133
155
  await runPrintMode(session, {
134
- mode: mode === 'rpc' ? 'json' : mode,
156
+ mode,
135
157
  messages: cliFlags.messages,
136
158
  });
137
159
  process.exit(0);
@@ -224,5 +246,13 @@ if (enabledModelPatterns && enabledModelPatterns.length > 0) {
224
246
  session.setScopedModels(scopedModels);
225
247
  }
226
248
  }
249
+ if (!process.stdin.isTTY) {
250
+ process.stderr.write('[gsd] Error: Interactive mode requires a terminal (TTY).\n');
251
+ process.stderr.write('[gsd] Non-interactive alternatives:\n');
252
+ process.stderr.write('[gsd] gsd --print "your message" Single-shot prompt\n');
253
+ process.stderr.write('[gsd] gsd --mode rpc JSON-RPC over stdin/stdout\n');
254
+ process.stderr.write('[gsd] gsd --mode text "message" Text output mode\n');
255
+ process.exit(1);
256
+ }
227
257
  const interactiveMode = new InteractiveMode(session);
228
258
  await interactiveMode.run();
package/dist/logo.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Shared GSD block-letter ASCII logo.
3
+ *
4
+ * Single source of truth — imported by:
5
+ * - scripts/postinstall.js (via dist/logo.js)
6
+ * - src/onboarding.ts (via ./logo.js)
7
+ */
8
+ /** Raw logo lines — no ANSI codes, no leading newline. */
9
+ export declare const GSD_LOGO: readonly string[];
10
+ /**
11
+ * Render the logo block with a color function applied to each line.
12
+ *
13
+ * @param color — e.g. `(s) => `\x1b[36m${s}\x1b[0m`` or picocolors.cyan
14
+ * @returns Ready-to-write string with leading/trailing newlines.
15
+ */
16
+ export declare function renderLogo(color: (s: string) => string): string;
package/dist/logo.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Shared GSD block-letter ASCII logo.
3
+ *
4
+ * Single source of truth — imported by:
5
+ * - scripts/postinstall.js (via dist/logo.js)
6
+ * - src/onboarding.ts (via ./logo.js)
7
+ */
8
+ /** Raw logo lines — no ANSI codes, no leading newline. */
9
+ export const GSD_LOGO = [
10
+ ' ██████╗ ███████╗██████╗ ',
11
+ ' ██╔════╝ ██╔════╝██╔══██╗',
12
+ ' ██║ ███╗███████╗██║ ██║',
13
+ ' ██║ ██║╚════██║██║ ██║',
14
+ ' ╚██████╔╝███████║██████╔╝',
15
+ ' ╚═════╝ ╚══════╝╚═════╝ ',
16
+ ];
17
+ /**
18
+ * Render the logo block with a color function applied to each line.
19
+ *
20
+ * @param color — e.g. `(s) => `\x1b[36m${s}\x1b[0m`` or picocolors.cyan
21
+ * @returns Ready-to-write string with leading/trailing newlines.
22
+ */
23
+ export function renderLogo(color) {
24
+ return '\n' + GSD_LOGO.map(color).join('\n') + '\n';
25
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Unified first-run onboarding wizard.
3
+ *
4
+ * Replaces the raw API-key-only wizard with a branded, clack-based experience
5
+ * that guides users through LLM provider authentication before the TUI launches.
6
+ *
7
+ * Flow: logo → choose LLM provider → authenticate (OAuth or API key) →
8
+ * optional tool keys → summary → TUI launches.
9
+ *
10
+ * All steps are skippable. All errors are recoverable. Never crashes boot.
11
+ */
12
+ import type { AuthStorage } from '@mariozechner/pi-coding-agent';
13
+ /**
14
+ * Determine if the onboarding wizard should run.
15
+ *
16
+ * Returns true when:
17
+ * - No LLM provider has credentials in authStorage
18
+ * - We're on a TTY (interactive terminal)
19
+ *
20
+ * Returns false (skip wizard) when:
21
+ * - Any LLM provider is already authed (returning user)
22
+ * - Not a TTY (piped input, subagent, CI)
23
+ */
24
+ export declare function shouldRunOnboarding(authStorage: AuthStorage): boolean;
25
+ /**
26
+ * Run the unified onboarding wizard.
27
+ *
28
+ * Walks the user through:
29
+ * 1. Choose LLM provider
30
+ * 2. Authenticate (OAuth or API key)
31
+ * 3. Optional tool API keys
32
+ * 4. Summary
33
+ *
34
+ * All steps are skippable. All errors are recoverable.
35
+ * Writes status to stderr during execution.
36
+ */
37
+ export declare function runOnboarding(authStorage: AuthStorage): Promise<void>;
38
+ /**
39
+ * Hydrate process.env from stored auth.json credentials for optional tool keys.
40
+ * Runs on every launch so extensions see Brave/Context7/Jina keys stored via the
41
+ * wizard on prior launches.
42
+ */
43
+ export declare function loadStoredEnvKeys(authStorage: AuthStorage): void;
@@ -0,0 +1,425 @@
1
+ /**
2
+ * Unified first-run onboarding wizard.
3
+ *
4
+ * Replaces the raw API-key-only wizard with a branded, clack-based experience
5
+ * that guides users through LLM provider authentication before the TUI launches.
6
+ *
7
+ * Flow: logo → choose LLM provider → authenticate (OAuth or API key) →
8
+ * optional tool keys → summary → TUI launches.
9
+ *
10
+ * All steps are skippable. All errors are recoverable. Never crashes boot.
11
+ */
12
+ import { exec } from 'node:child_process';
13
+ import { renderLogo } from './logo.js';
14
+ // ─── Constants ────────────────────────────────────────────────────────────────
15
+ const TOOL_KEYS = [
16
+ {
17
+ provider: 'brave',
18
+ envVar: 'BRAVE_API_KEY',
19
+ label: 'Brave Search',
20
+ hint: 'web search + search_and_read tools',
21
+ },
22
+ {
23
+ provider: 'brave_answers',
24
+ envVar: 'BRAVE_ANSWERS_KEY',
25
+ label: 'Brave Answers',
26
+ hint: 'AI-summarised search answers',
27
+ },
28
+ {
29
+ provider: 'context7',
30
+ envVar: 'CONTEXT7_API_KEY',
31
+ label: 'Context7',
32
+ hint: 'up-to-date library docs',
33
+ },
34
+ {
35
+ provider: 'jina',
36
+ envVar: 'JINA_API_KEY',
37
+ label: 'Jina AI',
38
+ hint: 'clean web page extraction',
39
+ },
40
+ {
41
+ provider: 'slack_bot',
42
+ envVar: 'SLACK_BOT_TOKEN',
43
+ label: 'Slack Bot',
44
+ hint: 'remote questions in auto-mode',
45
+ },
46
+ {
47
+ provider: 'discord_bot',
48
+ envVar: 'DISCORD_BOT_TOKEN',
49
+ label: 'Discord Bot',
50
+ hint: 'remote questions in auto-mode',
51
+ },
52
+ ];
53
+ /** Known LLM provider IDs that, if authed, mean the user doesn't need onboarding */
54
+ const LLM_PROVIDER_IDS = [
55
+ 'anthropic',
56
+ 'openai',
57
+ 'github-copilot',
58
+ 'openai-codex',
59
+ 'google-gemini-cli',
60
+ 'google-antigravity',
61
+ 'google',
62
+ 'groq',
63
+ 'xai',
64
+ 'openrouter',
65
+ 'mistral',
66
+ ];
67
+ /** API key prefix validation — loose checks to catch obvious mistakes */
68
+ const API_KEY_PREFIXES = {
69
+ anthropic: ['sk-ant-'],
70
+ openai: ['sk-'],
71
+ };
72
+ // ─── Dynamic imports ──────────────────────────────────────────────────────────
73
+ /**
74
+ * Dynamically import @clack/prompts and picocolors.
75
+ * These are transitive dependencies — they exist in node_modules but are not
76
+ * in our direct dependencies. We use dynamic import with a fallback so the
77
+ * module doesn't crash if they're missing.
78
+ */
79
+ async function loadClack() {
80
+ try {
81
+ return await import('@clack/prompts');
82
+ }
83
+ catch {
84
+ throw new Error('[gsd] @clack/prompts not found — onboarding wizard requires this dependency');
85
+ }
86
+ }
87
+ async function loadPico() {
88
+ try {
89
+ return await import('picocolors');
90
+ }
91
+ catch {
92
+ // Fallback: return identity functions
93
+ const identity = (s) => s;
94
+ return {
95
+ default: { cyan: identity, green: identity, yellow: identity, dim: identity, bold: identity, red: identity, reset: identity },
96
+ cyan: identity, green: identity, yellow: identity, dim: identity, bold: identity, red: identity, reset: identity,
97
+ };
98
+ }
99
+ }
100
+ // ─── Utilities ────────────────────────────────────────────────────────────────
101
+ /** Open a URL in the system browser (best-effort, non-blocking) */
102
+ function openBrowser(url) {
103
+ const cmd = process.platform === 'darwin' ? 'open' :
104
+ process.platform === 'win32' ? 'start' :
105
+ 'xdg-open';
106
+ exec(`${cmd} "${url}"`, () => {
107
+ // Ignore errors — user can manually open the URL
108
+ });
109
+ }
110
+ // ─── Public API ───────────────────────────────────────────────────────────────
111
+ /**
112
+ * Determine if the onboarding wizard should run.
113
+ *
114
+ * Returns true when:
115
+ * - No LLM provider has credentials in authStorage
116
+ * - We're on a TTY (interactive terminal)
117
+ *
118
+ * Returns false (skip wizard) when:
119
+ * - Any LLM provider is already authed (returning user)
120
+ * - Not a TTY (piped input, subagent, CI)
121
+ */
122
+ export function shouldRunOnboarding(authStorage) {
123
+ if (!process.stdin.isTTY)
124
+ return false;
125
+ // Check if any LLM provider has credentials
126
+ const authedProviders = authStorage.list();
127
+ const hasLlmAuth = authedProviders.some(id => LLM_PROVIDER_IDS.includes(id));
128
+ return !hasLlmAuth;
129
+ }
130
+ /**
131
+ * Run the unified onboarding wizard.
132
+ *
133
+ * Walks the user through:
134
+ * 1. Choose LLM provider
135
+ * 2. Authenticate (OAuth or API key)
136
+ * 3. Optional tool API keys
137
+ * 4. Summary
138
+ *
139
+ * All steps are skippable. All errors are recoverable.
140
+ * Writes status to stderr during execution.
141
+ */
142
+ export async function runOnboarding(authStorage) {
143
+ let p;
144
+ let pc;
145
+ try {
146
+ ;
147
+ [p, pc] = await Promise.all([loadClack(), loadPico()]);
148
+ }
149
+ catch (err) {
150
+ // If clack isn't available, fall back silently — don't block boot
151
+ process.stderr.write(`[gsd] Onboarding wizard unavailable: ${err instanceof Error ? err.message : String(err)}\n`);
152
+ return;
153
+ }
154
+ // ── Intro ─────────────────────────────────────────────────────────────────
155
+ process.stderr.write(renderLogo(pc.cyan));
156
+ p.intro(pc.bold('Welcome to GSD — let\'s get you set up'));
157
+ // ── LLM Provider Selection ────────────────────────────────────────────────
158
+ let llmConfigured = false;
159
+ let llmProviderName = '';
160
+ try {
161
+ llmConfigured = await runLlmStep(p, pc, authStorage);
162
+ }
163
+ catch (err) {
164
+ // User cancelled (Ctrl+C in clack throws) or unexpected error
165
+ if (isCancelError(p, err)) {
166
+ p.cancel('Setup cancelled — you can run /login inside GSD later.');
167
+ return;
168
+ }
169
+ p.log.warn(`LLM setup failed: ${err instanceof Error ? err.message : String(err)}`);
170
+ p.log.info('You can configure your LLM provider later with /login inside GSD.');
171
+ }
172
+ // ── Tool API Keys ─────────────────────────────────────────────────────────
173
+ let toolKeyCount = 0;
174
+ try {
175
+ toolKeyCount = await runToolKeysStep(p, pc, authStorage);
176
+ }
177
+ catch (err) {
178
+ if (isCancelError(p, err)) {
179
+ p.cancel('Setup cancelled.');
180
+ return;
181
+ }
182
+ p.log.warn(`Tool key setup failed: ${err instanceof Error ? err.message : String(err)}`);
183
+ }
184
+ // ── Summary ───────────────────────────────────────────────────────────────
185
+ const summaryLines = [];
186
+ if (llmConfigured) {
187
+ // Re-read what provider was stored
188
+ const authed = authStorage.list().filter(id => LLM_PROVIDER_IDS.includes(id));
189
+ if (authed.length > 0) {
190
+ const name = authed[0];
191
+ summaryLines.push(`${pc.green('✓')} LLM provider: ${name}`);
192
+ }
193
+ else {
194
+ summaryLines.push(`${pc.green('✓')} LLM provider configured`);
195
+ }
196
+ }
197
+ else {
198
+ summaryLines.push(`${pc.yellow('↷')} LLM provider: skipped — use /login inside GSD`);
199
+ }
200
+ if (toolKeyCount > 0) {
201
+ summaryLines.push(`${pc.green('✓')} ${toolKeyCount} tool key${toolKeyCount > 1 ? 's' : ''} saved`);
202
+ }
203
+ else {
204
+ summaryLines.push(`${pc.dim('↷')} Tool keys: none configured`);
205
+ }
206
+ p.note(summaryLines.join('\n'), 'Setup complete');
207
+ p.outro(pc.dim('Launching GSD...'));
208
+ }
209
+ // ─── LLM Authentication Step ──────────────────────────────────────────────────
210
+ async function runLlmStep(p, pc, authStorage) {
211
+ // Build the OAuth provider list dynamically from what's registered
212
+ const oauthProviders = authStorage.getOAuthProviders();
213
+ const oauthMap = new Map(oauthProviders.map(op => [op.id, op]));
214
+ const choice = await p.select({
215
+ message: 'Choose your LLM provider',
216
+ options: [
217
+ { value: 'anthropic-oauth', label: 'Anthropic — Claude (OAuth login)', hint: 'recommended' },
218
+ { value: 'anthropic-api-key', label: 'Anthropic — Claude (API key)' },
219
+ { value: 'openai-api-key', label: 'OpenAI (API key)' },
220
+ { value: 'github-copilot-oauth', label: 'GitHub Copilot (OAuth login)' },
221
+ { value: 'openai-codex-oauth', label: 'ChatGPT Plus/Pro — Codex (OAuth login)' },
222
+ { value: 'google-gemini-cli-oauth', label: 'Google Gemini CLI (OAuth login)' },
223
+ { value: 'google-antigravity-oauth', label: 'Antigravity — Gemini 3, Claude, GPT-OSS (OAuth login)' },
224
+ { value: 'other-api-key', label: 'Other provider (API key)' },
225
+ { value: 'skip', label: 'Skip for now', hint: 'use /login inside GSD later' },
226
+ ],
227
+ });
228
+ if (p.isCancel(choice) || choice === 'skip')
229
+ return false;
230
+ // ── OAuth flows ───────────────────────────────────────────────────────────
231
+ if (choice === 'anthropic-oauth') {
232
+ return await runOAuthFlow(p, pc, authStorage, 'anthropic', oauthMap);
233
+ }
234
+ if (choice === 'github-copilot-oauth') {
235
+ return await runOAuthFlow(p, pc, authStorage, 'github-copilot', oauthMap);
236
+ }
237
+ if (choice === 'openai-codex-oauth') {
238
+ return await runOAuthFlow(p, pc, authStorage, 'openai-codex', oauthMap);
239
+ }
240
+ if (choice === 'google-gemini-cli-oauth') {
241
+ return await runOAuthFlow(p, pc, authStorage, 'google-gemini-cli', oauthMap);
242
+ }
243
+ if (choice === 'google-antigravity-oauth') {
244
+ return await runOAuthFlow(p, pc, authStorage, 'google-antigravity', oauthMap);
245
+ }
246
+ // ── API key flows ─────────────────────────────────────────────────────────
247
+ if (choice === 'anthropic-api-key') {
248
+ return await runApiKeyFlow(p, pc, authStorage, 'anthropic', 'Anthropic');
249
+ }
250
+ if (choice === 'openai-api-key') {
251
+ return await runApiKeyFlow(p, pc, authStorage, 'openai', 'OpenAI');
252
+ }
253
+ if (choice === 'other-api-key') {
254
+ return await runOtherProviderFlow(p, pc, authStorage);
255
+ }
256
+ return false;
257
+ }
258
+ // ─── OAuth Flow ───────────────────────────────────────────────────────────────
259
+ async function runOAuthFlow(p, pc, authStorage, providerId, oauthMap) {
260
+ const providerInfo = oauthMap.get(providerId);
261
+ const providerName = providerInfo?.name ?? providerId;
262
+ const usesCallbackServer = providerInfo?.usesCallbackServer ?? false;
263
+ const s = p.spinner();
264
+ s.start(`Authenticating with ${providerName}...`);
265
+ try {
266
+ await authStorage.login(providerId, {
267
+ onAuth: (info) => {
268
+ s.stop(`Opening browser for ${providerName}`);
269
+ openBrowser(info.url);
270
+ p.log.info(`${pc.dim('URL:')} ${pc.cyan(info.url)}`);
271
+ if (info.instructions) {
272
+ p.log.info(pc.yellow(info.instructions));
273
+ }
274
+ },
275
+ onPrompt: async (prompt) => {
276
+ const result = await p.text({
277
+ message: prompt.message,
278
+ placeholder: prompt.placeholder,
279
+ });
280
+ if (p.isCancel(result))
281
+ return '';
282
+ return result;
283
+ },
284
+ onProgress: (message) => {
285
+ p.log.step(pc.dim(message));
286
+ },
287
+ onManualCodeInput: usesCallbackServer
288
+ ? async () => {
289
+ const result = await p.text({
290
+ message: 'Paste the redirect URL from your browser:',
291
+ placeholder: 'http://localhost:...',
292
+ });
293
+ if (p.isCancel(result))
294
+ return '';
295
+ return result;
296
+ }
297
+ : undefined,
298
+ });
299
+ p.log.success(`Authenticated with ${pc.green(providerName)}`);
300
+ return true;
301
+ }
302
+ catch (err) {
303
+ s.stop(`${providerName} authentication failed`);
304
+ const errorMsg = err instanceof Error ? err.message : String(err);
305
+ p.log.warn(`OAuth error: ${errorMsg}`);
306
+ // Offer retry or skip
307
+ const retry = await p.select({
308
+ message: 'What would you like to do?',
309
+ options: [
310
+ { value: 'retry', label: 'Try again' },
311
+ { value: 'skip', label: 'Skip — configure later with /login' },
312
+ ],
313
+ });
314
+ if (p.isCancel(retry) || retry === 'skip')
315
+ return false;
316
+ // Recursive retry
317
+ return runOAuthFlow(p, pc, authStorage, providerId, oauthMap);
318
+ }
319
+ }
320
+ // ─── API Key Flow ─────────────────────────────────────────────────────────────
321
+ async function runApiKeyFlow(p, pc, authStorage, providerId, providerLabel) {
322
+ const key = await p.password({
323
+ message: `Paste your ${providerLabel} API key:`,
324
+ mask: '●',
325
+ });
326
+ if (p.isCancel(key) || !key)
327
+ return false;
328
+ const trimmed = key.trim();
329
+ if (!trimmed)
330
+ return false;
331
+ // Basic prefix validation
332
+ const expectedPrefixes = API_KEY_PREFIXES[providerId];
333
+ if (expectedPrefixes && !expectedPrefixes.some(pfx => trimmed.startsWith(pfx))) {
334
+ p.log.warn(`Key doesn't start with expected prefix (${expectedPrefixes.join(' or ')}). Saving anyway.`);
335
+ }
336
+ authStorage.set(providerId, { type: 'api_key', key: trimmed });
337
+ p.log.success(`API key saved for ${pc.green(providerLabel)}`);
338
+ return true;
339
+ }
340
+ // ─── "Other Provider" Sub-Flow ────────────────────────────────────────────────
341
+ const OTHER_PROVIDERS = [
342
+ { value: 'google', label: 'Google (Gemini)' },
343
+ { value: 'groq', label: 'Groq' },
344
+ { value: 'xai', label: 'xAI (Grok)' },
345
+ { value: 'openrouter', label: 'OpenRouter' },
346
+ { value: 'mistral', label: 'Mistral' },
347
+ ];
348
+ async function runOtherProviderFlow(p, pc, authStorage) {
349
+ const provider = await p.select({
350
+ message: 'Select provider',
351
+ options: OTHER_PROVIDERS.map(op => ({
352
+ value: op.value,
353
+ label: op.label,
354
+ })),
355
+ });
356
+ if (p.isCancel(provider))
357
+ return false;
358
+ const label = OTHER_PROVIDERS.find(op => op.value === provider)?.label ?? String(provider);
359
+ return runApiKeyFlow(p, pc, authStorage, provider, label);
360
+ }
361
+ // ─── Tool API Keys Step ───────────────────────────────────────────────────────
362
+ async function runToolKeysStep(p, pc, authStorage) {
363
+ // Filter to keys not already configured
364
+ const missing = TOOL_KEYS.filter(tk => !authStorage.has(tk.provider) && !process.env[tk.envVar]);
365
+ if (missing.length === 0)
366
+ return 0;
367
+ const wantToolKeys = await p.confirm({
368
+ message: 'Set up optional tool API keys? (web search, docs, etc.)',
369
+ initialValue: false,
370
+ });
371
+ if (p.isCancel(wantToolKeys) || !wantToolKeys)
372
+ return 0;
373
+ let savedCount = 0;
374
+ for (const tk of missing) {
375
+ const key = await p.password({
376
+ message: `${tk.label} ${pc.dim(`(${tk.hint})`)} — Enter to skip:`,
377
+ mask: '●',
378
+ });
379
+ if (p.isCancel(key))
380
+ break;
381
+ const trimmed = key?.trim();
382
+ if (trimmed) {
383
+ authStorage.set(tk.provider, { type: 'api_key', key: trimmed });
384
+ process.env[tk.envVar] = trimmed;
385
+ p.log.success(`${tk.label} saved`);
386
+ savedCount++;
387
+ }
388
+ else {
389
+ // Store empty key so wizard doesn't re-ask on next launch
390
+ authStorage.set(tk.provider, { type: 'api_key', key: '' });
391
+ p.log.info(pc.dim(`${tk.label} skipped`));
392
+ }
393
+ }
394
+ return savedCount;
395
+ }
396
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
397
+ /** Check if an error is a clack cancel signal */
398
+ function isCancelError(p, err) {
399
+ // Clack throws a symbol when the user cancels (Ctrl+C)
400
+ return p.isCancel(err);
401
+ }
402
+ // ─── Env hydration (migrated from wizard.ts) ─────────────────────────────────
403
+ /**
404
+ * Hydrate process.env from stored auth.json credentials for optional tool keys.
405
+ * Runs on every launch so extensions see Brave/Context7/Jina keys stored via the
406
+ * wizard on prior launches.
407
+ */
408
+ export function loadStoredEnvKeys(authStorage) {
409
+ const providers = [
410
+ ['brave', 'BRAVE_API_KEY'],
411
+ ['brave_answers', 'BRAVE_ANSWERS_KEY'],
412
+ ['context7', 'CONTEXT7_API_KEY'],
413
+ ['jina', 'JINA_API_KEY'],
414
+ ['slack_bot', 'SLACK_BOT_TOKEN'],
415
+ ['discord_bot', 'DISCORD_BOT_TOKEN'],
416
+ ];
417
+ for (const [provider, envVar] of providers) {
418
+ if (!process.env[envVar]) {
419
+ const cred = authStorage.get(provider);
420
+ if (cred?.type === 'api_key' && cred.key) {
421
+ process.env[envVar] = cred.key;
422
+ }
423
+ }
424
+ }
425
+ }
package/dist/wizard.js CHANGED
@@ -81,6 +81,7 @@ export function loadStoredEnvKeys(authStorage) {
81
81
  ['brave_answers', 'BRAVE_ANSWERS_KEY'],
82
82
  ['context7', 'CONTEXT7_API_KEY'],
83
83
  ['jina', 'JINA_API_KEY'],
84
+ ['tavily', 'TAVILY_API_KEY'],
84
85
  ['slack_bot', 'SLACK_BOT_TOKEN'],
85
86
  ['discord_bot', 'DISCORD_BOT_TOKEN'],
86
87
  ];
@@ -122,6 +123,13 @@ const API_KEYS = [
122
123
  hint: '(clean page extraction)',
123
124
  description: 'High-quality web page content extraction',
124
125
  },
126
+ {
127
+ provider: 'tavily',
128
+ envVar: 'TAVILY_API_KEY',
129
+ label: 'Tavily Search',
130
+ hint: '(search-the-web + search_and_read tools, starts with tvly-)',
131
+ description: 'Web search and page extraction (alternative to Brave)',
132
+ },
125
133
  {
126
134
  provider: 'slack_bot',
127
135
  envVar: 'SLACK_BOT_TOKEN',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.3.8",
3
+ "version": "2.3.9",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {