gsd-pi 2.3.9 → 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 +4 -2
- package/dist/cli.js +13 -4
- package/dist/loader.js +3 -26
- package/dist/logo.d.ts +2 -2
- package/dist/logo.js +1 -1
- package/dist/onboarding.d.ts +2 -2
- package/dist/onboarding.js +17 -24
- package/dist/wizard.d.ts +0 -8
- package/dist/wizard.js +0 -169
- package/package.json +3 -1
- package/scripts/postinstall.js +98 -55
- package/src/resources/extensions/bg-shell/index.ts +2 -1
- package/src/resources/extensions/gsd/index.ts +2 -3
- package/src/resources/extensions/shared/terminal.ts +23 -0
- package/src/resources/extensions/subagent/index.ts +1 -2
- package/src/resources/extensions/voice/index.ts +2 -1
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
|
|
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
|
-
├─
|
|
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
|
|
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
|
-
//
|
|
69
|
-
if (!isPrintMode) {
|
|
70
|
-
await
|
|
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 {
|
|
5
|
-
import { agentDir
|
|
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
|
-
//
|
|
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:
|
|
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``
|
|
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``
|
|
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) {
|
package/dist/onboarding.d.ts
CHANGED
|
@@ -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
|
|
8
|
-
* optional tool keys
|
|
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
|
*/
|
package/dist/onboarding.js
CHANGED
|
@@ -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
|
|
8
|
-
* optional tool keys
|
|
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
|
-
*
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "2.3.11",
|
|
4
4
|
"description": "GSD — Get Shit Done coding agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -44,7 +44,9 @@
|
|
|
44
44
|
"prepublishOnly": "npm run sync-pkg-version && npm run build"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
+
"@clack/prompts": "^1.1.0",
|
|
47
48
|
"@mariozechner/pi-coding-agent": "^0.57.1",
|
|
49
|
+
"picocolors": "^1.1.1",
|
|
48
50
|
"playwright": "^1.58.2"
|
|
49
51
|
},
|
|
50
52
|
"devDependencies": {
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,18 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import { exec as execCb } from 'child_process'
|
|
3
4
|
import { createRequire } from 'module'
|
|
4
5
|
import os from 'os'
|
|
5
|
-
import { fileURLToPath } from 'url'
|
|
6
6
|
import { dirname, resolve } from 'path'
|
|
7
|
+
import { fileURLToPath } from 'url'
|
|
7
8
|
|
|
8
9
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
10
|
const require = createRequire(import.meta.url)
|
|
10
11
|
const pkg = require(resolve(__dirname, '..', 'package.json'))
|
|
12
|
+
const cwd = resolve(__dirname, '..')
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Async exec helper — captures stdout+stderr, never inherits to terminal
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
function run(cmd, options = {}) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
execCb(cmd, { cwd, ...options }, (error, stdout, stderr) => {
|
|
20
|
+
resolve({ ok: !error, stdout, stderr, error })
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
}
|
|
11
24
|
|
|
12
|
-
//
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Redirect stdout → stderr so npm always shows postinstall output.
|
|
27
|
+
// npm ≥7 suppresses stdout from lifecycle scripts by default; stderr is
|
|
28
|
+
// always forwarded. Clack writes to process.stdout, so we reroute it.
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
process.stdout.write = process.stderr.write.bind(process.stderr)
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// ASCII banner — printed before clack UI for brand recognition
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
13
35
|
const cyan = '\x1b[36m'
|
|
14
|
-
const green = '\x1b[32m'
|
|
15
|
-
const yellow = '\x1b[33m'
|
|
16
36
|
const dim = '\x1b[2m'
|
|
17
37
|
const reset = '\x1b[0m'
|
|
18
38
|
|
|
@@ -27,58 +47,81 @@ const banner =
|
|
|
27
47
|
' ╚═════╝ ╚══════╝╚═════╝ ' +
|
|
28
48
|
reset + '\n' +
|
|
29
49
|
'\n' +
|
|
30
|
-
` Get Shit Done ${dim}v${pkg.version}${reset}\n`
|
|
31
|
-
` A standalone coding agent that plans, executes, and ships.\n` +
|
|
32
|
-
'\n' +
|
|
33
|
-
` ${green}✓${reset} Installed successfully\n` +
|
|
34
|
-
` ${dim}Run ${reset}${cyan}gsd${reset}${dim} to get started.${reset}\n`
|
|
35
|
-
|
|
36
|
-
function run(command, options = {}) {
|
|
37
|
-
return execSync(command, {
|
|
38
|
-
cwd: resolve(__dirname, '..'),
|
|
39
|
-
encoding: 'utf8',
|
|
40
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
41
|
-
...options,
|
|
42
|
-
})
|
|
43
|
-
}
|
|
50
|
+
` Get Shit Done ${dim}v${pkg.version}${reset}\n`
|
|
44
51
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Main — wrapped in async IIFE, with graceful fallback if clack fails
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
;(async () => {
|
|
56
|
+
process.stderr.write(banner)
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// Apply patches to upstream dependencies (non-fatal)
|
|
52
|
-
try {
|
|
53
|
-
const output = run('npx patch-package')
|
|
54
|
-
printCaptured(output)
|
|
55
|
-
process.stderr.write(`\n ${green}✓${reset} Patches applied\n`)
|
|
56
|
-
} catch (error) {
|
|
57
|
-
printCaptured(error.stdout)
|
|
58
|
-
printCaptured(error.stderr)
|
|
59
|
-
process.stderr.write(`\n ${yellow}⚠${reset} Failed to apply patches — run ${cyan}npx patch-package${reset} manually\n`)
|
|
60
|
-
}
|
|
58
|
+
let p, pc
|
|
61
59
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
60
|
+
try {
|
|
61
|
+
p = await import('@clack/prompts')
|
|
62
|
+
pc = (await import('picocolors')).default
|
|
63
|
+
} catch {
|
|
64
|
+
// Clack or picocolors unavailable — fall back to minimal output
|
|
65
|
+
process.stderr.write(` Run gsd to get started.\n\n`)
|
|
66
|
+
await run('npx patch-package')
|
|
67
|
+
await run('npx playwright install chromium')
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- Branded intro -------------------------------------------------------
|
|
72
|
+
p.intro('Setup')
|
|
73
|
+
|
|
74
|
+
const results = []
|
|
75
|
+
const s = p.spinner()
|
|
76
|
+
|
|
77
|
+
// --- Step 1: Apply patches -----------------------------------------------
|
|
78
|
+
s.start('Applying patches…')
|
|
79
|
+
const patchResult = await run('npx patch-package')
|
|
80
|
+
if (patchResult.ok) {
|
|
81
|
+
s.stop('Patches applied')
|
|
82
|
+
results.push({ label: 'Patches applied', ok: true })
|
|
79
83
|
} else {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
s.stop(pc.yellow('Patches — skipped (non-fatal)'))
|
|
85
|
+
results.push({
|
|
86
|
+
label: 'Patches skipped — run ' + pc.cyan('npx patch-package') + ' manually',
|
|
87
|
+
ok: false,
|
|
88
|
+
})
|
|
83
89
|
}
|
|
84
|
-
|
|
90
|
+
|
|
91
|
+
// --- Step 2: Playwright browser ------------------------------------------
|
|
92
|
+
// Avoid --with-deps: install scripts should not block on interactive sudo
|
|
93
|
+
// prompts. If Linux libs are missing, suggest the explicit follow-up.
|
|
94
|
+
s.start('Setting up browser tools…')
|
|
95
|
+
const pwResult = await run('npx playwright install chromium')
|
|
96
|
+
if (pwResult.ok) {
|
|
97
|
+
s.stop('Browser tools ready')
|
|
98
|
+
results.push({ label: 'Browser tools ready', ok: true })
|
|
99
|
+
} else {
|
|
100
|
+
const output = `${pwResult.stdout ?? ''}${pwResult.stderr ?? ''}`
|
|
101
|
+
if (os.platform() === 'linux' && output.includes('Host system is missing dependencies to run browsers.')) {
|
|
102
|
+
s.stop(pc.yellow('Browser downloaded, missing Linux deps'))
|
|
103
|
+
results.push({
|
|
104
|
+
label: 'Run ' + pc.cyan('sudo npx playwright install-deps chromium') + ' to finish setup',
|
|
105
|
+
ok: false,
|
|
106
|
+
})
|
|
107
|
+
} else {
|
|
108
|
+
s.stop(pc.yellow('Browser tools — skipped (non-fatal)'))
|
|
109
|
+
results.push({
|
|
110
|
+
label: 'Browser tools unavailable — run ' + pc.cyan('npx playwright install chromium'),
|
|
111
|
+
ok: false,
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// --- Summary note --------------------------------------------------------
|
|
117
|
+
const lines = results.map(
|
|
118
|
+
(r) => (r.ok ? pc.green('✓') : pc.yellow('⚠')) + ' ' + r.label
|
|
119
|
+
)
|
|
120
|
+
lines.push('')
|
|
121
|
+
lines.push('Run ' + pc.cyan('gsd') + ' to get started.')
|
|
122
|
+
|
|
123
|
+
p.note(lines.join('\n'), 'Installed')
|
|
124
|
+
|
|
125
|
+
// --- Outro ---------------------------------------------------------------
|
|
126
|
+
p.outro(pc.green('Done!'))
|
|
127
|
+
})()
|
|
@@ -48,6 +48,7 @@ import { createConnection } from "node:net";
|
|
|
48
48
|
import { randomUUID } from "node:crypto";
|
|
49
49
|
import { writeFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
50
50
|
import { join } from "node:path";
|
|
51
|
+
import { shortcutDesc } from "../shared/terminal.js";
|
|
51
52
|
import { createRequire } from "node:module";
|
|
52
53
|
|
|
53
54
|
// ── Windows VT Input Restoration ────────────────────────────────────────────
|
|
@@ -2356,7 +2357,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2356
2357
|
// ── Ctrl+Alt+B shortcut ──────────────────────────────────────────────
|
|
2357
2358
|
|
|
2358
2359
|
pi.registerShortcut(Key.ctrlAlt("b"), {
|
|
2359
|
-
description: "Open background process manager",
|
|
2360
|
+
description: shortcutDesc("Open background process manager", "/bg"),
|
|
2360
2361
|
handler: async (ctx) => {
|
|
2361
2362
|
latestCtx = ctx;
|
|
2362
2363
|
await ctx.ui.custom<void>(
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
import { Key } from "@mariozechner/pi-tui";
|
|
48
48
|
import { join } from "node:path";
|
|
49
49
|
import { existsSync } from "node:fs";
|
|
50
|
+
import { shortcutDesc } from "../shared/terminal.js";
|
|
50
51
|
import { Text } from "@mariozechner/pi-tui";
|
|
51
52
|
|
|
52
53
|
// ── ASCII logo ────────────────────────────────────────────────────────────
|
|
@@ -184,10 +185,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
184
185
|
});
|
|
185
186
|
|
|
186
187
|
// ── Ctrl+Alt+G shortcut — GSD dashboard overlay ────────────────────────
|
|
187
|
-
// Requires Kitty keyboard protocol or modifyOtherKeys support.
|
|
188
|
-
// Terminals without support (macOS Terminal.app, JetBrains): use /gsd status instead.
|
|
189
188
|
pi.registerShortcut(Key.ctrlAlt("g"), {
|
|
190
|
-
description: "Open GSD dashboard
|
|
189
|
+
description: shortcutDesc("Open GSD dashboard", "/gsd status"),
|
|
191
190
|
handler: async (ctx) => {
|
|
192
191
|
// Only show if .gsd/ exists
|
|
193
192
|
if (!existsSync(join(process.cwd(), ".gsd"))) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal capability detection for keyboard shortcut support.
|
|
3
|
+
*
|
|
4
|
+
* Ctrl+Alt shortcuts require the Kitty keyboard protocol or modifyOtherKeys.
|
|
5
|
+
* Terminals that lack this support silently swallow the key combos.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const UNSUPPORTED_TERMS = ["apple_terminal"];
|
|
9
|
+
|
|
10
|
+
export function supportsCtrlAltShortcuts(): boolean {
|
|
11
|
+
const term = (process.env.TERM_PROGRAM || "").toLowerCase();
|
|
12
|
+
const jetbrains = (process.env.TERMINAL_EMULATOR || "").toLowerCase().includes("jetbrains");
|
|
13
|
+
return !UNSUPPORTED_TERMS.some((t) => term.includes(t)) && !jetbrains;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns a shortcut description that includes a slash-command fallback hint
|
|
18
|
+
* when the current terminal likely can't fire Ctrl+Alt combos.
|
|
19
|
+
*/
|
|
20
|
+
export function shortcutDesc(base: string, fallbackCmd: string): string {
|
|
21
|
+
if (supportsCtrlAltShortcuts()) return base;
|
|
22
|
+
return `${base} — shortcut may not work in this terminal, use ${fallbackCmd}`;
|
|
23
|
+
}
|
|
@@ -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
|
-
|
|
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: [
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { shortcutDesc } from "../shared/terminal.js";
|
|
2
3
|
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
|
3
4
|
import { isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
4
5
|
import { spawn, execSync, type ChildProcess } from "node:child_process";
|
|
@@ -131,7 +132,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
131
132
|
});
|
|
132
133
|
|
|
133
134
|
pi.registerShortcut("ctrl+alt+v", {
|
|
134
|
-
description: "Toggle voice mode",
|
|
135
|
+
description: shortcutDesc("Toggle voice mode", "/voice"),
|
|
135
136
|
handler: async (ctx) => toggleVoice(ctx),
|
|
136
137
|
});
|
|
137
138
|
|