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.
- package/lib/wispy-repl.mjs +144 -95
- package/package.json +1 -1
package/lib/wispy-repl.mjs
CHANGED
|
@@ -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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
apiKey = await ask(green(" API key: "));
|
|
187
|
-
apiKey = apiKey.trim();
|
|
170
|
+
} catch { /* not running */ }
|
|
188
171
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
198
|
+
console.log(dim(" no existing config found.\n"));
|
|
221
199
|
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
${
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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" ? "" : `
|
|
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(
|
|
1837
|
-
${dim(
|
|
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();
|