wispy-cli 2.1.0 → 2.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/core/onboarding.mjs +489 -373
- package/package.json +5 -5
package/core/onboarding.mjs
CHANGED
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
* core/onboarding.mjs — Unified first-run wizard for Wispy
|
|
3
3
|
*
|
|
4
4
|
* Handles interactive setup from scratch:
|
|
5
|
-
* - AI
|
|
6
|
-
* -
|
|
5
|
+
* - AI providers (multi-select, checkbox)
|
|
6
|
+
* - API key collection + validation
|
|
7
|
+
* - Default provider selection
|
|
8
|
+
* - Messaging channels (multi-select)
|
|
7
9
|
* - Workstream
|
|
8
|
-
* -
|
|
10
|
+
* - About you (name, role, language)
|
|
9
11
|
* - Security level
|
|
10
12
|
* - Server / cloud mode
|
|
11
13
|
*
|
|
14
|
+
* Uses @inquirer/prompts for proper interactive TUI (space-to-select,
|
|
15
|
+
* arrow-key navigation, masked password input).
|
|
16
|
+
*
|
|
12
17
|
* Usage:
|
|
13
18
|
* import { OnboardingWizard } from './onboarding.mjs';
|
|
14
19
|
* const wizard = new OnboardingWizard();
|
|
@@ -18,9 +23,10 @@
|
|
|
18
23
|
|
|
19
24
|
import os from "node:os";
|
|
20
25
|
import path from "node:path";
|
|
21
|
-
import { createInterface } from "node:readline";
|
|
22
26
|
import { mkdir, writeFile, readFile } from "node:fs/promises";
|
|
23
27
|
|
|
28
|
+
import { confirm, checkbox, select, input, password } from "@inquirer/prompts";
|
|
29
|
+
|
|
24
30
|
import {
|
|
25
31
|
WISPY_DIR,
|
|
26
32
|
CONFIG_PATH,
|
|
@@ -31,116 +37,119 @@ import {
|
|
|
31
37
|
} from "./config.mjs";
|
|
32
38
|
|
|
33
39
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
34
|
-
// ANSI helpers (no deps)
|
|
40
|
+
// ANSI helpers (no extra deps)
|
|
35
41
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
36
42
|
|
|
37
|
-
const bold
|
|
38
|
-
const dim
|
|
39
|
-
const green
|
|
40
|
-
const cyan
|
|
41
|
-
const yellow= (s) => `\x1b[33m${s}\x1b[0m`;
|
|
42
|
-
const red
|
|
43
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
44
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
45
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
46
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
47
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
48
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
43
49
|
|
|
44
50
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
45
51
|
// Provider registry (display order for wizard)
|
|
46
52
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
47
53
|
|
|
48
54
|
const PROVIDER_LIST = [
|
|
49
|
-
{ key: "google", label: "Google AI (Gemini)
|
|
50
|
-
{ key: "anthropic", label: "Anthropic (Claude)",
|
|
51
|
-
{ key: "openai", label: "OpenAI (GPT-4o)",
|
|
52
|
-
{ key: "groq", label: "Groq
|
|
53
|
-
{ key: "deepseek", label: "DeepSeek
|
|
54
|
-
{ key: "ollama", label: "Ollama
|
|
55
|
-
{ key: "openrouter", label: "OpenRouter
|
|
56
|
-
{ key: null, label: "I'll set this up later", tag: "", signupUrl: null, defaultModel: null },
|
|
55
|
+
{ key: "google", label: "Google AI (Gemini) — free tier ⭐", defaultModel: "gemini-2.5-flash", signupUrl: "https://aistudio.google.com/apikey" },
|
|
56
|
+
{ key: "anthropic", label: "Anthropic (Claude) — best quality", defaultModel: "claude-sonnet-4-20250514", signupUrl: "https://console.anthropic.com/settings/keys" },
|
|
57
|
+
{ key: "openai", label: "OpenAI (GPT-4o)", defaultModel: "gpt-4o", signupUrl: "https://platform.openai.com/api-keys" },
|
|
58
|
+
{ key: "groq", label: "Groq — fastest, free", defaultModel: "llama-3.3-70b-versatile", signupUrl: "https://console.groq.com/keys" },
|
|
59
|
+
{ key: "deepseek", label: "DeepSeek — cheap & good", defaultModel: "deepseek-chat", signupUrl: "https://platform.deepseek.com/api_keys" },
|
|
60
|
+
{ key: "ollama", label: "Ollama — local, no key needed", defaultModel: "llama3.2", signupUrl: null },
|
|
61
|
+
{ key: "openrouter", label: "OpenRouter — access any model", defaultModel: "anthropic/claude-sonnet-4-20250514", signupUrl: "https://openrouter.ai/keys" },
|
|
57
62
|
];
|
|
58
63
|
|
|
64
|
+
function getProviderLabel(key) {
|
|
65
|
+
return PROVIDER_LIST.find((p) => p.key === key)?.label ?? key;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getProviderDefaultModel(key) {
|
|
69
|
+
return PROVIDER_LIST.find((p) => p.key === key)?.defaultModel ?? "unknown";
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
60
73
|
// Security level definitions
|
|
61
74
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
62
75
|
|
|
63
76
|
const SECURITY_LEVELS = {
|
|
64
|
-
careful: {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
permissions: { run_command: "approve", write_file: "approve", git: "approve" },
|
|
68
|
-
},
|
|
69
|
-
balanced: {
|
|
70
|
-
label: "Balanced",
|
|
71
|
-
desc: "ask for dangerous commands only",
|
|
72
|
-
permissions: { run_command: "approve", write_file: "notify", git: "approve" },
|
|
73
|
-
},
|
|
74
|
-
trust: {
|
|
75
|
-
label: "Trust me",
|
|
76
|
-
desc: "auto-approve everything (power users)",
|
|
77
|
-
permissions: { run_command: "auto", write_file: "auto", git: "auto" },
|
|
78
|
-
},
|
|
77
|
+
careful: { label: "Careful", permissions: { run_command: "approve", write_file: "approve", git: "approve" } },
|
|
78
|
+
balanced: { label: "Balanced", permissions: { run_command: "approve", write_file: "notify", git: "approve" } },
|
|
79
|
+
yolo: { label: "YOLO", permissions: { run_command: "auto", write_file: "auto", git: "auto" } },
|
|
79
80
|
};
|
|
80
81
|
|
|
81
82
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
82
|
-
//
|
|
83
|
-
// ──────────────────────────────────────────────────────────────────────────────
|
|
84
|
-
|
|
85
|
-
function makeRl() {
|
|
86
|
-
return createInterface({ input: process.stdin, output: process.stdout });
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function ask(rl, question) {
|
|
90
|
-
return new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim())));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function askWithDefault(rl, question, defaultVal) {
|
|
94
|
-
const answer = await ask(rl, question);
|
|
95
|
-
return answer === "" ? defaultVal : answer;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ──────────────────────────────────────────────────────────────────────────────
|
|
99
|
-
// API key validation (basic connectivity check)
|
|
83
|
+
// API key validation
|
|
100
84
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
101
85
|
|
|
102
86
|
async function validateApiKey(provider, key) {
|
|
103
87
|
if (provider === "ollama") return { ok: true, model: "llama3.2" };
|
|
104
88
|
try {
|
|
105
|
-
const info = PROVIDERS[provider];
|
|
106
|
-
if (!info) return { ok: false, error: "Unknown provider" };
|
|
107
|
-
|
|
108
89
|
if (provider === "google") {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
90
|
+
const r = await fetch(
|
|
91
|
+
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash?key=${key}`,
|
|
92
|
+
{ signal: AbortSignal.timeout(6000) }
|
|
93
|
+
);
|
|
94
|
+
return r.ok ? { ok: true, model: "gemini-2.5-flash" } : { ok: false, error: `HTTP ${r.status}` };
|
|
113
95
|
}
|
|
114
|
-
|
|
115
96
|
if (provider === "anthropic") {
|
|
116
97
|
const r = await fetch("https://api.anthropic.com/v1/models", {
|
|
117
98
|
headers: { "x-api-key": key, "anthropic-version": "2023-06-01" },
|
|
118
99
|
signal: AbortSignal.timeout(6000),
|
|
119
100
|
});
|
|
120
|
-
|
|
121
|
-
return { ok: false, error: `HTTP ${r.status}` };
|
|
101
|
+
return r.ok ? { ok: true, model: "claude-sonnet-4-20250514" } : { ok: false, error: `HTTP ${r.status}` };
|
|
122
102
|
}
|
|
123
|
-
|
|
124
103
|
if (provider === "openai") {
|
|
125
104
|
const r = await fetch("https://api.openai.com/v1/models", {
|
|
126
105
|
headers: { Authorization: `Bearer ${key}` },
|
|
127
106
|
signal: AbortSignal.timeout(6000),
|
|
128
107
|
});
|
|
129
|
-
|
|
130
|
-
return { ok: false, error: `HTTP ${r.status}` };
|
|
108
|
+
return r.ok ? { ok: true, model: "gpt-4o" } : { ok: false, error: `HTTP ${r.status}` };
|
|
131
109
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
110
|
+
if (provider === "groq") {
|
|
111
|
+
const r = await fetch("https://api.groq.com/openai/v1/models", {
|
|
112
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
113
|
+
signal: AbortSignal.timeout(6000),
|
|
114
|
+
});
|
|
115
|
+
return r.ok ? { ok: true, model: "llama-3.3-70b-versatile" } : { ok: false, error: `HTTP ${r.status}` };
|
|
116
|
+
}
|
|
117
|
+
if (provider === "deepseek") {
|
|
118
|
+
const r = await fetch("https://api.deepseek.com/models", {
|
|
119
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
120
|
+
signal: AbortSignal.timeout(6000),
|
|
121
|
+
});
|
|
122
|
+
return r.ok ? { ok: true, model: "deepseek-chat" } : { ok: false, error: `HTTP ${r.status}` };
|
|
123
|
+
}
|
|
124
|
+
if (provider === "openrouter") {
|
|
125
|
+
const r = await fetch("https://openrouter.ai/api/v1/models", {
|
|
126
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
127
|
+
signal: AbortSignal.timeout(6000),
|
|
128
|
+
});
|
|
129
|
+
return r.ok ? { ok: true, model: "anthropic/claude-sonnet-4-20250514" } : { ok: false, error: `HTTP ${r.status}` };
|
|
130
|
+
}
|
|
131
|
+
// unknown provider — trust key format
|
|
132
|
+
return { ok: true, model: getProviderDefaultModel(provider) };
|
|
135
133
|
} catch (err) {
|
|
136
134
|
return { ok: false, error: err.message };
|
|
137
135
|
}
|
|
138
136
|
}
|
|
139
137
|
|
|
140
138
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
141
|
-
//
|
|
139
|
+
// Channel helpers
|
|
142
140
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
143
141
|
|
|
142
|
+
async function validateTelegramToken(token) {
|
|
143
|
+
try {
|
|
144
|
+
const r = await fetch(`https://api.telegram.org/bot${token}/getMe`, { signal: AbortSignal.timeout(6000) });
|
|
145
|
+
if (r.ok) {
|
|
146
|
+
const data = await r.json();
|
|
147
|
+
return { ok: true, username: data.result?.username };
|
|
148
|
+
}
|
|
149
|
+
return { ok: false };
|
|
150
|
+
} catch { return { ok: false }; }
|
|
151
|
+
}
|
|
152
|
+
|
|
144
153
|
async function saveKeyToEnvFile(provider, key, filePath) {
|
|
145
154
|
const info = PROVIDERS[provider];
|
|
146
155
|
if (!info || !info.envKeys?.[0]) return;
|
|
@@ -148,7 +157,6 @@ async function saveKeyToEnvFile(provider, key, filePath) {
|
|
|
148
157
|
const line = `\nexport ${envVar}="${key}" # wispy-cli\n`;
|
|
149
158
|
try {
|
|
150
159
|
const existing = await readFile(filePath, "utf8").catch(() => "");
|
|
151
|
-
// Remove old entry if present
|
|
152
160
|
const filtered = existing
|
|
153
161
|
.split("\n")
|
|
154
162
|
.filter((l) => !l.includes(`${envVar}=`) || !l.includes("wispy-cli"))
|
|
@@ -160,18 +168,51 @@ async function saveKeyToEnvFile(provider, key, filePath) {
|
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
163
|
-
//
|
|
171
|
+
// Summary box printer
|
|
164
172
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
165
173
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
174
|
+
function printSummaryBox(config) {
|
|
175
|
+
const providerKeys = Object.keys(config.providers ?? {});
|
|
176
|
+
const defaultP = config.defaultProvider ?? providerKeys[0];
|
|
177
|
+
const defaultModel = config.providers?.[defaultP]?.model ?? getProviderDefaultModel(defaultP ?? "");
|
|
178
|
+
const defaultLabel = defaultP ? `${getProviderLabel(defaultP)} (${defaultModel})` : "none";
|
|
179
|
+
|
|
180
|
+
const providerNames = providerKeys.map((k) => getProviderLabel(k).split(" —")[0]).join(", ") || "none";
|
|
181
|
+
|
|
182
|
+
const channelKeys = Object.entries(config.channels ?? {})
|
|
183
|
+
.filter(([, v]) => v?.enabled)
|
|
184
|
+
.map(([k]) => k.charAt(0).toUpperCase() + k.slice(1));
|
|
185
|
+
const channelsStr = channelKeys.length > 0 ? channelKeys.map((c) => `${c} ✅`).join(", ") : "none";
|
|
186
|
+
|
|
187
|
+
const workstream = config.workstream ?? "default";
|
|
188
|
+
const security = SECURITY_LEVELS[config.security ?? "careful"]?.label ?? "Careful";
|
|
189
|
+
const lang = config.language ?? "auto";
|
|
190
|
+
|
|
191
|
+
const lines = [
|
|
192
|
+
` ✅ ${bold("Wispy is ready!")} `,
|
|
193
|
+
` `,
|
|
194
|
+
` Providers: ${providerNames}`,
|
|
195
|
+
` Default: ${defaultLabel}`,
|
|
196
|
+
` Channels: ${channelsStr}`,
|
|
197
|
+
` Workstream: ${workstream}`,
|
|
198
|
+
` Security: ${security} 🔒`,
|
|
199
|
+
` Language: ${lang}`,
|
|
200
|
+
` `,
|
|
201
|
+
` ${cyan("wispy")} — interactive chat `,
|
|
202
|
+
` ${cyan("wispy tui")} — workspace UI `,
|
|
203
|
+
` ${cyan("wispy --serve")} — start channels + cron `,
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
const w = 48;
|
|
207
|
+
const bar = "─".repeat(w);
|
|
208
|
+
console.log(`\n┌${bar}┐`);
|
|
209
|
+
for (const line of lines) {
|
|
210
|
+
// Strip ANSI for length calc
|
|
211
|
+
const plain = line.replace(/\x1b\[[0-9;]*m/g, "");
|
|
212
|
+
const pad = Math.max(0, w - plain.length);
|
|
213
|
+
console.log(`│${line}${" ".repeat(pad)}│`);
|
|
214
|
+
}
|
|
215
|
+
console.log(`└${bar}┘\n`);
|
|
175
216
|
}
|
|
176
217
|
|
|
177
218
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -185,289 +226,392 @@ export class OnboardingWizard {
|
|
|
185
226
|
|
|
186
227
|
// ── Step 1: Welcome ────────────────────────────────────────────────────────
|
|
187
228
|
|
|
188
|
-
async stepWelcome(
|
|
229
|
+
async stepWelcome() {
|
|
189
230
|
console.log("");
|
|
190
231
|
console.log(`🌿 ${bold("Welcome to Wispy")} — your personal AI assistant`);
|
|
232
|
+
console.log(dim(" This takes about 2 minutes. Press Ctrl+C anytime to skip.\n"));
|
|
233
|
+
|
|
234
|
+
let ready;
|
|
235
|
+
try {
|
|
236
|
+
ready = await confirm({
|
|
237
|
+
message: "Ready to set up?",
|
|
238
|
+
default: true,
|
|
239
|
+
});
|
|
240
|
+
} catch { ready = false; }
|
|
241
|
+
|
|
242
|
+
if (!ready) {
|
|
243
|
+
console.log(dim("\n Setup skipped. Run 'wispy setup' anytime.\n"));
|
|
244
|
+
process.exit(0);
|
|
245
|
+
}
|
|
191
246
|
console.log("");
|
|
192
|
-
console.log("Let's get you set up. This takes about 2 minutes.");
|
|
193
|
-
console.log(dim("Press Enter to start, or Ctrl+C to skip (you can run 'wispy setup' later)."));
|
|
194
|
-
console.log("");
|
|
195
|
-
await ask(rl, "");
|
|
196
247
|
}
|
|
197
248
|
|
|
198
|
-
// ── Step 2: AI
|
|
249
|
+
// ── Step 2: AI Providers (multi-select) ────────────────────────────────────
|
|
199
250
|
|
|
200
|
-
async stepProvider(
|
|
201
|
-
console.log(`🤖 ${bold("
|
|
202
|
-
PROVIDER_LIST.forEach((p, i) => {
|
|
203
|
-
const tag = p.tag ? dim(` — ${p.tag}`) : "";
|
|
204
|
-
console.log(` ${i + 1}. ${p.label}${tag}`);
|
|
205
|
-
});
|
|
206
|
-
console.log("");
|
|
251
|
+
async stepProvider() {
|
|
252
|
+
console.log(`🤖 ${bold("AI Providers")}\n`);
|
|
207
253
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
254
|
+
let selectedKeys = [];
|
|
255
|
+
try {
|
|
256
|
+
selectedKeys = await checkbox({
|
|
257
|
+
message: "Which AI providers do you want to use? (space to select, enter to confirm)",
|
|
258
|
+
choices: PROVIDER_LIST.map((p, i) => ({
|
|
259
|
+
name: p.label,
|
|
260
|
+
value: p.key,
|
|
261
|
+
checked: i === 0, // Google pre-checked
|
|
262
|
+
})),
|
|
263
|
+
validate: (val) => val.length > 0 || "Select at least one provider (or press Ctrl+C to skip)",
|
|
264
|
+
});
|
|
265
|
+
} catch {
|
|
266
|
+
console.log(dim("\n Skipped. Run 'wispy setup provider' later.\n"));
|
|
267
|
+
return {};
|
|
268
|
+
}
|
|
211
269
|
|
|
212
|
-
if (!
|
|
213
|
-
console.log(dim("\n
|
|
270
|
+
if (!selectedKeys || selectedKeys.length === 0) {
|
|
271
|
+
console.log(dim("\n No providers selected. Run 'wispy setup provider' later.\n"));
|
|
214
272
|
return {};
|
|
215
273
|
}
|
|
216
274
|
|
|
217
|
-
|
|
275
|
+
// Collect API keys for each selected provider
|
|
276
|
+
const providers = {};
|
|
277
|
+
for (const provKey of selectedKeys) {
|
|
278
|
+
if (provKey === "ollama") {
|
|
279
|
+
process.stdout.write(`\n ${dim("Checking Ollama at http://localhost:11434...")}`);
|
|
280
|
+
try {
|
|
281
|
+
const r = await fetch("http://localhost:11434/api/tags", { signal: AbortSignal.timeout(3000) });
|
|
282
|
+
if (r.ok) {
|
|
283
|
+
process.stdout.write(green(" ✅\n"));
|
|
284
|
+
providers.ollama = { model: "llama3.2" };
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
} catch {}
|
|
288
|
+
process.stdout.write(yellow(" ⚠️ not running — saved anyway\n"));
|
|
289
|
+
providers.ollama = { model: "llama3.2" };
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const info = PROVIDER_LIST.find((p) => p.key === provKey);
|
|
294
|
+
console.log("");
|
|
295
|
+
if (info?.signupUrl) {
|
|
296
|
+
console.log(dim(` Get a key at: ${info.signupUrl}`));
|
|
297
|
+
}
|
|
218
298
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
299
|
+
let validated = false;
|
|
300
|
+
let attempts = 0;
|
|
301
|
+
let apiKey = null;
|
|
302
|
+
|
|
303
|
+
while (!validated && attempts < 3) {
|
|
304
|
+
try {
|
|
305
|
+
apiKey = await password({
|
|
306
|
+
message: ` 🔑 ${info?.label ?? provKey} API key:`,
|
|
307
|
+
mask: "*",
|
|
308
|
+
});
|
|
309
|
+
} catch {
|
|
310
|
+
console.log(dim(`\n Skipping ${provKey}.\n`));
|
|
311
|
+
break;
|
|
226
312
|
}
|
|
227
|
-
} catch {}
|
|
228
|
-
console.log(yellow("\n ⚠️ Ollama not running. Start it with: ollama serve\n"));
|
|
229
|
-
return { provider: "ollama", model: "llama3.2" };
|
|
230
|
-
}
|
|
231
313
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
console.log("");
|
|
314
|
+
if (!apiKey || apiKey.trim() === "") {
|
|
315
|
+
console.log(dim(` Skipping ${provKey}.`));
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
238
318
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
319
|
+
attempts++;
|
|
320
|
+
process.stdout.write(" Validating...");
|
|
321
|
+
const result = await validateApiKey(provKey, apiKey.trim());
|
|
322
|
+
|
|
323
|
+
if (result.ok) {
|
|
324
|
+
console.log(green(` ✅ Connected! (${result.model ?? getProviderDefaultModel(provKey)})`));
|
|
325
|
+
providers[provKey] = {
|
|
326
|
+
apiKey: apiKey.trim(),
|
|
327
|
+
model: result.model ?? getProviderDefaultModel(provKey),
|
|
328
|
+
};
|
|
329
|
+
validated = true;
|
|
330
|
+
} else {
|
|
331
|
+
console.log(red(` ✗ ${result.error ?? "Invalid key"}`));
|
|
332
|
+
if (attempts < 3) console.log(dim(" Try again, or press Enter to skip."));
|
|
333
|
+
else {
|
|
334
|
+
// save anyway with warning
|
|
335
|
+
console.log(yellow(" Saving key anyway — check it later."));
|
|
336
|
+
providers[provKey] = {
|
|
337
|
+
apiKey: apiKey.trim(),
|
|
338
|
+
model: getProviderDefaultModel(provKey),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
257
342
|
}
|
|
258
343
|
}
|
|
259
344
|
|
|
260
|
-
|
|
261
|
-
|
|
345
|
+
const providerKeys = Object.keys(providers);
|
|
346
|
+
if (providerKeys.length === 0) {
|
|
262
347
|
return {};
|
|
263
348
|
}
|
|
264
349
|
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
350
|
+
// Step 3: Default provider (if multiple)
|
|
351
|
+
let defaultProvider = providerKeys[0];
|
|
352
|
+
if (providerKeys.length > 1) {
|
|
353
|
+
console.log("");
|
|
354
|
+
try {
|
|
355
|
+
defaultProvider = await select({
|
|
356
|
+
message: "⭐ Which provider should be the default?",
|
|
357
|
+
choices: providerKeys.map((k) => ({
|
|
358
|
+
name: getProviderLabel(k).split(" —")[0],
|
|
359
|
+
value: k,
|
|
360
|
+
})),
|
|
361
|
+
});
|
|
362
|
+
} catch {
|
|
363
|
+
defaultProvider = providerKeys[0];
|
|
364
|
+
}
|
|
277
365
|
}
|
|
278
366
|
|
|
279
|
-
const storeInConfig = saveChoice !== "2"; // save in config unless env-only
|
|
280
|
-
|
|
281
|
-
const model = selected.defaultModel;
|
|
282
367
|
console.log("");
|
|
283
|
-
|
|
284
|
-
return {
|
|
285
|
-
provider: selected.key,
|
|
286
|
-
model,
|
|
287
|
-
...(storeInConfig ? { apiKey } : {}),
|
|
288
|
-
};
|
|
368
|
+
return { providers, defaultProvider };
|
|
289
369
|
}
|
|
290
370
|
|
|
291
|
-
// ── Step
|
|
292
|
-
|
|
293
|
-
async stepChannels(rl) {
|
|
294
|
-
console.log(`📱 ${bold("Want to connect messaging channels?")} ${dim("(you can skip and add later)")}\n`);
|
|
295
|
-
console.log(" 1. Telegram bot — chat with your AI on Telegram");
|
|
296
|
-
console.log(" 2. Discord bot — add AI to your Discord server");
|
|
297
|
-
console.log(" 3. Slack bot — integrate with your workspace");
|
|
298
|
-
console.log(" 4. Skip for now");
|
|
299
|
-
console.log("");
|
|
371
|
+
// ── Step 4: Channels (multi-select) ───────────────────────────────────────
|
|
300
372
|
|
|
301
|
-
|
|
302
|
-
|
|
373
|
+
async stepChannels() {
|
|
374
|
+
console.log(`📱 ${bold("Messaging Channels")}\n`);
|
|
303
375
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
376
|
+
let selectedChannels = [];
|
|
377
|
+
try {
|
|
378
|
+
selectedChannels = await checkbox({
|
|
379
|
+
message: "Connect messaging channels? (space to select, enter to skip)",
|
|
380
|
+
choices: [
|
|
381
|
+
{ name: "Telegram", value: "telegram" },
|
|
382
|
+
{ name: "Discord", value: "discord" },
|
|
383
|
+
{ name: "Slack", value: "slack" },
|
|
384
|
+
{ name: "WhatsApp", value: "whatsapp" },
|
|
385
|
+
{ name: "Signal", value: "signal" },
|
|
386
|
+
{ name: "Email", value: "email" },
|
|
387
|
+
],
|
|
388
|
+
});
|
|
389
|
+
} catch {
|
|
390
|
+
console.log(dim(" Channels skipped.\n"));
|
|
391
|
+
return { channels: {} };
|
|
313
392
|
}
|
|
314
393
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
console.log(" 2. Send /newbot and follow prompts");
|
|
319
|
-
console.log(" 3. Paste the bot token here:");
|
|
394
|
+
const channels = {};
|
|
395
|
+
|
|
396
|
+
for (const ch of selectedChannels) {
|
|
320
397
|
console.log("");
|
|
398
|
+
if (ch === "telegram") {
|
|
399
|
+
console.log(` 🤖 ${bold("Telegram")} — create a bot at @BotFather → /newbot`);
|
|
400
|
+
try {
|
|
401
|
+
const token = await input({ message: " Bot token (or Enter to skip):", default: "" });
|
|
402
|
+
if (token) {
|
|
403
|
+
process.stdout.write(" Checking...");
|
|
404
|
+
const result = await validateTelegramToken(token);
|
|
405
|
+
if (result.ok) {
|
|
406
|
+
console.log(green(` ✅ @${result.username}`));
|
|
407
|
+
} else {
|
|
408
|
+
console.log(yellow(" ⚠️ Could not verify — saved anyway"));
|
|
409
|
+
}
|
|
410
|
+
channels.telegram = { enabled: true, token };
|
|
411
|
+
}
|
|
412
|
+
} catch { /* skipped */ }
|
|
413
|
+
}
|
|
321
414
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
channels.telegram = { enabled: true, token };
|
|
332
|
-
}
|
|
415
|
+
if (ch === "discord") {
|
|
416
|
+
console.log(` 🤖 ${bold("Discord")} — https://discord.com/developers/applications → Bot → Token`);
|
|
417
|
+
try {
|
|
418
|
+
const token = await password({ message: " Bot token (or Enter to skip):", mask: "*" });
|
|
419
|
+
if (token) {
|
|
420
|
+
channels.discord = { enabled: true, token };
|
|
421
|
+
console.log(green(" ✅ Token saved"));
|
|
422
|
+
}
|
|
423
|
+
} catch { /* skipped */ }
|
|
333
424
|
}
|
|
334
|
-
}
|
|
335
425
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
426
|
+
if (ch === "slack") {
|
|
427
|
+
console.log(` 🤖 ${bold("Slack")} — https://api.slack.com/apps → Bot Token (xoxb-...)`);
|
|
428
|
+
try {
|
|
429
|
+
const token = await password({ message: " Bot token (xoxb-..., or Enter to skip):", mask: "*" });
|
|
430
|
+
if (token) {
|
|
431
|
+
const signingSecret = await password({ message: " Signing secret (or Enter to skip):", mask: "*" });
|
|
432
|
+
channels.slack = { enabled: true, token, ...(signingSecret ? { signingSecret } : {}) };
|
|
433
|
+
console.log(green(" ✅ Credentials saved"));
|
|
434
|
+
}
|
|
435
|
+
} catch { /* skipped */ }
|
|
436
|
+
}
|
|
342
437
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
channels.
|
|
346
|
-
console.log(green(" ✅ Token saved"));
|
|
438
|
+
if (ch === "whatsapp") {
|
|
439
|
+
console.log(dim(` 📱 WhatsApp requires whatsapp-web.js. Run 'wispy setup channels' to configure.`));
|
|
440
|
+
channels.whatsapp = { enabled: false, _pending: true };
|
|
347
441
|
}
|
|
348
|
-
}
|
|
349
442
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
console.log(" 3. Install to workspace → copy Bot User OAuth Token");
|
|
355
|
-
console.log("");
|
|
443
|
+
if (ch === "signal") {
|
|
444
|
+
console.log(dim(` 📡 Signal support is experimental. Run 'wispy setup channels' to configure.`));
|
|
445
|
+
channels.signal = { enabled: false, _pending: true };
|
|
446
|
+
}
|
|
356
447
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
448
|
+
if (ch === "email") {
|
|
449
|
+
console.log(` 📧 ${bold("Email")} — IMAP/SMTP`);
|
|
450
|
+
try {
|
|
451
|
+
const emailAddr = await input({ message: " Email address (or Enter to skip):", default: "" });
|
|
452
|
+
if (emailAddr) {
|
|
453
|
+
const emailPass = await password({ message: " App password:", mask: "*" });
|
|
454
|
+
channels.email = {
|
|
455
|
+
enabled: true,
|
|
456
|
+
address: emailAddr,
|
|
457
|
+
password: emailPass,
|
|
458
|
+
};
|
|
459
|
+
console.log(green(" ✅ Email saved (run 'wispy setup channels' for IMAP/SMTP settings)"));
|
|
460
|
+
}
|
|
461
|
+
} catch { /* skipped */ }
|
|
362
462
|
}
|
|
363
463
|
}
|
|
364
464
|
|
|
465
|
+
if (Object.keys(channels).length === 0) {
|
|
466
|
+
console.log(dim(" No channels configured. Run 'wispy setup channels' later.\n"));
|
|
467
|
+
}
|
|
365
468
|
console.log("");
|
|
366
469
|
return { channels };
|
|
367
470
|
}
|
|
368
471
|
|
|
369
|
-
// ── Step
|
|
472
|
+
// ── Step 5: Workstream ─────────────────────────────────────────────────────
|
|
370
473
|
|
|
371
|
-
async stepWorkstream(
|
|
372
|
-
console.log(`📂 ${bold("
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
474
|
+
async stepWorkstream() {
|
|
475
|
+
console.log(`📂 ${bold("Workstream")}\n`);
|
|
476
|
+
let create = false;
|
|
477
|
+
try {
|
|
478
|
+
create = await confirm({
|
|
479
|
+
message: "Create your first workstream?",
|
|
480
|
+
default: false,
|
|
481
|
+
});
|
|
482
|
+
} catch { /* skipped */ }
|
|
379
483
|
|
|
380
|
-
if (
|
|
484
|
+
if (!create) {
|
|
381
485
|
console.log(dim(" Using default workstream.\n"));
|
|
382
486
|
return { workstream: "default" };
|
|
383
487
|
}
|
|
384
488
|
|
|
385
|
-
|
|
386
|
-
|
|
489
|
+
let wsName = "default";
|
|
490
|
+
try {
|
|
491
|
+
wsName = await input({
|
|
492
|
+
message: "Workstream name:",
|
|
493
|
+
default: "default",
|
|
494
|
+
});
|
|
495
|
+
wsName = (wsName || "default").replace(/[^a-z0-9_-]/gi, "-").toLowerCase() || "default";
|
|
496
|
+
} catch { wsName = "default"; }
|
|
387
497
|
|
|
388
|
-
console.log(green(` ✅ Workstream "${
|
|
389
|
-
return { workstream:
|
|
498
|
+
console.log(green(` ✅ Workstream "${wsName}" created.\n`));
|
|
499
|
+
return { workstream: wsName };
|
|
390
500
|
}
|
|
391
501
|
|
|
392
|
-
// ── Step
|
|
502
|
+
// ── Step 6: About you ─────────────────────────────────────────────────────
|
|
503
|
+
|
|
504
|
+
async stepMemory() {
|
|
505
|
+
console.log(`👤 ${bold("About You")}\n`);
|
|
393
506
|
|
|
394
|
-
|
|
395
|
-
|
|
507
|
+
let userName = "";
|
|
508
|
+
let userRole = "";
|
|
509
|
+
let language = "auto";
|
|
396
510
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
511
|
+
try {
|
|
512
|
+
userName = await input({ message: "Your name (optional):", default: "" });
|
|
513
|
+
} catch { /* skipped */ }
|
|
514
|
+
|
|
515
|
+
try {
|
|
516
|
+
userRole = await input({ message: "Your role (developer, student, designer...):", default: "" });
|
|
517
|
+
} catch { /* skipped */ }
|
|
400
518
|
|
|
401
|
-
|
|
519
|
+
try {
|
|
520
|
+
language = await select({
|
|
521
|
+
message: "🌐 Preferred language:",
|
|
522
|
+
choices: [
|
|
523
|
+
{ name: "Auto-detect", value: "auto" },
|
|
524
|
+
{ name: "English", value: "en" },
|
|
525
|
+
{ name: "한국어", value: "ko" },
|
|
526
|
+
{ name: "日本語", value: "ja" },
|
|
527
|
+
{ name: "中文", value: "zh" },
|
|
528
|
+
],
|
|
529
|
+
default: "auto",
|
|
530
|
+
});
|
|
531
|
+
} catch { language = "auto"; }
|
|
532
|
+
|
|
533
|
+
// Write memory/user.md
|
|
402
534
|
const lines = ["# About Me", ""];
|
|
403
|
-
if (
|
|
404
|
-
if (
|
|
405
|
-
if (
|
|
535
|
+
if (userName) lines.push(`**Name:** ${userName}`, "");
|
|
536
|
+
if (userRole) lines.push(`**Role:** ${userRole}`, "");
|
|
537
|
+
if (language && language !== "auto") lines.push(`**Language:** ${language}`, "");
|
|
406
538
|
lines.push(`_Generated by wispy setup on ${new Date().toISOString().slice(0, 10)}_`, "");
|
|
407
539
|
|
|
408
540
|
try {
|
|
409
541
|
await mkdir(path.join(WISPY_DIR, "memory"), { recursive: true });
|
|
410
542
|
await writeFile(path.join(WISPY_DIR, "memory", "user.md"), lines.join("\n"), "utf8");
|
|
411
|
-
console.log(green(`\n Saved to ~/.wispy/memory/user.md
|
|
543
|
+
console.log(green(`\n ✅ Saved to ~/.wispy/memory/user.md\n`));
|
|
412
544
|
} catch (err) {
|
|
413
545
|
console.log(yellow(`\n Could not save memory file: ${err.message}\n`));
|
|
414
546
|
}
|
|
415
547
|
|
|
416
|
-
return { _memoryName:
|
|
548
|
+
return { language, _memoryName: userName, _memoryRole: userRole };
|
|
417
549
|
}
|
|
418
550
|
|
|
419
|
-
// ── Step
|
|
551
|
+
// ── Step 7: Security ───────────────────────────────────────────────────────
|
|
420
552
|
|
|
421
|
-
async stepSecurity(
|
|
422
|
-
console.log(`🔒 ${bold("Security
|
|
423
|
-
console.log(` 1. Careful — ${dim("ask before running commands or modifying files (recommended)")}`);
|
|
424
|
-
console.log(` 2. Balanced — ${dim("ask for dangerous commands only")}`);
|
|
425
|
-
console.log(` 3. Trust me — ${dim("auto-approve everything (power users)")}`);
|
|
426
|
-
console.log("");
|
|
553
|
+
async stepSecurity() {
|
|
554
|
+
console.log(`🔒 ${bold("Security Level")}\n`);
|
|
427
555
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
556
|
+
let security = "careful";
|
|
557
|
+
try {
|
|
558
|
+
security = await select({
|
|
559
|
+
message: "Choose your security level:",
|
|
560
|
+
choices: [
|
|
561
|
+
{ name: "Careful — ask before commands/file changes (recommended)", value: "careful" },
|
|
562
|
+
{ name: "Balanced — ask for dangerous operations only", value: "balanced" },
|
|
563
|
+
{ name: "YOLO — auto-approve everything", value: "yolo" },
|
|
564
|
+
],
|
|
565
|
+
default: "careful",
|
|
566
|
+
});
|
|
567
|
+
} catch { security = "careful"; }
|
|
432
568
|
|
|
569
|
+
const levelInfo = SECURITY_LEVELS[security];
|
|
433
570
|
console.log(green(`\n ✅ Security: ${levelInfo.label}\n`));
|
|
434
571
|
return { security, permissions: levelInfo.permissions };
|
|
435
572
|
}
|
|
436
573
|
|
|
437
|
-
// ── Step
|
|
574
|
+
// ── Step 8: Server / Cloud ─────────────────────────────────────────────────
|
|
438
575
|
|
|
439
|
-
async stepServer(
|
|
440
|
-
console.log(`☁️ ${bold("
|
|
441
|
-
console.log(" 1. Yes, set up server mode");
|
|
442
|
-
console.log(" 2. Generate deployment files (Docker/Railway/Fly)");
|
|
443
|
-
console.log(" 3. Connect to existing Wispy server");
|
|
444
|
-
console.log(" 4. Skip for now");
|
|
445
|
-
console.log("");
|
|
576
|
+
async stepServer() {
|
|
577
|
+
console.log(`☁️ ${bold("Server / Cloud Mode")}\n`);
|
|
446
578
|
|
|
447
|
-
|
|
579
|
+
let mode = "skip";
|
|
580
|
+
try {
|
|
581
|
+
mode = await select({
|
|
582
|
+
message: "How do you want to run Wispy?",
|
|
583
|
+
choices: [
|
|
584
|
+
{ name: "Skip for now", value: "skip" },
|
|
585
|
+
{ name: "Start server mode (access from anywhere)", value: "server" },
|
|
586
|
+
{ name: "Generate deployment files (Docker/Railway)", value: "deploy" },
|
|
587
|
+
{ name: "Connect to existing Wispy server", value: "connect" },
|
|
588
|
+
],
|
|
589
|
+
default: "skip",
|
|
590
|
+
});
|
|
591
|
+
} catch { mode = "skip"; }
|
|
448
592
|
|
|
449
|
-
if (
|
|
593
|
+
if (mode === "skip") {
|
|
450
594
|
console.log(dim(" Skipped. Run 'wispy deploy init' or 'wispy server' later.\n"));
|
|
451
595
|
return { server: { enabled: false } };
|
|
452
596
|
}
|
|
453
597
|
|
|
454
|
-
if (
|
|
455
|
-
|
|
598
|
+
if (mode === "server") {
|
|
599
|
+
let portStr = "18790";
|
|
600
|
+
let host = "0.0.0.0";
|
|
601
|
+
try {
|
|
602
|
+
portStr = await input({ message: "Port:", default: "18790" });
|
|
603
|
+
host = await input({ message: "Host:", default: "0.0.0.0" });
|
|
604
|
+
} catch { /* defaults */ }
|
|
456
605
|
const port = parseInt(portStr) || 18790;
|
|
457
|
-
const host = await askWithDefault(rl, " Host [0.0.0.0]: ", "0.0.0.0");
|
|
458
|
-
|
|
459
|
-
// Generate token
|
|
460
606
|
const { randomBytes } = await import("node:crypto");
|
|
461
607
|
const token = randomBytes(24).toString("hex");
|
|
462
|
-
|
|
463
608
|
console.log(green(`\n ✅ Server mode: ${host}:${port}`));
|
|
464
609
|
console.log(dim(` Token: ${token}`));
|
|
465
610
|
console.log(dim(" Start with: wispy server\n"));
|
|
466
|
-
|
|
467
611
|
return { server: { enabled: true, port, host, token } };
|
|
468
612
|
}
|
|
469
613
|
|
|
470
|
-
if (
|
|
614
|
+
if (mode === "deploy") {
|
|
471
615
|
console.log(dim("\n Running deploy init...\n"));
|
|
472
616
|
try {
|
|
473
617
|
const { DeployManager } = await import("./deploy.mjs");
|
|
@@ -481,14 +625,17 @@ export class OnboardingWizard {
|
|
|
481
625
|
return { server: { enabled: false } };
|
|
482
626
|
}
|
|
483
627
|
|
|
484
|
-
if (
|
|
485
|
-
|
|
486
|
-
|
|
628
|
+
if (mode === "connect") {
|
|
629
|
+
let url = "";
|
|
630
|
+
let token = "";
|
|
631
|
+
try {
|
|
632
|
+
url = await input({ message: "Wispy server URL (e.g. https://my.server:18790):", default: "" });
|
|
633
|
+
token = await password({ message: "Token (optional):", mask: "*" });
|
|
634
|
+
} catch { /* skipped */ }
|
|
487
635
|
if (url) {
|
|
488
636
|
try {
|
|
489
|
-
|
|
490
|
-
await
|
|
491
|
-
await wfNode(
|
|
637
|
+
await mkdir(WISPY_DIR, { recursive: true });
|
|
638
|
+
await writeFile(
|
|
492
639
|
path.join(WISPY_DIR, "remote.json"),
|
|
493
640
|
JSON.stringify({ url: url.replace(/\/$/, ""), token, connectedAt: new Date().toISOString() }, null, 2),
|
|
494
641
|
"utf8"
|
|
@@ -504,72 +651,33 @@ export class OnboardingWizard {
|
|
|
504
651
|
return { server: { enabled: false } };
|
|
505
652
|
}
|
|
506
653
|
|
|
507
|
-
// ── Step 8: Summary ────────────────────────────────────────────────────────
|
|
508
|
-
|
|
509
|
-
printSummary(config) {
|
|
510
|
-
const providerKey = config.provider;
|
|
511
|
-
const providerInfo = PROVIDER_LIST.find((p) => p.key === providerKey);
|
|
512
|
-
const providerLabel = providerInfo ? providerInfo.label : providerKey ?? dim("none");
|
|
513
|
-
|
|
514
|
-
const model = config.model ?? dim("not set");
|
|
515
|
-
const workstream = config.workstream ?? "default";
|
|
516
|
-
const security = SECURITY_LEVELS[config.security ?? "careful"]?.label ?? "Careful";
|
|
517
|
-
|
|
518
|
-
const tg = config.channels?.telegram?.enabled
|
|
519
|
-
? green(`Telegram (@${config.channels.telegram.token?.slice(0, 4) ?? "..."}...)`)
|
|
520
|
-
: null;
|
|
521
|
-
const dc = config.channels?.discord?.enabled ? green("Discord") : null;
|
|
522
|
-
const sl = config.channels?.slack?.enabled ? green("Slack") : null;
|
|
523
|
-
const channelsStr = [tg, dc, sl].filter(Boolean).join(", ") || dim("none");
|
|
524
|
-
|
|
525
|
-
console.log(`✅ ${bold("Wispy is ready!")}\n`);
|
|
526
|
-
console.log(` Provider: ${cyan(providerLabel)} ${dim(`(${model})`)}`);
|
|
527
|
-
console.log(` Channels: ${channelsStr}`);
|
|
528
|
-
console.log(` Workstream: ${workstream}`);
|
|
529
|
-
console.log(` Security: ${security}`);
|
|
530
|
-
console.log(` Memory: ${dim("user.md saved")}`);
|
|
531
|
-
console.log(` Config: ${dim("~/.wispy/config.json")}`);
|
|
532
|
-
console.log("");
|
|
533
|
-
console.log("Getting started:");
|
|
534
|
-
console.log(` ${cyan("wispy")} Interactive chat`);
|
|
535
|
-
console.log(` ${cyan("wispy --tui")} Full terminal UI`);
|
|
536
|
-
console.log(` ${cyan("wispy --serve")} Start channels + cron`);
|
|
537
|
-
console.log(` ${cyan('wispy "hello"')} Quick one-shot`);
|
|
538
|
-
console.log("");
|
|
539
|
-
console.log("Happy building! 🌿");
|
|
540
|
-
console.log("");
|
|
541
|
-
}
|
|
542
|
-
|
|
543
654
|
// ── Public: run full wizard ────────────────────────────────────────────────
|
|
544
655
|
|
|
545
656
|
async run() {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
// Trap Ctrl+C gracefully
|
|
549
|
-
rl.on("SIGINT", () => {
|
|
657
|
+
// Graceful Ctrl+C handler
|
|
658
|
+
process.on("SIGINT", () => {
|
|
550
659
|
console.log(dim("\n\n Setup skipped. Run 'wispy setup' anytime.\n"));
|
|
551
660
|
process.exit(0);
|
|
552
661
|
});
|
|
553
662
|
|
|
554
663
|
try {
|
|
555
|
-
await this.stepWelcome(
|
|
556
|
-
|
|
557
|
-
const providerResult = await this.stepProvider(rl);
|
|
558
|
-
const channelsResult = await this.stepChannels(rl);
|
|
559
|
-
const workstreamResult = await this.stepWorkstream(rl);
|
|
560
|
-
const memoryResult = await this.stepMemory(rl);
|
|
561
|
-
const securityResult = await this.stepSecurity(rl);
|
|
562
|
-
const serverResult = await this.stepServer(rl);
|
|
664
|
+
await this.stepWelcome();
|
|
563
665
|
|
|
564
|
-
|
|
666
|
+
const providerResult = await this.stepProvider();
|
|
667
|
+
const channelsResult = await this.stepChannels();
|
|
668
|
+
const workstreamResult = await this.stepWorkstream();
|
|
669
|
+
const memoryResult = await this.stepMemory();
|
|
670
|
+
const securityResult = await this.stepSecurity();
|
|
671
|
+
const serverResult = await this.stepServer();
|
|
565
672
|
|
|
566
|
-
// Merge everything
|
|
673
|
+
// Merge everything
|
|
567
674
|
const existingConfig = await loadConfig();
|
|
568
675
|
const config = {
|
|
569
676
|
...existingConfig,
|
|
570
677
|
...providerResult,
|
|
571
678
|
...channelsResult,
|
|
572
679
|
...workstreamResult,
|
|
680
|
+
...memoryResult,
|
|
573
681
|
...securityResult,
|
|
574
682
|
...serverResult,
|
|
575
683
|
onboarded: true,
|
|
@@ -581,12 +689,15 @@ export class OnboardingWizard {
|
|
|
581
689
|
delete config._memoryRole;
|
|
582
690
|
|
|
583
691
|
await saveConfig(config);
|
|
584
|
-
|
|
585
|
-
this.printSummary(config);
|
|
692
|
+
printSummaryBox(config);
|
|
586
693
|
|
|
587
694
|
return config;
|
|
588
695
|
} catch (err) {
|
|
589
|
-
|
|
696
|
+
// ExitPromptError = user pressed Ctrl+C inside a prompt
|
|
697
|
+
if (err?.name === "ExitPromptError" || err?.code === "ERR_USE_AFTER_CLOSE") {
|
|
698
|
+
console.log(dim("\n\n Setup skipped. Run 'wispy setup' anytime.\n"));
|
|
699
|
+
process.exit(0);
|
|
700
|
+
}
|
|
590
701
|
throw err;
|
|
591
702
|
}
|
|
592
703
|
}
|
|
@@ -594,35 +705,31 @@ export class OnboardingWizard {
|
|
|
594
705
|
// ── Public: run individual step ────────────────────────────────────────────
|
|
595
706
|
|
|
596
707
|
async runStep(step) {
|
|
597
|
-
|
|
598
|
-
rl.on("SIGINT", () => {
|
|
708
|
+
process.on("SIGINT", () => {
|
|
599
709
|
console.log(dim("\n Cancelled.\n"));
|
|
600
|
-
rl.close();
|
|
601
710
|
process.exit(0);
|
|
602
711
|
});
|
|
603
712
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
server: () => this.stepServer(rl),
|
|
614
|
-
};
|
|
713
|
+
const stepMap = {
|
|
714
|
+
welcome: () => this.stepWelcome(),
|
|
715
|
+
provider: () => this.stepProvider(),
|
|
716
|
+
channels: () => this.stepChannels(),
|
|
717
|
+
workstream: () => this.stepWorkstream(),
|
|
718
|
+
memory: () => this.stepMemory(),
|
|
719
|
+
security: () => this.stepSecurity(),
|
|
720
|
+
server: () => this.stepServer(),
|
|
721
|
+
};
|
|
615
722
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
}
|
|
723
|
+
if (!stepMap[step]) {
|
|
724
|
+
console.log(red(` Unknown step: ${step}`));
|
|
725
|
+
console.log(dim(" Valid steps: provider, channels, workstream, memory, security, server"));
|
|
726
|
+
return {};
|
|
727
|
+
}
|
|
622
728
|
|
|
729
|
+
let partial = {};
|
|
730
|
+
try {
|
|
623
731
|
partial = await stepMap[step]();
|
|
624
732
|
|
|
625
|
-
// Merge with existing config and save
|
|
626
733
|
if (Object.keys(partial).length > 0) {
|
|
627
734
|
const existing = await loadConfig();
|
|
628
735
|
const updated = { ...existing, ...partial };
|
|
@@ -631,8 +738,12 @@ export class OnboardingWizard {
|
|
|
631
738
|
await saveConfig(updated);
|
|
632
739
|
console.log(green(" ✅ Config saved.\n"));
|
|
633
740
|
}
|
|
634
|
-
}
|
|
635
|
-
|
|
741
|
+
} catch (err) {
|
|
742
|
+
if (err?.name === "ExitPromptError" || err?.code === "ERR_USE_AFTER_CLOSE") {
|
|
743
|
+
console.log(dim("\n Cancelled.\n"));
|
|
744
|
+
return {};
|
|
745
|
+
}
|
|
746
|
+
throw err;
|
|
636
747
|
}
|
|
637
748
|
|
|
638
749
|
return partial;
|
|
@@ -659,9 +770,7 @@ export async function isFirstRun() {
|
|
|
659
770
|
* Print `wispy status` output — comprehensive current config view
|
|
660
771
|
*/
|
|
661
772
|
export async function printStatus() {
|
|
662
|
-
const { readdir
|
|
663
|
-
const { existsSync } = await import("node:fs");
|
|
664
|
-
const { SESSIONS_DIR, MEMORY_DIR: MEM_DIR } = await import("./config.mjs");
|
|
773
|
+
const { readdir } = await import("node:fs/promises");
|
|
665
774
|
|
|
666
775
|
const config = await loadConfig();
|
|
667
776
|
|
|
@@ -679,7 +788,6 @@ export async function printStatus() {
|
|
|
679
788
|
const mFiles = await readdir(path.join(WISPY_DIR, "memory"));
|
|
680
789
|
memFiles = mFiles.filter((f) => f.endsWith(".md") || f.endsWith(".txt"));
|
|
681
790
|
memCount = memFiles.length;
|
|
682
|
-
// Count daily files
|
|
683
791
|
try {
|
|
684
792
|
const dailyFiles = await readdir(path.join(WISPY_DIR, "memory", "daily"));
|
|
685
793
|
memCount += dailyFiles.length;
|
|
@@ -708,27 +816,34 @@ export async function printStatus() {
|
|
|
708
816
|
nodeCount = (nodeData.nodes ?? []).length;
|
|
709
817
|
} catch {}
|
|
710
818
|
|
|
711
|
-
// Provider info
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
819
|
+
// Provider info (support both old single-provider and new multi-provider config)
|
|
820
|
+
let providerDisplay;
|
|
821
|
+
if (config.providers && Object.keys(config.providers).length > 0) {
|
|
822
|
+
const provKeys = Object.keys(config.providers);
|
|
823
|
+
const defaultP = config.defaultProvider ?? provKeys[0];
|
|
824
|
+
const defaultModel = config.providers[defaultP]?.model ?? getProviderDefaultModel(defaultP);
|
|
825
|
+
const names = provKeys.map((k) => `${getProviderLabel(k).split(" —")[0]} ${green("✅")}`).join(", ");
|
|
826
|
+
providerDisplay = `${names}\n Default: ${cyan(getProviderLabel(defaultP).split(" —")[0])} ${dim(`(${defaultModel})`)}`;
|
|
827
|
+
} else if (config.provider) {
|
|
828
|
+
const model = config.model ?? getProviderDefaultModel(config.provider);
|
|
829
|
+
const label = getProviderLabel(config.provider);
|
|
830
|
+
const hasKey = !!(config.apiKey || config.provider === "ollama");
|
|
831
|
+
providerDisplay = `${cyan(label)} ${dim(`(${model})`)}` + (hasKey ? ` ${green("✅")}` : ` ${yellow("⚠️ no key")}`);
|
|
832
|
+
} else {
|
|
833
|
+
providerDisplay = red("not configured");
|
|
834
|
+
}
|
|
720
835
|
|
|
721
836
|
// Channels
|
|
722
|
-
const tgOk
|
|
723
|
-
const dcOk
|
|
724
|
-
const slOk
|
|
837
|
+
const tgOk = config.channels?.telegram?.enabled;
|
|
838
|
+
const dcOk = config.channels?.discord?.enabled;
|
|
839
|
+
const slOk = config.channels?.slack?.enabled;
|
|
725
840
|
const chanStr = [
|
|
726
841
|
`Telegram ${tgOk ? green("✅") : red("❌")}`,
|
|
727
|
-
`Discord
|
|
728
|
-
`Slack
|
|
842
|
+
`Discord ${dcOk ? green("✅") : red("❌")}`,
|
|
843
|
+
`Slack ${slOk ? green("✅") : red("❌")}`,
|
|
729
844
|
].join(dim(" | "));
|
|
730
845
|
|
|
731
|
-
const mode
|
|
846
|
+
const mode = remote?.url ? yellow("remote") : green("local");
|
|
732
847
|
const security = SECURITY_LEVELS[config.security ?? "careful"]?.label ?? "Careful";
|
|
733
848
|
|
|
734
849
|
console.log(`\n🌿 ${bold("Wispy Status")}\n`);
|
|
@@ -738,12 +853,13 @@ export async function printStatus() {
|
|
|
738
853
|
console.log(` Channels: ${chanStr}`);
|
|
739
854
|
console.log(` Workstream: ${config.workstream ?? "default"}`);
|
|
740
855
|
console.log(` Security: ${security}`);
|
|
856
|
+
console.log(` Language: ${config.language ?? "auto"}`);
|
|
741
857
|
console.log(` Memory: ${memCount} file${memCount !== 1 ? "s" : ""}`
|
|
742
858
|
+ (memFiles.length > 0 ? dim(` (${memFiles.slice(0, 3).join(", ")}${memFiles.length > 3 ? "..." : ""})`) : ""));
|
|
743
859
|
console.log(` Sessions: ${sessionCount} total`);
|
|
744
860
|
console.log(` Cron jobs: ${cronCount} active`);
|
|
745
861
|
console.log(` Nodes: ${nodeCount} connected`);
|
|
746
|
-
console.log(` Server: ${config.server?.enabled ? green("On :"+config.server.port) : "Off"}`);
|
|
862
|
+
console.log(` Server: ${config.server?.enabled ? green("On :" + config.server.port) : "Off"}`);
|
|
747
863
|
console.log("");
|
|
748
864
|
console.log(` ${dim("Config:")} ${dim(CONFIG_PATH)}`);
|
|
749
865
|
console.log(` ${dim("Data: ")} ${dim(WISPY_DIR + "/")}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wispy-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "🌿 Wispy — AI workspace assistant with trustworthy execution (harness, receipts, approvals, diffs)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Minseo & Poropo",
|
|
@@ -57,12 +57,12 @@
|
|
|
57
57
|
"LICENSE"
|
|
58
58
|
],
|
|
59
59
|
"scripts": {
|
|
60
|
-
"postinstall": "node scripts/postinstall.mjs",
|
|
61
60
|
"test": "node --test --test-force-exit --test-timeout=15000 tests/*.test.mjs",
|
|
62
61
|
"test:basic": "node --test --test-force-exit test/basic.test.mjs",
|
|
63
62
|
"test:verbose": "node --test --test-force-exit --test-timeout=15000 --test-reporter=spec tests/*.test.mjs"
|
|
64
63
|
},
|
|
65
64
|
"dependencies": {
|
|
65
|
+
"@inquirer/prompts": "^8.3.2",
|
|
66
66
|
"cron-parser": "^5.5.0",
|
|
67
67
|
"ink": "^5.2.1",
|
|
68
68
|
"ink-spinner": "^5.0.0",
|
|
@@ -74,10 +74,10 @@
|
|
|
74
74
|
"@slack/bolt": ">=3.0.0",
|
|
75
75
|
"discord.js": ">=14.0.0",
|
|
76
76
|
"grammy": ">=1.0.0",
|
|
77
|
-
"
|
|
78
|
-
"qrcode-terminal": ">=0.12.0",
|
|
77
|
+
"imapflow": ">=1.0.0",
|
|
79
78
|
"nodemailer": ">=6.0.0",
|
|
80
|
-
"
|
|
79
|
+
"qrcode-terminal": ">=0.12.0",
|
|
80
|
+
"whatsapp-web.js": ">=1.0.0"
|
|
81
81
|
},
|
|
82
82
|
"peerDependenciesMeta": {
|
|
83
83
|
"grammy": {
|