wispy-cli 0.1.1 โ†’ 0.2.0

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 (2) hide show
  1. package/lib/wispy-repl.mjs +144 -95
  2. package/package.json +1 -1
@@ -132,111 +132,137 @@ ${bold("macOS Keychain (auto-detected):")}
132
132
 
133
133
  async function runOnboarding() {
134
134
  const { createInterface: createRL } = await import("node:readline");
135
+ const { execSync } = await import("node:child_process");
135
136
 
136
- console.log(`
137
- ${bold("๐ŸŒฟ Welcome to Wispy!")}
138
- ${dim("Let's get you set up in 30 seconds.")}
139
- `);
140
-
141
- const rl = createRL({ input: process.stdin, output: process.stdout });
142
- const ask = (q) => new Promise(r => rl.question(q, r));
143
-
144
- // Step 1: Choose provider
145
- console.log(`${bold("Step 1:")} Choose your AI provider\n`);
146
- console.log(` ${green("1")} Google AI (Gemini) ${dim("โ† free, recommended")}`)
147
- console.log(` ${cyan("2")} Anthropic (Claude) ${dim("paid")}`)
148
- console.log(` ${cyan("3")} OpenAI (GPT-4o) ${dim("paid")}`)
149
- console.log(` ${cyan("4")} OpenRouter ${dim("any model, some free")}`)
150
- console.log(` ${cyan("5")} Groq ${dim("free, fast")}`)
151
- console.log(` ${cyan("6")} DeepSeek ${dim("cheap")}`)
152
- console.log(` ${cyan("7")} Ollama ${dim("local, no key needed")}`)
137
+ // Splash
138
+ console.log("");
139
+ console.log(box([
140
+ "",
141
+ `${bold("๐ŸŒฟ W I S P Y")}`,
142
+ "",
143
+ `${dim("AI workspace assistant")}`,
144
+ `${dim("with multi-agent orchestration")}`,
145
+ "",
146
+ ]));
153
147
  console.log("");
154
148
 
155
- const choice = await ask(green(" Pick a number (1-7): "));
156
- const providerMap = { "1": "google", "2": "anthropic", "3": "openai", "4": "openrouter", "5": "groq", "6": "deepseek", "7": "ollama" };
157
- const chosenProvider = providerMap[choice.trim()] ?? "google";
158
- const chosenConfig = PROVIDERS[chosenProvider];
159
-
160
- let apiKey = null;
161
-
162
- if (chosenProvider === "ollama") {
163
- console.log(`\n${green("โœ…")} Ollama selected โ€” no API key needed!`);
164
- console.log(dim(" Make sure Ollama is running: ollama serve"));
165
- const host = await ask(green(" Ollama host (enter for default localhost:11434): "));
166
- const ollamaHost = host.trim() || "http://localhost:11434";
167
-
168
- // Save config
169
- const configPath = path.join(WISPY_DIR, "config.json");
170
- await mkdir(WISPY_DIR, { recursive: true });
171
- const existing = await readFileOr(configPath, "{}");
172
- const config = JSON.parse(existing);
173
- config.provider = "ollama";
174
- config.ollamaHost = ollamaHost;
175
- await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
176
-
177
- process.env.OLLAMA_HOST = ollamaHost;
178
- } else {
179
- // Step 2: Get API key
180
- console.log(`\n${bold("Step 2:")} Enter your API key`);
181
- if (chosenConfig.signupUrl) {
182
- console.log(dim(` Get one here: ${chosenConfig.signupUrl}`));
149
+ // Auto-detect 1: Ollama running locally?
150
+ process.stdout.write(dim(" Checking environment..."));
151
+ try {
152
+ const resp = await fetch("http://localhost:11434/api/tags", { signal: AbortSignal.timeout(2000) });
153
+ if (resp.ok) {
154
+ console.log(green(" found Ollama! โœ“\n"));
155
+ console.log(box([
156
+ `${green("โœ“")} Using local Ollama ${dim("โ€” no API key needed")}`,
157
+ "",
158
+ ` ${dim("Your AI runs entirely on your machine.")}`,
159
+ ` ${dim("No data leaves your computer.")}`,
160
+ ]));
161
+ const configPath = path.join(WISPY_DIR, "config.json");
162
+ await mkdir(WISPY_DIR, { recursive: true });
163
+ const config = JSON.parse(await readFileOr(configPath, "{}"));
164
+ config.provider = "ollama";
165
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
166
+ process.env.OLLAMA_HOST = "http://localhost:11434";
167
+ console.log("");
168
+ return;
183
169
  }
184
- console.log("");
185
-
186
- apiKey = await ask(green(" API key: "));
187
- apiKey = apiKey.trim();
170
+ } catch { /* not running */ }
188
171
 
189
- if (!apiKey) {
190
- console.log(red("\n No key entered. Run wispy again when you have one."));
191
- rl.close();
192
- process.exit(1);
172
+ // Auto-detect 2: macOS Keychain
173
+ const keychainProviders = [
174
+ { service: "google-ai-key", provider: "google", label: "Google AI (Gemini)" },
175
+ { service: "anthropic-api-key", provider: "anthropic", label: "Anthropic (Claude)" },
176
+ { service: "openai-api-key", provider: "openai", label: "OpenAI (GPT)" },
177
+ ];
178
+ for (const kc of keychainProviders) {
179
+ const key = await tryKeychainKey(kc.service);
180
+ if (key) {
181
+ console.log(green(` found ${kc.label} key! โœ“\n`));
182
+ console.log(box([
183
+ `${green("โœ“")} ${kc.label} ${dim("โ€” auto-detected from Keychain")}`,
184
+ "",
185
+ ` ${dim("Ready to go. No setup needed.")}`,
186
+ ]));
187
+ const configPath = path.join(WISPY_DIR, "config.json");
188
+ await mkdir(WISPY_DIR, { recursive: true });
189
+ const config = JSON.parse(await readFileOr(configPath, "{}"));
190
+ config.provider = kc.provider;
191
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
192
+ process.env[PROVIDERS[kc.provider].envKeys[0]] = key;
193
+ console.log("");
194
+ return;
193
195
  }
194
-
195
- // Save to config
196
- const configPath = path.join(WISPY_DIR, "config.json");
197
- await mkdir(WISPY_DIR, { recursive: true });
198
- const existing = await readFileOr(configPath, "{}");
199
- const config = JSON.parse(existing);
200
- config.provider = chosenProvider;
201
- config.apiKey = apiKey;
202
- await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
203
-
204
- // Set env for current session
205
- process.env[chosenConfig.envKeys[0]] = apiKey;
206
- }
207
-
208
- // Step 3: Name (optional)
209
- console.log(`\n${bold("Step 3:")} What should I call you? ${dim("(enter to skip)")}`);
210
- const userName = await ask(green(" Your name: "));
211
- if (userName.trim()) {
212
- await mkdir(path.join(WISPY_DIR, "memory"), { recursive: true });
213
- await appendFile(
214
- path.join(WISPY_DIR, "memory", "user.md"),
215
- `\n- [${new Date().toISOString().slice(0,16)}] Name: ${userName.trim()}\n`,
216
- "utf8",
217
- );
218
196
  }
219
197
 
220
- rl.close();
198
+ console.log(dim(" no existing config found.\n"));
221
199
 
222
- console.log(`
223
- ${green("โœ… Setup complete!")}
200
+ // Nothing auto-detected โ€” elegant key setup
201
+ console.log(box([
202
+ `${bold("Quick Setup")} ${dim("โ€” one step, 10 seconds")}`,
203
+ "",
204
+ ` Wispy needs an AI provider to work.`,
205
+ ` The easiest: ${bold("Google AI")} ${dim("(free, no credit card)")}`,
206
+ ]));
207
+ console.log("");
224
208
 
225
- ${bold("You're ready to go:")}
226
- ${cyan("wispy")} Start chatting
227
- ${cyan('wispy "hello"')} Quick message
228
- ${cyan("wispy -w project")} Use a workstream
229
- ${cyan("wispy --help")} All options
209
+ // Auto-open browser
210
+ try {
211
+ execSync('open "https://aistudio.google.com/apikey" 2>/dev/null || xdg-open "https://aistudio.google.com/apikey" 2>/dev/null', { stdio: "ignore" });
212
+ console.log(` ${green("โ†’")} Browser opened to ${underline("aistudio.google.com/apikey")}`);
213
+ } catch {
214
+ console.log(` ${green("โ†’")} Visit: ${underline("https://aistudio.google.com/apikey")}`);
215
+ }
216
+ console.log(` ${dim(' Click "Create API Key" โ†’ copy โ†’ paste below')}`);
217
+ console.log("");
230
218
 
231
- ${dim("๐ŸŒฟ Enjoy Wispy!")}
232
- `);
219
+ const rl = createRL({ input: process.stdin, output: process.stdout });
220
+ const ask = (q) => new Promise(r => rl.question(q, r));
221
+
222
+ const apiKey = (await ask(` ${green("API key")} ${dim("(paste here)")}: `)).trim();
233
223
 
234
- // Re-detect provider with new config
235
- const newDetected = await detectProvider();
236
- if (newDetected) {
237
- // Update globals โ€” but since they're const, we need to restart
238
- // For this session, just continue with the values we set
224
+ if (!apiKey) {
225
+ console.log("");
226
+ console.log(box([
227
+ `${dim("No key? Try local AI instead:")}`,
228
+ "",
229
+ ` ${cyan("brew install ollama")}`,
230
+ ` ${cyan("ollama serve")}`,
231
+ ` ${cyan("wispy")} ${dim("โ† will auto-detect")}`,
232
+ ]));
233
+ console.log("");
234
+ rl.close();
235
+ process.exit(0);
239
236
  }
237
+
238
+ // Auto-detect provider from key format
239
+ let chosenProvider = "google";
240
+ if (apiKey.startsWith("sk-ant-")) chosenProvider = "anthropic";
241
+ else if (apiKey.startsWith("sk-or-")) chosenProvider = "openrouter";
242
+ else if (apiKey.startsWith("sk-")) chosenProvider = "openai";
243
+ else if (apiKey.startsWith("gsk_")) chosenProvider = "groq";
244
+
245
+ // Save
246
+ const configPath = path.join(WISPY_DIR, "config.json");
247
+ await mkdir(WISPY_DIR, { recursive: true });
248
+ const config = JSON.parse(await readFileOr(configPath, "{}"));
249
+ config.provider = chosenProvider;
250
+ config.apiKey = apiKey;
251
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
252
+ process.env[PROVIDERS[chosenProvider].envKeys[0]] = apiKey;
253
+
254
+ rl.close();
255
+
256
+ console.log("");
257
+ console.log(box([
258
+ `${green("โœ“")} Connected to ${bold(PROVIDERS[chosenProvider].label)}`,
259
+ "",
260
+ ` ${cyan("wispy")} ${dim("start chatting")}`,
261
+ ` ${cyan('wispy "do something"')} ${dim("quick command")}`,
262
+ ` ${cyan("wispy -w project")} ${dim("use a workstream")}`,
263
+ ` ${cyan("wispy --help")} ${dim("all options")}`,
264
+ ]));
265
+ console.log("");
240
266
  }
241
267
 
242
268
  let detected = await detectProvider();
@@ -255,6 +281,29 @@ const green = (s) => `\x1b[32m${s}\x1b[0m`;
255
281
  const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
256
282
  const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
257
283
  const red = (s) => `\x1b[31m${s}\x1b[0m`;
284
+ const magenta = (s) => `\x1b[35m${s}\x1b[0m`;
285
+ const bgGreen = (s) => `\x1b[42m\x1b[30m${s}\x1b[0m`;
286
+ const underline = (s) => `\x1b[4m${s}\x1b[0m`;
287
+
288
+ function box(lines, { padding = 1, border = "rounded" } = {}) {
289
+ const chars = border === "rounded"
290
+ ? { tl: "โ•ญ", tr: "โ•ฎ", bl: "โ•ฐ", br: "โ•ฏ", h: "โ”€", v: "โ”‚" }
291
+ : { tl: "โ”Œ", tr: "โ”", bl: "โ””", br: "โ”˜", h: "โ”€", v: "โ”‚" };
292
+
293
+ // Strip ANSI for width calculation
294
+ const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
295
+ const maxW = Math.max(...lines.map(l => stripAnsi(l).length)) + padding * 2;
296
+ const pad = " ".repeat(padding);
297
+
298
+ const top = ` ${chars.tl}${chars.h.repeat(maxW)}${chars.tr}`;
299
+ const bot = ` ${chars.bl}${chars.h.repeat(maxW)}${chars.br}`;
300
+ const mid = lines.map(l => {
301
+ const visible = stripAnsi(l).length;
302
+ const rightPad = " ".repeat(Math.max(0, maxW - padding * 2 - visible));
303
+ return ` ${chars.v}${pad}${l}${rightPad}${pad}${chars.v}`;
304
+ });
305
+ return [top, ...mid, bot].join("\n");
306
+ }
258
307
 
259
308
  // ---------------------------------------------------------------------------
260
309
  // File helpers
@@ -1830,11 +1879,11 @@ ${bold("Wispy Commands:")}
1830
1879
  // ---------------------------------------------------------------------------
1831
1880
 
1832
1881
  async function runRepl() {
1833
- const wsLabel = ACTIVE_WORKSTREAM === "default" ? "" : ` [${cyan(ACTIVE_WORKSTREAM)}]`;
1882
+ const wsLabel = ACTIVE_WORKSTREAM === "default" ? "" : ` ${dim("ยท")} ${cyan(ACTIVE_WORKSTREAM)}`;
1834
1883
  const providerLabel = PROVIDERS[PROVIDER]?.label ?? PROVIDER;
1835
1884
  console.log(`
1836
- ${bold("๐ŸŒฟ Wispy")}${wsLabel} ${dim(`โ€” ${providerLabel} (${MODEL})`)}
1837
- ${dim("Type a message to chat. /help for commands. Ctrl+C to exit.")}
1885
+ ${bold("๐ŸŒฟ Wispy")}${wsLabel} ${dim(`ยท ${MODEL}`)}
1886
+ ${dim(`${providerLabel} ยท /help for commands ยท Ctrl+C to exit`)}
1838
1887
  `);
1839
1888
 
1840
1889
  const systemPrompt = await buildSystemPrompt();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wispy-cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "๐ŸŒฟ Wispy โ€” AI workspace assistant with multi-agent orchestration",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Minseo & Poropo",