wispy-cli 2.7.5 → 2.7.7
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/bin/wispy.mjs +402 -109
- package/core/config.mjs +54 -27
- package/core/onboarding.mjs +23 -2
- package/package.json +1 -1
package/bin/wispy.mjs
CHANGED
|
@@ -1,123 +1,416 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* wispy-cli
|
|
5
|
-
*
|
|
4
|
+
* wispy-cli — main entry point
|
|
5
|
+
*
|
|
6
|
+
* Dispatches to the correct handler based on the first argument:
|
|
7
|
+
* - Operator commands (setup, config, doctor, etc.) → operator CLI or inline handler
|
|
8
|
+
* - Server commands → server manager
|
|
9
|
+
* - Everything else → REPL (interactive chat)
|
|
10
|
+
*
|
|
11
|
+
* v2.7.x — rewired to properly route all commands.
|
|
6
12
|
*/
|
|
7
|
-
(async () => {
|
|
8
|
-
const { fileURLToPath } = await import('url');
|
|
9
|
-
const { dirname } = await import('path');
|
|
10
|
-
const { readFile } = await import('fs/promises');
|
|
11
|
-
const baseDir = dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { readFile } from "node:fs/promises";
|
|
17
|
+
import { existsSync } from "node:fs";
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = dirname(__filename);
|
|
21
|
+
const rootDir = join(__dirname, "..");
|
|
22
|
+
|
|
23
|
+
const args = process.argv.slice(2);
|
|
24
|
+
const command = args[0];
|
|
25
|
+
|
|
26
|
+
// ── Flags ─────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
if (args.includes("--version") || command === "--version" || command === "-v") {
|
|
29
|
+
try {
|
|
30
|
+
const pkg = JSON.parse(await readFile(join(rootDir, "package.json"), "utf8"));
|
|
31
|
+
console.log(`wispy-cli v${pkg.version}`);
|
|
32
|
+
} catch {
|
|
33
|
+
console.error("Failed to read version from package.json");
|
|
18
34
|
}
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
19
37
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
if (args.includes("--help") || command === "--help" || command === "-h" || command === "help") {
|
|
39
|
+
const pkg = JSON.parse(await readFile(join(rootDir, "package.json"), "utf8"));
|
|
40
|
+
console.log(`
|
|
41
|
+
wispy-cli v${pkg.version}
|
|
42
|
+
AI workspace assistant — chat, automate, and orchestrate from the terminal.
|
|
43
|
+
|
|
44
|
+
Usage:
|
|
45
|
+
wispy Start interactive REPL
|
|
46
|
+
wispy <message> One-shot chat
|
|
47
|
+
wispy setup Run first-time setup wizard
|
|
48
|
+
wispy config [show|get|set|delete|reset|path|edit]
|
|
49
|
+
Manage configuration
|
|
50
|
+
wispy model Show or change AI model
|
|
51
|
+
wispy doctor Check system health
|
|
52
|
+
wispy trust [level|log] Security level & audit
|
|
53
|
+
wispy ws [start-client|run-debug]
|
|
54
|
+
WebSocket operations
|
|
55
|
+
wispy skill [list|run] Manage learned skills
|
|
56
|
+
wispy teach <name> Create skill from conversation
|
|
57
|
+
wispy improve <name> Improve a skill
|
|
58
|
+
wispy where Show current mode (local/cloud)
|
|
59
|
+
wispy handoff [cloud|local] Sync between environments
|
|
60
|
+
wispy server [start|stop|status]
|
|
61
|
+
Manage HTTP/WS server
|
|
62
|
+
wispy tui Launch full terminal UI
|
|
63
|
+
wispy overview Director view of workstreams
|
|
64
|
+
|
|
65
|
+
Options:
|
|
66
|
+
-w, --workstream <name> Set active workstream
|
|
67
|
+
--session <id> Resume a session
|
|
68
|
+
--model <name> Override AI model
|
|
69
|
+
--provider <name> Override AI provider
|
|
70
|
+
--help, -h Show this help
|
|
71
|
+
--version, -v Show version
|
|
72
|
+
`);
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Setup (first-run wizard) ──────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
if (command === "setup") {
|
|
79
|
+
try {
|
|
80
|
+
const { OnboardingWizard } = await import(join(rootDir, "core/onboarding.mjs"));
|
|
81
|
+
const wizard = new OnboardingWizard();
|
|
82
|
+
await wizard.run();
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error("Setup failed:", err.message);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Config ────────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
if (command === "config") {
|
|
93
|
+
try {
|
|
94
|
+
const { loadConfig, saveConfig, WISPY_DIR, CONFIG_PATH } = await import(join(rootDir, "core/config.mjs"));
|
|
95
|
+
const sub = args[1];
|
|
96
|
+
const config = await loadConfig();
|
|
97
|
+
|
|
98
|
+
if (!sub || sub === "show") {
|
|
99
|
+
console.log(JSON.stringify(config, null, 2));
|
|
100
|
+
} else if (sub === "path") {
|
|
101
|
+
console.log(CONFIG_PATH);
|
|
102
|
+
} else if (sub === "get") {
|
|
103
|
+
const key = args[2];
|
|
104
|
+
if (!key) { console.error("Usage: wispy config get <key>"); process.exit(1); }
|
|
105
|
+
console.log(config[key] !== undefined ? JSON.stringify(config[key], null, 2) : `Key '${key}' not found`);
|
|
106
|
+
} else if (sub === "set") {
|
|
107
|
+
const key = args[2], value = args[3];
|
|
108
|
+
if (!key || value === undefined) { console.error("Usage: wispy config set <key> <value>"); process.exit(1); }
|
|
109
|
+
try { config[key] = JSON.parse(value); } catch { config[key] = value; }
|
|
110
|
+
await saveConfig(config);
|
|
111
|
+
console.log(`Set ${key} = ${JSON.stringify(config[key])}`);
|
|
112
|
+
} else if (sub === "delete") {
|
|
113
|
+
const key = args[2];
|
|
114
|
+
if (!key) { console.error("Usage: wispy config delete <key>"); process.exit(1); }
|
|
115
|
+
delete config[key];
|
|
116
|
+
await saveConfig(config);
|
|
117
|
+
console.log(`Deleted '${key}'`);
|
|
118
|
+
} else if (sub === "reset") {
|
|
119
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
120
|
+
const yes = await confirm({ message: "Reset all config to defaults?", default: false });
|
|
121
|
+
if (yes) { await saveConfig({}); console.log("Config reset to defaults."); }
|
|
122
|
+
} else if (sub === "edit") {
|
|
123
|
+
const editor = process.env.EDITOR || "nano";
|
|
124
|
+
const { execSync } = await import("node:child_process");
|
|
125
|
+
execSync(`${editor} "${CONFIG_PATH}"`, { stdio: "inherit" });
|
|
126
|
+
} else {
|
|
127
|
+
console.error(`Unknown config subcommand: ${sub}`);
|
|
128
|
+
console.log("Available: show, get, set, delete, reset, path, edit");
|
|
129
|
+
process.exit(1);
|
|
43
130
|
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error("Config error:", err.message);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
process.exit(0);
|
|
136
|
+
}
|
|
44
137
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
138
|
+
// ── Doctor ────────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
if (command === "doctor") {
|
|
141
|
+
try {
|
|
142
|
+
const { loadConfig, detectProvider, WISPY_DIR } = await import(join(rootDir, "core/config.mjs"));
|
|
143
|
+
const config = await loadConfig();
|
|
144
|
+
const provider = await detectProvider();
|
|
145
|
+
const pkg = JSON.parse(await readFile(join(rootDir, "package.json"), "utf8"));
|
|
146
|
+
|
|
147
|
+
console.log(`\n wispy-cli v${pkg.version}`);
|
|
148
|
+
console.log(` Config dir: ${WISPY_DIR}`);
|
|
149
|
+
console.log(` Provider: ${provider?.provider ?? "none"}`);
|
|
150
|
+
console.log(` Model: ${provider?.model ?? "none"}`);
|
|
151
|
+
console.log(` API key: ${provider?.key ? "✓ set" : "✗ missing"}`);
|
|
152
|
+
|
|
153
|
+
// Check optional components
|
|
154
|
+
const checks = [
|
|
155
|
+
{ name: "Config file", ok: existsSync(join(WISPY_DIR, "config.json")) },
|
|
156
|
+
{ name: "Memory dir", ok: existsSync(join(WISPY_DIR, "memory")) },
|
|
157
|
+
{ name: "Sessions dir", ok: existsSync(join(WISPY_DIR, "sessions")) },
|
|
158
|
+
{ name: "MCP config", ok: existsSync(join(WISPY_DIR, "mcp.json")) },
|
|
159
|
+
];
|
|
160
|
+
console.log("");
|
|
161
|
+
for (const c of checks) {
|
|
162
|
+
console.log(` ${c.ok ? "✓" : "✗"} ${c.name}`);
|
|
163
|
+
}
|
|
164
|
+
console.log("");
|
|
165
|
+
|
|
166
|
+
if (!provider?.key) {
|
|
167
|
+
console.log(" Run 'wispy setup' to configure an API key.\n");
|
|
168
|
+
} else {
|
|
169
|
+
console.log(" All good!\n");
|
|
170
|
+
}
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.error("Doctor error:", err.message);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Trust ─────────────────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
if (command === "trust") {
|
|
181
|
+
try {
|
|
182
|
+
const { handleTrustCommand } = await import(join(rootDir, "lib/commands/trust.mjs"));
|
|
183
|
+
await handleTrustCommand(args.slice(1));
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.error("Trust command error:", err.message);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
process.exit(0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── WebSocket ─────────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
if (command === "ws") {
|
|
194
|
+
try {
|
|
195
|
+
const { handleWsCommand } = await import(join(rootDir, "lib/commands/ws.mjs"));
|
|
196
|
+
await handleWsCommand(args);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.error("WS command error:", err.message);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Skills ────────────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
if (command === "skill" || command === "skills") {
|
|
207
|
+
try {
|
|
208
|
+
const { handleSkillCommand } = await import(join(rootDir, "lib/commands/skills-cmd.mjs"));
|
|
209
|
+
await handleSkillCommand(args.slice(1));
|
|
210
|
+
} catch (err) {
|
|
211
|
+
console.error("Skill command error:", err.message);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
process.exit(0);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (command === "teach") {
|
|
218
|
+
try {
|
|
219
|
+
const { cmdTeach } = await import(join(rootDir, "lib/commands/skills-cmd.mjs"));
|
|
220
|
+
await cmdTeach(args[1]);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
console.error("Teach error:", err.message);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
process.exit(0);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (command === "improve") {
|
|
229
|
+
try {
|
|
230
|
+
const { cmdImproveSkill } = await import(join(rootDir, "lib/commands/skills-cmd.mjs"));
|
|
231
|
+
await cmdImproveSkill(args[1], args.slice(2).join(" "));
|
|
232
|
+
} catch (err) {
|
|
233
|
+
console.error("Improve error:", err.message);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
process.exit(0);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── Continuity ────────────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
if (command === "where") {
|
|
242
|
+
try {
|
|
243
|
+
const { cmdWhere } = await import(join(rootDir, "lib/commands/continuity.mjs"));
|
|
244
|
+
await cmdWhere();
|
|
245
|
+
} catch (err) {
|
|
246
|
+
console.error("Where error:", err.message);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
process.exit(0);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (command === "handoff") {
|
|
253
|
+
try {
|
|
254
|
+
const { handleContinuityCommand } = await import(join(rootDir, "lib/commands/continuity.mjs"));
|
|
255
|
+
await handleContinuityCommand(args.slice(1));
|
|
256
|
+
} catch (err) {
|
|
257
|
+
console.error("Handoff error:", err.message);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
process.exit(0);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── Model ─────────────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
if (command === "model") {
|
|
266
|
+
try {
|
|
267
|
+
const { loadConfig, saveConfig, detectProvider, PROVIDERS } = await import(join(rootDir, "core/config.mjs"));
|
|
268
|
+
|
|
269
|
+
const sub = args[1];
|
|
270
|
+
|
|
271
|
+
// wispy model list → print all providers and their models
|
|
272
|
+
if (sub === "list") {
|
|
273
|
+
console.log("\n Available models by provider:\n");
|
|
274
|
+
for (const [key, p] of Object.entries(PROVIDERS)) {
|
|
275
|
+
console.log(` ${key}`);
|
|
276
|
+
const models = p.models ?? [p.defaultModel];
|
|
277
|
+
for (const m of models) {
|
|
278
|
+
const tag = m === p.defaultModel ? " (default)" : "";
|
|
279
|
+
console.log(` ${m}${tag}`);
|
|
118
280
|
}
|
|
281
|
+
}
|
|
282
|
+
console.log("");
|
|
283
|
+
process.exit(0);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// wispy model <name> → set directly
|
|
287
|
+
if (sub && sub !== "list") {
|
|
288
|
+
const config = await loadConfig();
|
|
289
|
+
config.model = sub;
|
|
290
|
+
await saveConfig(config);
|
|
291
|
+
console.log(`Model set to: ${sub}`);
|
|
292
|
+
process.exit(0);
|
|
119
293
|
}
|
|
294
|
+
|
|
295
|
+
// wispy model (no args) → show current + interactive picker
|
|
296
|
+
const { select } = await import("@inquirer/prompts");
|
|
297
|
+
const config = await loadConfig();
|
|
298
|
+
const detected = await detectProvider();
|
|
299
|
+
|
|
300
|
+
console.log(`\n Current provider: ${detected?.provider ?? "none"}`);
|
|
301
|
+
console.log(` Current model: ${detected?.model ?? "none"}\n`);
|
|
302
|
+
|
|
303
|
+
// Find providers with configured keys (or local providers)
|
|
304
|
+
const availableProviders = Object.entries(PROVIDERS).filter(([key, p]) => {
|
|
305
|
+
if (p.local) return true;
|
|
306
|
+
// Check env keys
|
|
307
|
+
for (const k of (p.envKeys ?? [])) {
|
|
308
|
+
if (process.env[k]) return true;
|
|
309
|
+
}
|
|
310
|
+
// Check config
|
|
311
|
+
if (config.provider === key && config.apiKey) return true;
|
|
312
|
+
if (config.providers?.[key]) return true;
|
|
313
|
+
return false;
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
let providerChoices = availableProviders.map(([key, p]) => ({
|
|
317
|
+
name: p.label ?? key,
|
|
318
|
+
value: key,
|
|
319
|
+
}));
|
|
320
|
+
|
|
321
|
+
// If no providers detected, show all
|
|
322
|
+
if (providerChoices.length === 0) {
|
|
323
|
+
providerChoices = Object.entries(PROVIDERS).map(([key, p]) => ({
|
|
324
|
+
name: p.label ?? key,
|
|
325
|
+
value: key,
|
|
326
|
+
}));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let selectedProvider;
|
|
330
|
+
try {
|
|
331
|
+
selectedProvider = await select({
|
|
332
|
+
message: "Select provider:",
|
|
333
|
+
choices: providerChoices,
|
|
334
|
+
default: detected?.provider,
|
|
335
|
+
});
|
|
336
|
+
} catch {
|
|
337
|
+
console.log(" Cancelled.");
|
|
338
|
+
process.exit(0);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const providerInfo = PROVIDERS[selectedProvider];
|
|
342
|
+
const modelList = providerInfo.models ?? [providerInfo.defaultModel];
|
|
343
|
+
|
|
344
|
+
let selectedModel;
|
|
345
|
+
try {
|
|
346
|
+
selectedModel = await select({
|
|
347
|
+
message: "Select model:",
|
|
348
|
+
choices: modelList.map((m) => ({
|
|
349
|
+
name: m === providerInfo.defaultModel ? `${m} (default)` : m,
|
|
350
|
+
value: m,
|
|
351
|
+
})),
|
|
352
|
+
default: providerInfo.defaultModel,
|
|
353
|
+
});
|
|
354
|
+
} catch {
|
|
355
|
+
console.log(" Cancelled.");
|
|
356
|
+
process.exit(0);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
config.provider = selectedProvider;
|
|
360
|
+
config.model = selectedModel;
|
|
361
|
+
await saveConfig(config);
|
|
362
|
+
console.log(`\n Provider set to: ${selectedProvider}`);
|
|
363
|
+
console.log(` Model set to: ${selectedModel}\n`);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
console.error("Model error:", err.message);
|
|
366
|
+
process.exit(1);
|
|
120
367
|
}
|
|
368
|
+
process.exit(0);
|
|
369
|
+
}
|
|
121
370
|
|
|
122
|
-
|
|
123
|
-
|
|
371
|
+
// ── TUI ───────────────────────────────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
if (command === "tui") {
|
|
374
|
+
try {
|
|
375
|
+
await import(join(rootDir, "bin/wispy-tui.mjs"));
|
|
376
|
+
} catch (err) {
|
|
377
|
+
console.error("TUI launch error:", err.message);
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
// TUI handles its own lifecycle
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ── Overview ──────────────────────────────────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
if (command === "overview") {
|
|
386
|
+
// Delegate to REPL with overview flag
|
|
387
|
+
process.env.WISPY_OVERVIEW = "1";
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ── Server ────────────────────────────────────────────────────────────────────
|
|
391
|
+
|
|
392
|
+
if (command === "server") {
|
|
393
|
+
// Delegate to REPL which has server handling built in
|
|
394
|
+
// (lib/wispy-repl.mjs handles server start/stop/status)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ── Default: launch REPL ──────────────────────────────────────────────────────
|
|
398
|
+
// For no args, one-shot messages, or unrecognized commands that the REPL handles
|
|
399
|
+
|
|
400
|
+
if (command === "server" || command === "overview") {
|
|
401
|
+
// Already set up env flags above, fall through to REPL
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// If we get here and it's not a recognized command above, go to REPL
|
|
405
|
+
// The REPL handles: interactive chat, one-shot "wispy <message>", server, overview
|
|
406
|
+
try {
|
|
407
|
+
await import(join(rootDir, "lib/wispy-repl.mjs"));
|
|
408
|
+
} catch (err) {
|
|
409
|
+
if (err.code === "ERR_MODULE_NOT_FOUND") {
|
|
410
|
+
console.error(`Module not found: ${err.message}`);
|
|
411
|
+
console.error("Try running: npm install");
|
|
412
|
+
} else {
|
|
413
|
+
console.error("Failed to start wispy:", err.message);
|
|
414
|
+
}
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
package/core/config.mjs
CHANGED
|
@@ -19,47 +19,74 @@ export const MEMORY_DIR = path.join(WISPY_DIR, "memory");
|
|
|
19
19
|
|
|
20
20
|
export const PROVIDERS = {
|
|
21
21
|
// ── Tier 0: Custom API providers ────────────────────────────────────────────
|
|
22
|
-
google: { envKeys: ["GOOGLE_AI_KEY", "GOOGLE_GENERATIVE_AI_KEY", "GEMINI_API_KEY"], defaultModel: "gemini-2.5-flash", label: "Google AI (Gemini)", signupUrl: "https://aistudio.google.com/apikey"
|
|
23
|
-
|
|
22
|
+
google: { envKeys: ["GOOGLE_AI_KEY", "GOOGLE_GENERATIVE_AI_KEY", "GEMINI_API_KEY"], defaultModel: "gemini-2.5-flash", label: "Google AI (Gemini)", signupUrl: "https://aistudio.google.com/apikey",
|
|
23
|
+
models: ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash", "gemini-2.0-flash-lite", "gemini-1.5-pro", "gemini-1.5-flash"] },
|
|
24
|
+
anthropic: { envKeys: ["ANTHROPIC_API_KEY"], defaultModel: "claude-sonnet-4-20250514", label: "Anthropic (Claude)", signupUrl: "https://console.anthropic.com/settings/keys",
|
|
25
|
+
models: ["claude-opus-4-20250514", "claude-sonnet-4-20250514", "claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022", "claude-3-opus-20240229"] },
|
|
24
26
|
|
|
25
27
|
// ── Tier 1: Popular OpenAI-compat ──────────────────────────────────────────
|
|
26
|
-
openai: { envKeys: ["OPENAI_API_KEY"], defaultModel: "gpt-4o", label: "OpenAI", signupUrl: "https://platform.openai.com/api-keys"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
openai: { envKeys: ["OPENAI_API_KEY"], defaultModel: "gpt-4o", label: "OpenAI", signupUrl: "https://platform.openai.com/api-keys",
|
|
29
|
+
models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "o1", "o1-mini", "o3", "o3-mini", "o4-mini"] },
|
|
30
|
+
xai: { envKeys: ["XAI_API_KEY"], defaultModel: "grok-3-mini", label: "xAI (Grok)", signupUrl: "https://console.x.ai/",
|
|
31
|
+
models: ["grok-3", "grok-3-mini", "grok-3-fast", "grok-2", "grok-2-mini"] },
|
|
32
|
+
mistral: { envKeys: ["MISTRAL_API_KEY"], defaultModel: "mistral-large-latest", label: "Mistral", signupUrl: "https://console.mistral.ai/api-keys/",
|
|
33
|
+
models: ["mistral-large-latest", "mistral-medium-latest", "mistral-small-latest", "codestral-latest", "open-mistral-nemo"] },
|
|
34
|
+
together: { envKeys: ["TOGETHER_API_KEY"], defaultModel: "meta-llama/Llama-3.3-70B-Instruct-Turbo", label: "Together AI", signupUrl: "https://api.together.xyz/settings/api-keys",
|
|
35
|
+
models: ["meta-llama/Llama-3.3-70B-Instruct-Turbo", "meta-llama/Llama-3.1-8B-Instruct-Turbo", "mistralai/Mixtral-8x22B-Instruct-v0.1", "Qwen/Qwen2.5-72B-Instruct-Turbo", "deepseek-ai/deepseek-r1"] },
|
|
36
|
+
nvidia: { envKeys: ["NVIDIA_API_KEY"], defaultModel: "meta/llama-3.3-70b-instruct", label: "NVIDIA NIM", signupUrl: "https://build.nvidia.com/",
|
|
37
|
+
models: ["meta/llama-3.3-70b-instruct", "meta/llama-3.1-8b-instruct", "mistralai/mistral-7b-instruct-v0.3", "google/gemma-2-9b-it"] },
|
|
31
38
|
|
|
32
39
|
// ── Tier 1: Fast/free ───────────────────────────────────────────────────────
|
|
33
|
-
groq: { envKeys: ["GROQ_API_KEY"], defaultModel: "llama-3.3-70b-versatile", label: "Groq (fast inference)", signupUrl: "https://console.groq.com/keys"
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
groq: { envKeys: ["GROQ_API_KEY"], defaultModel: "llama-3.3-70b-versatile", label: "Groq (fast inference)", signupUrl: "https://console.groq.com/keys",
|
|
41
|
+
models: ["llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768", "gemma2-9b-it", "llama-3.3-70b-specdec"] },
|
|
42
|
+
deepseek: { envKeys: ["DEEPSEEK_API_KEY"], defaultModel: "deepseek-chat", label: "DeepSeek", signupUrl: "https://platform.deepseek.com/api_keys",
|
|
43
|
+
models: ["deepseek-chat", "deepseek-coder", "deepseek-reasoner"] },
|
|
44
|
+
chutes: { envKeys: ["CHUTES_API_KEY"], defaultModel: "deepseek-ai/DeepSeek-V3-0324", label: "Chutes", signupUrl: "https://chutes.ai/",
|
|
45
|
+
models: ["deepseek-ai/DeepSeek-V3-0324", "deepseek-ai/DeepSeek-R1", "Qwen/Qwen3-235B-A22B"] },
|
|
36
46
|
|
|
37
47
|
// ── Tier 1: Aggregators ─────────────────────────────────────────────────────
|
|
38
|
-
openrouter: { envKeys: ["OPENROUTER_API_KEY"], defaultModel: "anthropic/claude-sonnet-4-20250514", label: "OpenRouter (multi-model)", signupUrl: "https://openrouter.ai/keys"
|
|
39
|
-
|
|
48
|
+
openrouter: { envKeys: ["OPENROUTER_API_KEY"], defaultModel: "anthropic/claude-sonnet-4-20250514", label: "OpenRouter (multi-model)", signupUrl: "https://openrouter.ai/keys",
|
|
49
|
+
models: ["anthropic/claude-sonnet-4-20250514", "anthropic/claude-opus-4", "openai/gpt-4o", "openai/o3", "google/gemini-2.5-pro", "google/gemini-2.5-flash", "meta-llama/llama-3.3-70b-instruct", "deepseek/deepseek-r1"] },
|
|
50
|
+
vercelai: { envKeys: ["VERCEL_AI_TOKEN"], defaultModel: "anthropic/claude-3-5-sonnet", label: "Vercel AI Gateway", signupUrl: "https://vercel.com/docs/ai-gateway",
|
|
51
|
+
models: ["anthropic/claude-3-5-sonnet", "anthropic/claude-3-5-haiku", "openai/gpt-4o", "openai/gpt-4o-mini"] },
|
|
40
52
|
|
|
41
53
|
// ── Tier 2: Useful ──────────────────────────────────────────────────────────
|
|
42
|
-
kimi: { envKeys: ["MOONSHOT_API_KEY", "KIMI_API_KEY"], defaultModel: "moonshot-v1-8k", label: "Kimi/Moonshot", signupUrl: "https://platform.moonshot.cn/console/api-keys"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
kimi: { envKeys: ["MOONSHOT_API_KEY", "KIMI_API_KEY"], defaultModel: "moonshot-v1-8k", label: "Kimi/Moonshot", signupUrl: "https://platform.moonshot.cn/console/api-keys",
|
|
55
|
+
models: ["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"] },
|
|
56
|
+
minimax: { envKeys: ["MINIMAX_API_KEY"], defaultModel: "MiniMax-Text-01", label: "MiniMax", signupUrl: "https://www.minimaxi.com/",
|
|
57
|
+
models: ["MiniMax-Text-01", "abab6.5s-chat", "abab6.5g-chat"] },
|
|
58
|
+
venice: { envKeys: ["VENICE_API_KEY"], defaultModel: "llama-3.3-70b", label: "Venice AI", signupUrl: "https://venice.ai/chat/api",
|
|
59
|
+
models: ["llama-3.3-70b", "mistral-31-24b", "deepseek-r1-671b"] },
|
|
60
|
+
huggingface: { envKeys: ["HF_TOKEN", "HUGGINGFACE_API_KEY"], defaultModel: "meta-llama/Llama-3.3-70B-Instruct", label: "Hugging Face", signupUrl: "https://huggingface.co/settings/tokens", special: "hf_model_in_url",
|
|
61
|
+
models: ["meta-llama/Llama-3.3-70B-Instruct", "meta-llama/Llama-3.1-8B-Instruct", "mistralai/Mistral-7B-Instruct-v0.3", "Qwen/Qwen2.5-72B-Instruct"] },
|
|
62
|
+
cloudflare: { envKeys: ["CF_API_TOKEN"], defaultModel: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", label: "Cloudflare AI", signupUrl: "https://dash.cloudflare.com/", special: "cf_account_id",
|
|
63
|
+
models: ["@cf/meta/llama-3.3-70b-instruct-fp8-fast", "@cf/mistral/mistral-7b-instruct-v0.2", "@cf/google/gemma-7b-it", "@cf/qwen/qwen1.5-14b-chat-awq"] },
|
|
47
64
|
|
|
48
65
|
// ── Tier 3: Regional / niche ────────────────────────────────────────────────
|
|
49
|
-
volcengine: { envKeys: ["VOLCENGINE_API_KEY", "ARK_API_KEY"], defaultModel: "doubao-pro-32k", label: "Volcengine (ByteDance)", signupUrl: "https://console.volcengine.com/ark"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
volcengine: { envKeys: ["VOLCENGINE_API_KEY", "ARK_API_KEY"], defaultModel: "doubao-pro-32k", label: "Volcengine (ByteDance)", signupUrl: "https://console.volcengine.com/ark",
|
|
67
|
+
models: ["doubao-pro-32k", "doubao-pro-128k", "doubao-lite-32k"] },
|
|
68
|
+
byteplus: { envKeys: ["BYTEPLUS_API_KEY"], defaultModel: "doubao-pro-32k", label: "BytePlus", signupUrl: "https://console.byteplux.com/",
|
|
69
|
+
models: ["doubao-pro-32k", "doubao-lite-32k"] },
|
|
70
|
+
zai: { envKeys: ["ZAI_API_KEY", "GLM_API_KEY", "ZHIPU_API_KEY"], defaultModel: "glm-4-flash", label: "Z.AI / GLM", signupUrl: "https://open.bigmodel.cn/",
|
|
71
|
+
models: ["glm-4-flash", "glm-4-plus", "glm-4-long", "glm-z1-flash"] },
|
|
72
|
+
dashscope: { envKeys: ["DASHSCOPE_API_KEY"], defaultModel: "qwen-max", label: "DashScope (Alibaba)", signupUrl: "https://dashscope.aliyun.com/",
|
|
73
|
+
models: ["qwen-max", "qwen-plus", "qwen-turbo", "qwen-long", "qwen2.5-72b-instruct"] },
|
|
74
|
+
xiaomi: { envKeys: ["XIAOMI_API_KEY"], defaultModel: "MiMo-7B-RL", label: "Xiaomi AI", signupUrl: "https://ai.xiaomi.com/",
|
|
75
|
+
models: ["MiMo-7B-RL", "MiMo-7B-SFT"] },
|
|
54
76
|
|
|
55
77
|
// ── OAuth / subscription-based ────────────────────────────────────────────
|
|
56
|
-
"github-copilot": { envKeys: ["GITHUB_COPILOT_TOKEN"], defaultModel: "gpt-4o", label: "GitHub Copilot", signupUrl: "https://github.com/features/copilot", auth: "oauth"
|
|
78
|
+
"github-copilot": { envKeys: ["GITHUB_COPILOT_TOKEN"], defaultModel: "gpt-4o", label: "GitHub Copilot", signupUrl: "https://github.com/features/copilot", auth: "oauth",
|
|
79
|
+
models: ["gpt-4o", "gpt-4o-mini", "claude-sonnet-4-20250514", "o3-mini"] },
|
|
57
80
|
|
|
58
81
|
// ── Local / self-hosted ─────────────────────────────────────────────────────
|
|
59
|
-
ollama: { envKeys: [], defaultModel: "llama3.2", label: "Ollama (local)", signupUrl: null, local: true
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
82
|
+
ollama: { envKeys: [], defaultModel: "llama3.2", label: "Ollama (local)", signupUrl: null, local: true,
|
|
83
|
+
models: ["llama3.2", "llama3.1", "llama3.1:70b", "mistral", "mixtral", "deepseek-r1", "qwen2.5", "phi4", "gemma3"] },
|
|
84
|
+
vllm: { envKeys: [], defaultModel: "meta-llama/Llama-3.3-70B-Instruct", label: "vLLM (self-hosted)", signupUrl: "https://docs.vllm.ai/", local: true,
|
|
85
|
+
models: ["meta-llama/Llama-3.3-70B-Instruct", "meta-llama/Llama-3.1-8B-Instruct", "mistralai/Mistral-7B-Instruct-v0.3", "Qwen/Qwen2.5-72B-Instruct"] },
|
|
86
|
+
sglang: { envKeys: [], defaultModel: "meta-llama/Llama-3.3-70B-Instruct", label: "SGLang (self-hosted)", signupUrl: "https://docs.sglang.ai/", local: true,
|
|
87
|
+
models: ["meta-llama/Llama-3.3-70B-Instruct", "meta-llama/Llama-3.1-8B-Instruct", "deepseek-ai/DeepSeek-V3", "Qwen/Qwen2.5-72B-Instruct"] },
|
|
88
|
+
litellm: { envKeys: ["LITELLM_API_KEY"], defaultModel: "gpt-4o", label: "LiteLLM (proxy)", signupUrl: "https://docs.litellm.ai/", local: true,
|
|
89
|
+
models: ["gpt-4o", "claude-sonnet-4-20250514", "gemini/gemini-2.5-flash", "bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0"] },
|
|
63
90
|
};
|
|
64
91
|
|
|
65
92
|
async function tryKeychainKey(service) {
|
package/core/onboarding.mjs
CHANGED
|
@@ -569,10 +569,31 @@ export class OnboardingWizard {
|
|
|
569
569
|
const result = await validateApiKey(provKey, apiKey.trim());
|
|
570
570
|
|
|
571
571
|
if (result.ok) {
|
|
572
|
-
console.log(green(` ✅ Connected
|
|
572
|
+
console.log(green(` ✅ Connected!`));
|
|
573
|
+
|
|
574
|
+
// Model selection step
|
|
575
|
+
const provInfo = PROVIDERS[provKey];
|
|
576
|
+
const modelList = provInfo?.models ?? [getProviderDefaultModel(provKey)];
|
|
577
|
+
let chosenModel = result.model ?? getProviderDefaultModel(provKey);
|
|
578
|
+
if (modelList.length > 1) {
|
|
579
|
+
try {
|
|
580
|
+
chosenModel = await select({
|
|
581
|
+
message: ` Select model for ${info?.label ?? provKey}:`,
|
|
582
|
+
choices: modelList.map((m) => ({
|
|
583
|
+
name: m === getProviderDefaultModel(provKey) ? `${m} (default)` : m,
|
|
584
|
+
value: m,
|
|
585
|
+
})),
|
|
586
|
+
default: chosenModel,
|
|
587
|
+
});
|
|
588
|
+
} catch {
|
|
589
|
+
// keep default
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
console.log(dim(` Using model: ${chosenModel}`));
|
|
593
|
+
|
|
573
594
|
providers[provKey] = {
|
|
574
595
|
apiKey: apiKey.trim(),
|
|
575
|
-
model:
|
|
596
|
+
model: chosenModel,
|
|
576
597
|
};
|
|
577
598
|
validated = true;
|
|
578
599
|
} else {
|