gsd-pi 2.3.10 → 2.3.11

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 CHANGED
@@ -200,7 +200,7 @@ Both terminals read and write the same `.gsd/` files on disk. Your decisions in
200
200
 
201
201
  ### First launch
202
202
 
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.
203
+ On first run, GSD launches a branded setup wizard that walks you through LLM provider selection (OAuth or API key), then optional tool API keys (Brave Search, Context7, Jina, Slack, Discord). Every step is skippable — press Enter to skip any. Run `gsd config` anytime to re-run the wizard.
204
204
 
205
205
  ### Commands
206
206
 
@@ -223,6 +223,7 @@ On first run, GSD prompts for optional API keys (Brave Search, Google Gemini, Co
223
223
  | `Ctrl+Alt+G` | Toggle dashboard overlay |
224
224
  | `Ctrl+Alt+V` | Toggle voice transcription |
225
225
  | `Ctrl+Alt+B` | Show background shell processes |
226
+ | `gsd config` | Re-run the setup wizard (LLM provider + tool keys) |
226
227
 
227
228
  ---
228
229
 
@@ -360,7 +361,8 @@ GSD is a TypeScript application that embeds the Pi coding agent SDK.
360
361
  gsd (CLI binary)
361
362
  └─ loader.ts Sets PI_PACKAGE_DIR, GSD env vars, dynamic-imports cli.ts
362
363
  └─ cli.ts Wires SDK managers, loads extensions, starts InteractiveMode
363
- ├─ wizard.ts First-run API key collection (Brave/Gemini/Context7/Jina)
364
+ ├─ onboarding.ts First-run setup wizard (LLM provider + tool keys)
365
+ ├─ wizard.ts Env hydration from stored auth.json credentials
364
366
  ├─ app-paths.ts ~/.gsd/agent/, ~/.gsd/sessions/, auth.json
365
367
  ├─ resource-loader.ts Syncs bundled extensions + agents to ~/.gsd/agent/
366
368
  └─ src/resources/
package/dist/cli.js CHANGED
@@ -4,7 +4,8 @@ import { join } from 'node:path';
4
4
  import { agentDir, sessionsDir, authFilePath } from './app-paths.js';
5
5
  import { initResources } from './resource-loader.js';
6
6
  import { ensureManagedTools } from './tool-bootstrap.js';
7
- import { loadStoredEnvKeys, runWizardIfNeeded } from './wizard.js';
7
+ import { loadStoredEnvKeys } from './wizard.js';
8
+ import { shouldRunOnboarding, runOnboarding } from './onboarding.js';
8
9
  function parseCliArgs(argv) {
9
10
  const flags = { extensions: [], messages: [] };
10
11
  const args = argv.slice(2); // skip node + script
@@ -49,6 +50,8 @@ function parseCliArgs(argv) {
49
50
  process.stdout.write(' --tools <a,b,c> Restrict available tools\n');
50
51
  process.stdout.write(' --version, -v Print version and exit\n');
51
52
  process.stdout.write(' --help, -h Print this help and exit\n');
53
+ process.stdout.write('\nSubcommands:\n');
54
+ process.stdout.write(' config Re-run the setup wizard\n');
52
55
  process.exit(0);
53
56
  }
54
57
  else if (!arg.startsWith('--') && !arg.startsWith('-')) {
@@ -59,15 +62,21 @@ function parseCliArgs(argv) {
59
62
  }
60
63
  const cliFlags = parseCliArgs(process.argv);
61
64
  const isPrintMode = cliFlags.print || cliFlags.mode !== undefined;
65
+ // `gsd config` — replay the setup wizard and exit
66
+ if (cliFlags.messages[0] === 'config') {
67
+ const authStorage = AuthStorage.create(authFilePath);
68
+ await runOnboarding(authStorage);
69
+ process.exit(0);
70
+ }
62
71
  // Pi's tool bootstrap can mis-detect already-installed fd/rg on some systems
63
72
  // because spawnSync(..., ["--version"]) returns EPERM despite a zero exit code.
64
73
  // Provision local managed binaries first so Pi sees them without probing PATH.
65
74
  ensureManagedTools(join(agentDir, 'bin'));
66
75
  const authStorage = AuthStorage.create(authFilePath);
67
76
  loadStoredEnvKeys(authStorage);
68
- // Skip the setup wizard in print mode it requires TTY interaction
69
- if (!isPrintMode) {
70
- await runWizardIfNeeded(authStorage);
77
+ // Run onboarding wizard on first launch (no LLM provider configured)
78
+ if (!isPrintMode && shouldRunOnboarding(authStorage)) {
79
+ await runOnboarding(authStorage);
71
80
  }
72
81
  const modelRegistry = new ModelRegistry(authStorage);
73
82
  const settingsManager = SettingsManager.create(agentDir);
package/dist/loader.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { fileURLToPath } from 'url';
3
3
  import { dirname, resolve, join } from 'path';
4
- import { existsSync, readFileSync } from 'fs';
5
- import { agentDir, appRoot } from './app-paths.js';
4
+ import { readFileSync } from 'fs';
5
+ import { agentDir } from './app-paths.js';
6
6
  // pkg/ is a shim directory: contains gsd's piConfig (package.json) and pi's
7
7
  // theme assets (dist/modes/interactive/theme/) without a src/ directory.
8
8
  // This allows config.js to:
@@ -14,30 +14,7 @@ const pkgDir = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'pkg');
14
14
  process.env.PI_PACKAGE_DIR = pkgDir;
15
15
  process.env.PI_SKIP_VERSION_CHECK = '1'; // GSD ships its own update check — suppress pi's
16
16
  process.title = 'gsd';
17
- // Print branded banner on first launch (before ~/.gsd/ exists)
18
- if (!existsSync(appRoot)) {
19
- const cyan = '\x1b[36m';
20
- const green = '\x1b[32m';
21
- const dim = '\x1b[2m';
22
- const reset = '\x1b[0m';
23
- let version = '';
24
- try {
25
- const pkgJson = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf-8'));
26
- version = pkgJson.version ?? '';
27
- }
28
- catch { /* ignore */ }
29
- process.stderr.write('\n' +
30
- cyan +
31
- ' ██████╗ ███████╗██████╗ \n' +
32
- ' ██╔════╝ ██╔════╝██╔══██╗\n' +
33
- ' ██║ ███╗███████╗██║ ██║\n' +
34
- ' ██║ ██║╚════██║██║ ██║\n' +
35
- ' ╚██████╔╝███████║██████╔╝\n' +
36
- ' ╚═════╝ ╚══════╝╚═════╝ ' +
37
- reset + '\n\n' +
38
- ` Get Shit Done ${dim}v${version}${reset}\n` +
39
- ` ${green}Welcome.${reset} Setting up your environment...\n\n`);
40
- }
17
+ // First-launch branding is handled by the onboarding wizard (src/onboarding.ts)
41
18
  // GSD_CODING_AGENT_DIR — tells pi's getAgentDir() to return ~/.gsd/agent/ instead of ~/.gsd/agent/
42
19
  process.env.GSD_CODING_AGENT_DIR = agentDir;
43
20
  // NODE_PATH — make gsd's own node_modules available to extensions loaded via jiti.
package/dist/logo.d.ts CHANGED
@@ -6,11 +6,11 @@
6
6
  * - src/onboarding.ts (via ./logo.js)
7
7
  */
8
8
  /** Raw logo lines — no ANSI codes, no leading newline. */
9
- export declare const GSD_LOGO: readonly string[];
9
+ export declare const GSD_LOGO: string[];
10
10
  /**
11
11
  * Render the logo block with a color function applied to each line.
12
12
  *
13
- * @param color — e.g. `(s) => `\x1b[36m${s}\x1b[0m`` or picocolors.cyan
13
+ * @param color — e.g. picocolors.cyan or `(s) => `\x1b[36m${s}\x1b[0m``
14
14
  * @returns Ready-to-write string with leading/trailing newlines.
15
15
  */
16
16
  export declare function renderLogo(color: (s: string) => string): string;
package/dist/logo.js CHANGED
@@ -17,7 +17,7 @@ export const GSD_LOGO = [
17
17
  /**
18
18
  * Render the logo block with a color function applied to each line.
19
19
  *
20
- * @param color — e.g. `(s) => `\x1b[36m${s}\x1b[0m`` or picocolors.cyan
20
+ * @param color — e.g. picocolors.cyan or `(s) => `\x1b[36m${s}\x1b[0m``
21
21
  * @returns Ready-to-write string with leading/trailing newlines.
22
22
  */
23
23
  export function renderLogo(color) {
@@ -4,8 +4,8 @@
4
4
  * Replaces the raw API-key-only wizard with a branded, clack-based experience
5
5
  * that guides users through LLM provider authentication before the TUI launches.
6
6
  *
7
- * Flow: logo choose LLM provider authenticate (OAuth or API key)
8
- * optional tool keys summary TUI launches.
7
+ * Flow: logo -> choose LLM provider -> authenticate (OAuth or API key) ->
8
+ * optional tool keys -> summary -> TUI launches.
9
9
  *
10
10
  * All steps are skippable. All errors are recoverable. Never crashes boot.
11
11
  */
@@ -4,8 +4,8 @@
4
4
  * Replaces the raw API-key-only wizard with a branded, clack-based experience
5
5
  * that guides users through LLM provider authentication before the TUI launches.
6
6
  *
7
- * Flow: logo choose LLM provider authenticate (OAuth or API key)
8
- * optional tool keys summary TUI launches.
7
+ * Flow: logo -> choose LLM provider -> authenticate (OAuth or API key) ->
8
+ * optional tool keys -> summary -> TUI launches.
9
9
  *
10
10
  * All steps are skippable. All errors are recoverable. Never crashes boot.
11
11
  */
@@ -69,12 +69,17 @@ const API_KEY_PREFIXES = {
69
69
  anthropic: ['sk-ant-'],
70
70
  openai: ['sk-'],
71
71
  };
72
+ const OTHER_PROVIDERS = [
73
+ { value: 'google', label: 'Google (Gemini)' },
74
+ { value: 'groq', label: 'Groq' },
75
+ { value: 'xai', label: 'xAI (Grok)' },
76
+ { value: 'openrouter', label: 'OpenRouter' },
77
+ { value: 'mistral', label: 'Mistral' },
78
+ ];
72
79
  // ─── Dynamic imports ──────────────────────────────────────────────────────────
73
80
  /**
74
81
  * 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.
82
+ * Dynamic import with fallback so the module doesn't crash if they're missing.
78
83
  */
79
84
  async function loadClack() {
80
85
  try {
@@ -86,15 +91,13 @@ async function loadClack() {
86
91
  }
87
92
  async function loadPico() {
88
93
  try {
89
- return await import('picocolors');
94
+ const mod = await import('picocolors');
95
+ return mod.default ?? mod;
90
96
  }
91
97
  catch {
92
98
  // Fallback: return identity functions
93
99
  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
- };
100
+ return { cyan: identity, green: identity, yellow: identity, dim: identity, bold: identity, red: identity, reset: identity };
98
101
  }
99
102
  }
100
103
  // ─── Utilities ────────────────────────────────────────────────────────────────
@@ -107,6 +110,10 @@ function openBrowser(url) {
107
110
  // Ignore errors — user can manually open the URL
108
111
  });
109
112
  }
113
+ /** Check if an error is a clack cancel signal */
114
+ function isCancelError(p, err) {
115
+ return p.isCancel(err);
116
+ }
110
117
  // ─── Public API ───────────────────────────────────────────────────────────────
111
118
  /**
112
119
  * Determine if the onboarding wizard should run.
@@ -156,7 +163,6 @@ export async function runOnboarding(authStorage) {
156
163
  p.intro(pc.bold('Welcome to GSD — let\'s get you set up'));
157
164
  // ── LLM Provider Selection ────────────────────────────────────────────────
158
165
  let llmConfigured = false;
159
- let llmProviderName = '';
160
166
  try {
161
167
  llmConfigured = await runLlmStep(p, pc, authStorage);
162
168
  }
@@ -338,13 +344,6 @@ async function runApiKeyFlow(p, pc, authStorage, providerId, providerLabel) {
338
344
  return true;
339
345
  }
340
346
  // ─── "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
347
  async function runOtherProviderFlow(p, pc, authStorage) {
349
348
  const provider = await p.select({
350
349
  message: 'Select provider',
@@ -393,12 +392,6 @@ async function runToolKeysStep(p, pc, authStorage) {
393
392
  }
394
393
  return savedCount;
395
394
  }
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
395
  // ─── Env hydration (migrated from wizard.ts) ─────────────────────────────────
403
396
  /**
404
397
  * Hydrate process.env from stored auth.json credentials for optional tool keys.
package/dist/wizard.d.ts CHANGED
@@ -5,11 +5,3 @@ import type { AuthStorage } from '@mariozechner/pi-coding-agent';
5
5
  * wizard on prior launches.
6
6
  */
7
7
  export declare function loadStoredEnvKeys(authStorage: AuthStorage): void;
8
- /**
9
- * Check for missing optional tool API keys and prompt for them if on a TTY.
10
- *
11
- * Anthropic auth is handled by pi's own OAuth/API key flow — we don't touch it.
12
- * This wizard only collects Brave Search, Context7, and Jina keys which are needed
13
- * for web search and documentation tools.
14
- */
15
- export declare function runWizardIfNeeded(authStorage: AuthStorage): Promise<void>;
package/dist/wizard.js CHANGED
@@ -1,74 +1,3 @@
1
- import { createInterface } from 'readline';
2
- // ─── Colors ──────────────────────────────────────────────────────────────────
3
- const cyan = '\x1b[36m';
4
- const green = '\x1b[32m';
5
- const yellow = '\x1b[33m';
6
- const dim = '\x1b[2m';
7
- const bold = '\x1b[1m';
8
- const reset = '\x1b[0m';
9
- // ─── Masked input ─────────────────────────────────────────────────────────────
10
- /**
11
- * Prompt for masked input using raw mode stdin.
12
- * Handles backspace, Ctrl+C, and Enter.
13
- * Falls back to plain readline if setRawMode is unavailable (e.g. some SSH contexts).
14
- */
15
- async function promptMasked(label, hint) {
16
- return new Promise((resolve) => {
17
- const question = ` ${cyan}›${reset} ${label} ${dim}${hint}${reset}\n `;
18
- try {
19
- process.stdout.write(question);
20
- process.stdin.setRawMode(true);
21
- process.stdin.resume();
22
- process.stdin.setEncoding('utf8');
23
- let value = '';
24
- const redraw = () => {
25
- process.stdout.clearLine(0);
26
- process.stdout.cursorTo(0);
27
- if (value.length === 0) {
28
- process.stdout.write(' ');
29
- }
30
- else {
31
- const dots = '●'.repeat(Math.min(value.length, 24));
32
- const counter = value.length > 24 ? ` ${dim}(${value.length})${reset}` : ` ${dim}${value.length}${reset}`;
33
- process.stdout.write(` ${dots}${counter}`);
34
- }
35
- };
36
- const handler = (ch) => {
37
- if (ch === '\r' || ch === '\n') {
38
- process.stdin.setRawMode(false);
39
- process.stdin.pause();
40
- process.stdin.off('data', handler);
41
- process.stdout.write('\n');
42
- resolve(value);
43
- }
44
- else if (ch === '\u0003') {
45
- process.stdin.setRawMode(false);
46
- process.stdout.write('\n');
47
- process.exit(0);
48
- }
49
- else if (ch === '\u007f' || ch === '\b') {
50
- if (value.length > 0) {
51
- value = value.slice(0, -1);
52
- }
53
- redraw();
54
- }
55
- else {
56
- value += ch;
57
- redraw();
58
- }
59
- };
60
- process.stdin.on('data', handler);
61
- }
62
- catch (_err) {
63
- process.stdout.write(` ${dim}(input will be visible)${reset}\n `);
64
- const rl = createInterface({ input: process.stdin, output: process.stdout });
65
- rl.question('', (answer) => {
66
- rl.close();
67
- resolve(answer);
68
- });
69
- }
70
- });
71
- }
72
1
  // ─── Env hydration ────────────────────────────────────────────────────────────
73
2
  /**
74
3
  * Hydrate process.env from stored auth.json credentials for optional tool keys.
@@ -94,101 +23,3 @@ export function loadStoredEnvKeys(authStorage) {
94
23
  }
95
24
  }
96
25
  }
97
- const API_KEYS = [
98
- {
99
- provider: 'brave',
100
- envVar: 'BRAVE_API_KEY',
101
- label: 'Brave Search',
102
- hint: '(search-the-web + search_and_read tools)',
103
- description: 'Web search and page extraction',
104
- },
105
- {
106
- provider: 'brave_answers',
107
- envVar: 'BRAVE_ANSWERS_KEY',
108
- label: 'Brave Answers',
109
- hint: '(AI-summarised search answers)',
110
- description: 'AI-generated search summaries',
111
- },
112
- {
113
- provider: 'context7',
114
- envVar: 'CONTEXT7_API_KEY',
115
- label: 'Context7',
116
- hint: '(up-to-date library docs)',
117
- description: 'Live library and framework documentation',
118
- },
119
- {
120
- provider: 'jina',
121
- envVar: 'JINA_API_KEY',
122
- label: 'Jina AI',
123
- hint: '(clean page extraction)',
124
- description: 'High-quality web page content extraction',
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
- },
133
- {
134
- provider: 'slack_bot',
135
- envVar: 'SLACK_BOT_TOKEN',
136
- label: 'Slack Bot',
137
- hint: '(remote questions in auto-mode)',
138
- description: 'Bot token for remote questions via Slack',
139
- },
140
- {
141
- provider: 'discord_bot',
142
- envVar: 'DISCORD_BOT_TOKEN',
143
- label: 'Discord Bot',
144
- hint: '(remote questions in auto-mode)',
145
- description: 'Bot token for remote questions via Discord',
146
- },
147
- ];
148
- /**
149
- * Check for missing optional tool API keys and prompt for them if on a TTY.
150
- *
151
- * Anthropic auth is handled by pi's own OAuth/API key flow — we don't touch it.
152
- * This wizard only collects Brave Search, Context7, and Jina keys which are needed
153
- * for web search and documentation tools.
154
- */
155
- export async function runWizardIfNeeded(authStorage) {
156
- const missing = API_KEYS.filter(k => !authStorage.has(k.provider) && !process.env[k.envVar]);
157
- if (missing.length === 0)
158
- return;
159
- // Non-TTY: warn and continue
160
- if (!process.stdin.isTTY) {
161
- const names = missing.map(k => k.label).join(', ');
162
- process.stderr.write(`[gsd] Warning: optional tool API keys not configured (${names}). Some tools may not work.\n`);
163
- return;
164
- }
165
- // ── Header ──────────────────────────────────────────────────────────────────
166
- process.stdout.write(`\n ${bold}Optional API keys${reset}\n` +
167
- ` ${dim}─────────────────────────────────────────────${reset}\n` +
168
- ` These unlock additional tools. All optional — press ${cyan}Enter${reset} to skip any.\n\n`);
169
- // ── Prompts ─────────────────────────────────────────────────────────────────
170
- let savedCount = 0;
171
- for (const key of missing) {
172
- const value = await promptMasked(key.label, key.hint);
173
- if (value.trim()) {
174
- authStorage.set(key.provider, { type: 'api_key', key: value.trim() });
175
- process.env[key.envVar] = value.trim();
176
- process.stdout.write(` ${green}✓${reset} ${key.label} saved\n\n`);
177
- savedCount++;
178
- }
179
- else {
180
- authStorage.set(key.provider, { type: 'api_key', key: '' });
181
- process.stdout.write(` ${dim}↷ ${key.label} skipped${reset}\n\n`);
182
- }
183
- }
184
- // ── Footer ───────────────────────────────────────────────────────────────────
185
- process.stdout.write(` ${dim}─────────────────────────────────────────────${reset}\n`);
186
- if (savedCount > 0) {
187
- process.stdout.write(` ${green}✓${reset} ${savedCount} key${savedCount > 1 ? 's' : ''} saved to ${dim}~/.gsd/agent/auth.json${reset}\n` +
188
- ` ${dim}Run ${reset}${cyan}/login${reset}${dim} inside gsd to connect your LLM provider.${reset}\n\n`);
189
- }
190
- else {
191
- process.stdout.write(` ${yellow}↷${reset} All keys skipped — you can add them later via ${dim}~/.gsd/agent/auth.json${reset}\n` +
192
- ` ${dim}Run ${reset}${cyan}/login${reset}${dim} inside gsd to connect your LLM provider.${reset}\n\n`);
193
- }
194
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.3.10",
3
+ "version": "2.3.11",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -654,8 +654,7 @@ export default function (pi: ExtensionAPI) {
654
654
  const output = isError
655
655
  ? (r.errorMessage || r.stderr || getFinalOutput(r.messages) || "(no output)")
656
656
  : getFinalOutput(r.messages);
657
- const preview = output.slice(0, 200) + (output.length > 200 ? "..." : "");
658
- return `[${r.agent}] ${r.exitCode === 0 ? "completed" : `failed (exit ${r.exitCode})`}: ${preview || "(no output)"}`;
657
+ return `[${r.agent}] ${r.exitCode === 0 ? "completed" : `failed (exit ${r.exitCode})`}: ${output || "(no output)"}`;
659
658
  });
660
659
  return {
661
660
  content: [