wispy-cli 2.2.1 → 2.3.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/bin/wispy.mjs +623 -54
- package/lib/commands/ws.mjs +20 -0
- package/lib/wispy-repl.mjs +28 -10
- package/package.json +1 -1
package/bin/wispy.mjs
CHANGED
|
@@ -18,73 +18,590 @@
|
|
|
18
18
|
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
import path from "node:path";
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
21
22
|
|
|
22
23
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
24
|
|
|
24
25
|
const args = process.argv.slice(2);
|
|
25
26
|
|
|
27
|
+
// ── Global helpers ─────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
// Color helpers (reused throughout)
|
|
30
|
+
const _green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
31
|
+
const _red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
32
|
+
const _yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
33
|
+
const _cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
34
|
+
const _bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
35
|
+
const _dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
36
|
+
|
|
37
|
+
// Debug mode (--debug flag or WISPY_DEBUG env)
|
|
38
|
+
const DEBUG = args.includes("--debug") || process.env.WISPY_DEBUG === "1";
|
|
39
|
+
if (DEBUG) {
|
|
40
|
+
process.env.WISPY_DEBUG = "1";
|
|
41
|
+
// Remove --debug from args so it doesn't confuse subcommands
|
|
42
|
+
const idx = args.indexOf("--debug");
|
|
43
|
+
if (idx !== -1) args.splice(idx, 1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Friendly error display
|
|
47
|
+
function friendlyError(err, exitCode = 1) {
|
|
48
|
+
if (DEBUG) {
|
|
49
|
+
console.error(_red(`\n❌ Error: ${err.message}`));
|
|
50
|
+
console.error(_dim(err.stack));
|
|
51
|
+
} else {
|
|
52
|
+
console.error(_red(`\n❌ Error: ${err.message}`));
|
|
53
|
+
console.error(_dim(" Run with --debug for more details."));
|
|
54
|
+
}
|
|
55
|
+
process.exit(exitCode);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Read version once
|
|
59
|
+
let _version;
|
|
60
|
+
function getVersion() {
|
|
61
|
+
if (_version) return _version;
|
|
62
|
+
try {
|
|
63
|
+
_version = JSON.parse(readFileSync(path.join(__dirname, "..", "package.json"), "utf8")).version;
|
|
64
|
+
} catch { _version = "?.?.?"; }
|
|
65
|
+
return _version;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Global SIGINT handler (individual commands may override this)
|
|
69
|
+
process.on("SIGINT", () => {
|
|
70
|
+
console.log(_dim("\n\nInterrupted."));
|
|
71
|
+
process.exit(130);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ── --version / -v / version ───────────────────────────────────────────────────
|
|
75
|
+
if (args[0] === "--version" || args[0] === "-v" || args[0] === "version") {
|
|
76
|
+
console.log(`wispy-cli v${getVersion()}`);
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── --help / -h ────────────────────────────────────────────────────────────────
|
|
81
|
+
if (args[0] === "--help" || args[0] === "-h") {
|
|
82
|
+
const v = getVersion();
|
|
83
|
+
console.log(`
|
|
84
|
+
${_bold("🌿 Wispy")} ${_dim(`v${v}`)} — AI workspace assistant
|
|
85
|
+
|
|
86
|
+
${_bold("Usage:")} wispy [command] [options]
|
|
87
|
+
|
|
88
|
+
${_bold("AI Interaction:")}
|
|
89
|
+
${_cyan("wispy")} Start interactive REPL
|
|
90
|
+
${_cyan('wispy "message"')} One-shot message
|
|
91
|
+
${_cyan("wispy tui")} Workspace TUI
|
|
92
|
+
${_cyan("wispy dry <cmd>")} Run in dry-run mode
|
|
93
|
+
|
|
94
|
+
${_bold("Workstreams:")}
|
|
95
|
+
${_cyan("wispy ws")} List workstreams
|
|
96
|
+
${_cyan("wispy ws new <name>")} Create workstream
|
|
97
|
+
${_cyan("wispy ws switch <name>")} Switch workstream
|
|
98
|
+
${_cyan("wispy ws archive <name>")} Archive workstream
|
|
99
|
+
${_cyan("wispy ws delete <name>")} Delete workstream
|
|
100
|
+
|
|
101
|
+
${_bold("Trust & Security:")}
|
|
102
|
+
${_cyan("wispy trust")} Show trust level & policies
|
|
103
|
+
${_cyan("wispy trust <level>")} Set trust (careful/balanced/yolo)
|
|
104
|
+
${_cyan("wispy audit")} View audit log
|
|
105
|
+
|
|
106
|
+
${_bold("Continuity:")}
|
|
107
|
+
${_cyan("wispy where")} Show current mode
|
|
108
|
+
${_cyan("wispy handoff")} Generate handoff summary
|
|
109
|
+
|
|
110
|
+
${_bold("Skills:")}
|
|
111
|
+
${_cyan("wispy skill list")} List skills
|
|
112
|
+
${_cyan("wispy skill show <name>")} Show skill details
|
|
113
|
+
${_cyan("wispy teach <name>")} Create skill from conversation
|
|
114
|
+
${_cyan("wispy improve <name> [notes]")} Improve a skill
|
|
115
|
+
|
|
116
|
+
${_bold("Channels & Bots:")}
|
|
117
|
+
${_cyan("wispy channel setup <name>")} Setup channel (telegram/discord/slack/…)
|
|
118
|
+
${_cyan("wispy channel list")} List configured channels
|
|
119
|
+
${_cyan("wispy channel test <name>")} Test channel connection
|
|
120
|
+
${_cyan("wispy --serve")} Start all channel bots
|
|
121
|
+
|
|
122
|
+
${_bold("Server & Deploy:")}
|
|
123
|
+
${_cyan("wispy server")} Start API server
|
|
124
|
+
${_cyan("wispy deploy")} Deploy help & config generators
|
|
125
|
+
${_cyan("wispy node")} Node (multi-machine) management
|
|
126
|
+
${_cyan("wispy sync")} Sync with remote server
|
|
127
|
+
|
|
128
|
+
${_bold("Cron & Automation:")}
|
|
129
|
+
${_cyan("wispy cron list")} List cron jobs
|
|
130
|
+
${_cyan("wispy cron add")} Add a cron job
|
|
131
|
+
${_cyan("wispy cron start")} Start scheduler
|
|
132
|
+
|
|
133
|
+
${_bold("Config & Maintenance:")}
|
|
134
|
+
${_cyan("wispy setup")} Configure wispy interactively
|
|
135
|
+
${_cyan("wispy update")} Update to latest version
|
|
136
|
+
${_cyan("wispy migrate")} Import from OpenClaw
|
|
137
|
+
${_cyan("wispy doctor")} Check system health
|
|
138
|
+
${_cyan("wispy version")} Show version
|
|
139
|
+
|
|
140
|
+
${_bold("Shell Completions:")}
|
|
141
|
+
${_cyan("wispy completion bash")} Bash completion script
|
|
142
|
+
${_cyan("wispy completion zsh")} Zsh completion script
|
|
143
|
+
|
|
144
|
+
${_bold("Flags:")}
|
|
145
|
+
${_cyan("--help, -h")} Show this help
|
|
146
|
+
${_cyan("--version, -v")} Show version
|
|
147
|
+
${_cyan("--debug")} Verbose logs & stack traces
|
|
148
|
+
${_cyan("WISPY_DEBUG=1")} Same as --debug via env
|
|
149
|
+
|
|
150
|
+
${_dim("Run 'wispy help <command>' for detailed help on a specific command.")}
|
|
151
|
+
`);
|
|
152
|
+
process.exit(0);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── help sub-command (detailed per-command help) ──────────────────────────────
|
|
156
|
+
if (args[0] === "help") {
|
|
157
|
+
const topic = args[1];
|
|
158
|
+
const helpTexts = {
|
|
159
|
+
ws: `
|
|
160
|
+
${_bold("wispy ws")} — Workstream management
|
|
161
|
+
|
|
162
|
+
${_bold("Usage:")}
|
|
163
|
+
wispy ws List all workstreams
|
|
164
|
+
wispy ws new <name> Create a new workstream
|
|
165
|
+
wispy ws switch <name> Switch to a workstream
|
|
166
|
+
wispy ws archive <name> Archive a workstream (move sessions/memory)
|
|
167
|
+
wispy ws delete <name> Permanently delete a workstream
|
|
168
|
+
wispy ws status Status overview of all workstreams
|
|
169
|
+
wispy ws search <query> Search across all workstreams
|
|
170
|
+
|
|
171
|
+
${_bold("Examples:")}
|
|
172
|
+
wispy ws new project-x
|
|
173
|
+
wispy ws switch project-x
|
|
174
|
+
wispy ws archive old-project
|
|
175
|
+
`,
|
|
176
|
+
trust: `
|
|
177
|
+
${_bold("wispy trust")} — Trust levels and security
|
|
178
|
+
|
|
179
|
+
${_bold("Usage:")}
|
|
180
|
+
wispy trust Show current trust level and policies
|
|
181
|
+
wispy trust careful Require approval for everything
|
|
182
|
+
wispy trust balanced Approve only risky operations
|
|
183
|
+
wispy trust yolo Auto-approve everything
|
|
184
|
+
|
|
185
|
+
${_bold("Trust levels:")}
|
|
186
|
+
careful — Review every tool call before execution
|
|
187
|
+
balanced — Auto-approve safe ops, review destructive ones
|
|
188
|
+
yolo — Full automation (use with care!)
|
|
189
|
+
`,
|
|
190
|
+
deploy: `
|
|
191
|
+
${_bold("wispy deploy")} — Deployment helpers
|
|
192
|
+
|
|
193
|
+
${_bold("Usage:")}
|
|
194
|
+
wispy deploy init Generate Dockerfile + compose + .env.example
|
|
195
|
+
wispy deploy dockerfile Print Dockerfile
|
|
196
|
+
wispy deploy compose Print docker-compose.yml
|
|
197
|
+
wispy deploy systemd Print systemd unit
|
|
198
|
+
wispy deploy railway Print railway.json
|
|
199
|
+
wispy deploy fly Print fly.toml
|
|
200
|
+
wispy deploy render Print render.yaml
|
|
201
|
+
wispy deploy modal Generate Modal serverless config
|
|
202
|
+
wispy deploy daytona Generate Daytona workspace config
|
|
203
|
+
wispy deploy vps user@host SSH deploy to VPS
|
|
204
|
+
wispy deploy status <url> Check remote server health
|
|
205
|
+
|
|
206
|
+
${_bold("Examples:")}
|
|
207
|
+
wispy deploy init
|
|
208
|
+
wispy deploy vps root@my.vps
|
|
209
|
+
wispy connect https://my.vps:18790 --token <token>
|
|
210
|
+
`,
|
|
211
|
+
cron: `
|
|
212
|
+
${_bold("wispy cron")} — Scheduled task management
|
|
213
|
+
|
|
214
|
+
${_bold("Usage:")}
|
|
215
|
+
wispy cron list List all cron jobs
|
|
216
|
+
wispy cron add Interactive job creation
|
|
217
|
+
wispy cron remove <id> Delete a cron job
|
|
218
|
+
wispy cron run <id> Run a job immediately
|
|
219
|
+
wispy cron history <id> Show past runs
|
|
220
|
+
wispy cron start Start scheduler in foreground
|
|
221
|
+
|
|
222
|
+
${_bold("Schedule types:")}
|
|
223
|
+
cron — standard cron expression (e.g. "0 9 * * *")
|
|
224
|
+
every — interval in minutes
|
|
225
|
+
at — one-time ISO datetime
|
|
226
|
+
`,
|
|
227
|
+
channel: `
|
|
228
|
+
${_bold("wispy channel")} — Messaging channel setup
|
|
229
|
+
|
|
230
|
+
${_bold("Usage:")}
|
|
231
|
+
wispy channel setup telegram Telegram bot setup
|
|
232
|
+
wispy channel setup discord Discord bot setup
|
|
233
|
+
wispy channel setup slack Slack bot setup
|
|
234
|
+
wispy channel setup whatsapp WhatsApp setup
|
|
235
|
+
wispy channel setup email Email setup
|
|
236
|
+
wispy channel list List configured channels
|
|
237
|
+
wispy channel test <name> Test channel connection
|
|
238
|
+
|
|
239
|
+
${_bold("Running bots:")}
|
|
240
|
+
wispy --serve Start all configured bots
|
|
241
|
+
wispy --telegram Start Telegram bot only
|
|
242
|
+
wispy --discord Start Discord bot only
|
|
243
|
+
`,
|
|
244
|
+
sync: `
|
|
245
|
+
${_bold("wispy sync")} — Remote sync
|
|
246
|
+
|
|
247
|
+
${_bold("Usage:")}
|
|
248
|
+
wispy sync Bidirectional sync
|
|
249
|
+
wispy sync push Push local → remote
|
|
250
|
+
wispy sync pull Pull remote → local
|
|
251
|
+
wispy sync status Show sync status
|
|
252
|
+
wispy sync auto Enable auto-sync
|
|
253
|
+
wispy sync auto --off Disable auto-sync
|
|
254
|
+
|
|
255
|
+
${_bold("Flags:")}
|
|
256
|
+
--remote <url> Remote server URL
|
|
257
|
+
--token <token> Auth token
|
|
258
|
+
--strategy newer-wins Conflict resolution (newer-wins|local-wins|remote-wins)
|
|
259
|
+
--memory-only Only sync memory files
|
|
260
|
+
--sessions-only Only sync sessions
|
|
261
|
+
`,
|
|
262
|
+
skill: `
|
|
263
|
+
${_bold("wispy skill")} — Skill management
|
|
264
|
+
|
|
265
|
+
${_bold("Usage:")}
|
|
266
|
+
wispy skill list List all installed skills
|
|
267
|
+
wispy skill show <name> Show skill details
|
|
268
|
+
wispy teach <name> Create a skill from current conversation
|
|
269
|
+
wispy improve <name> Improve an existing skill
|
|
270
|
+
|
|
271
|
+
${_bold("In REPL:")}
|
|
272
|
+
/skills List skills
|
|
273
|
+
/teach <name> Teach from conversation
|
|
274
|
+
/<skill-name> Invoke any skill
|
|
275
|
+
`,
|
|
276
|
+
doctor: `
|
|
277
|
+
${_bold("wispy doctor")} — System health check
|
|
278
|
+
|
|
279
|
+
Checks Node.js version, config, API keys, directory permissions,
|
|
280
|
+
optional dependencies, and remote server connectivity.
|
|
281
|
+
|
|
282
|
+
${_bold("Usage:")}
|
|
283
|
+
wispy doctor
|
|
284
|
+
`,
|
|
285
|
+
node: `
|
|
286
|
+
${_bold("wispy node")} — Multi-machine node management
|
|
287
|
+
|
|
288
|
+
${_bold("Usage:")}
|
|
289
|
+
wispy node pair Generate pairing code
|
|
290
|
+
wispy node connect <code> --url <url> Connect as a node
|
|
291
|
+
wispy node list Show registered nodes
|
|
292
|
+
wispy node status Ping all nodes
|
|
293
|
+
wispy node remove <id> Unregister a node
|
|
294
|
+
`,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
if (topic && helpTexts[topic]) {
|
|
298
|
+
console.log(helpTexts[topic]);
|
|
299
|
+
} else if (topic) {
|
|
300
|
+
console.log(_yellow(`\n⚠️ No detailed help for '${topic}'. Try 'wispy --help' for the full command list.\n`));
|
|
301
|
+
} else {
|
|
302
|
+
// Generic help — same as --help
|
|
303
|
+
args[0] = "--help";
|
|
304
|
+
// Re-trigger help by falling through (not possible here, just print it)
|
|
305
|
+
console.log(`Run ${_cyan("wispy --help")} for the full command list.`);
|
|
306
|
+
console.log(`Run ${_cyan("wispy help <command>")} for per-command help.`);
|
|
307
|
+
console.log(`\nAvailable topics: ${Object.keys(helpTexts).map(k => _cyan(k)).join(", ")}`);
|
|
308
|
+
}
|
|
309
|
+
process.exit(0);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ── completion sub-command ────────────────────────────────────────────────────
|
|
313
|
+
if (args[0] === "completion") {
|
|
314
|
+
const shell = args[1] ?? "bash";
|
|
315
|
+
const cmds = ["ws", "trust", "where", "handoff", "skill", "teach", "improve", "dry",
|
|
316
|
+
"deploy", "server", "node", "channel", "cron", "audit", "sync", "setup", "update",
|
|
317
|
+
"tui", "migrate", "version", "doctor", "help", "completion", "status",
|
|
318
|
+
"connect", "disconnect", "log"];
|
|
319
|
+
const flags = ["--help", "--version", "--debug", "--serve", "--telegram", "--discord", "--slack"];
|
|
320
|
+
|
|
321
|
+
if (shell === "bash") {
|
|
322
|
+
console.log(`# wispy bash completion
|
|
323
|
+
# Add to ~/.bashrc: eval "$(wispy completion bash)"
|
|
324
|
+
_wispy_completion() {
|
|
325
|
+
local cur prev words
|
|
326
|
+
COMPREPLY=()
|
|
327
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
328
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
329
|
+
local commands="${cmds.join(" ")}"
|
|
330
|
+
local flags="${flags.join(" ")}"
|
|
331
|
+
|
|
332
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
333
|
+
COMPREPLY=( $(compgen -W "\${commands} \${flags}" -- "\${cur}") )
|
|
334
|
+
fi
|
|
335
|
+
}
|
|
336
|
+
complete -F _wispy_completion wispy`);
|
|
337
|
+
} else if (shell === "zsh") {
|
|
338
|
+
console.log(`# wispy zsh completion
|
|
339
|
+
# Add to ~/.zshrc: eval "$(wispy completion zsh)"
|
|
340
|
+
_wispy() {
|
|
341
|
+
local -a commands
|
|
342
|
+
commands=(
|
|
343
|
+
${cmds.map(c => `'${c}'`).join("\n ")}
|
|
344
|
+
)
|
|
345
|
+
local -a flags
|
|
346
|
+
flags=(${flags.map(f => `'${f}'`).join(" ")})
|
|
347
|
+
_arguments -C \\
|
|
348
|
+
'1:command:->cmds' \\
|
|
349
|
+
'*::arg:->args'
|
|
350
|
+
case $state in
|
|
351
|
+
cmds) _describe 'wispy commands' commands ;;
|
|
352
|
+
esac
|
|
353
|
+
}
|
|
354
|
+
compdef _wispy wispy`);
|
|
355
|
+
} else if (shell === "fish") {
|
|
356
|
+
const completions = cmds.map(c => `complete -c wispy -f -n '__fish_use_subcommand' -a '${c}'`).join("\n");
|
|
357
|
+
console.log(`# wispy fish completion\n# Save to ~/.config/fish/completions/wispy.fish\n${completions}`);
|
|
358
|
+
} else {
|
|
359
|
+
console.error(_red(`❌ Unknown shell: ${shell}. Use: bash, zsh, or fish`));
|
|
360
|
+
process.exit(2);
|
|
361
|
+
}
|
|
362
|
+
process.exit(0);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ── doctor sub-command ────────────────────────────────────────────────────────
|
|
366
|
+
if (args[0] === "doctor") {
|
|
367
|
+
const { access, readFile: rf, stat } = await import("node:fs/promises");
|
|
368
|
+
const { homedir } = await import("node:os");
|
|
369
|
+
const wispyDir = path.join(homedir(), ".wispy");
|
|
370
|
+
const configPath = path.join(wispyDir, "config.json");
|
|
371
|
+
const memoryDir = path.join(wispyDir, "memory");
|
|
372
|
+
const sessionsDir = path.join(wispyDir, "sessions");
|
|
373
|
+
|
|
374
|
+
console.log(`\n${_bold("🩺 Wispy Doctor")} ${_dim(`v${getVersion()}`)}\n`);
|
|
375
|
+
|
|
376
|
+
let allOk = true;
|
|
377
|
+
const issues = [];
|
|
378
|
+
|
|
379
|
+
function check(label, ok, detail = "") {
|
|
380
|
+
if (ok) {
|
|
381
|
+
console.log(` ${_green("✅")} ${label}${detail ? _dim(" " + detail) : ""}`);
|
|
382
|
+
} else {
|
|
383
|
+
console.log(` ${_red("❌")} ${label}${detail ? _dim(" " + detail) : ""}`);
|
|
384
|
+
allOk = false;
|
|
385
|
+
issues.push(label);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function info(label, detail) {
|
|
390
|
+
console.log(` ${_cyan("ℹ️ ")} ${label}${detail ? _dim(" " + detail) : ""}`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// 1. Node.js version
|
|
394
|
+
const [major] = process.version.replace("v", "").split(".").map(Number);
|
|
395
|
+
check("Node.js version", major >= 18, `${process.version} (required: >= 18)`);
|
|
396
|
+
|
|
397
|
+
// 2. Config file
|
|
398
|
+
let config = null;
|
|
399
|
+
try {
|
|
400
|
+
const raw = await rf(configPath, "utf8");
|
|
401
|
+
config = JSON.parse(raw);
|
|
402
|
+
check("Config file", true, configPath);
|
|
403
|
+
} catch (e) {
|
|
404
|
+
if (e.code === "ENOENT") {
|
|
405
|
+
check("Config file", false, `Not found at ${configPath} — run 'wispy setup'`);
|
|
406
|
+
} else {
|
|
407
|
+
check("Config file", false, `Malformed JSON — run 'wispy setup' to reconfigure`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// 3. API key configured
|
|
412
|
+
if (config) {
|
|
413
|
+
const provider = config.provider ?? "unknown";
|
|
414
|
+
const envMap = {
|
|
415
|
+
google: "GOOGLE_AI_KEY",
|
|
416
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
417
|
+
openai: "OPENAI_API_KEY",
|
|
418
|
+
groq: "GROQ_API_KEY",
|
|
419
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
420
|
+
deepseek: "DEEPSEEK_API_KEY",
|
|
421
|
+
ollama: null,
|
|
422
|
+
};
|
|
423
|
+
const envKey = envMap[provider];
|
|
424
|
+
if (envKey === null) {
|
|
425
|
+
check("AI provider (Ollama)", true, "no key needed");
|
|
426
|
+
} else if (envKey) {
|
|
427
|
+
const key = config.apiKey || process.env[envKey];
|
|
428
|
+
check(`API key (${provider})`, !!key && key.length > 10, key ? "configured" : `run 'wispy setup provider'`);
|
|
429
|
+
} else {
|
|
430
|
+
check("AI provider", false, `Unknown provider: ${provider}`);
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
info("AI provider", "skipped (no config)");
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// 4. Memory directory writable
|
|
437
|
+
try {
|
|
438
|
+
const { mkdir: mkd, writeFile: wf, unlink: unl } = await import("node:fs/promises");
|
|
439
|
+
await mkd(memoryDir, { recursive: true });
|
|
440
|
+
const testFile = path.join(memoryDir, ".write-test");
|
|
441
|
+
await wf(testFile, "test", "utf8");
|
|
442
|
+
await unl(testFile);
|
|
443
|
+
check("Memory dir writable", true, memoryDir);
|
|
444
|
+
} catch (e) {
|
|
445
|
+
check("Memory dir writable", false, e.message);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// 5. Sessions directory writable
|
|
449
|
+
try {
|
|
450
|
+
const { mkdir: mkd, writeFile: wf, unlink: unl } = await import("node:fs/promises");
|
|
451
|
+
await mkd(sessionsDir, { recursive: true });
|
|
452
|
+
const testFile = path.join(sessionsDir, ".write-test");
|
|
453
|
+
await wf(testFile, "test", "utf8");
|
|
454
|
+
await unl(testFile);
|
|
455
|
+
check("Sessions dir writable", true, sessionsDir);
|
|
456
|
+
} catch (e) {
|
|
457
|
+
check("Sessions dir writable", false, e.message);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// 6. Optional deps
|
|
461
|
+
console.log(`\n ${_bold("Optional dependencies:")}`);
|
|
462
|
+
const optDeps = [
|
|
463
|
+
{ pkg: "grammy", label: "Telegram (grammy)" },
|
|
464
|
+
{ pkg: "discord.js", label: "Discord (discord.js)" },
|
|
465
|
+
{ pkg: "@slack/bolt", label: "Slack (@slack/bolt)" },
|
|
466
|
+
{ pkg: "whatsapp-web.js", label: "WhatsApp (whatsapp-web.js)" },
|
|
467
|
+
{ pkg: "nodemailer", label: "Email (nodemailer)" },
|
|
468
|
+
{ pkg: "imapflow", label: "Email IMAP (imapflow)" },
|
|
469
|
+
];
|
|
470
|
+
for (const dep of optDeps) {
|
|
471
|
+
try {
|
|
472
|
+
await import(dep.pkg);
|
|
473
|
+
check(dep.label, true, "installed");
|
|
474
|
+
} catch {
|
|
475
|
+
console.log(` ${_dim("–")} ${_dim(dep.label + " not installed (optional)")}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// 7. Remote server (if configured)
|
|
480
|
+
const remotePath = path.join(wispyDir, "remote.json");
|
|
481
|
+
try {
|
|
482
|
+
const remote = JSON.parse(await rf(remotePath, "utf8"));
|
|
483
|
+
if (remote?.url) {
|
|
484
|
+
console.log(`\n ${_bold("Remote server:")}`);
|
|
485
|
+
process.stdout.write(` ${_cyan("🔄")} Checking ${remote.url}... `);
|
|
486
|
+
try {
|
|
487
|
+
const resp = await fetch(`${remote.url}/api/health`, { signal: AbortSignal.timeout(5000) });
|
|
488
|
+
if (resp.ok) {
|
|
489
|
+
console.log(_green("✅ reachable"));
|
|
490
|
+
} else {
|
|
491
|
+
console.log(_yellow(`⚠️ HTTP ${resp.status}`));
|
|
492
|
+
allOk = false;
|
|
493
|
+
}
|
|
494
|
+
} catch {
|
|
495
|
+
console.log(_red("❌ unreachable"));
|
|
496
|
+
allOk = false;
|
|
497
|
+
issues.push("Remote server unreachable");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
} catch {}
|
|
501
|
+
|
|
502
|
+
console.log("");
|
|
503
|
+
if (allOk) {
|
|
504
|
+
console.log(`${_green("✅ All checks passed!")}\n`);
|
|
505
|
+
} else {
|
|
506
|
+
console.log(`${_yellow("⚠️ Issues found:")} ${issues.join(", ")}`);
|
|
507
|
+
console.log(_dim(" Run 'wispy setup' to fix configuration issues.\n"));
|
|
508
|
+
}
|
|
509
|
+
process.exit(allOk ? 0 : 1);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ── config validation helper ───────────────────────────────────────────────────
|
|
513
|
+
async function validateConfigOnStartup() {
|
|
514
|
+
const { homedir } = await import("node:os");
|
|
515
|
+
const { readFile: rf } = await import("node:fs/promises");
|
|
516
|
+
const configPath = path.join(homedir(), ".wispy", "config.json");
|
|
517
|
+
try {
|
|
518
|
+
const raw = await rf(configPath, "utf8");
|
|
519
|
+
JSON.parse(raw);
|
|
520
|
+
} catch (e) {
|
|
521
|
+
if (e instanceof SyntaxError) {
|
|
522
|
+
console.error(_red("❌ Config file corrupted. Run 'wispy setup' to reconfigure."));
|
|
523
|
+
process.exit(1);
|
|
524
|
+
}
|
|
525
|
+
// File not found is OK (first run)
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
26
529
|
// ── ws sub-command ────────────────────────────────────────────────────────────
|
|
27
530
|
if (args[0] === "ws") {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
531
|
+
try {
|
|
532
|
+
const { handleWsCommand } = await import(
|
|
533
|
+
path.join(__dirname, "..", "lib", "commands", "ws.mjs")
|
|
534
|
+
);
|
|
535
|
+
await handleWsCommand(args);
|
|
536
|
+
} catch (e) { friendlyError(e); }
|
|
32
537
|
process.exit(0);
|
|
33
538
|
}
|
|
34
539
|
|
|
35
540
|
// ── trust sub-command ─────────────────────────────────────────────────────────
|
|
36
541
|
if (args[0] === "trust") {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
542
|
+
try {
|
|
543
|
+
const { handleTrustCommand } = await import(
|
|
544
|
+
path.join(__dirname, "..", "lib", "commands", "trust.mjs")
|
|
545
|
+
);
|
|
546
|
+
await handleTrustCommand(args);
|
|
547
|
+
} catch (e) { friendlyError(e); }
|
|
41
548
|
process.exit(0);
|
|
42
549
|
}
|
|
43
550
|
|
|
44
551
|
// ── where sub-command ─────────────────────────────────────────────────────────
|
|
45
552
|
if (args[0] === "where") {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
553
|
+
try {
|
|
554
|
+
const { cmdWhere } = await import(
|
|
555
|
+
path.join(__dirname, "..", "lib", "commands", "continuity.mjs")
|
|
556
|
+
);
|
|
557
|
+
await cmdWhere();
|
|
558
|
+
} catch (e) { friendlyError(e); }
|
|
50
559
|
process.exit(0);
|
|
51
560
|
}
|
|
52
561
|
|
|
53
562
|
// ── handoff sub-command ───────────────────────────────────────────────────────
|
|
54
563
|
if (args[0] === "handoff") {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
564
|
+
try {
|
|
565
|
+
const { handleContinuityCommand } = await import(
|
|
566
|
+
path.join(__dirname, "..", "lib", "commands", "continuity.mjs")
|
|
567
|
+
);
|
|
568
|
+
await handleContinuityCommand(args);
|
|
569
|
+
} catch (e) { friendlyError(e); }
|
|
59
570
|
process.exit(0);
|
|
60
571
|
}
|
|
61
572
|
|
|
62
573
|
// ── skill sub-command ─────────────────────────────────────────────────────────
|
|
63
574
|
if (args[0] === "skill") {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
575
|
+
try {
|
|
576
|
+
const { handleSkillCommand } = await import(
|
|
577
|
+
path.join(__dirname, "..", "lib", "commands", "skills-cmd.mjs")
|
|
578
|
+
);
|
|
579
|
+
await handleSkillCommand(args);
|
|
580
|
+
} catch (e) { friendlyError(e); }
|
|
68
581
|
process.exit(0);
|
|
69
582
|
}
|
|
70
583
|
|
|
71
584
|
// ── teach sub-command ─────────────────────────────────────────────────────────
|
|
72
585
|
if (args[0] === "teach") {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
586
|
+
try {
|
|
587
|
+
const { cmdTeach } = await import(
|
|
588
|
+
path.join(__dirname, "..", "lib", "commands", "skills-cmd.mjs")
|
|
589
|
+
);
|
|
590
|
+
await cmdTeach(args[1]);
|
|
591
|
+
} catch (e) { friendlyError(e); }
|
|
77
592
|
process.exit(0);
|
|
78
593
|
}
|
|
79
594
|
|
|
80
595
|
// ── improve sub-command ───────────────────────────────────────────────────────
|
|
81
596
|
if (args[0] === "improve") {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
597
|
+
try {
|
|
598
|
+
const { cmdImproveSkill } = await import(
|
|
599
|
+
path.join(__dirname, "..", "lib", "commands", "skills-cmd.mjs")
|
|
600
|
+
);
|
|
601
|
+
const name = args[1];
|
|
602
|
+
const feedback = args.slice(2).join(" ").replace(/^["']|["']$/g, "");
|
|
603
|
+
await cmdImproveSkill(name, feedback);
|
|
604
|
+
} catch (e) { friendlyError(e); }
|
|
88
605
|
process.exit(0);
|
|
89
606
|
}
|
|
90
607
|
|
|
@@ -103,38 +620,40 @@ if (args[0] === "dry") {
|
|
|
103
620
|
|
|
104
621
|
// ── setup / init sub-command ──────────────────────────────────────────────────
|
|
105
622
|
if (args[0] === "setup" || args[0] === "init") {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
623
|
+
// Handle Ctrl+C gracefully in setup
|
|
624
|
+
process.removeAllListeners("SIGINT");
|
|
625
|
+
process.on("SIGINT", () => { console.log(_dim("\nSetup cancelled.")); process.exit(130); });
|
|
626
|
+
try {
|
|
627
|
+
const { OnboardingWizard } = await import(
|
|
628
|
+
path.join(__dirname, "..", "core", "onboarding.mjs")
|
|
629
|
+
);
|
|
630
|
+
const wizard = new OnboardingWizard();
|
|
631
|
+
const sub = args[1]; // e.g. "provider", "channels", "security"
|
|
632
|
+
if (sub && sub !== "wizard") {
|
|
633
|
+
await wizard.runStep(sub);
|
|
634
|
+
} else {
|
|
635
|
+
await wizard.run();
|
|
636
|
+
}
|
|
637
|
+
} catch (e) { friendlyError(e); }
|
|
116
638
|
process.exit(0);
|
|
117
639
|
}
|
|
118
640
|
|
|
119
641
|
// ── update sub-command ────────────────────────────────────────────────────────
|
|
120
642
|
if (args[0] === "update") {
|
|
121
|
-
const { execSync } = await import("node:child_process");
|
|
122
|
-
console.log("🌿 Checking for updates...");
|
|
123
643
|
try {
|
|
124
|
-
const
|
|
644
|
+
const { execSync } = await import("node:child_process");
|
|
645
|
+
console.log(_cyan("🔄 Checking for updates..."));
|
|
646
|
+
const current = getVersion();
|
|
125
647
|
const latest = execSync("npm info wispy-cli version", { encoding: "utf8" }).trim();
|
|
126
648
|
if (current === latest) {
|
|
127
|
-
console.log(`✅ Already on latest version (${current})`);
|
|
649
|
+
console.log(_green(`✅ Already on latest version (${current})`));
|
|
128
650
|
} else {
|
|
129
|
-
console.log(`📦 ${current} → ${latest}`);
|
|
130
|
-
console.log("Updating...");
|
|
651
|
+
console.log(`📦 ${_dim(current)} → ${_bold(latest)}`);
|
|
652
|
+
console.log(_cyan("🔄 Updating..."));
|
|
131
653
|
execSync("npm install -g wispy-cli@latest", { stdio: "inherit" });
|
|
132
|
-
console.log(`\n✅ Updated to ${latest}`);
|
|
654
|
+
console.log(_green(`\n✅ Updated to ${latest}`));
|
|
133
655
|
}
|
|
134
|
-
} catch (e) {
|
|
135
|
-
console.error(`❌ Update failed: ${e.message}`);
|
|
136
|
-
process.exit(1);
|
|
137
|
-
}
|
|
656
|
+
} catch (e) { friendlyError(e); }
|
|
138
657
|
process.exit(0);
|
|
139
658
|
}
|
|
140
659
|
|
|
@@ -147,7 +666,7 @@ if (args[0] === "status") {
|
|
|
147
666
|
);
|
|
148
667
|
await printStatus();
|
|
149
668
|
process.exit(0);
|
|
150
|
-
} catch {}
|
|
669
|
+
} catch (e) { if (DEBUG) console.error(e); }
|
|
151
670
|
|
|
152
671
|
// Fallback: original status (remote check)
|
|
153
672
|
const { readFile } = await import("node:fs/promises");
|
|
@@ -1062,6 +1581,45 @@ if (args[0] === "channel") {
|
|
|
1062
1581
|
process.exit(0);
|
|
1063
1582
|
}
|
|
1064
1583
|
|
|
1584
|
+
// ── Unknown command detection ─────────────────────────────────────────────────
|
|
1585
|
+
// Any non-flag argument that wasn't matched above is an unknown command
|
|
1586
|
+
const _KNOWN_COMMANDS = new Set([
|
|
1587
|
+
"ws", "trust", "where", "handoff", "skill", "teach", "improve", "dry",
|
|
1588
|
+
"setup", "init", "update", "status", "connect", "disconnect", "sync",
|
|
1589
|
+
"deploy", "migrate", "cron", "audit", "log", "server", "node", "channel",
|
|
1590
|
+
"tui", "help", "doctor", "completion", "version",
|
|
1591
|
+
// serve flags (handled below)
|
|
1592
|
+
"--serve", "--telegram", "--discord", "--slack", "--server",
|
|
1593
|
+
"--help", "-h", "--version", "-v", "--debug", "--tui",
|
|
1594
|
+
// workstream flags
|
|
1595
|
+
"-w", "--workstream",
|
|
1596
|
+
]);
|
|
1597
|
+
const _firstArg = args[0];
|
|
1598
|
+
if (_firstArg && _firstArg[0] !== "-" && !_KNOWN_COMMANDS.has(_firstArg)) {
|
|
1599
|
+
// Not a known command — but could be a one-shot message (no quotes needed)
|
|
1600
|
+
// Heuristic: if it looks like a real command word (no spaces, short), warn.
|
|
1601
|
+
// If it looks like a natural language sentence, fall through to REPL one-shot mode.
|
|
1602
|
+
const looksLikeCommand = /^[a-z][a-z0-9_-]{0,20}$/.test(_firstArg);
|
|
1603
|
+
if (looksLikeCommand) {
|
|
1604
|
+
// Show unknown command error
|
|
1605
|
+
const suggestions = [
|
|
1606
|
+
{ cmd: "setup", desc: "configure wispy" },
|
|
1607
|
+
{ cmd: "tui", desc: "workspace UI" },
|
|
1608
|
+
{ cmd: "ws", desc: "workstream management" },
|
|
1609
|
+
{ cmd: "doctor", desc: "check system health" },
|
|
1610
|
+
{ cmd: "help", desc: "show detailed help" },
|
|
1611
|
+
];
|
|
1612
|
+
console.error(`\n${_red(`❌ Unknown command: ${_firstArg}`)}`);
|
|
1613
|
+
console.error(`\n${_bold("Did you mean one of these?")}`);
|
|
1614
|
+
for (const { cmd, desc } of suggestions) {
|
|
1615
|
+
console.error(` ${_cyan("wispy " + cmd.padEnd(12))}— ${desc}`);
|
|
1616
|
+
}
|
|
1617
|
+
console.error(`\n${_dim("Run wispy --help for all commands.")}\n`);
|
|
1618
|
+
process.exit(2);
|
|
1619
|
+
}
|
|
1620
|
+
// Otherwise fall through to one-shot message mode in REPL
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1065
1623
|
// ── Bot / serve modes ─────────────────────────────────────────────────────────
|
|
1066
1624
|
const serveMode = args.includes("--serve");
|
|
1067
1625
|
const telegramMode = args.includes("--telegram");
|
|
@@ -1146,17 +1704,28 @@ if (isInteractiveStart) {
|
|
|
1146
1704
|
}
|
|
1147
1705
|
}
|
|
1148
1706
|
|
|
1707
|
+
// Validate config before starting interactive modes
|
|
1708
|
+
await validateConfigOnStartup();
|
|
1709
|
+
|
|
1149
1710
|
// ── TUI mode ──────────────────────────────────────────────────────────────────
|
|
1150
1711
|
// `wispy ui` is the primary way; `--tui` is kept as a hidden backwards-compat alias
|
|
1151
1712
|
const tuiMode = args[0] === "tui" || args.includes("--tui");
|
|
1152
1713
|
|
|
1153
1714
|
if (tuiMode) {
|
|
1715
|
+
// Override SIGINT for clean TUI exit
|
|
1716
|
+
process.removeAllListeners("SIGINT");
|
|
1717
|
+
process.on("SIGINT", () => { process.exit(130); });
|
|
1718
|
+
|
|
1154
1719
|
const newArgs = args.filter(a => a !== "--tui" && a !== "tui");
|
|
1155
1720
|
process.argv = [process.argv[0], process.argv[1], ...newArgs];
|
|
1156
1721
|
const tuiScript = path.join(__dirname, "..", "lib", "wispy-tui.mjs");
|
|
1157
|
-
|
|
1722
|
+
try {
|
|
1723
|
+
await import(tuiScript);
|
|
1724
|
+
} catch (e) { friendlyError(e); }
|
|
1158
1725
|
} else {
|
|
1159
1726
|
// Default: interactive REPL
|
|
1160
1727
|
const mainScript = path.join(__dirname, "..", "lib", "wispy-repl.mjs");
|
|
1161
|
-
|
|
1728
|
+
try {
|
|
1729
|
+
await import(mainScript);
|
|
1730
|
+
} catch (e) { friendlyError(e); }
|
|
1162
1731
|
}
|
package/lib/commands/ws.mjs
CHANGED
|
@@ -221,6 +221,16 @@ export async function cmdWsArchive(name) {
|
|
|
221
221
|
return;
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
// Confirmation prompt
|
|
225
|
+
const { createInterface } = await import("node:readline");
|
|
226
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
227
|
+
const answer = await new Promise(r => rl.question(`Archive '${name}'? Sessions and memory will be moved. [Y/n] `, r));
|
|
228
|
+
rl.close();
|
|
229
|
+
if (answer.trim().toLowerCase() === "n") {
|
|
230
|
+
console.log(dim("Cancelled."));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
224
234
|
const archiveWsDir = path.join(ARCHIVE_DIR, name);
|
|
225
235
|
await mkdir(archiveWsDir, { recursive: true });
|
|
226
236
|
|
|
@@ -356,6 +366,16 @@ export async function cmdWsDelete(name) {
|
|
|
356
366
|
return;
|
|
357
367
|
}
|
|
358
368
|
|
|
369
|
+
// Confirmation prompt
|
|
370
|
+
const { createInterface } = await import("node:readline");
|
|
371
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
372
|
+
const answer = await new Promise(r => rl.question(`⚠️ Are you sure? This will delete all sessions and memory for '${name}'. [y/N] `, r));
|
|
373
|
+
rl.close();
|
|
374
|
+
if (answer.trim().toLowerCase() !== "y") {
|
|
375
|
+
console.log(dim("Cancelled."));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
359
379
|
const wsDir = path.join(WORKSTREAMS_DIR, name);
|
|
360
380
|
const convFile = path.join(CONVERSATIONS_DIR, `${name}.json`);
|
|
361
381
|
|
package/lib/wispy-repl.mjs
CHANGED
|
@@ -1151,10 +1151,13 @@ async function runRepl(engine) {
|
|
|
1151
1151
|
} catch (err) {
|
|
1152
1152
|
if (err.message.includes("429") || err.message.includes("rate")) {
|
|
1153
1153
|
console.log(yellow("\n\n⏳ Rate limited — wait a moment and try again."));
|
|
1154
|
-
} else if (err.message.includes("401") || err.message.includes("403")) {
|
|
1155
|
-
console.log(red("\n\n
|
|
1154
|
+
} else if (err.message.includes("401") || err.message.includes("403") || err.message.includes("invalid") || err.message.includes("API key")) {
|
|
1155
|
+
console.log(red("\n\n❌ API key is invalid. Run 'wispy setup provider' to reconfigure."));
|
|
1156
|
+
} else if (err.message.includes("ENOTFOUND") || err.message.includes("ECONNREFUSED") || err.message.includes("fetch")) {
|
|
1157
|
+
console.log(red("\n\n❌ Cannot reach API. Check your connection."));
|
|
1156
1158
|
} else {
|
|
1157
1159
|
console.log(red(`\n\n❌ Error: ${err.message.slice(0, 200)}`));
|
|
1160
|
+
if (process.env.WISPY_DEBUG === "1") console.error(dim(err.stack));
|
|
1158
1161
|
}
|
|
1159
1162
|
}
|
|
1160
1163
|
|
|
@@ -1163,7 +1166,7 @@ async function runRepl(engine) {
|
|
|
1163
1166
|
|
|
1164
1167
|
rl.on("close", () => {
|
|
1165
1168
|
console.log(dim(`\n🌿 Bye! (${engine.providers.formatCost()})`));
|
|
1166
|
-
engine.destroy();
|
|
1169
|
+
try { engine.destroy(); } catch {}
|
|
1167
1170
|
process.exit(0);
|
|
1168
1171
|
});
|
|
1169
1172
|
}
|
|
@@ -1180,15 +1183,21 @@ async function runOneShot(engine, message) {
|
|
|
1180
1183
|
console.log("");
|
|
1181
1184
|
console.log(dim(engine.providers.formatCost()));
|
|
1182
1185
|
} catch (err) {
|
|
1183
|
-
if (err.message.includes("429")) {
|
|
1184
|
-
console.error(yellow("\n⏳ Rate limited — try again
|
|
1186
|
+
if (err.message.includes("429") || err.message.includes("rate")) {
|
|
1187
|
+
console.error(yellow("\n⏳ Rate limited — wait a moment and try again."));
|
|
1188
|
+
} else if (err.message.includes("401") || err.message.includes("403") || err.message.includes("invalid") || err.message.includes("API key")) {
|
|
1189
|
+
console.error(red("\n❌ API key is invalid. Run 'wispy setup provider' to reconfigure."));
|
|
1190
|
+
} else if (err.message.includes("ENOTFOUND") || err.message.includes("ECONNREFUSED") || err.message.includes("fetch")) {
|
|
1191
|
+
console.error(red("\n❌ Cannot reach API. Check your connection."));
|
|
1185
1192
|
} else {
|
|
1186
|
-
console.error(red(`\n❌ ${err.message.slice(0, 200)}`));
|
|
1193
|
+
console.error(red(`\n❌ Error: ${err.message.slice(0, 200)}`));
|
|
1194
|
+
if (process.env.WISPY_DEBUG === "1") console.error(dim(err.stack));
|
|
1195
|
+
else console.error(dim(" Run with WISPY_DEBUG=1 for details."));
|
|
1187
1196
|
}
|
|
1188
|
-
engine.destroy();
|
|
1197
|
+
try { engine.destroy(); } catch {}
|
|
1189
1198
|
process.exit(1);
|
|
1190
1199
|
}
|
|
1191
|
-
engine.destroy();
|
|
1200
|
+
try { engine.destroy(); } catch {}
|
|
1192
1201
|
process.exit(0);
|
|
1193
1202
|
}
|
|
1194
1203
|
|
|
@@ -1243,6 +1252,8 @@ const engine = new WispyEngine({ workstream: ACTIVE_WORKSTREAM });
|
|
|
1243
1252
|
const initResult = await engine.init();
|
|
1244
1253
|
|
|
1245
1254
|
if (!initResult) {
|
|
1255
|
+
// Show friendly message before onboarding
|
|
1256
|
+
console.log(yellow("\n⚠️ No API key configured. Run 'wispy setup' to get started.\n"));
|
|
1246
1257
|
// Delegate to unified onboarding wizard
|
|
1247
1258
|
try {
|
|
1248
1259
|
const { OnboardingWizard } = await import("../core/onboarding.mjs");
|
|
@@ -1262,8 +1273,15 @@ if (!initResult) {
|
|
|
1262
1273
|
|
|
1263
1274
|
// Graceful cleanup
|
|
1264
1275
|
process.on("exit", () => { try { engine.destroy(); } catch {} });
|
|
1265
|
-
process.on("SIGINT", () => {
|
|
1266
|
-
|
|
1276
|
+
process.on("SIGINT", () => {
|
|
1277
|
+
console.log(dim("\n\n🌿 Bye!"));
|
|
1278
|
+
try { engine.destroy(); } catch {}
|
|
1279
|
+
process.exit(130);
|
|
1280
|
+
});
|
|
1281
|
+
process.on("SIGTERM", () => {
|
|
1282
|
+
try { engine.destroy(); } catch {}
|
|
1283
|
+
process.exit(0);
|
|
1284
|
+
});
|
|
1267
1285
|
|
|
1268
1286
|
// Auto-start background server
|
|
1269
1287
|
const serverStatus = await startServerIfNeeded();
|