wispy-cli 0.1.0 → 0.1.2
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/lib/wispy-repl.mjs +112 -7
- package/package.json +1 -1
package/lib/wispy-repl.mjs
CHANGED
|
@@ -130,10 +130,101 @@ ${bold("macOS Keychain (auto-detected):")}
|
|
|
130
130
|
`);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
133
|
+
async function runOnboarding() {
|
|
134
|
+
const { createInterface: createRL } = await import("node:readline");
|
|
135
|
+
const { execSync } = await import("node:child_process");
|
|
136
|
+
|
|
137
|
+
console.log(`\n${bold("🌿 Welcome to Wispy!")}\n`);
|
|
138
|
+
|
|
139
|
+
// Auto-detect 1: Ollama running locally?
|
|
140
|
+
try {
|
|
141
|
+
const resp = await fetch("http://localhost:11434/api/tags", { signal: AbortSignal.timeout(2000) });
|
|
142
|
+
if (resp.ok) {
|
|
143
|
+
console.log(`${green("✅")} Found Ollama running locally — no API key needed!`);
|
|
144
|
+
const configPath = path.join(WISPY_DIR, "config.json");
|
|
145
|
+
await mkdir(WISPY_DIR, { recursive: true });
|
|
146
|
+
const config = JSON.parse(await readFileOr(configPath, "{}"));
|
|
147
|
+
config.provider = "ollama";
|
|
148
|
+
await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
149
|
+
process.env.OLLAMA_HOST = "http://localhost:11434";
|
|
150
|
+
console.log(dim(" Using local Ollama. Run wispy to start.\n"));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
} catch { /* not running */ }
|
|
154
|
+
|
|
155
|
+
// Auto-detect 2: macOS Keychain
|
|
156
|
+
const keychainProviders = [
|
|
157
|
+
{ service: "google-ai-key", provider: "google", label: "Google AI" },
|
|
158
|
+
{ service: "anthropic-api-key", provider: "anthropic", label: "Anthropic" },
|
|
159
|
+
{ service: "openai-api-key", provider: "openai", label: "OpenAI" },
|
|
160
|
+
];
|
|
161
|
+
for (const kc of keychainProviders) {
|
|
162
|
+
const key = await tryKeychainKey(kc.service);
|
|
163
|
+
if (key) {
|
|
164
|
+
console.log(`${green("✅")} Found ${kc.label} key in macOS Keychain — using it automatically!`);
|
|
165
|
+
const configPath = path.join(WISPY_DIR, "config.json");
|
|
166
|
+
await mkdir(WISPY_DIR, { recursive: true });
|
|
167
|
+
const config = JSON.parse(await readFileOr(configPath, "{}"));
|
|
168
|
+
config.provider = kc.provider;
|
|
169
|
+
await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
170
|
+
process.env[PROVIDERS[kc.provider].envKeys[0]] = key;
|
|
171
|
+
console.log(dim(" Ready to go. Run wispy to start.\n"));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Nothing auto-detected — minimal setup: open browser, paste key
|
|
177
|
+
console.log(`${dim("No API key found. Let's set one up — takes 10 seconds.")}\n`);
|
|
178
|
+
|
|
179
|
+
const rl = createRL({ input: process.stdin, output: process.stdout });
|
|
180
|
+
const ask = (q) => new Promise(r => rl.question(q, r));
|
|
181
|
+
|
|
182
|
+
console.log(` The easiest way: ${bold("Google AI")} (free, no credit card)\n`);
|
|
183
|
+
|
|
184
|
+
// Auto-open browser
|
|
185
|
+
try {
|
|
186
|
+
execSync('open "https://aistudio.google.com/apikey" 2>/dev/null || xdg-open "https://aistudio.google.com/apikey" 2>/dev/null', { stdio: "ignore" });
|
|
187
|
+
console.log(` ${green("→")} Opened browser to get your free API key`);
|
|
188
|
+
console.log(` ${dim(" Click 'Create API Key' and paste it here:")}\n`);
|
|
189
|
+
} catch {
|
|
190
|
+
console.log(` ${dim(" Go to:")} ${cyan("https://aistudio.google.com/apikey")}`);
|
|
191
|
+
console.log(` ${dim(" Click 'Create API Key' and paste it here:")}\n`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const apiKey = (await ask(green(" API key: "))).trim();
|
|
195
|
+
|
|
196
|
+
if (!apiKey) {
|
|
197
|
+
console.log(`\n ${dim("No key? You can also use Ollama for free local AI:")}`);
|
|
198
|
+
console.log(` ${cyan("brew install ollama && ollama serve")}`);
|
|
199
|
+
console.log(` ${dim("Then run wispy again.\n")}`);
|
|
200
|
+
rl.close();
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Detect provider from key format
|
|
205
|
+
let chosenProvider = "google"; // default
|
|
206
|
+
if (apiKey.startsWith("sk-ant-")) chosenProvider = "anthropic";
|
|
207
|
+
else if (apiKey.startsWith("sk-")) chosenProvider = "openai";
|
|
208
|
+
else if (apiKey.startsWith("sk-or-")) chosenProvider = "openrouter";
|
|
209
|
+
else if (apiKey.startsWith("gsk_")) chosenProvider = "groq";
|
|
210
|
+
|
|
211
|
+
// Save
|
|
212
|
+
const configPath = path.join(WISPY_DIR, "config.json");
|
|
213
|
+
await mkdir(WISPY_DIR, { recursive: true });
|
|
214
|
+
const config = JSON.parse(await readFileOr(configPath, "{}"));
|
|
215
|
+
config.provider = chosenProvider;
|
|
216
|
+
config.apiKey = apiKey;
|
|
217
|
+
await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
218
|
+
process.env[PROVIDERS[chosenProvider].envKeys[0]] = apiKey;
|
|
219
|
+
|
|
220
|
+
rl.close();
|
|
221
|
+
console.log(`\n${green("✅")} Ready! Using ${PROVIDERS[chosenProvider].label}.\n`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let detected = await detectProvider();
|
|
225
|
+
let PROVIDER = detected?.provider ?? "none";
|
|
226
|
+
let API_KEY = detected?.key ?? null;
|
|
227
|
+
let MODEL = detected?.model ?? "unknown";
|
|
137
228
|
const MAX_CONTEXT_CHARS = 40_000;
|
|
138
229
|
|
|
139
230
|
// ---------------------------------------------------------------------------
|
|
@@ -1991,10 +2082,24 @@ if (args[0] && operatorCommands.has(args[0])) {
|
|
|
1991
2082
|
process.exit(0);
|
|
1992
2083
|
}
|
|
1993
2084
|
|
|
1994
|
-
// Check API key
|
|
2085
|
+
// Check API key — if none found, run interactive onboarding
|
|
1995
2086
|
if (!API_KEY && PROVIDER !== "ollama") {
|
|
1996
|
-
|
|
1997
|
-
|
|
2087
|
+
await runOnboarding();
|
|
2088
|
+
// Re-detect after onboarding saved config
|
|
2089
|
+
const redetected = await detectProvider();
|
|
2090
|
+
if (redetected) {
|
|
2091
|
+
// Patch module-level state for this session
|
|
2092
|
+
Object.assign(detected, redetected);
|
|
2093
|
+
}
|
|
2094
|
+
if (redetected) {
|
|
2095
|
+
PROVIDER = redetected.provider;
|
|
2096
|
+
API_KEY = redetected.key;
|
|
2097
|
+
MODEL = redetected.model;
|
|
2098
|
+
}
|
|
2099
|
+
if (!API_KEY && PROVIDER !== "ollama") {
|
|
2100
|
+
console.log(dim("\nRun wispy again to start chatting!"));
|
|
2101
|
+
process.exit(0);
|
|
2102
|
+
}
|
|
1998
2103
|
}
|
|
1999
2104
|
|
|
2000
2105
|
// Auto-start server before entering REPL or one-shot
|