gsd-pi 0.1.9 → 0.2.1
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/dist/loader.js +1 -0
- package/dist/wizard.d.ts +0 -5
- package/dist/wizard.js +73 -56
- package/package.json +1 -1
- package/src/resources/extensions/shared/ui.ts +3 -3
package/dist/loader.js
CHANGED
|
@@ -12,6 +12,7 @@ const pkgDir = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'pkg');
|
|
|
12
12
|
// MUST be set before any dynamic import of pi SDK fires — this is what config.js
|
|
13
13
|
// reads to determine APP_NAME and CONFIG_DIR_NAME
|
|
14
14
|
process.env.PI_PACKAGE_DIR = pkgDir;
|
|
15
|
+
process.env.PI_SKIP_VERSION_CHECK = '1'; // GSD ships its own update check — suppress pi's
|
|
15
16
|
process.title = 'gsd';
|
|
16
17
|
// Print branded banner on first launch (before ~/.gsd/ exists)
|
|
17
18
|
if (!existsSync(appRoot)) {
|
package/dist/wizard.d.ts
CHANGED
|
@@ -11,10 +11,5 @@ export declare function loadStoredEnvKeys(authStorage: AuthStorage): void;
|
|
|
11
11
|
* Anthropic auth is handled by pi's own OAuth/API key flow — we don't touch it.
|
|
12
12
|
* This wizard only collects Brave Search, Context7, and Jina keys which are needed
|
|
13
13
|
* for web search and documentation tools.
|
|
14
|
-
*
|
|
15
|
-
* Behavior:
|
|
16
|
-
* - All optional keys present (env or auth.json): return silently
|
|
17
|
-
* - Non-TTY with missing optional keys: warn to stderr and continue (non-fatal)
|
|
18
|
-
* - TTY with missing optional keys: interactive prompts, skip on empty input
|
|
19
14
|
*/
|
|
20
15
|
export declare function runWizardIfNeeded(authStorage: AuthStorage): Promise<void>;
|
package/dist/wizard.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
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 ─────────────────────────────────────────────────────────────
|
|
2
10
|
/**
|
|
3
|
-
*
|
|
11
|
+
* Prompt for masked input using raw mode stdin.
|
|
4
12
|
* Handles backspace, Ctrl+C, and Enter.
|
|
5
13
|
* Falls back to plain readline if setRawMode is unavailable (e.g. some SSH contexts).
|
|
6
14
|
*/
|
|
7
|
-
async function promptMasked(
|
|
15
|
+
async function promptMasked(label, hint) {
|
|
8
16
|
return new Promise((resolve) => {
|
|
17
|
+
const question = ` ${cyan}›${reset} ${label} ${dim}${hint}${reset}\n `;
|
|
9
18
|
try {
|
|
10
19
|
process.stdout.write(question);
|
|
11
20
|
process.stdin.setRawMode(true);
|
|
@@ -21,19 +30,17 @@ async function promptMasked(question) {
|
|
|
21
30
|
resolve(value);
|
|
22
31
|
}
|
|
23
32
|
else if (ch === '\u0003') {
|
|
24
|
-
// Ctrl+C — restore raw mode and exit cleanly
|
|
25
33
|
process.stdin.setRawMode(false);
|
|
26
34
|
process.stdout.write('\n');
|
|
27
35
|
process.exit(0);
|
|
28
36
|
}
|
|
29
37
|
else if (ch === '\u007f') {
|
|
30
|
-
// Backspace
|
|
31
38
|
if (value.length > 0) {
|
|
32
39
|
value = value.slice(0, -1);
|
|
33
40
|
}
|
|
34
41
|
process.stdout.clearLine(0);
|
|
35
42
|
process.stdout.cursorTo(0);
|
|
36
|
-
process.stdout.write(
|
|
43
|
+
process.stdout.write(' ' + '*'.repeat(value.length));
|
|
37
44
|
}
|
|
38
45
|
else {
|
|
39
46
|
value += ch;
|
|
@@ -43,16 +50,16 @@ async function promptMasked(question) {
|
|
|
43
50
|
process.stdin.on('data', handler);
|
|
44
51
|
}
|
|
45
52
|
catch (_err) {
|
|
46
|
-
|
|
47
|
-
process.stdout.write(' (note: input will be visible)\n');
|
|
53
|
+
process.stdout.write(` ${dim}(input will be visible)${reset}\n `);
|
|
48
54
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
49
|
-
rl.question(
|
|
55
|
+
rl.question('', (answer) => {
|
|
50
56
|
rl.close();
|
|
51
57
|
resolve(answer);
|
|
52
58
|
});
|
|
53
59
|
}
|
|
54
60
|
});
|
|
55
61
|
}
|
|
62
|
+
// ─── Env hydration ────────────────────────────────────────────────────────────
|
|
56
63
|
/**
|
|
57
64
|
* Hydrate process.env from stored auth.json credentials for optional tool keys.
|
|
58
65
|
* Runs on every launch so extensions see Brave/Context7/Jina keys stored via the
|
|
@@ -74,69 +81,79 @@ export function loadStoredEnvKeys(authStorage) {
|
|
|
74
81
|
}
|
|
75
82
|
}
|
|
76
83
|
}
|
|
84
|
+
const API_KEYS = [
|
|
85
|
+
{
|
|
86
|
+
provider: 'brave',
|
|
87
|
+
envVar: 'BRAVE_API_KEY',
|
|
88
|
+
label: 'Brave Search',
|
|
89
|
+
hint: '(search-the-web + search_and_read tools)',
|
|
90
|
+
description: 'Web search and page extraction',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
provider: 'brave_answers',
|
|
94
|
+
envVar: 'BRAVE_ANSWERS_KEY',
|
|
95
|
+
label: 'Brave Answers',
|
|
96
|
+
hint: '(AI-summarised search answers)',
|
|
97
|
+
description: 'AI-generated search summaries',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
provider: 'context7',
|
|
101
|
+
envVar: 'CONTEXT7_API_KEY',
|
|
102
|
+
label: 'Context7',
|
|
103
|
+
hint: '(up-to-date library docs)',
|
|
104
|
+
description: 'Live library and framework documentation',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
provider: 'jina',
|
|
108
|
+
envVar: 'JINA_API_KEY',
|
|
109
|
+
label: 'Jina AI',
|
|
110
|
+
hint: '(clean page extraction)',
|
|
111
|
+
description: 'High-quality web page content extraction',
|
|
112
|
+
},
|
|
113
|
+
];
|
|
77
114
|
/**
|
|
78
115
|
* Check for missing optional tool API keys and prompt for them if on a TTY.
|
|
79
116
|
*
|
|
80
117
|
* Anthropic auth is handled by pi's own OAuth/API key flow — we don't touch it.
|
|
81
118
|
* This wizard only collects Brave Search, Context7, and Jina keys which are needed
|
|
82
119
|
* for web search and documentation tools.
|
|
83
|
-
*
|
|
84
|
-
* Behavior:
|
|
85
|
-
* - All optional keys present (env or auth.json): return silently
|
|
86
|
-
* - Non-TTY with missing optional keys: warn to stderr and continue (non-fatal)
|
|
87
|
-
* - TTY with missing optional keys: interactive prompts, skip on empty input
|
|
88
120
|
*/
|
|
89
121
|
export async function runWizardIfNeeded(authStorage) {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
const needsContext7 = !authStorage.has('context7') && !process.env.CONTEXT7_API_KEY;
|
|
93
|
-
const needsJina = !authStorage.has('jina') && !process.env.JINA_API_KEY;
|
|
94
|
-
if (!needsBrave && !needsBraveAnswers && !needsContext7 && !needsJina) {
|
|
122
|
+
const missing = API_KEYS.filter(k => !authStorage.has(k.provider) && !process.env[k.envVar]);
|
|
123
|
+
if (missing.length === 0)
|
|
95
124
|
return;
|
|
96
|
-
|
|
97
|
-
const missing = [
|
|
98
|
-
needsBrave && 'Brave Search',
|
|
99
|
-
needsBraveAnswers && 'Brave Answers',
|
|
100
|
-
needsContext7 && 'Context7',
|
|
101
|
-
needsJina && 'Jina',
|
|
102
|
-
]
|
|
103
|
-
.filter(Boolean)
|
|
104
|
-
.join(', ');
|
|
105
|
-
// Non-TTY: just warn and let the session start without them
|
|
125
|
+
// Non-TTY: warn and continue
|
|
106
126
|
if (!process.stdin.isTTY) {
|
|
107
|
-
|
|
127
|
+
const names = missing.map(k => k.label).join(', ');
|
|
128
|
+
process.stderr.write(`[gsd] Warning: optional tool API keys not configured (${names}). Some tools may not work.\n`);
|
|
108
129
|
return;
|
|
109
130
|
}
|
|
110
|
-
//
|
|
111
|
-
process.stdout.write(`\n
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
131
|
+
// ── Header ──────────────────────────────────────────────────────────────────
|
|
132
|
+
process.stdout.write(`\n ${bold}Optional API keys${reset}\n` +
|
|
133
|
+
` ${dim}─────────────────────────────────────────────${reset}\n` +
|
|
134
|
+
` These unlock additional tools. All optional — press ${cyan}Enter${reset} to skip any.\n\n`);
|
|
135
|
+
// ── Prompts ─────────────────────────────────────────────────────────────────
|
|
136
|
+
let savedCount = 0;
|
|
137
|
+
for (const key of missing) {
|
|
138
|
+
const value = await promptMasked(key.label, key.hint);
|
|
139
|
+
if (value.trim()) {
|
|
140
|
+
authStorage.set(key.provider, { type: 'api_key', key: value.trim() });
|
|
141
|
+
process.env[key.envVar] = value.trim();
|
|
142
|
+
process.stdout.write(` ${green}✓${reset} ${key.label} saved\n\n`);
|
|
143
|
+
savedCount++;
|
|
118
144
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const key = await promptMasked('Brave Answers API key (optional, for AI-generated answers): ');
|
|
122
|
-
if (key) {
|
|
123
|
-
authStorage.set('brave_answers', { type: 'api_key', key });
|
|
124
|
-
process.env.BRAVE_ANSWERS_KEY = key;
|
|
145
|
+
else {
|
|
146
|
+
process.stdout.write(` ${dim}↷ ${key.label} skipped${reset}\n\n`);
|
|
125
147
|
}
|
|
126
148
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
149
|
+
// ── Footer ───────────────────────────────────────────────────────────────────
|
|
150
|
+
process.stdout.write(` ${dim}─────────────────────────────────────────────${reset}\n`);
|
|
151
|
+
if (savedCount > 0) {
|
|
152
|
+
process.stdout.write(` ${green}✓${reset} ${savedCount} key${savedCount > 1 ? 's' : ''} saved to ${dim}~/.gsd/agent/auth.json${reset}\n` +
|
|
153
|
+
` ${dim}Run ${reset}${cyan}/login${reset}${dim} inside gsd to connect your LLM provider.${reset}\n\n`);
|
|
133
154
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
authStorage.set('jina', { type: 'api_key', key });
|
|
138
|
-
process.env.JINA_API_KEY = key;
|
|
139
|
-
}
|
|
155
|
+
else {
|
|
156
|
+
process.stdout.write(` ${yellow}↷${reset} All keys skipped — you can add them later via ${dim}~/.gsd/agent/auth.json${reset}\n` +
|
|
157
|
+
` ${dim}Run ${reset}${cyan}/login${reset}${dim} inside gsd to connect your LLM provider.${reset}\n\n`);
|
|
140
158
|
}
|
|
141
|
-
process.stdout.write('[gsd] Keys saved. Starting...\n\n');
|
|
142
159
|
}
|
package/package.json
CHANGED
|
@@ -266,8 +266,8 @@ export function makeUI(theme: Theme, width: number): UI {
|
|
|
266
266
|
optionUnselected: (num, label, description, opts = {}) => {
|
|
267
267
|
const { isCommitted = false, isFocusDimmed = false } = opts;
|
|
268
268
|
const marker = isCommitted ? theme.fg("success", ` ${GLYPH.check}`) : "";
|
|
269
|
-
const labelColor = isFocusDimmed ? "dim" : "text";
|
|
270
|
-
const descColor = isFocusDimmed ? "dim" : "muted";
|
|
269
|
+
const labelColor = isFocusDimmed ? (isCommitted ? "text" : "dim") : "text";
|
|
270
|
+
const descColor = isFocusDimmed ? (isCommitted ? "muted" : "dim") : "muted";
|
|
271
271
|
return [
|
|
272
272
|
...wrap(`${INDENT.option} ${theme.fg(labelColor, `${num}. ${label}`)}${marker}`),
|
|
273
273
|
...wrapIndented(`${INDENT.description}${theme.fg(descColor, description)}`, INDENT.description),
|
|
@@ -287,7 +287,7 @@ export function makeUI(theme: Theme, width: number): UI {
|
|
|
287
287
|
checkboxUnselected: (label, description, isChecked, isFocusDimmed = false) => {
|
|
288
288
|
const box = isChecked ? theme.fg("success", GLYPH.checkedBox) : theme.fg("dim", GLYPH.uncheckedBox);
|
|
289
289
|
const labelColor = isFocusDimmed ? (isChecked ? "text" : "dim") : "text";
|
|
290
|
-
const descColor = isFocusDimmed ? "dim" : "muted";
|
|
290
|
+
const descColor = isFocusDimmed ? (isChecked ? "muted" : "dim") : "muted";
|
|
291
291
|
return [
|
|
292
292
|
add(`${INDENT.option} ${box} ${theme.fg(labelColor, label)}`),
|
|
293
293
|
...wrapIndented(`${INDENT.description}${theme.fg(descColor, description)}`, INDENT.description),
|