wispy-cli 2.7.0 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/wispy.mjs +34 -2929
  2. package/package.json +2 -1
package/bin/wispy.mjs CHANGED
@@ -1,2945 +1,50 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Wispy CLI entry point
5
- *
6
- * Flags:
7
- * ui Launch workspace TUI
8
- * --tui Alias for tui (kept for compat)
9
- * --serve Start all configured channel bots
10
- * --telegram Start Telegram bot only
11
- * --discord Start Discord bot only
12
- * --slack Start Slack bot only
13
- * channel setup <name> Interactive channel token setup
14
- * channel list List configured channels
15
- * channel test <name> Test channel connection
16
- * (default) Launch interactive REPL
4
+ * wispy-cli
5
+ * Async refactored dispatcher avoids direct top-level awaits in Node modules.
17
6
  */
7
+ (async () => {
8
+ const { fileURLToPath } = await import('url');
9
+ const { dirname } = await import('path');
10
+ const baseDir = dirname(fileURLToPath(import.meta.url));
18
11
 
19
- import { fileURLToPath } from "node:url";
20
- import path from "node:path";
21
- import { readFileSync } from "node:fs";
12
+ const args = process.argv.slice(2);
22
13
 
23
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+ async function handleCommand(command) {
15
+ const inquirer = (await import('inquirer')).default;
24
16
 
25
- const args = process.argv.slice(2);
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("Auth:")}
134
- ${_cyan("wispy auth")} Show auth status for all providers
135
- ${_cyan("wispy auth github-copilot")} Sign in with GitHub (Copilot OAuth)
136
- ${_cyan("wispy auth refresh <provider>")} Refresh expired OAuth token
137
- ${_cyan("wispy auth revoke <provider>")} Remove saved auth token
138
-
139
- ${_bold("Config & Maintenance:")}
140
- ${_cyan("wispy setup")} Configure wispy interactively ${_dim("(30 AI providers)")}
141
- ${_cyan("wispy model")} Show / switch AI model
142
- ${_cyan("wispy model list")} List available models per provider
143
- ${_cyan("wispy model set <p:model>")} Switch model (e.g. xai:grok-3)
144
- ${_cyan("wispy update")} Update to latest version
145
- ${_cyan("wispy migrate")} Import from OpenClaw
146
- ${_cyan("wispy doctor")} Check system health
147
- ${_cyan("wispy version")} Show version
148
-
149
- ${_bold("Shell Completions:")}
150
- ${_cyan("wispy completion bash")} Bash completion script
151
- ${_cyan("wispy completion zsh")} Zsh completion script
152
-
153
- ${_bold("Flags:")}
154
- ${_cyan("--help, -h")} Show this help
155
- ${_cyan("--version, -v")} Show version
156
- ${_cyan("--debug")} Verbose logs & stack traces
157
- ${_cyan("WISPY_DEBUG=1")} Same as --debug via env
158
-
159
- ${_dim("Run 'wispy help <command>' for detailed help on a specific command.")}
160
- `);
161
- process.exit(0);
162
- }
163
-
164
- // ── help sub-command (detailed per-command help) ──────────────────────────────
165
- if (args[0] === "help") {
166
- const topic = args[1];
167
- const helpTexts = {
168
- ws: `
169
- ${_bold("wispy ws")} — Workstream management
170
-
171
- ${_bold("Usage:")}
172
- wispy ws List all workstreams
173
- wispy ws new <name> Create a new workstream
174
- wispy ws switch <name> Switch to a workstream
175
- wispy ws archive <name> Archive a workstream (move sessions/memory)
176
- wispy ws delete <name> Permanently delete a workstream
177
- wispy ws status Status overview of all workstreams
178
- wispy ws search <query> Search across all workstreams
179
-
180
- ${_bold("Examples:")}
181
- wispy ws new project-x
182
- wispy ws switch project-x
183
- wispy ws archive old-project
184
- `,
185
- trust: `
186
- ${_bold("wispy trust")} — Trust levels and security
187
-
188
- ${_bold("Usage:")}
189
- wispy trust Show current trust level and policies
190
- wispy trust careful Require approval for everything
191
- wispy trust balanced Approve only risky operations
192
- wispy trust yolo Auto-approve everything
193
-
194
- ${_bold("Trust levels:")}
195
- careful — Review every tool call before execution
196
- balanced — Auto-approve safe ops, review destructive ones
197
- yolo — Full automation (use with care!)
198
- `,
199
- deploy: `
200
- ${_bold("wispy deploy")} — Deployment helpers
201
-
202
- ${_bold("Usage:")}
203
- wispy deploy init Generate Dockerfile + compose + .env.example
204
- wispy deploy dockerfile Print Dockerfile
205
- wispy deploy compose Print docker-compose.yml
206
- wispy deploy systemd Print systemd unit
207
- wispy deploy railway Print railway.json
208
- wispy deploy fly Print fly.toml
209
- wispy deploy render Print render.yaml
210
- wispy deploy modal Generate Modal serverless config
211
- wispy deploy daytona Generate Daytona workspace config
212
- wispy deploy vps user@host SSH deploy to VPS
213
- wispy deploy status <url> Check remote server health
214
-
215
- ${_bold("Examples:")}
216
- wispy deploy init
217
- wispy deploy vps root@my.vps
218
- wispy connect https://my.vps:18790 --token <token>
219
- `,
220
- cron: `
221
- ${_bold("wispy cron")} — Scheduled task management
222
-
223
- ${_bold("Usage:")}
224
- wispy cron list List all cron jobs
225
- wispy cron add Interactive job creation
226
- wispy cron remove <id> Delete a cron job
227
- wispy cron run <id> Run a job immediately
228
- wispy cron history <id> Show past runs
229
- wispy cron start Start scheduler in foreground
230
-
231
- ${_bold("Schedule types:")}
232
- cron — standard cron expression (e.g. "0 9 * * *")
233
- every — interval in minutes
234
- at — one-time ISO datetime
235
- `,
236
- channel: `
237
- ${_bold("wispy channel")} — Messaging channel setup
238
-
239
- ${_bold("Usage:")}
240
- wispy channel setup telegram Telegram bot setup
241
- wispy channel setup discord Discord bot setup
242
- wispy channel setup slack Slack bot setup
243
- wispy channel setup whatsapp WhatsApp setup
244
- wispy channel setup email Email setup
245
- wispy channel list List configured channels
246
- wispy channel test <name> Test channel connection
247
-
248
- ${_bold("Running bots:")}
249
- wispy --serve Start all configured bots
250
- wispy --telegram Start Telegram bot only
251
- wispy --discord Start Discord bot only
252
- `,
253
- sync: `
254
- ${_bold("wispy sync")} — Remote sync
255
-
256
- ${_bold("Usage:")}
257
- wispy sync Bidirectional sync
258
- wispy sync push Push local → remote
259
- wispy sync pull Pull remote → local
260
- wispy sync status Show sync status
261
- wispy sync auto Enable auto-sync
262
- wispy sync auto --off Disable auto-sync
263
-
264
- ${_bold("Flags:")}
265
- --remote <url> Remote server URL
266
- --token <token> Auth token
267
- --strategy newer-wins Conflict resolution (newer-wins|local-wins|remote-wins)
268
- --memory-only Only sync memory files
269
- --sessions-only Only sync sessions
270
- `,
271
- skill: `
272
- ${_bold("wispy skill")} — Skill management
273
-
274
- ${_bold("Usage:")}
275
- wispy skill list List all installed skills
276
- wispy skill show <name> Show skill details
277
- wispy teach <name> Create a skill from current conversation
278
- wispy improve <name> Improve an existing skill
279
-
280
- ${_bold("In REPL:")}
281
- /skills List skills
282
- /teach <name> Teach from conversation
283
- /<skill-name> Invoke any skill
284
- `,
285
- doctor: `
286
- ${_bold("wispy doctor")} — System health check
287
-
288
- Checks Node.js version, config, API keys, directory permissions,
289
- optional dependencies, and remote server connectivity.
290
-
291
- ${_bold("Usage:")}
292
- wispy doctor
293
- `,
294
- node: `
295
- ${_bold("wispy node")} — Multi-machine node management
296
-
297
- ${_bold("Usage:")}
298
- wispy node pair Generate pairing code
299
- wispy node connect <code> --url <url> Connect as a node
300
- wispy node list Show registered nodes
301
- wispy node status Ping all nodes
302
- wispy node remove <id> Unregister a node
303
- `,
304
- };
305
-
306
- if (topic && helpTexts[topic]) {
307
- console.log(helpTexts[topic]);
308
- } else if (topic) {
309
- console.log(_yellow(`\n⚠️ No detailed help for '${topic}'. Try 'wispy --help' for the full command list.\n`));
310
- } else {
311
- // Generic help — same as --help
312
- args[0] = "--help";
313
- // Re-trigger help by falling through (not possible here, just print it)
314
- console.log(`Run ${_cyan("wispy --help")} for the full command list.`);
315
- console.log(`Run ${_cyan("wispy help <command>")} for per-command help.`);
316
- console.log(`\nAvailable topics: ${Object.keys(helpTexts).map(k => _cyan(k)).join(", ")}`);
317
- }
318
- process.exit(0);
319
- }
320
-
321
- // ── completion sub-command ────────────────────────────────────────────────────
322
- if (args[0] === "completion") {
323
- const shell = args[1] ?? "bash";
324
-
325
- // Commands with descriptions for rich completions
326
- const cmdSpecs = [
327
- { cmd: "ws", desc: "Workstream management (list/new/switch/archive/delete/status/search)" },
328
- { cmd: "trust", desc: "Trust level & security policies (careful/balanced/yolo)" },
329
- { cmd: "where", desc: "Show current mode & context" },
330
- { cmd: "handoff", desc: "Generate handoff summary & sync" },
331
- { cmd: "skill", desc: "Skill management (list/show)" },
332
- { cmd: "teach", desc: "Create skill from current conversation" },
333
- { cmd: "improve", desc: "Improve an existing skill" },
334
- { cmd: "dry", desc: "Run next command in dry-run (preview-only) mode" },
335
- { cmd: "deploy", desc: "Deployment helpers (Dockerfile/compose/systemd/vps/…)" },
336
- { cmd: "server", desc: "Start the Wispy API server" },
337
- { cmd: "node", desc: "Multi-machine node management (pair/connect/list/status)" },
338
- { cmd: "channel", desc: "Messaging channel setup (telegram/discord/slack/email/…)" },
339
- { cmd: "cron", desc: "Scheduled tasks (list/add/remove/run/history/start)" },
340
- { cmd: "audit", desc: "View audit log & replay sessions" },
341
- { cmd: "log", desc: "Alias for audit" },
342
- { cmd: "sync", desc: "Sync with remote server (push/pull/status/auto)" },
343
- { cmd: "setup", desc: "Interactive setup wizard" },
344
- { cmd: "init", desc: "Alias for setup" },
345
- { cmd: "update", desc: "Update wispy-cli to latest version" },
346
- { cmd: "tui", desc: "Launch the workspace TUI" },
347
- { cmd: "migrate", desc: "Import from OpenClaw or other sources" },
348
- { cmd: "version", desc: "Show version" },
349
- { cmd: "doctor", desc: "Check system health & API keys" },
350
- { cmd: "help", desc: "Show help (optionally for a specific command)" },
351
- { cmd: "completion", desc: "Print shell completion script (bash/zsh/fish)" },
352
- { cmd: "status", desc: "Show wispy status & remote connection info" },
353
- { cmd: "connect", desc: "Connect to a remote Wispy server" },
354
- { cmd: "disconnect", desc: "Disconnect from remote server (go back to local)" },
355
- { cmd: "auth", desc: "OAuth auth management (github-copilot, refresh, revoke)" },
356
- ];
357
-
358
- // Sub-command completions for nested commands
359
- const subCmds = {
360
- ws: ["new", "switch", "archive", "delete", "status", "search"],
361
- trust: ["careful", "balanced", "yolo", "log"],
362
- skill: ["list", "show"],
363
- deploy: ["init", "dockerfile", "compose", "systemd", "railway", "fly", "render", "modal", "daytona", "vps", "status"],
364
- cron: ["list", "add", "remove", "run", "history", "start"],
365
- audit: ["replay", "export", "--today", "--session", "--tool", "--limit"],
366
- channel: ["setup", "list", "test"],
367
- node: ["pair", "connect", "list", "status", "remove"],
368
- sync: ["push", "pull", "status", "auto"],
369
- auth: ["github-copilot", "refresh", "revoke"],
370
- server: ["start", "stop", "status"],
371
- completion: ["bash", "zsh", "fish"],
372
- help: cmdSpecs.map(c => c.cmd),
373
- };
374
-
375
- const cmds = cmdSpecs.map(c => c.cmd);
376
- const flags = ["--help", "--version", "--debug", "--serve", "--telegram", "--discord", "--slack"];
377
-
378
- if (shell === "bash") {
379
- const subCmdsStr = Object.entries(subCmds)
380
- .map(([k, v]) => ` ${k}) COMPREPLY=( $(compgen -W "${v.join(' ')}" -- "\${cur}") ) ;;`)
381
- .join('\n');
382
-
383
- console.log(`# wispy bash completion
384
- # Add to ~/.bashrc: eval "$(wispy completion bash)"
385
- _wispy_completion() {
386
- local cur prev words
387
- COMPREPLY=()
388
- cur="\${COMP_WORDS[COMP_CWORD]}"
389
- prev="\${COMP_WORDS[COMP_CWORD-1]}"
390
- local commands="${cmds.join(" ")}"
391
- local flags="${flags.join(" ")}"
392
-
393
- if [[ \${COMP_CWORD} -eq 1 ]]; then
394
- COMPREPLY=( $(compgen -W "\${commands} \${flags}" -- "\${cur}") )
395
- elif [[ \${COMP_CWORD} -eq 2 ]]; then
396
- case "\${prev}" in
397
- ${subCmdsStr}
398
- esac
399
- fi
400
- }
401
- complete -F _wispy_completion wispy`);
402
-
403
- } else if (shell === "zsh") {
404
- const cmdDescLines = cmdSpecs.map(c => ` '${c.cmd}:${c.desc.replace(/'/g, "''")}'`).join('\n');
405
- const subCmdLines = Object.entries(subCmds)
406
- .map(([k, v]) => ` (${k}) _values '${k} commands' ${v.map(s => `'${s}'`).join(' ')} ;;`)
407
- .join('\n');
408
-
409
- console.log(`# wispy zsh completion
410
- # Add to ~/.zshrc: eval "$(wispy completion zsh)"
411
- # Or for permanent use: wispy completion zsh > /usr/local/share/zsh/site-functions/_wispy
412
- _wispy() {
413
- local context state state_descr line
414
- typeset -A opt_args
415
-
416
- local -a commands
417
- commands=(
418
- ${cmdDescLines}
419
- )
420
-
421
- _arguments -C \\
422
- '(- *)'{-h,--help}'[Show help]' \\
423
- '(- *)'{-v,--version}'[Show version]' \\
424
- '--debug[Enable verbose logs]' \\
425
- '--serve[Start all channel bots]' \\
426
- '--telegram[Start Telegram bot]' \\
427
- '--discord[Start Discord bot]' \\
428
- '--slack[Start Slack bot]' \\
429
- '1:command:->cmd' \\
430
- '*::arg:->args'
431
-
432
- case $state in
433
- cmd)
434
- _describe 'wispy commands' commands
435
- ;;
436
- args)
437
- case $line[1] in
438
- ${subCmdLines}
439
- esac
440
- ;;
441
- esac
442
- }
443
- compdef _wispy wispy`);
444
-
445
- } else if (shell === "fish") {
446
- const mainComps = cmdSpecs
447
- .map(c => `complete -c wispy -f -n '__fish_use_subcommand' -a '${c.cmd}' -d '${c.desc.replace(/'/g, "\\'")}'`)
448
- .join("\n");
449
- const subComps = Object.entries(subCmds)
450
- .flatMap(([k, v]) => v.map(s => `complete -c wispy -f -n '__fish_seen_subcommand_from ${k}' -a '${s}'`))
451
- .join("\n");
452
-
453
- console.log(`# wispy fish completion
454
- # Save to ~/.config/fish/completions/wispy.fish
455
- ${mainComps}
456
- ${subComps}`);
457
-
458
- } else {
459
- console.error(_red(`❌ Unknown shell: ${shell}. Use: bash, zsh, or fish`));
460
- process.exit(2);
461
- }
462
- process.exit(0);
463
- }
464
-
465
- // ── doctor sub-command ────────────────────────────────────────────────────────
466
- if (args[0] === "doctor") {
467
- const { access, readFile: rf, stat } = await import("node:fs/promises");
468
- const { homedir } = await import("node:os");
469
- const wispyDir = path.join(homedir(), ".wispy");
470
- const configPath = path.join(wispyDir, "config.json");
471
- const memoryDir = path.join(wispyDir, "memory");
472
- const sessionsDir = path.join(wispyDir, "sessions");
473
-
474
- console.log(`\n${_bold("🩺 Wispy Doctor")} ${_dim(`v${getVersion()}`)}\n`);
475
-
476
- let allOk = true;
477
- const issues = [];
478
-
479
- function check(label, ok, detail = "") {
480
- if (ok) {
481
- console.log(` ${_green("✅")} ${label}${detail ? _dim(" " + detail) : ""}`);
482
- } else {
483
- console.log(` ${_red("❌")} ${label}${detail ? _dim(" " + detail) : ""}`);
484
- allOk = false;
485
- issues.push(label);
486
- }
487
- }
488
-
489
- function info(label, detail) {
490
- console.log(` ${_cyan("ℹ️ ")} ${label}${detail ? _dim(" " + detail) : ""}`);
491
- }
492
-
493
- // 1. Node.js version
494
- const [major] = process.version.replace("v", "").split(".").map(Number);
495
- check("Node.js version", major >= 18, `${process.version} (required: >= 18)`);
496
-
497
- // 2. Config file
498
- let config = null;
499
- try {
500
- const raw = await rf(configPath, "utf8");
501
- config = JSON.parse(raw);
502
- check("Config file", true, configPath);
503
- } catch (e) {
504
- if (e.code === "ENOENT") {
505
- check("Config file", false, `Not found at ${configPath} — run 'wispy setup'`);
506
- } else {
507
- check("Config file", false, `Malformed JSON — run 'wispy setup' to reconfigure`);
508
- }
509
- }
510
-
511
- // 3. API key configured
512
- if (config) {
513
- const envMap = {
514
- google: ["GOOGLE_AI_KEY", "GOOGLE_GENERATIVE_AI_KEY", "GEMINI_API_KEY"],
515
- anthropic: ["ANTHROPIC_API_KEY"],
516
- openai: ["OPENAI_API_KEY"],
517
- groq: ["GROQ_API_KEY"],
518
- openrouter: ["OPENROUTER_API_KEY"],
519
- deepseek: ["DEEPSEEK_API_KEY"],
520
- xai: ["XAI_API_KEY"],
521
- mistral: ["MISTRAL_API_KEY"],
522
- together: ["TOGETHER_API_KEY"],
523
- nvidia: ["NVIDIA_API_KEY"],
524
- kimi: ["MOONSHOT_API_KEY", "KIMI_API_KEY"],
525
- minimax: ["MINIMAX_API_KEY"],
526
- chutes: ["CHUTES_API_KEY"],
527
- venice: ["VENICE_API_KEY"],
528
- huggingface: ["HF_TOKEN", "HUGGINGFACE_API_KEY"],
529
- cloudflare: ["CF_API_TOKEN"],
530
- volcengine: ["VOLCENGINE_API_KEY", "ARK_API_KEY"],
531
- byteplus: ["BYTEPLUS_API_KEY"],
532
- zai: ["ZAI_API_KEY", "GLM_API_KEY", "ZHIPU_API_KEY"],
533
- dashscope: ["DASHSCOPE_API_KEY"],
534
- xiaomi: ["XIAOMI_API_KEY"],
535
- vercelai: ["VERCEL_AI_TOKEN"],
536
- litellm: ["LITELLM_API_KEY"],
537
- ollama: null, // no key
538
- vllm: null, // no key
539
- sglang: null, // no key
540
- };
541
- // Support both old (config.provider) and new (config.providers) format
542
- const providers = config.providers ? Object.keys(config.providers) : (config.provider ? [config.provider] : []);
543
- if (providers.length === 0) {
544
- check("AI provider", false, "no provider configured — run 'wispy setup provider'");
545
- } else {
546
- for (const provider of providers) {
547
- const envKeys = envMap[provider];
548
- if (envKeys === null) {
549
- check(`AI provider (${provider})`, true, "no key needed");
550
- } else if (envKeys) {
551
- const key = config.providers?.[provider]?.apiKey || config.apiKey
552
- || envKeys.reduce((found, k) => found || process.env[k], null);
553
- check(`API key (${provider})`, !!key && key.length > 8, key ? "configured" : `set env var or run 'wispy setup provider'`);
554
- } else {
555
- check(`AI provider (${provider})`, false, `unknown provider`);
556
- }
557
- }
558
- }
559
- } else {
560
- info("AI provider", "skipped (no config)");
561
- }
562
-
563
- // 4. Memory directory writable
564
- try {
565
- const { mkdir: mkd, writeFile: wf, unlink: unl } = await import("node:fs/promises");
566
- await mkd(memoryDir, { recursive: true });
567
- const testFile = path.join(memoryDir, ".write-test");
568
- await wf(testFile, "test", "utf8");
569
- await unl(testFile);
570
- check("Memory dir writable", true, memoryDir);
571
- } catch (e) {
572
- check("Memory dir writable", false, e.message);
573
- }
574
-
575
- // 5. Sessions directory writable
576
- try {
577
- const { mkdir: mkd, writeFile: wf, unlink: unl } = await import("node:fs/promises");
578
- await mkd(sessionsDir, { recursive: true });
579
- const testFile = path.join(sessionsDir, ".write-test");
580
- await wf(testFile, "test", "utf8");
581
- await unl(testFile);
582
- check("Sessions dir writable", true, sessionsDir);
583
- } catch (e) {
584
- check("Sessions dir writable", false, e.message);
585
- }
586
-
587
- // 6. Optional deps
588
- console.log(`\n ${_bold("Optional dependencies:")}`);
589
- const optDeps = [
590
- { pkg: "grammy", label: "Telegram (grammy)" },
591
- { pkg: "discord.js", label: "Discord (discord.js)" },
592
- { pkg: "@slack/bolt", label: "Slack (@slack/bolt)" },
593
- { pkg: "whatsapp-web.js", label: "WhatsApp (whatsapp-web.js)" },
594
- { pkg: "nodemailer", label: "Email (nodemailer)" },
595
- { pkg: "imapflow", label: "Email IMAP (imapflow)" },
596
- ];
597
- for (const dep of optDeps) {
598
- try {
599
- await import(dep.pkg);
600
- check(dep.label, true, "installed");
601
- } catch {
602
- console.log(` ${_dim("–")} ${_dim(dep.label + " not installed (optional)")}`);
603
- }
604
- }
605
-
606
- // 7. Remote server (if configured)
607
- const remotePath = path.join(wispyDir, "remote.json");
608
- try {
609
- const remote = JSON.parse(await rf(remotePath, "utf8"));
610
- if (remote?.url) {
611
- console.log(`\n ${_bold("Remote server:")}`);
612
- process.stdout.write(` ${_cyan("🔄")} Checking ${remote.url}... `);
613
- try {
614
- const resp = await fetch(`${remote.url}/api/health`, { signal: AbortSignal.timeout(5000) });
615
- if (resp.ok) {
616
- console.log(_green("✅ reachable"));
617
- } else {
618
- console.log(_yellow(`⚠️ HTTP ${resp.status}`));
619
- allOk = false;
620
- }
621
- } catch {
622
- console.log(_red("❌ unreachable"));
623
- allOk = false;
624
- issues.push("Remote server unreachable");
625
- }
626
- }
627
- } catch {}
628
-
629
- console.log("");
630
- if (allOk) {
631
- console.log(`${_green("✅ All checks passed!")}\n`);
632
- } else {
633
- console.log(`${_yellow("⚠️ Issues found:")} ${issues.join(", ")}`);
634
- console.log(_dim(" Run 'wispy setup' to fix configuration issues.\n"));
635
- }
636
- process.exit(allOk ? 0 : 1);
637
- }
638
-
639
- // ── config validation helper ───────────────────────────────────────────────────
640
- async function validateConfigOnStartup() {
641
- const { homedir } = await import("node:os");
642
- const { readFile: rf } = await import("node:fs/promises");
643
- const configPath = path.join(homedir(), ".wispy", "config.json");
644
- try {
645
- const raw = await rf(configPath, "utf8");
646
- JSON.parse(raw);
647
- } catch (e) {
648
- if (e instanceof SyntaxError) {
649
- console.error(_red("❌ Config file corrupted. Run 'wispy setup' to reconfigure."));
650
- process.exit(1);
651
- }
652
- // File not found is OK (first run)
653
- }
654
- }
655
-
656
- // ── ws sub-command ────────────────────────────────────────────────────────────
657
- if (args[0] === "ws") {
658
- try {
659
- const { handleWsCommand } = await import(
660
- path.join(__dirname, "..", "lib", "commands", "ws.mjs")
661
- );
662
- await handleWsCommand(args);
663
- } catch (e) { friendlyError(e); }
664
- process.exit(0);
665
- }
666
-
667
- // ── trust sub-command ─────────────────────────────────────────────────────────
668
- if (args[0] === "trust") {
669
- try {
670
- const { handleTrustCommand } = await import(
671
- path.join(__dirname, "..", "lib", "commands", "trust.mjs")
672
- );
673
- await handleTrustCommand(args);
674
- } catch (e) { friendlyError(e); }
675
- process.exit(0);
676
- }
677
-
678
- // ── where sub-command ─────────────────────────────────────────────────────────
679
- if (args[0] === "where") {
680
- try {
681
- const { cmdWhere } = await import(
682
- path.join(__dirname, "..", "lib", "commands", "continuity.mjs")
683
- );
684
- await cmdWhere();
685
- } catch (e) { friendlyError(e); }
686
- process.exit(0);
687
- }
688
-
689
- // ── handoff sub-command ───────────────────────────────────────────────────────
690
- if (args[0] === "handoff") {
691
- try {
692
- const { handleContinuityCommand } = await import(
693
- path.join(__dirname, "..", "lib", "commands", "continuity.mjs")
694
- );
695
- await handleContinuityCommand(args);
696
- } catch (e) { friendlyError(e); }
697
- process.exit(0);
698
- }
699
-
700
- // ── skill sub-command ─────────────────────────────────────────────────────────
701
- if (args[0] === "skill") {
702
- try {
703
- const { handleSkillCommand } = await import(
704
- path.join(__dirname, "..", "lib", "commands", "skills-cmd.mjs")
705
- );
706
- await handleSkillCommand(args);
707
- } catch (e) { friendlyError(e); }
708
- process.exit(0);
709
- }
710
-
711
- // ── teach sub-command ─────────────────────────────────────────────────────────
712
- if (args[0] === "teach") {
713
- try {
714
- const { cmdTeach } = await import(
715
- path.join(__dirname, "..", "lib", "commands", "skills-cmd.mjs")
716
- );
717
- await cmdTeach(args[1]);
718
- } catch (e) { friendlyError(e); }
719
- process.exit(0);
720
- }
721
-
722
- // ── improve sub-command ───────────────────────────────────────────────────────
723
- if (args[0] === "improve") {
724
- try {
725
- const { cmdImproveSkill } = await import(
726
- path.join(__dirname, "..", "lib", "commands", "skills-cmd.mjs")
727
- );
728
- const name = args[1];
729
- const feedback = args.slice(2).join(" ").replace(/^["']|["']$/g, "");
730
- await cmdImproveSkill(name, feedback);
731
- } catch (e) { friendlyError(e); }
732
- process.exit(0);
733
- }
734
-
735
- // ── dry sub-command ───────────────────────────────────────────────────────────
736
- if (args[0] === "dry") {
737
- // Re-launch wispy with DRY_RUN env set, passing remaining args
738
- const { spawn } = await import("node:child_process");
739
- const remaining = args.slice(1);
740
- const child = spawn(process.execPath, [process.argv[1], ...remaining], {
741
- stdio: "inherit",
742
- env: { ...process.env, WISPY_DRY_RUN: "1" },
743
- });
744
- child.on("exit", (code) => process.exit(code ?? 0));
745
- await new Promise(() => {}); // keep alive until child exits
746
- }
747
-
748
- // ── setup / init sub-command ──────────────────────────────────────────────────
749
- if (args[0] === "setup" || args[0] === "init") {
750
- // Handle Ctrl+C gracefully in setup
751
- process.removeAllListeners("SIGINT");
752
- process.on("SIGINT", () => { console.log(_dim("\nSetup cancelled.")); process.exit(130); });
753
- try {
754
- const { OnboardingWizard } = await import(
755
- path.join(__dirname, "..", "core", "onboarding.mjs")
756
- );
757
- const wizard = new OnboardingWizard();
758
- const sub = args[1]; // e.g. "provider", "channels", "security"
759
- if (sub && sub !== "wizard") {
760
- await wizard.runStep(sub);
761
- } else {
762
- await wizard.run();
763
- }
764
- } catch (e) { friendlyError(e); }
765
-
766
- // Shell completion tip
767
- console.log(_dim(`
768
- 💡 Tip: Enable tab completion in your shell:
769
- eval "$(wispy completion zsh)" # zsh — add to ~/.zshrc for permanent
770
- eval "$(wispy completion bash)" # bash — add to ~/.bashrc for permanent
771
- `));
772
- process.exit(0);
773
- }
774
-
775
- // ── update sub-command ────────────────────────────────────────────────────────
776
- if (args[0] === "update") {
777
- try {
778
- const { execSync } = await import("node:child_process");
779
- console.log(_cyan("🔄 Checking for updates..."));
780
- const current = getVersion();
781
- const latest = execSync("npm info wispy-cli version", { encoding: "utf8" }).trim();
782
- if (current === latest) {
783
- console.log(_green(`✅ Already on latest version (${current})`));
784
- } else {
785
- console.log(`📦 ${_dim(current)} → ${_bold(latest)}`);
786
- console.log(_cyan("🔄 Updating..."));
787
- execSync("npm install -g wispy-cli@latest", { stdio: "inherit" });
788
- console.log(_green(`\n✅ Updated to ${latest}`));
789
- }
790
- } catch (e) { friendlyError(e); }
791
- process.exit(0);
792
- }
793
-
794
- // ── model sub-command ─────────────────────────────────────────────────────────
795
- if (args[0] === "model") {
796
- const { loadConfig, saveConfig, PROVIDERS } = await import(
797
- path.join(__dirname, "..", "core", "config.mjs")
798
- );
799
-
800
- const KNOWN_MODELS = {
801
- google: ["gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash", "gemini-1.5-pro"],
802
- anthropic: ["claude-sonnet-4-20250514", "claude-opus-4-6", "claude-haiku-3.5", "claude-3-5-sonnet-20241022"],
803
- openai: ["gpt-4o", "gpt-4o-mini", "gpt-4.1", "gpt-4.1-mini", "o4-mini", "o3"],
804
- xai: ["grok-3", "grok-3-mini", "grok-2-1212"],
805
- mistral: ["mistral-large-latest", "mistral-small-latest", "codestral-latest", "open-mistral-nemo"],
806
- groq: ["llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768", "gemma2-9b-it"],
807
- deepseek: ["deepseek-chat", "deepseek-reasoner"],
808
- together: ["meta-llama/Llama-3.3-70B-Instruct-Turbo", "Qwen/Qwen2.5-72B-Instruct-Turbo"],
809
- openrouter: ["anthropic/claude-sonnet-4-20250514", "openai/gpt-4o", "google/gemini-2.5-flash"],
810
- chutes: ["deepseek-ai/DeepSeek-V3-0324", "deepseek-ai/DeepSeek-R1"],
811
- nvidia: ["meta/llama-3.3-70b-instruct", "nvidia/llama-3.1-nemotron-70b-instruct"],
812
- kimi: ["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"],
813
- zai: ["glm-4-flash", "glm-4-plus", "glm-z1-flash"],
814
- dashscope: ["qwen-max", "qwen-plus", "qwen-turbo"],
815
- volcengine: ["doubao-pro-32k", "doubao-lite-32k"],
816
- ollama: ["llama3.2", "llama3.1", "qwen2.5", "phi3", "mistral"],
817
- };
818
-
819
- const sub = args[1];
820
- const config = await loadConfig();
821
- const configuredProviders = config.providers ? Object.keys(config.providers) : (config.provider ? [config.provider] : []);
822
-
823
- // wispy model (no args) — interactive menu
824
- if (!sub) {
825
- const defaultP = config.defaultProvider ?? configuredProviders[0];
826
- const currentModel = config.providers?.[defaultP]?.model ?? config.model ?? PROVIDERS[defaultP]?.defaultModel ?? "unknown";
827
- console.log(`\n🤖 ${_bold("Current model")}: ${_cyan(defaultP)}:${_bold(currentModel)}\n`);
828
-
829
- try {
830
- const { select } = await import("@inquirer/prompts");
831
-
832
- // Build choices from configured providers only
833
- const modelChoices = [];
834
- for (const p of configuredProviders) {
835
- const models = KNOWN_MODELS[p] ?? [];
836
- for (const m of models) {
837
- const cur = config.providers?.[p]?.model ?? PROVIDERS[p]?.defaultModel;
838
- modelChoices.push({
839
- name: `${m} (${p})${cur === m ? _dim(" ✓ current") : ""}`,
840
- value: `${p}:${m}`,
841
- short: `${p}:${m}`,
842
- });
843
- }
844
- }
845
-
846
- if (modelChoices.length === 0) {
847
- console.log(_dim(" No models available. Run 'wispy setup' to configure a provider.\n"));
848
- process.exit(0);
849
- }
850
-
851
- let modelChoice;
852
- try {
853
- modelChoice = await select({ message: "Switch model:", choices: modelChoices });
854
- } catch (e) {
855
- if (e.name === "ExitPromptError") { process.exit(130); }
856
- throw e;
857
- }
858
-
859
- const colonIdx = modelChoice.indexOf(":");
860
- const provName = modelChoice.slice(0, colonIdx);
861
- const modelName = modelChoice.slice(colonIdx + 1);
862
-
863
- if (!config.providers) config.providers = {};
864
- if (!config.providers[provName]) config.providers[provName] = {};
865
- config.providers[provName].model = modelName;
866
- if (!config.defaultProvider) config.defaultProvider = provName;
867
- await saveConfig(config);
868
- console.log(_green(`\n✅ Switched to ${_cyan(provName)}:${_bold(modelName)}\n`));
869
- } catch (e) {
870
- if (e.name === "ExitPromptError") { process.exit(130); }
871
- // Fallback — just show current
872
- if (configuredProviders.length > 1) {
873
- console.log(` Configured providers: ${configuredProviders.join(", ")}`);
874
- console.log(` ${_dim("Use 'wispy model list' to see all options")}`);
875
- console.log(` ${_dim("Use 'wispy model set <provider:model>' to switch")}\n`);
876
- }
877
- }
878
- process.exit(0);
879
- }
880
-
881
- // wispy model list
882
- if (sub === "list") {
883
- console.log(`\n🤖 ${_bold("Available models")}\n`);
884
- for (const p of configuredProviders) {
885
- const currentModel = config.providers?.[p]?.model ?? PROVIDERS[p]?.defaultModel ?? "";
886
- const models = KNOWN_MODELS[p] ?? [currentModel];
887
- console.log(` ${_cyan(p)}:`);
888
- for (const m of models) {
889
- const isCurrent = m === currentModel;
890
- console.log(` ${isCurrent ? _green("●") : " "} ${m}${isCurrent ? _dim(" (current)") : ""}`);
891
- }
892
- }
893
- console.log(`\n ${_dim("Switch with: wispy model set <provider:model>")}\n`);
894
- process.exit(0);
895
- }
896
-
897
- // wispy model set <provider:model>
898
- if (sub === "set") {
899
- const spec = args[2];
900
- if (!spec || !spec.includes(":")) {
901
- console.error(_red(`\n❌ Usage: wispy model set <provider:model>\n Example: wispy model set xai:grok-3\n`));
902
- process.exit(1);
903
- }
904
- const colonIdx = spec.indexOf(":");
905
- const provName = spec.slice(0, colonIdx);
906
- const modelName = spec.slice(colonIdx + 1);
907
-
908
- if (!PROVIDERS[provName]) {
909
- console.error(_red(`\n❌ Unknown provider: ${provName}\n Available: ${Object.keys(PROVIDERS).join(", ")}\n`));
910
- process.exit(1);
911
- }
912
-
913
- // Update config
914
- if (!config.providers) config.providers = {};
915
- if (!config.providers[provName]) config.providers[provName] = {};
916
- config.providers[provName].model = modelName;
917
- if (!config.defaultProvider) config.defaultProvider = provName;
918
-
919
- await saveConfig(config);
920
- console.log(_green(`\n✅ Switched to ${_cyan(provName)}:${_bold(modelName)}\n`));
921
- process.exit(0);
922
- }
923
-
924
- console.error(_red(`\n❌ Unknown subcommand: wispy model ${sub}\n`));
925
- console.log(_dim(" Usage: wispy model | wispy model list | wispy model set <provider:model>\n"));
926
- process.exit(1);
927
- }
928
-
929
- // ── config sub-command ────────────────────────────────────────────────────────
930
- if (args[0] === "config") {
931
- const { loadConfig, saveConfig, WISPY_DIR, CONFIG_PATH } = await import(
932
- path.join(__dirname, "..", "core", "config.mjs")
933
- );
934
- const sub = args[1];
935
- const config = await loadConfig();
936
-
937
- if (!sub || sub === "show") {
938
- // wispy config — interactive config menu
939
- const { select: cfgSelect } = await import("@inquirer/prompts");
940
-
941
- // Show current status first
942
- const providers = config.providers ? Object.keys(config.providers) : (config.provider ? [config.provider] : []);
943
- const providerStr = providers.length > 0 ? providers.join(", ") : _dim("not set");
944
- const defaultProv = config.defaultProvider ?? config.provider ?? _dim("not set");
945
- const security = config.security ?? config.securityLevel ?? _dim("not set");
946
- const language = config.language ?? _dim("auto");
947
- const wsName = config.workstream ?? "default";
948
-
949
- console.log(`\n${_bold("Wispy Config")}\n`);
950
- console.log(` Providers: ${providerStr}`);
951
- console.log(` Default: ${defaultProv}`);
952
- console.log(` Security: ${security}`);
953
- console.log(` Language: ${language}`);
954
- console.log(` Workstream: ${wsName}`);
955
- console.log(` Config file: ${_dim(CONFIG_PATH)}`);
956
- console.log("");
957
-
958
- const action = await cfgSelect({
959
- message: "What do you want to change?",
960
- choices: [
961
- { name: "Providers — add/remove AI providers", value: "provider" },
962
- { name: "Security — change trust level", value: "security" },
963
- { name: "Language — set preferred language", value: "language" },
964
- { name: "Channels — configure messaging bots", value: "channels" },
965
- { name: "Server — cloud/server settings", value: "server" },
966
- { name: "View raw config (JSON)", value: "raw" },
967
- { name: "Reset everything", value: "reset" },
968
- { name: "Done", value: "done" },
969
- ],
970
- });
971
-
972
- if (action === "done") {
973
- process.exit(0);
974
- } else if (action === "raw") {
975
- const display = JSON.parse(JSON.stringify(config));
976
- if (display.providers) {
977
- for (const [k, v] of Object.entries(display.providers)) {
978
- if (v.apiKey) v.apiKey = v.apiKey.slice(0, 6) + "..." + v.apiKey.slice(-4);
979
- }
980
- }
981
- console.log(JSON.stringify(display, null, 2));
982
- } else if (action === "reset") {
983
- const { confirm: cfgConfirm } = await import("@inquirer/prompts");
984
- const yes = await cfgConfirm({ message: "Reset all configuration?", default: false });
985
- if (yes) {
986
- const { writeFile: wf } = await import("node:fs/promises");
987
- await wf(CONFIG_PATH, "{}\n");
988
- console.log(`${_green("✓")} Config reset. Run 'wispy setup' to reconfigure.`);
989
- }
990
- } else {
991
- // Delegate to setup wizard step
992
- const { OnboardingWizard } = await import(
993
- path.join(__dirname, "..", "core", "onboarding.mjs")
994
- );
995
- const wizard = new OnboardingWizard();
996
- await wizard.runStep(action);
997
- }
998
-
999
- } else if (sub === "get") {
1000
- // wispy config get <key>
1001
- const key = args[2];
1002
- if (!key) { console.error(_red("Usage: wispy config get <key>")); process.exit(2); }
1003
- const val = key.split(".").reduce((o, k) => o?.[k], config);
1004
- if (val === undefined) {
1005
- console.log(_dim(`(not set)`));
1006
- } else {
1007
- console.log(typeof val === "object" ? JSON.stringify(val, null, 2) : String(val));
1008
- }
1009
-
1010
- } else if (sub === "set") {
1011
- // wispy config set <key> <value>
1012
- const key = args[2];
1013
- const value = args.slice(3).join(" ");
1014
- if (!key || !value) { console.error(_red("Usage: wispy config set <key> <value>")); process.exit(2); }
1015
-
1016
- // Parse value
1017
- let parsed = value;
1018
- if (value === "true") parsed = true;
1019
- else if (value === "false") parsed = false;
1020
- else if (/^\d+$/.test(value)) parsed = parseInt(value);
1021
-
1022
- // Set nested key
1023
- const keys = key.split(".");
1024
- let obj = config;
1025
- for (let i = 0; i < keys.length - 1; i++) {
1026
- if (!obj[keys[i]]) obj[keys[i]] = {};
1027
- obj = obj[keys[i]];
1028
- }
1029
- obj[keys[keys.length - 1]] = parsed;
1030
- await saveConfig(config);
1031
- console.log(`${_green("✓")} ${key} = ${parsed}`);
1032
-
1033
- } else if (sub === "delete" || sub === "unset") {
1034
- // wispy config delete <key>
1035
- const key = args[2];
1036
- if (!key) { console.error(_red("Usage: wispy config delete <key>")); process.exit(2); }
1037
- const keys = key.split(".");
1038
- let obj = config;
1039
- for (let i = 0; i < keys.length - 1; i++) {
1040
- if (!obj[keys[i]]) break;
1041
- obj = obj[keys[i]];
1042
- }
1043
- delete obj[keys[keys.length - 1]];
1044
- await saveConfig(config);
1045
- console.log(`${_green("✓")} ${key} removed`);
1046
-
1047
- } else if (sub === "reset") {
1048
- // wispy config reset
1049
- const { confirm } = await import("@inquirer/prompts");
1050
- const yes = await confirm({ message: "Reset all configuration? This cannot be undone.", default: false });
1051
- if (yes) {
1052
- const { writeFile } = await import("node:fs/promises");
1053
- await writeFile(CONFIG_PATH, "{}\n");
1054
- console.log(`${_green("✓")} Config reset. Run 'wispy setup' to reconfigure.`);
1055
- } else {
1056
- console.log(_dim("Cancelled."));
1057
- }
1058
-
1059
- } else if (sub === "path") {
1060
- // wispy config path
1061
- console.log(CONFIG_PATH);
1062
-
1063
- } else if (sub === "edit") {
1064
- // wispy config edit — open in $EDITOR
1065
- const editor = process.env.EDITOR ?? process.env.VISUAL ?? "nano";
1066
- const { spawn: sp } = await import("node:child_process");
1067
- sp(editor, [CONFIG_PATH], { stdio: "inherit" });
1068
-
1069
- } else {
1070
- console.log(`
1071
- ${_bold("wispy config")} — manage configuration
1072
-
1073
- ${_cyan("wispy config")} Show current config (keys masked)
1074
- ${_cyan("wispy config get <key>")} Get a specific value (dot notation)
1075
- ${_cyan("wispy config set <key> <val>")} Set a value
1076
- ${_cyan("wispy config delete <key>")} Remove a key
1077
- ${_cyan("wispy config reset")} Reset to defaults
1078
- ${_cyan("wispy config path")} Show config file path
1079
- ${_cyan("wispy config edit")} Open in $EDITOR
1080
-
1081
- ${_dim("Examples:")}
1082
- ${_dim("wispy config get defaultProvider")}
1083
- ${_dim("wispy config set security careful")}
1084
- ${_dim("wispy config set language ko")}
1085
- `);
1086
- }
1087
- process.exit(0);
1088
- }
1089
-
1090
- // ── status sub-command ────────────────────────────────────────────────────────
1091
- if (args[0] === "status") {
1092
- // Try the enhanced status from onboarding.mjs first
1093
- try {
1094
- const { printStatus } = await import(
1095
- path.join(__dirname, "..", "core", "onboarding.mjs")
1096
- );
1097
- await printStatus();
1098
- process.exit(0);
1099
- } catch (e) { if (DEBUG) console.error(e); }
1100
-
1101
- // Fallback: original status (remote check)
1102
- const { readFile } = await import("node:fs/promises");
1103
- const { homedir } = await import("node:os");
1104
- const { join } = await import("node:path");
1105
- const { DeployManager } = await import(
1106
- path.join(__dirname, "..", "core", "deploy.mjs")
1107
- );
1108
-
1109
- const remotePath = join(homedir(), ".wispy", "remote.json");
1110
- let remote = null;
1111
- try {
1112
- remote = JSON.parse(await readFile(remotePath, "utf8"));
1113
- } catch {}
1114
-
1115
- const bold = (s) => `\x1b[1m${s}\x1b[0m`;
1116
- const green = (s) => `\x1b[32m${s}\x1b[0m`;
1117
- const dim = (s) => `\x1b[2m${s}\x1b[0m`;
1118
- const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
1119
- const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
1120
-
1121
- console.log(`\n🌿 ${bold("Wispy Status")}\n`);
1122
-
1123
- if (remote?.url) {
1124
- console.log(` Mode: ${yellow("remote")}`);
1125
- console.log(` Server: ${cyan(remote.url)}`);
1126
- console.log(` Token: ${dim(remote.token ? remote.token.slice(0, 8) + "..." : "none")}`);
1127
-
1128
- const dm = new DeployManager();
1129
- process.stdout.write(" Health: checking... ");
1130
- const status = await dm.checkRemote(remote.url);
1131
- if (status.alive) {
1132
- console.log(green("✓ alive"));
1133
- if (status.version) console.log(` Version: ${status.version}`);
1134
- if (status.uptime != null) console.log(` Uptime: ${Math.floor(status.uptime)}s`);
1135
- if (status.latency) console.log(` Latency: ${status.latency}ms`);
1136
- } else {
1137
- console.log(`\x1b[31m✗ unreachable\x1b[0m`);
1138
- if (status.error) console.log(` Error: ${dim(status.error)}`);
1139
- }
1140
- } else {
1141
- console.log(` Mode: ${green("local")}`);
1142
- console.log(` Server: http://localhost:18790 ${dim("(when running wispy server)")}`);
1143
- console.log(dim("\n Tip: use `wispy connect <url> --token <token>` to use a remote server"));
1144
- }
1145
- console.log("");
1146
- process.exit(0);
1147
- }
1148
-
1149
- // ── connect sub-command ───────────────────────────────────────────────────────
1150
- if (args[0] === "connect" && args[1]) {
1151
- const { writeFile, mkdir } = await import("node:fs/promises");
1152
- const { homedir } = await import("node:os");
1153
- const { join } = await import("node:path");
1154
- const { DeployManager } = await import(
1155
- path.join(__dirname, "..", "core", "deploy.mjs")
1156
- );
1157
-
1158
- const url = args[1].replace(/\/$/, "");
1159
- const tokenIdx = args.indexOf("--token");
1160
- const token = tokenIdx !== -1 ? args[tokenIdx + 1] : "";
1161
-
1162
- const green = (s) => `\x1b[32m${s}\x1b[0m`;
1163
- const red = (s) => `\x1b[31m${s}\x1b[0m`;
1164
- const dim = (s) => `\x1b[2m${s}\x1b[0m`;
1165
-
1166
- process.stdout.write(`\n🔗 Checking ${url}... `);
1167
- const dm = new DeployManager();
1168
- const status = await dm.checkRemote(url);
1169
-
1170
- if (!status.alive) {
1171
- console.log(red("unreachable"));
1172
- if (status.error) console.log(dim(` ${status.error}`));
1173
- console.log(red("\n❌ Could not connect to remote wispy server."));
1174
- process.exit(1);
1175
- }
1176
-
1177
- console.log(green("✓ alive"));
1178
-
1179
- const wispyDir = join(homedir(), ".wispy");
1180
- await mkdir(wispyDir, { recursive: true });
1181
- await writeFile(
1182
- join(wispyDir, "remote.json"),
1183
- JSON.stringify({ url, token, connectedAt: new Date().toISOString() }, null, 2),
1184
- "utf8"
1185
- );
1186
-
1187
- console.log(green(`\n✅ Connected to ${url}`));
1188
- console.log(dim(" Local wispy will now proxy to the remote server."));
1189
- console.log(dim(" Run `wispy disconnect` to go back to local mode.\n"));
1190
- process.exit(0);
1191
- }
1192
-
1193
- // ── disconnect sub-command ────────────────────────────────────────────────────
1194
- if (args[0] === "disconnect") {
1195
- const { unlink } = await import("node:fs/promises");
1196
- const { homedir } = await import("node:os");
1197
- const { join } = await import("node:path");
1198
-
1199
- const remotePath = join(homedir(), ".wispy", "remote.json");
1200
- try {
1201
- await unlink(remotePath);
1202
- console.log("\n✅ Disconnected. Wispy is back in local mode.\n");
1203
- } catch {
1204
- console.log("\n🌿 Already in local mode.\n");
1205
- }
1206
- process.exit(0);
1207
- }
1208
-
1209
- // ── sync sub-command ──────────────────────────────────────────────────────────
1210
- if (args[0] === "sync") {
1211
- const { SyncManager } = await import(
1212
- path.join(__dirname, "..", "core", "sync.mjs")
1213
- );
1214
-
1215
- const sub = args[1]; // push | pull | status | auto | (undefined = bidirectional)
1216
-
1217
- const green = (s) => `\x1b[32m${s}\x1b[0m`;
1218
- const red = (s) => `\x1b[31m${s}\x1b[0m`;
1219
- const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
1220
- const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
1221
- const bold = (s) => `\x1b[1m${s}\x1b[0m`;
1222
- const dim = (s) => `\x1b[2m${s}\x1b[0m`;
1223
-
1224
- // Interactive sync menu when no subcommand
1225
- if (!sub) {
1226
- try {
1227
- const { select, Separator } = await import("@inquirer/prompts");
1228
- const cfg = await SyncManager.loadConfig();
1229
- const remoteUrl = cfg.remoteUrl;
1230
-
1231
- if (!remoteUrl) {
1232
- console.log(_dim("\nNo remote configured. Connect first: wispy connect <url>\n"));
1233
- process.exit(0);
1234
- }
1235
-
1236
- console.log(`\nRemote: ${_cyan(remoteUrl)} ✓\n`);
1237
-
1238
- let syncAction;
1239
- try {
1240
- syncAction = await select({
1241
- message: "Sync actions:",
1242
- choices: [
1243
- { name: "Sync now (bidirectional)", value: "sync" },
1244
- { name: "Push local → remote", value: "push" },
1245
- { name: "Pull remote → local", value: "pull" },
1246
- { name: "View what would sync (status)", value: "status" },
1247
- { name: "Enable auto-sync", value: "auto" },
1248
- { name: "Disconnect remote", value: "disconnect" },
1249
- ],
1250
- });
1251
- } catch (e) {
1252
- if (e.name === "ExitPromptError") { process.exit(130); }
1253
- throw e;
1254
- }
1255
-
1256
- if (["sync", "push", "pull", "status", "auto"].includes(syncAction)) {
1257
- args[1] = syncAction;
1258
- } else if (syncAction === "disconnect") {
1259
- const { confirm } = await import("@inquirer/prompts");
1260
- let ok;
1261
- try { ok = await confirm({ message: "Disconnect remote?", default: false }); }
1262
- catch (e) { if (e.name === "ExitPromptError") { process.exit(130); } throw e; }
1263
- if (ok) { await SyncManager.disableAuto?.(); console.log(_dim("Remote disconnected.")); }
1264
- process.exit(0);
1265
- }
1266
- } catch (e) {
1267
- if (e.name === "ExitPromptError") { process.exit(130); }
1268
- // Fall through
1269
- }
1270
- }
1271
-
1272
- // Parse flags
1273
- const strategyIdx = args.indexOf("--strategy");
1274
- const strategy = strategyIdx !== -1 ? args[strategyIdx + 1] : null;
1275
- const memoryOnly = args.includes("--memory-only");
1276
- const sessionsOnly = args.includes("--sessions-only");
1277
- const remoteUrlIdx = args.indexOf("--remote");
1278
- const remoteUrlArg = remoteUrlIdx !== -1 ? args[remoteUrlIdx + 1] : null;
1279
- const tokenIdx = args.indexOf("--token");
1280
- const tokenArg = tokenIdx !== -1 ? args[tokenIdx + 1] : null;
1281
-
1282
- // Load config (or use overrides)
1283
- const cfg = await SyncManager.loadConfig();
1284
- const remoteUrl = remoteUrlArg ?? cfg.remoteUrl;
1285
- const token = tokenArg ?? cfg.token;
1286
-
1287
- // ── auto enable/disable ──
1288
- if (sub === "auto") {
1289
- const enable = !args.includes("--off");
1290
- const disable = args.includes("--off");
1291
- if (disable) {
1292
- await SyncManager.disableAuto();
1293
- console.log(`\n${yellow("⏸")} Auto-sync ${bold("disabled")}.\n`);
1294
- } else if (!remoteUrl) {
1295
- console.log(`\n${red("✗")} No remote URL configured.`);
1296
- console.log(dim(` Use: wispy sync auto --remote https://vps.com:18790 --token <token>\n`));
1297
- process.exit(1);
1298
- } else {
1299
- await SyncManager.enableAuto(remoteUrl, token ?? "");
1300
- console.log(`\n${green("✓")} Auto-sync ${bold("enabled")}.`);
1301
- console.log(` Remote: ${cyan(remoteUrl)}`);
1302
- console.log(dim(" Sessions and memory will be synced automatically.\n"));
1303
- }
1304
- process.exit(0);
1305
- }
1306
-
1307
- // ── status ──
1308
- if (sub === "status") {
1309
- if (!remoteUrl) {
1310
- console.log(`\n${red("✗")} No remote configured. Set via sync.json or --remote flag.\n`);
1311
- process.exit(1);
1312
- }
1313
- console.log(`\n🔄 ${bold("Sync Status")}\n`);
1314
- console.log(` Remote: ${cyan(remoteUrl)}`);
1315
- const mgr = new SyncManager({ remoteUrl, token, strategy: strategy ?? "newer-wins" });
1316
- const s = await mgr.status(remoteUrl, token);
1317
- console.log(` Connection: ${s.reachable ? green("✅ reachable") : red("✗ unreachable")}\n`);
1318
-
1319
- const fmt = (label, info) => {
1320
- const parts = [];
1321
- if (info.localOnly > 0) parts.push(`${yellow(info.localOnly + " local only")}`);
1322
- if (info.remoteOnly > 0) parts.push(`${cyan(info.remoteOnly + " remote only")}`);
1323
- if (info.inSync > 0) parts.push(`${green(info.inSync + " in sync")}`);
1324
- console.log(` ${bold(label.padEnd(14))} ${parts.join(", ") || dim("(empty)")}`);
1325
- };
1326
- fmt("Memory:", s.memory);
1327
- fmt("Sessions:", s.sessions);
1328
- fmt("Cron:", s.cron);
1329
- fmt("Workstreams:",s.workstreams);
1330
- fmt("Permissions:",s.permissions);
1331
-
1332
- const needPull = (s.memory.remoteOnly + s.sessions.remoteOnly + s.cron.remoteOnly + s.workstreams.remoteOnly + s.permissions.remoteOnly);
1333
- const needPush = (s.memory.localOnly + s.sessions.localOnly + s.cron.localOnly + s.workstreams.localOnly + s.permissions.localOnly);
1334
- if (needPull > 0 || needPush > 0) {
1335
- console.log(`\n Action needed: ${needPull > 0 ? `pull ${yellow(needPull)} file(s)` : ""} ${needPush > 0 ? `push ${yellow(needPush)} file(s)` : ""}`.trimEnd());
1336
- console.log(dim(" Run 'wispy sync' to synchronize."));
1337
- } else {
1338
- console.log(`\n ${green("✓")} Everything in sync!`);
1339
- }
1340
- console.log("");
1341
- process.exit(0);
1342
- }
1343
-
1344
- // For push/pull/sync we need a remote URL
1345
- if (!remoteUrl) {
1346
- console.log(`\n${red("✗")} No remote configured.`);
1347
- console.log(dim(" Set remoteUrl in ~/.wispy/sync.json or use --remote <url>\n"));
1348
- process.exit(1);
1349
- }
1350
-
1351
- const opts = { strategy: strategy ?? cfg.strategy ?? "newer-wins", memoryOnly, sessionsOnly };
1352
- const mgr = new SyncManager({ remoteUrl, token, ...opts });
1353
-
1354
- if (sub === "push") {
1355
- console.log(`\n📤 ${bold("Pushing")} to ${cyan(remoteUrl)}...`);
1356
- const result = await mgr.push(remoteUrl, token, opts);
1357
- console.log(` Pushed: ${green(result.pushed)}`);
1358
- console.log(` Skipped: ${dim(result.skipped)}`);
1359
- if (result.errors.length) result.errors.forEach(e => console.log(` ${red("✗")} ${e}`));
1360
- console.log(`\n${result.errors.length === 0 ? green("✓ Done") : yellow("⚠ Done with errors")}.\n`);
1361
-
1362
- } else if (sub === "pull") {
1363
- console.log(`\n📥 ${bold("Pulling")} from ${cyan(remoteUrl)}...`);
1364
- const result = await mgr.pull(remoteUrl, token, opts);
1365
- console.log(` Pulled: ${green(result.pulled)}`);
1366
- console.log(` Skipped: ${dim(result.skipped)}`);
1367
- if (result.conflicts) console.log(` Conflicts: ${yellow(result.conflicts)} (saved as .conflict-* files)`);
1368
- if (result.errors.length) result.errors.forEach(e => console.log(` ${red("✗")} ${e}`));
1369
- console.log(`\n${result.errors.length === 0 ? green("✓ Done") : yellow("⚠ Done with errors")}.\n`);
1370
-
1371
- } else {
1372
- // Bidirectional sync (default)
1373
- console.log(`\n🔄 ${bold("Syncing")} with ${cyan(remoteUrl)}...`);
1374
- const result = await mgr.sync(remoteUrl, token, opts);
1375
- console.log(` Pushed: ${green(result.pushed)}`);
1376
- console.log(` Pulled: ${green(result.pulled)}`);
1377
- console.log(` Skipped: ${dim(result.skipped)}`);
1378
- if (result.conflicts) console.log(` Conflicts: ${yellow(result.conflicts)} (saved as .conflict-* files)`);
1379
- if (result.errors.length) result.errors.forEach(e => console.log(` ${red("✗")} ${e}`));
1380
- console.log(`\n${result.errors.length === 0 ? green("✓ Done") : yellow("⚠ Done with errors")}.\n`);
1381
- }
1382
-
1383
- process.exit(0);
1384
- }
1385
-
1386
- // ── deploy sub-command ────────────────────────────────────────────────────────
1387
- if (args[0] === "deploy") {
1388
- const { DeployManager } = await import(
1389
- path.join(__dirname, "..", "core", "deploy.mjs")
1390
- );
1391
-
1392
- const sub = args[1];
1393
- const dm = new DeployManager();
1394
-
1395
- const green = (s) => `\x1b[32m${s}\x1b[0m`;
1396
- const dim = (s) => `\x1b[2m${s}\x1b[0m`;
1397
- const bold = (s) => `\x1b[1m${s}\x1b[0m`;
1398
- const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
1399
-
1400
- if (!sub) {
1401
- // Interactive deploy menu
1402
- try {
1403
- const { select, Separator } = await import("@inquirer/prompts");
1404
- let deployTarget;
1405
- try {
1406
- deployTarget = await select({
1407
- message: "Deploy wispy to:",
1408
- choices: [
1409
- { name: "VPS (SSH + systemd)", value: "vps" },
1410
- { name: "Docker (Dockerfile + compose)", value: "docker" },
1411
- { name: "Railway", value: "railway" },
1412
- { name: "Fly.io", value: "fly" },
1413
- { name: "Render", value: "render" },
1414
- { name: "Modal (serverless)", value: "modal" },
1415
- { name: "Daytona", value: "daytona" },
1416
- new Separator("──────────"),
1417
- { name: "Generate all configs (deploy init)", value: "init" },
1418
- { name: "Check remote status", value: "status-check" },
1419
- ],
1420
- });
1421
- } catch (e) {
1422
- if (e.name === "ExitPromptError") { process.exit(130); }
1423
- throw e;
1424
- }
1425
-
1426
- if (deployTarget === "vps") {
1427
- const { input } = await import("@inquirer/prompts");
1428
- let target;
1429
- try { target = await input({ message: "SSH target (user@host):" }); }
1430
- catch (e) { if (e.name === "ExitPromptError") { process.exit(130); } throw e; }
1431
- if (target && target.trim()) {
1432
- try { await dm.deployVPS({ target: target.trim() }); }
1433
- catch (err) { console.error(_red(`\n❌ Deploy failed: ${err.message}`)); process.exit(1); }
1434
- }
1435
- } else if (deployTarget === "docker") {
1436
- const created = await dm.init(process.cwd());
1437
- for (const f of created) console.log(` ${f.includes("skipped") ? "⏭️ " : "✅"} ${f}`);
1438
- console.log(_dim("\n Next: docker-compose up -d\n"));
1439
- } else if (deployTarget === "railway") {
1440
- process.stdout.write(dm.generateRailwayConfig() + "\n");
1441
- } else if (deployTarget === "fly") {
1442
- process.stdout.write(dm.generateFlyConfig());
1443
- } else if (deployTarget === "render") {
1444
- process.stdout.write(dm.generateRenderConfig());
1445
- } else if (deployTarget === "modal") {
1446
- process.stdout.write(dm.generateModalConfig());
1447
- } else if (deployTarget === "daytona") {
1448
- const { mkdir: mkdDir, writeFile: wfDir } = await import("node:fs/promises");
1449
- const daytonaDir = path.join(process.cwd(), ".daytona");
1450
- await mkdDir(daytonaDir, { recursive: true });
1451
- const daytonaConfigPath = path.join(daytonaDir, "config.yaml");
1452
- let exists = false;
1453
- try { await (await import("node:fs/promises")).access(daytonaConfigPath); exists = true; } catch {}
1454
- if (!exists) {
1455
- await wfDir(daytonaConfigPath, dm.generateDaytonaConfig(), "utf8");
1456
- console.log(_green(`✅ Created .daytona/config.yaml`));
1457
- } else {
1458
- console.log(_yellow(`⏭️ .daytona/config.yaml already exists`));
1459
- }
1460
- } else if (deployTarget === "init") {
1461
- console.log("\n🌿 Initializing wispy deploy configs...\n");
1462
- const created = await dm.init(process.cwd());
1463
- for (const f of created) console.log(` ${f.includes("skipped") ? "⏭️ " : "✅"} ${f}`);
1464
- } else if (deployTarget === "status-check") {
1465
- const { input } = await import("@inquirer/prompts");
1466
- let url;
1467
- try { url = await input({ message: "Remote URL to check:" }); }
1468
- catch (e) { if (e.name === "ExitPromptError") { process.exit(130); } throw e; }
1469
- if (url && url.trim()) {
1470
- process.stdout.write(`\n📡 Checking ${_cyan(url.trim())}... `);
1471
- const status = await dm.checkRemote(url.trim());
1472
- if (status.alive) {
1473
- console.log(_green("✓ alive"));
1474
- if (status.version) console.log(` Version: ${status.version}`);
1475
- } else {
1476
- console.log(_red("✗ unreachable"));
1477
- }
1478
- }
1479
- }
1480
- process.exit(0);
1481
- } catch (e) {
1482
- if (e.name === "ExitPromptError") { process.exit(130); }
1483
- // Fall through to help
1484
- }
1485
- }
1486
-
1487
- if (sub === "dockerfile") {
1488
- process.stdout.write(dm.generateDockerfile());
1489
- process.exit(0);
1490
- }
1491
-
1492
- if (sub === "compose") {
1493
- process.stdout.write(dm.generateDockerCompose());
1494
- process.exit(0);
1495
- }
1496
-
1497
- if (sub === "systemd") {
1498
- process.stdout.write(dm.generateSystemd());
1499
- process.exit(0);
1500
- }
1501
-
1502
- if (sub === "railway") {
1503
- process.stdout.write(dm.generateRailwayConfig() + "\n");
1504
- process.exit(0);
1505
- }
1506
-
1507
- if (sub === "fly") {
1508
- process.stdout.write(dm.generateFlyConfig());
1509
- process.exit(0);
1510
- }
1511
-
1512
- if (sub === "render") {
1513
- process.stdout.write(dm.generateRenderConfig());
1514
- process.exit(0);
1515
- }
1516
-
1517
- if (sub === "init") {
1518
- console.log("\n🌿 Initializing wispy deploy configs...\n");
1519
- const created = await dm.init(process.cwd());
1520
- for (const f of created) {
1521
- console.log(` ${f.includes("skipped") ? "⏭️ " : "✅"} ${f}`);
1522
- }
1523
- console.log(dim("\n Next steps:"));
1524
- console.log(dim(" 1. Copy .env.example → .env and fill in your API keys"));
1525
- console.log(dim(" 2. docker-compose up -d — for Docker"));
1526
- console.log(dim(" 3. wispy deploy vps user@host — for raw VPS (no Docker)\n"));
1527
- process.exit(0);
1528
- }
1529
-
1530
- if (sub === "vps" && args[2]) {
1531
- const target = args[2];
1532
- const envIdx = args.indexOf("--env");
1533
- const envFile = envIdx !== -1 ? args[envIdx + 1] : null;
1534
-
1535
- try {
1536
- await dm.deployVPS({ target, envFile });
1537
- } catch (err) {
1538
- console.error(`\n❌ Deploy failed: ${err.message}`);
1539
- process.exit(1);
1540
- }
1541
- process.exit(0);
1542
- }
1543
-
1544
- if (sub === "status" && args[2]) {
1545
- const url = args[2];
1546
- process.stdout.write(`\n📡 Checking ${cyan(url)}... `);
1547
- const status = await dm.checkRemote(url);
1548
- if (status.alive) {
1549
- console.log(green("✓ alive"));
1550
- if (status.version) console.log(` Version: ${status.version}`);
1551
- if (status.uptime != null) console.log(` Uptime: ${Math.floor(status.uptime)}s`);
1552
- if (status.latency) console.log(` Latency: ${status.latency}ms`);
1553
- } else {
1554
- console.log(`\x1b[31m✗ unreachable\x1b[0m`);
1555
- if (status.error) console.log(` Error: ${dim(status.error)}`);
1556
- }
1557
- console.log("");
1558
- process.exit(0);
1559
- }
1560
-
1561
- if (sub === "modal") {
1562
- process.stdout.write(dm.generateModalConfig());
1563
- console.log(dim("\n# Save as modal_app.py, then: pip install modal && modal run modal_app.py"));
1564
- process.exit(0);
1565
- }
1566
-
1567
- if (sub === "daytona") {
1568
- const { mkdir: mkdirDaytona, writeFile: writeDaytona } = await import("node:fs/promises");
1569
- const daytonaDir = path.join(process.cwd(), ".daytona");
1570
- await mkdirDaytona(daytonaDir, { recursive: true });
1571
- const daytonaConfigPath = path.join(daytonaDir, "config.yaml");
1572
- let exists = false;
1573
- try { await (await import("node:fs/promises")).access(daytonaConfigPath); exists = true; } catch {}
1574
- if (!exists) {
1575
- await writeDaytona(daytonaConfigPath, dm.generateDaytonaConfig(), "utf8");
1576
- console.log(green(`✅ Created .daytona/config.yaml`));
1577
- } else {
1578
- console.log(yellow(`⏭️ .daytona/config.yaml already exists (skipped)`));
1579
- }
1580
- console.log(dim(" Push to your repo and connect via Daytona workspace."));
1581
- process.exit(0);
1582
- }
1583
-
1584
- // Help
1585
- console.log(`
1586
- 🚀 ${bold("Wispy Deploy Commands")}
1587
-
1588
- ${cyan("Config generators:")}
1589
- wispy deploy init — generate Dockerfile + compose + .env.example
1590
- wispy deploy dockerfile — print Dockerfile to stdout
1591
- wispy deploy compose — print docker-compose.yml
1592
- wispy deploy systemd — print systemd unit file
1593
- wispy deploy railway — print railway.json
1594
- wispy deploy fly — print fly.toml
1595
- wispy deploy render — print render.yaml
1596
- wispy deploy modal — generate Modal serverless config (modal_app.py)
1597
- wispy deploy daytona — generate Daytona workspace config (.daytona/config.yaml)
1598
-
1599
- ${cyan("Deploy:")}
1600
- wispy deploy vps user@host — SSH deploy: install + systemd setup
1601
- wispy deploy vps user@host --env .env — include env file
1602
-
1603
- ${cyan("Status:")}
1604
- wispy deploy status https://my.vps — check if remote wispy is alive
1605
-
1606
- ${cyan("Remote connect:")}
1607
- wispy connect https://my.vps:18790 --token <token> — use remote server
1608
- wispy disconnect — go back to local
1609
- wispy status — show current mode
1610
- `);
1611
- process.exit(0);
1612
- }
1613
-
1614
- // ── migrate sub-command ───────────────────────────────────────────────────────
1615
- if (args[0] === "migrate") {
1616
- const { OpenClawMigrator, WISPY_DIR } = await import(
1617
- path.join(__dirname, "..", "core", "index.mjs")
1618
- );
1619
-
1620
- const sub = args[1]; // "openclaw" (only supported source for now)
1621
- const dryRun = args.includes("--dry-run");
1622
- const memoryOnly = args.includes("--memory-only");
1623
-
1624
- const green = (s) => `\x1b[32m${s}\x1b[0m`;
1625
- const red = (s) => `\x1b[31m${s}\x1b[0m`;
1626
- const bold = (s) => `\x1b[1m${s}\x1b[0m`;
1627
- const dim = (s) => `\x1b[2m${s}\x1b[0m`;
1628
- const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
1629
-
1630
- if (!sub) {
1631
- // Interactive migrate menu
1632
- try {
1633
- const { select } = await import("@inquirer/prompts");
1634
- const { homedir } = await import("node:os");
1635
- const { existsSync } = await import("node:fs");
1636
- const { join } = await import("node:path");
1637
-
1638
- const hasOpenClaw = existsSync(join(homedir(), ".openclaw"));
1639
- const choices = [
17
+ if (!command) {
18
+ const answers = await inquirer.prompt([
1640
19
  {
1641
- name: `OpenClaw${hasOpenClaw ? green(" (detected at ~/.openclaw)") : dim(" (not found)")}`,
1642
- value: "openclaw",
1643
- },
1644
- { name: "Hermes Agent", value: "hermes" },
1645
- { name: "Manual import (JSON/YAML)", value: "manual" },
1646
- ];
1647
-
1648
- let migrateFrom;
1649
- try {
1650
- migrateFrom = await select({ message: "Migrate from:", choices });
1651
- } catch (e) {
1652
- if (e.name === "ExitPromptError") { process.exit(130); }
1653
- throw e;
1654
- }
1655
-
1656
- if (migrateFrom === "openclaw") {
1657
- args[1] = "openclaw";
1658
- // Fall through
1659
- } else if (migrateFrom === "hermes") {
1660
- console.log(dim("\nHermes migration coming soon. Use manual import for now.\n"));
1661
- process.exit(0);
1662
- } else if (migrateFrom === "manual") {
1663
- const { input } = await import("@inquirer/prompts");
1664
- let filePath;
1665
- try { filePath = await input({ message: "Path to JSON/YAML file:" }); }
1666
- catch (e) { if (e.name === "ExitPromptError") { process.exit(130); } throw e; }
1667
- console.log(dim(`\nManual import from ${filePath} — coming soon.\n`));
1668
- process.exit(0);
1669
- }
1670
- } catch (e) {
1671
- if (e.name === "ExitPromptError") { process.exit(130); }
1672
- args[1] = "openclaw";
1673
- }
1674
- }
1675
-
1676
- const sub2 = args[1]; // re-read after interactive
1677
-
1678
- if (!sub || sub === "openclaw" || sub2 === "openclaw") {
1679
- const subToUse = sub || sub2;
1680
- console.log(`\n🌿 ${bold("Wispy Migration Tool")} ${dim("— from OpenClaw")}\n`);
1681
- if (dryRun) console.log(yellow(" [DRY RUN — no files will be written]\n"));
1682
- if (memoryOnly) console.log(dim(" [memory-only mode]\n"));
1683
-
1684
- const migrator = new OpenClawMigrator(WISPY_DIR);
1685
- const result = await migrator.migrate({ dryRun, memoryOnly });
1686
-
1687
- console.log(migrator.formatReport());
1688
-
1689
- if (result.success) {
1690
- if (dryRun) {
1691
- console.log(dim("\nRun without --dry-run to apply changes.\n"));
1692
- } else {
1693
- const counts = [
1694
- result.report.memories.length > 0 && `${result.report.memories.length} memory files`,
1695
- result.report.userModel.length > 0 && `${result.report.userModel.length} profile files`,
1696
- result.report.cronJobs.length > 0 && `${result.report.cronJobs.length} cron jobs`,
1697
- result.report.channels.length > 0 && `${result.report.channels.length} channels`,
1698
- ].filter(Boolean);
1699
-
1700
- if (counts.length > 0) {
1701
- console.log(`\n${green("✅ Migration complete!")} Imported: ${counts.join(", ")}`);
1702
- } else {
1703
- console.log(`\n${dim("Nothing new to import (already migrated or empty).")}`);
1704
- }
1705
- console.log(dim("\nTip: run `wispy` to start chatting with your imported context.\n"));
1706
- }
1707
- } else {
1708
- console.error(`\n${red("❌ Migration failed:")} ${result.error}\n`);
1709
- process.exit(1);
1710
- }
1711
- } else {
1712
- console.log(`
1713
- 🔀 ${bold("Wispy Migrate Commands")}
1714
-
1715
- wispy migrate openclaw — import from OpenClaw (~/.openclaw/)
1716
- wispy migrate openclaw --dry-run — preview what would be imported
1717
- wispy migrate openclaw --memory-only — only import memories
1718
- `);
1719
- }
1720
-
1721
- process.exit(0);
1722
- }
1723
-
1724
- // ── cron sub-command ──────────────────────────────────────────────────────────
1725
- if (args[0] === "cron") {
1726
- const { WispyEngine, CronManager, WISPY_DIR } = await import(
1727
- path.join(__dirname, "..", "core", "index.mjs")
1728
- );
1729
- const { createInterface } = await import("node:readline");
1730
-
1731
- const sub = args[1];
1732
-
1733
- // Init engine for cron commands that need it
1734
- const engine = new WispyEngine();
1735
- await engine.init({ skipMcp: true });
1736
- const cron = new CronManager(WISPY_DIR, engine);
1737
- await cron.init();
1738
-
1739
- if (!sub) {
1740
- // Interactive cron menu
1741
- try {
1742
- const { select, Separator } = await import("@inquirer/prompts");
1743
- const jobs = cron.list();
1744
-
1745
- const choices = [];
1746
- if (jobs.length === 0) {
1747
- choices.push(new Separator(_dim("No jobs configured.")));
1748
- } else {
1749
- for (const j of jobs) {
1750
- const schedStr = j.schedule.kind === "cron" ? j.schedule.expr
1751
- : j.schedule.kind === "every" ? `every ${j.schedule.ms / 60000}min`
1752
- : `at ${j.schedule.time}`;
1753
- const lastRun = j.lastRun ? `last: ${_dim(formatCronRelative(j.lastRun))} ✓` : _dim("never run");
1754
- choices.push({
1755
- name: `${j.name} — ${schedStr} — ${lastRun}`,
1756
- value: { type: "job", id: j.id, name: j.name },
1757
- short: j.name,
1758
- });
1759
- }
1760
- }
1761
- choices.push(new Separator("──────────"));
1762
- choices.push({ name: "Add a new job", value: { type: "add" } });
1763
- choices.push({ name: "Start scheduler", value: { type: "start" } });
1764
-
1765
- function formatCronRelative(ts) {
1766
- if (!ts) return "never";
1767
- const diffMs = Date.now() - new Date(ts).getTime();
1768
- const diffMin = Math.floor(diffMs / 60000);
1769
- const diffH = Math.floor(diffMin / 60);
1770
- if (diffMin < 1) return "just now";
1771
- if (diffMin < 60) return `${diffMin}min ago`;
1772
- if (diffH < 24) return `${diffH}hr ago`;
1773
- return `${Math.floor(diffH / 24)}d ago`;
1774
- }
1775
-
1776
- let answer;
1777
- try {
1778
- answer = await select({ message: "Cron jobs:", choices });
1779
- } catch (e) {
1780
- if (e.name === "ExitPromptError") { engine.destroy?.(); process.exit(130); }
1781
- throw e;
1782
- }
1783
-
1784
- if (answer.type === "add") {
1785
- // fall through to the "add" sub-command handler below
1786
- args[1] = "add";
1787
- } else if (answer.type === "start") {
1788
- args[1] = "start";
1789
- } else if (answer.type === "job") {
1790
- // Job sub-menu
1791
- let jobAction;
1792
- try {
1793
- jobAction = await select({
1794
- message: `${answer.name}:`,
1795
- choices: [
1796
- { name: "Run now", value: "run" },
1797
- { name: "Edit (not yet implemented)", value: "edit" },
1798
- { name: "Remove", value: "remove" },
1799
- { name: "View history", value: "history" },
1800
- ],
1801
- });
1802
- } catch (e) {
1803
- if (e.name === "ExitPromptError") { engine.destroy?.(); process.exit(130); }
1804
- throw e;
1805
- }
1806
- if (jobAction === "run") {
1807
- console.log(`🌿 Running job: ${answer.name}...`);
1808
- const result = await cron.runNow(answer.id);
1809
- console.log(result.output ?? result.error);
1810
- } else if (jobAction === "remove") {
1811
- const { confirm } = await import("@inquirer/prompts");
1812
- let ok;
1813
- try { ok = await confirm({ message: `Remove job '${answer.name}'?`, default: false }); }
1814
- catch (e) { if (e.name === "ExitPromptError") { engine.destroy?.(); process.exit(130); } throw e; }
1815
- if (ok) {
1816
- await cron.remove(answer.id);
1817
- console.log(_green(`✅ Removed job: ${answer.name}`));
1818
- }
1819
- } else if (jobAction === "history") {
1820
- const history = await cron.getHistory(answer.id);
1821
- console.log(`\n📋 History for "${answer.name}" (last ${history.length} runs):\n`);
1822
- for (const h of history) {
1823
- const icon = h.status === "success" ? "✅" : "❌";
1824
- console.log(` ${icon} ${new Date(h.startedAt).toLocaleString()}`);
1825
- console.log(` ${h.output?.slice(0, 100) ?? h.error ?? ""}`);
1826
- }
1827
- if (history.length === 0) console.log(" No runs yet.");
1828
- } else if (jobAction === "edit") {
1829
- console.log(_dim("Edit via: wispy cron add (then remove the old one)"));
1830
- }
1831
- engine.destroy?.();
1832
- process.exit(0);
1833
- }
1834
- } catch (e) {
1835
- if (e.name === "ExitPromptError") { engine.destroy?.(); process.exit(130); }
1836
- // Fallback to list
1837
- args[1] = "list";
1838
- }
1839
- }
1840
-
1841
- if (!sub || sub === "list") {
1842
- const jobs = cron.list();
1843
- if (jobs.length === 0) {
1844
- console.log("No cron jobs configured. Use: wispy cron add");
1845
- } else {
1846
- console.log(`\n🕐 Cron Jobs (${jobs.length}):\n`);
1847
- for (const j of jobs) {
1848
- const status = j.enabled ? "✅" : "⏸️ ";
1849
- const schedStr = j.schedule.kind === "cron" ? j.schedule.expr
1850
- : j.schedule.kind === "every" ? `every ${j.schedule.ms / 60000}min`
1851
- : `at ${j.schedule.time}`;
1852
- console.log(` ${status} ${j.id.slice(0, 8)} ${j.name.padEnd(20)} ${schedStr}`);
1853
- console.log(` Task: ${j.task.slice(0, 60)}${j.task.length > 60 ? "..." : ""}`);
1854
- if (j.channel) console.log(` Channel: ${j.channel}`);
1855
- if (j.nextRun) console.log(` Next run: ${new Date(j.nextRun).toLocaleString()}`);
1856
- console.log("");
1857
- }
1858
- }
1859
- process.exit(0);
1860
- }
1861
-
1862
- if (sub === "add") {
1863
- const rl = createInterface({ input: process.stdin, output: process.stdout });
1864
- const ask = (q) => new Promise(r => rl.question(q, r));
1865
-
1866
- console.log("\n🕐 Add Cron Job\n");
1867
- const name = await ask(" Job name: ");
1868
- const task = await ask(" Task (what to do): ");
1869
- const schedKind = await ask(" Schedule type (cron/every/at) [cron]: ") || "cron";
1870
- let schedule = { kind: schedKind };
1871
-
1872
- if (schedKind === "cron") {
1873
- const expr = await ask(" Cron expression (e.g. '0 9 * * *'): ");
1874
- const tz = await ask(" Timezone [Asia/Seoul]: ") || "Asia/Seoul";
1875
- schedule = { kind: "cron", expr: expr.trim(), tz: tz.trim() };
1876
- } else if (schedKind === "every") {
1877
- const mins = await ask(" Interval in minutes: ");
1878
- schedule = { kind: "every", ms: parseFloat(mins) * 60_000 };
1879
- } else if (schedKind === "at") {
1880
- const time = await ask(" Run at (ISO datetime, e.g. 2025-01-01T09:00:00): ");
1881
- schedule = { kind: "at", time: time.trim() };
1882
- }
1883
-
1884
- const channel = await ask(" Channel (e.g. telegram:12345, or leave empty): ");
1885
- rl.close();
1886
-
1887
- const job = await cron.add({
1888
- name: name.trim(),
1889
- task: task.trim(),
1890
- schedule,
1891
- channel: channel.trim() || null,
1892
- enabled: true,
1893
- });
1894
-
1895
- console.log(`\n✅ Job created: ${job.id}`);
1896
- console.log(` Next run: ${job.nextRun ? new Date(job.nextRun).toLocaleString() : "N/A"}`);
1897
- process.exit(0);
1898
- }
1899
-
1900
- if (sub === "remove" && args[2]) {
1901
- const id = args[2];
1902
- // Support partial ID match
1903
- const all = cron.list();
1904
- const match = all.find(j => j.id === id || j.id.startsWith(id));
1905
- if (!match) {
1906
- console.error(`Job not found: ${id}`);
1907
- process.exit(1);
1908
- }
1909
- await cron.remove(match.id);
1910
- console.log(`✅ Removed job: ${match.name} (${match.id})`);
1911
- process.exit(0);
1912
- }
1913
-
1914
- if (sub === "run" && args[2]) {
1915
- const id = args[2];
1916
- const all = cron.list();
1917
- const match = all.find(j => j.id === id || j.id.startsWith(id));
1918
- if (!match) {
1919
- console.error(`Job not found: ${id}`);
1920
- process.exit(1);
1921
- }
1922
- console.log(`🌿 Running job: ${match.name}...`);
1923
- const result = await cron.runNow(match.id);
1924
- console.log(result.output ?? result.error);
1925
- process.exit(0);
1926
- }
1927
-
1928
- if (sub === "history" && args[2]) {
1929
- const id = args[2];
1930
- const all = cron.list();
1931
- const match = all.find(j => j.id === id || j.id.startsWith(id));
1932
- if (!match) {
1933
- console.error(`Job not found: ${id}`);
1934
- process.exit(1);
1935
- }
1936
- const history = await cron.getHistory(match.id);
1937
- console.log(`\n📋 History for "${match.name}" (last ${history.length} runs):\n`);
1938
- for (const h of history) {
1939
- const icon = h.status === "success" ? "✅" : "❌";
1940
- console.log(` ${icon} ${new Date(h.startedAt).toLocaleString()}`);
1941
- console.log(` ${h.output?.slice(0, 100) ?? h.error ?? ""}`);
1942
- console.log("");
1943
- }
1944
- if (history.length === 0) console.log(" No runs yet.");
1945
- process.exit(0);
1946
- }
1947
-
1948
- if (sub === "start") {
1949
- console.log("🌿 Starting cron scheduler... (Ctrl+C to stop)\n");
1950
- cron.start();
1951
- process.on("SIGINT", () => { cron.stop(); process.exit(0); });
1952
- process.on("SIGTERM", () => { cron.stop(); process.exit(0); });
1953
- setInterval(() => {}, 60_000);
1954
- await new Promise(() => {});
1955
- }
1956
-
1957
- console.log(`
1958
- 🕐 Wispy Cron Commands:
1959
-
1960
- wispy cron list — list all jobs
1961
- wispy cron add — interactive job creation
1962
- wispy cron remove <id> — delete a job
1963
- wispy cron run <id> — trigger immediately
1964
- wispy cron history <id> — show past runs
1965
- wispy cron start — start scheduler (foreground)
1966
- `);
1967
- process.exit(0);
1968
- }
1969
-
1970
- // ── audit / log sub-command ───────────────────────────────────────────────────
1971
- if (args[0] === "audit" || args[0] === "log") {
1972
- const { AuditLog, WISPY_DIR } = await import(
1973
- path.join(__dirname, "..", "core", "index.mjs")
1974
- );
1975
- const { writeFile } = await import("node:fs/promises");
1976
-
1977
- const audit = new AuditLog(WISPY_DIR);
1978
- const sub = args[1];
1979
-
1980
- const dim = (s) => `\x1b[2m${s}\x1b[0m`;
1981
- const bold = (s) => `\x1b[1m${s}\x1b[0m`;
1982
- const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
1983
- const green = (s) => `\x1b[32m${s}\x1b[0m`;
1984
- const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
1985
- const red = (s) => `\x1b[31m${s}\x1b[0m`;
1986
-
1987
- // Interactive audit menu when no subcommand
1988
- if (!sub) {
1989
- try {
1990
- const { select } = await import("@inquirer/prompts");
1991
- let auditAction;
1992
- try {
1993
- auditAction = await select({
1994
- message: "Audit log:",
20
+ type: 'list',
21
+ name: 'selectedCommand',
22
+ message: 'What would you like to do?',
1995
23
  choices: [
1996
- { name: "View recent events", value: "recent" },
1997
- { name: "Filter by tool", value: "by-tool" },
1998
- { name: "Filter by session", value: "by-session" },
1999
- { name: "Today's events only", value: "today" },
2000
- { name: "Replay a session", value: "replay" },
2001
- { name: "Export as markdown", value: "export-md" },
24
+ { name: 'Run WebSocket command', value: 'ws' },
25
+ { name: 'Get help', value: 'help' },
26
+ { name: 'Exit', value: null }
2002
27
  ],
2003
- });
2004
- } catch (e) {
2005
- if (e.name === "ExitPromptError") { process.exit(130); }
2006
- throw e;
2007
- }
2008
-
2009
- if (auditAction === "recent") {
2010
- // Fall through with no filter
2011
- } else if (auditAction === "by-tool") {
2012
- const { input } = await import("@inquirer/prompts");
2013
- let tool;
2014
- try { tool = await input({ message: "Tool name:" }); }
2015
- catch (e) { if (e.name === "ExitPromptError") { process.exit(130); } throw e; }
2016
- if (tool && tool.trim()) args.push("--tool", tool.trim());
2017
- } else if (auditAction === "by-session") {
2018
- const { input } = await import("@inquirer/prompts");
2019
- let sid;
2020
- try { sid = await input({ message: "Session ID:" }); }
2021
- catch (e) { if (e.name === "ExitPromptError") { process.exit(130); } throw e; }
2022
- if (sid && sid.trim()) args.push("--session", sid.trim());
2023
- } else if (auditAction === "today") {
2024
- args.push("--today");
2025
- } else if (auditAction === "replay") {
2026
- const { input } = await import("@inquirer/prompts");
2027
- let sid;
2028
- try { sid = await input({ message: "Session ID to replay:" }); }
2029
- catch (e) { if (e.name === "ExitPromptError") { process.exit(130); } throw e; }
2030
- if (sid && sid.trim()) {
2031
- args[1] = "replay";
2032
- args[2] = sid.trim();
2033
- }
2034
- } else if (auditAction === "export-md") {
2035
- const content = await audit.exportMarkdown();
2036
- const ts = new Date().toISOString().slice(0, 10);
2037
- const outFile = `wispy-audit-${ts}.md`;
2038
- const { writeFile: wf } = await import("node:fs/promises");
2039
- await wf(outFile, content, "utf8");
2040
- console.log(green(`✅ Exported to ${outFile}`));
2041
- process.exit(0);
2042
- }
2043
- } catch (e) {
2044
- if (e.name === "ExitPromptError") { process.exit(130); }
2045
- // Fall through to regular display
2046
- }
2047
- }
2048
-
2049
- function formatEvent(evt) {
2050
- const ts = new Date(evt.timestamp).toLocaleTimeString();
2051
- const icons = {
2052
- tool_call: "🔧",
2053
- tool_result: "✅",
2054
- approval_requested: "⚠️ ",
2055
- approval_granted: "✅",
2056
- approval_denied: "❌",
2057
- message_sent: "🌿",
2058
- message_received: "👤",
2059
- error: "🚨",
2060
- subagent_spawned: "🤖",
2061
- subagent_completed: "🎉",
2062
- cron_executed: "🕐",
2063
- };
2064
- const icon = icons[evt.type] ?? "•";
2065
- let detail = "";
2066
- if (evt.tool) detail += ` ${cyan(evt.tool)}`;
2067
- if (evt.content) detail += ` ${dim(evt.content.slice(0, 60))}`;
2068
- if (evt.message) detail += ` ${dim(evt.message.slice(0, 60))}`;
2069
- if (evt.label) detail += ` ${dim(evt.label)}`;
2070
- const sid = evt.sessionId ? dim(` [${evt.sessionId.slice(-8)}]`) : "";
2071
- return ` ${dim(ts)} ${icon} ${evt.type}${detail}${sid}`;
2072
- }
2073
-
2074
- if (sub === "replay" && args[2]) {
2075
- const sessionId = args[2];
2076
- const steps = await audit.getReplayTrace(sessionId);
2077
- if (steps.length === 0) {
2078
- console.log(dim(`No events found for session: ${sessionId}`));
2079
- } else {
2080
- console.log(`\n${bold("🎬 Replay:")} ${cyan(sessionId)}\n`);
2081
- for (const step of steps) {
2082
- const ts = new Date(step.timestamp).toLocaleTimeString();
2083
- const icons = {
2084
- user_message: "👤",
2085
- assistant_message: "🌿",
2086
- tool_call: "🔧",
2087
- tool_result: "✅",
2088
- approval_requested: "⚠️ ",
2089
- approval_granted: "✅",
2090
- approval_denied: "❌",
2091
- subagent_spawned: "🤖",
2092
- subagent_completed: "🎉",
2093
- };
2094
- const icon = icons[step.type] ?? "•";
2095
- let detail = "";
2096
- if (step.content) detail = dim(step.content.slice(0, 100));
2097
- if (step.tool) detail = `${cyan(step.tool)} ${dim(JSON.stringify(step.args ?? {}).slice(0, 60))}`;
2098
- console.log(` ${bold(`Step ${step.step}`)} ${dim(ts)} ${icon} ${detail}`);
2099
- }
2100
- }
2101
- process.exit(0);
2102
- }
2103
-
2104
- if (sub === "export") {
2105
- const format = args.includes("--format") ? args[args.indexOf("--format") + 1] : "json";
2106
- const outputIdx = args.indexOf("--output");
2107
- const output = outputIdx !== -1 ? args[outputIdx + 1] : null;
2108
-
2109
- let content;
2110
- if (format === "md" || format === "markdown") {
2111
- content = await audit.exportMarkdown();
2112
- } else {
2113
- content = await audit.exportJson();
2114
- }
2115
-
2116
- if (output) {
2117
- await writeFile(output, content, "utf8");
2118
- console.log(green(`✅ Exported to ${output}`));
2119
- } else {
2120
- console.log(content);
2121
- }
2122
- process.exit(0);
2123
- }
2124
-
2125
- // Build filter from flags
2126
- const filter = {};
2127
- const sessionIdx = args.indexOf("--session");
2128
- if (sessionIdx !== -1) filter.sessionId = args[sessionIdx + 1];
2129
- const toolIdx = args.indexOf("--tool");
2130
- if (toolIdx !== -1) filter.tool = args[toolIdx + 1];
2131
- if (args.includes("--today")) filter.date = new Date().toISOString().slice(0, 10);
2132
- const limitIdx = args.indexOf("--limit");
2133
- filter.limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1]) : 30;
2134
-
2135
- const events = await audit.search(filter);
2136
-
2137
- if (events.length === 0) {
2138
- console.log(dim("No audit events found."));
2139
- } else {
2140
- console.log(`\n${bold("📋 Audit Log")} ${dim(`(${events.length} events)`)}\n`);
2141
- for (const evt of events) {
2142
- console.log(formatEvent(evt));
2143
- }
2144
- console.log("");
2145
- }
2146
-
2147
- if (!sub) {
2148
- console.log(dim(`
2149
- wispy audit — show recent events
2150
- wispy audit --session <id> — filter by session
2151
- wispy audit --today — today's events
2152
- wispy audit --tool <name> — filter by tool
2153
- wispy audit replay <sessionId> — step-by-step replay
2154
- wispy audit export --format md — export as markdown
2155
- wispy audit export --output file.md — save to file
2156
- `));
2157
- }
2158
-
2159
- process.exit(0);
2160
- }
2161
-
2162
- // ── server sub-command ────────────────────────────────────────────────────────
2163
- if (args[0] === "server" || args.includes("--server")) {
2164
- const { WispyEngine, WispyServer } = await import(
2165
- path.join(__dirname, "..", "core", "index.mjs")
2166
- );
2167
-
2168
- const portIdx = args.indexOf("--port");
2169
- const hostIdx = args.indexOf("--host");
2170
- const port = portIdx !== -1 ? parseInt(args[portIdx + 1]) : undefined;
2171
- const host = hostIdx !== -1 ? args[hostIdx + 1] : undefined;
2172
-
2173
- console.log("🌿 Starting Wispy API server...");
2174
-
2175
- const engine = new WispyEngine();
2176
- const initResult = await engine.init();
2177
- if (!initResult) {
2178
- console.error("❌ No AI provider configured. Run `wispy` first to set up.");
2179
- process.exit(1);
2180
- }
2181
-
2182
- const server = new WispyServer(engine, { port, host });
2183
- await server.start();
2184
-
2185
- process.on("SIGINT", () => { server.stop(); process.exit(0); });
2186
- process.on("SIGTERM", () => { server.stop(); process.exit(0); });
2187
-
2188
- // Keep alive
2189
- setInterval(() => {}, 60_000);
2190
- await new Promise(() => {});
2191
- }
2192
-
2193
- // ── node sub-command ──────────────────────────────────────────────────────────
2194
- if (args[0] === "node") {
2195
- const { NodeManager, WISPY_DIR, CAPABILITIES } = await import(
2196
- path.join(__dirname, "..", "core", "index.mjs")
2197
- );
2198
- const { createInterface } = await import("node:readline");
2199
-
2200
- const sub = args[1];
2201
- const nodes = new NodeManager(WISPY_DIR);
2202
-
2203
- const dim = (s) => `\x1b[2m${s}\x1b[0m`;
2204
- const bold = (s) => `\x1b[1m${s}\x1b[0m`;
2205
- const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
2206
- const green = (s) => `\x1b[32m${s}\x1b[0m`;
2207
- const red = (s) => `\x1b[31m${s}\x1b[0m`;
2208
- const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
2209
-
2210
- if (!sub) {
2211
- // Interactive node menu
2212
- try {
2213
- const { select, Separator } = await import("@inquirer/prompts");
2214
- const nodeList = await nodes.list();
2215
-
2216
- const choices = [];
2217
- if (nodeList.length === 0) {
2218
- choices.push(new Separator(dim("No nodes connected.")));
2219
- } else {
2220
- const results = await nodes.status().catch(() => []);
2221
- for (const n of nodeList) {
2222
- const statusInfo = results.find(r => r.id === n.id);
2223
- const alive = statusInfo?.alive;
2224
- const statusStr = alive ? green("● online") : dim("● offline");
2225
- choices.push({
2226
- name: `${n.name} — ${n.host}:${n.port} — ${statusStr}`,
2227
- value: { type: "node", id: n.id, name: n.name },
2228
- short: n.name,
2229
- });
2230
- }
2231
- }
2232
- choices.push(new Separator("──────────"));
2233
- choices.push({ name: "Pair a new node (generate code)", value: { type: "pair" } });
2234
- choices.push({ name: "Connect to a node", value: { type: "connect" } });
2235
-
2236
- let answer;
2237
- try {
2238
- answer = await select({ message: "Nodes:", choices });
2239
- } catch (e) {
2240
- if (e.name === "ExitPromptError") { process.exit(130); }
2241
- throw e;
2242
- }
2243
-
2244
- if (answer.type === "pair") {
2245
- const code = await nodes.generatePairCode();
2246
- console.log(`\n🔗 Pairing Code: ${bold(green(code))}\n`);
2247
- console.log(` This code expires in 1 hour.`);
2248
- console.log(`\n On the remote machine:\n ${cyan(`wispy node connect ${code} --url http://localhost:18790`)}\n`);
2249
- } else if (answer.type === "connect") {
2250
- const { input } = await import("@inquirer/prompts");
2251
- let code, url;
2252
- try {
2253
- code = await input({ message: "Pairing code:" });
2254
- url = await input({ message: "Server URL:", default: "http://localhost:18790" });
2255
- } catch (e) {
2256
- if (e.name === "ExitPromptError") { process.exit(130); }
2257
- throw e;
2258
- }
2259
- if (code && code.trim()) args[1] = "connect", args[2] = code.trim(), args.push("--url"), args.push(url || "http://localhost:18790");
2260
- else process.exit(0);
2261
- } else if (answer.type === "node") {
2262
- let nodeAction;
2263
- try {
2264
- nodeAction = await select({
2265
- message: `${answer.name}:`,
2266
- choices: [
2267
- { name: "Ping", value: "ping" },
2268
- { name: "Remove", value: "remove" },
2269
- { name: "Execute command", value: "exec" },
2270
- ],
2271
- });
2272
- } catch (e) {
2273
- if (e.name === "ExitPromptError") { process.exit(130); }
2274
- throw e;
2275
28
  }
2276
- if (nodeAction === "ping") {
2277
- const results = await nodes.status();
2278
- const r = results.find(x => x.id === answer.id);
2279
- if (r?.alive) console.log(green(`● ${answer.name} — alive (${r.latency ?? "?"}ms)`));
2280
- else console.log(red(`● ${answer.name} — unreachable`));
2281
- } else if (nodeAction === "remove") {
2282
- const { confirm } = await import("@inquirer/prompts");
2283
- let ok;
2284
- try { ok = await confirm({ message: `Remove node '${answer.name}'?`, default: false }); }
2285
- catch (e) { if (e.name === "ExitPromptError") { process.exit(130); } throw e; }
2286
- if (ok) { await nodes.remove(answer.id); console.log(green(`✅ Removed node: ${answer.name}`)); }
2287
- } else if (nodeAction === "exec") {
2288
- console.log(dim("Execute via: wispy node exec <id> <command> (coming soon)"));
2289
- }
2290
- process.exit(0);
2291
- }
2292
- process.exit(0);
2293
- } catch (e) {
2294
- if (e.name === "ExitPromptError") { process.exit(130); }
2295
- // Fall through to help
29
+ ]);
30
+ command = answers.selectedCommand;
2296
31
  }
2297
- }
2298
-
2299
- if (sub === "pair") {
2300
- const code = await nodes.generatePairCode();
2301
- console.log(`\n🔗 Pairing Code: ${bold(green(code))}\n`);
2302
- console.log(` This code expires in 1 hour.`);
2303
- console.log(`\n On the remote machine, run:`);
2304
- console.log(` ${cyan(`wispy node connect ${code} --url http://localhost:18790`)}\n`);
2305
- process.exit(0);
2306
- }
2307
-
2308
- if (sub === "connect" && args[2]) {
2309
- const code = args[2];
2310
- const urlIdx = args.indexOf("--url");
2311
- const serverUrl = urlIdx !== -1 ? args[urlIdx + 1] : "http://localhost:18790";
2312
-
2313
- const rl = createInterface({ input: process.stdin, output: process.stdout });
2314
- const ask = (q) => new Promise(r => rl.question(q, r));
2315
-
2316
- console.log(`\n🔗 Connecting to Wispy at ${serverUrl}\n`);
2317
- const name = (await ask(" Node name (e.g. my-laptop): ")).trim() || `node-${Date.now().toString(36)}`;
2318
32
 
2319
- console.log(`\nAvailable capabilities:`);
2320
- const capList = Object.entries(CAPABILITIES);
2321
- capList.forEach(([k, v], i) => console.log(` ${i + 1}. ${k.padEnd(15)} — ${dim(v)}`));
2322
- const capInput = (await ask("\n Select capabilities (comma-separated numbers, e.g. 1,3): ")).trim();
2323
- rl.close();
2324
-
2325
- const selectedCaps = capInput
2326
- ? capInput.split(",").map(n => capList[parseInt(n.trim()) - 1]?.[0]).filter(Boolean)
2327
- : [];
2328
-
2329
- // Call server to confirm pair
2330
- try {
2331
- const tokenIdx = args.indexOf("--token");
2332
- const token = tokenIdx !== -1 ? args[tokenIdx + 1] : "";
2333
- const resp = await fetch(`${serverUrl}/api/nodes/pair`, {
2334
- method: "POST",
2335
- headers: { "Content-Type": "application/json", ...(token ? { "Authorization": `Bearer ${token}` } : {}) },
2336
- body: JSON.stringify({ code, name, capabilities: selectedCaps, host: "localhost", port: 18791 }),
2337
- });
2338
- if (!resp.ok) {
2339
- const err = await resp.json().catch(() => ({ error: resp.statusText }));
2340
- console.error(red(`❌ Pairing failed: ${err.error ?? resp.statusText}`));
2341
- process.exit(1);
2342
- }
2343
- const result = await resp.json();
2344
- console.log(green(`\n✅ Paired! Node ID: ${result.id}`));
2345
- } catch (err) {
2346
- console.error(red(`❌ Connection failed: ${err.message}`));
2347
- console.log(dim(" Make sure `wispy server` is running on the main machine."));
2348
- process.exit(1);
2349
- }
2350
- process.exit(0);
2351
- }
2352
-
2353
- if (sub === "list") {
2354
- const nodeList = await nodes.list();
2355
- if (nodeList.length === 0) {
2356
- console.log(dim("No nodes registered. Use: wispy node pair"));
2357
- } else {
2358
- console.log(`\n${bold("🌐 Registered Nodes:")}\n`);
2359
- for (const n of nodeList) {
2360
- const caps = n.capabilities.join(", ") || dim("none");
2361
- const last = n.lastSeen ? dim(new Date(n.lastSeen).toLocaleString()) : dim("never");
2362
- console.log(` ${green(n.id.slice(-12))} ${bold(n.name.padEnd(20))} ${n.host}:${n.port}`);
2363
- console.log(` Caps: ${caps} Last seen: ${last}`);
2364
- console.log("");
2365
- }
2366
- }
2367
- process.exit(0);
2368
- }
2369
-
2370
- if (sub === "status") {
2371
- const nodeList = await nodes.list();
2372
- if (nodeList.length === 0) {
2373
- console.log(dim("No nodes registered."));
2374
- process.exit(0);
2375
- }
2376
- console.log(`\n${bold("📡 Node Status:")}\n`);
2377
- const results = await nodes.status();
2378
- for (const r of results) {
2379
- const statusIcon = r.alive ? green("●") : red("●");
2380
- const latency = r.latency ? dim(` ${r.latency}ms`) : "";
2381
- const err = r.error ? red(` (${r.error})`) : "";
2382
- console.log(` ${statusIcon} ${r.name.padEnd(20)} ${r.host}:${r.port}${latency}${err}`);
33
+ switch (command) {
34
+ case "ws":
35
+ const { handleWsCommand } = await import(baseDir + '/../lib/commands/ws.mjs');
36
+ return await handleWsCommand(args);
37
+ case "help":
38
+ console.log("Available commands: ws, help");
39
+ break;
40
+ case null:
41
+ console.log("Goodbye!");
42
+ break;
43
+ default:
44
+ console.error(`Unknown command: ${command}`);
45
+ console.log("Try one of the available commands: ws, help");
2383
46
  }
2384
- console.log("");
2385
- process.exit(0);
2386
47
  }
2387
48
 
2388
- if (sub === "remove" && args[2]) {
2389
- const id = args[2];
2390
- const nodeList = await nodes.list();
2391
- const match = nodeList.find(n => n.id === id || n.id.endsWith(id));
2392
- if (!match) {
2393
- console.error(red(`Node not found: ${id}`));
2394
- process.exit(1);
2395
- }
2396
- await nodes.remove(match.id);
2397
- console.log(green(`✅ Removed node: ${match.name} (${match.id})`));
2398
- process.exit(0);
2399
- }
2400
-
2401
- // Help
2402
- console.log(`
2403
- 🌐 Wispy Node Commands:
2404
-
2405
- wispy node pair — generate pairing code
2406
- wispy node connect <code> --url <url> — connect as a node
2407
- wispy node list — show registered nodes
2408
- wispy node status — ping all nodes
2409
- wispy node remove <id> — unregister a node
2410
- `);
2411
- process.exit(0);
2412
- }
2413
-
2414
- // ── channel sub-command ───────────────────────────────────────────────────────
2415
- if (args[0] === "channel") {
2416
- const { channelSetup, channelList, channelTest } = await import(
2417
- path.join(__dirname, "..", "lib", "channels", "index.mjs")
2418
- );
2419
-
2420
- const sub = args[1];
2421
- const name = args[2];
2422
-
2423
- if (sub === "setup" && name) {
2424
- await channelSetup(name);
2425
- } else if (sub === "list") {
2426
- await channelList();
2427
- } else if (sub === "test" && name) {
2428
- await channelTest(name);
2429
- } else if (!sub) {
2430
- // Interactive channel menu
2431
- try {
2432
- const { select, Separator } = await import("@inquirer/prompts");
2433
- const { homedir } = await import("node:os");
2434
- const { readFile } = await import("node:fs/promises");
2435
- const { join } = await import("node:path");
2436
-
2437
- const channelDefs = [
2438
- { key: "telegram", label: "Telegram" },
2439
- { key: "discord", label: "Discord" },
2440
- { key: "slack", label: "Slack" },
2441
- { key: "whatsapp", label: "WhatsApp" },
2442
- { key: "signal", label: "Signal" },
2443
- { key: "email", label: "Email" },
2444
- ];
2445
-
2446
- // Check which are configured
2447
- const configPath = join(homedir(), ".wispy", "config.json");
2448
- let cfg = {};
2449
- try { cfg = JSON.parse(await readFile(configPath, "utf8")); } catch {}
2450
- const channels = cfg.channels ?? {};
2451
-
2452
- const choices = channelDefs.map(ch => {
2453
- const configured = !!channels[ch.key];
2454
- const status = configured ? _green("connected ✓") : _dim("not configured");
2455
- return {
2456
- name: `${ch.label} — ${status}`,
2457
- value: { key: ch.key, configured, label: ch.label },
2458
- short: ch.label,
2459
- };
2460
- });
2461
- choices.push(new Separator("──────────"));
2462
- choices.push({ name: "Start all bots (--serve)", value: { key: "__serve__" } });
2463
-
2464
- let answer;
2465
- try {
2466
- answer = await select({ message: "Channels:", choices });
2467
- } catch (e) {
2468
- if (e.name === "ExitPromptError") { process.exit(130); }
2469
- throw e;
2470
- }
2471
-
2472
- if (answer.key === "__serve__") {
2473
- // Re-run with --serve
2474
- const { spawn } = await import("node:child_process");
2475
- spawn(process.execPath, [process.argv[1], "--serve"], { stdio: "inherit" })
2476
- .on("exit", code => process.exit(code ?? 0));
2477
- await new Promise(() => {});
2478
- } else if (!answer.configured) {
2479
- await channelSetup(answer.key);
2480
- } else {
2481
- // Configured channel actions
2482
- let channelAction;
2483
- try {
2484
- channelAction = await select({
2485
- message: `${answer.label}:`,
2486
- choices: [
2487
- { name: "Test connection", value: "test" },
2488
- { name: "Reconfigure", value: "setup" },
2489
- { name: "Disconnect", value: "disconnect" },
2490
- ],
2491
- });
2492
- } catch (e) {
2493
- if (e.name === "ExitPromptError") { process.exit(130); }
2494
- throw e;
2495
- }
2496
- if (channelAction === "test") {
2497
- await channelTest(answer.key);
2498
- } else if (channelAction === "setup") {
2499
- await channelSetup(answer.key);
2500
- } else if (channelAction === "disconnect") {
2501
- const { confirm } = await import("@inquirer/prompts");
2502
- let ok;
2503
- try { ok = await confirm({ message: `Disconnect ${answer.label}?`, default: false }); }
2504
- catch (e) { if (e.name === "ExitPromptError") { process.exit(130); } throw e; }
2505
- if (ok) {
2506
- const { loadConfig, saveConfig } = await import(path.join(__dirname, "..", "core", "config.mjs"));
2507
- const c = await loadConfig();
2508
- if (c.channels) delete c.channels[answer.key];
2509
- await saveConfig(c);
2510
- console.log(_green(`✅ Disconnected ${answer.label}`));
2511
- }
2512
- }
2513
- }
2514
- } catch (e) {
2515
- if (e.name === "ExitPromptError") { process.exit(130); }
2516
- // Fallback help
2517
- console.log(`
2518
- 🌿 Wispy Channel Commands:
2519
-
2520
- wispy channel setup telegram — interactive Telegram bot setup
2521
- wispy channel setup discord — interactive Discord bot setup
2522
- wispy channel setup slack — interactive Slack bot setup
2523
- wispy channel setup whatsapp — WhatsApp setup
2524
- wispy channel setup email — Email setup
2525
- wispy channel list — show configured channels
2526
- wispy channel test <name> — test a channel connection
2527
- wispy --serve — start all configured channel bots
2528
- `);
2529
- }
2530
- } else {
2531
- console.log(`
2532
- 🌿 Wispy Channel Commands:
2533
-
2534
- wispy channel setup telegram — interactive Telegram bot setup
2535
- wispy channel setup discord — interactive Discord bot setup
2536
- wispy channel setup slack — interactive Slack bot setup
2537
- wispy channel setup whatsapp — WhatsApp setup (requires: npm install whatsapp-web.js qrcode-terminal)
2538
- wispy channel setup signal — Signal setup (requires: signal-cli)
2539
- wispy channel setup email — Email setup (requires: npm install nodemailer imapflow)
2540
- wispy channel list — show configured channels
2541
- wispy channel test <name> — test a channel connection
2542
-
2543
- wispy --serve — start all configured channel bots
2544
- wispy --telegram — start Telegram bot only
2545
- wispy --discord — start Discord bot only
2546
- wispy --slack — start Slack bot only
2547
- `);
2548
- }
2549
- process.exit(0);
2550
- }
2551
-
2552
- // ── auth sub-command ──────────────────────────────────────────────────────────
2553
- if (args[0] === "auth") {
2554
- const { AuthManager } = await import(
2555
- path.join(__dirname, "..", "core", "auth.mjs")
2556
- );
2557
- const { WISPY_DIR } = await import(
2558
- path.join(__dirname, "..", "core", "config.mjs")
2559
- );
2560
-
2561
- const sub = args[1]; // provider name | "refresh" | "revoke" | undefined
2562
-
2563
- const auth = new AuthManager(WISPY_DIR);
2564
-
2565
- // wispy auth — interactive menu
2566
- if (!sub) {
2567
- try {
2568
- const { select, Separator } = await import("@inquirer/prompts");
2569
- const statuses = await auth.allStatus().catch(() => []);
2570
-
2571
- // All known providers + configured ones
2572
- const KNOWN_PROVIDERS = ["google", "anthropic", "openai", "xai", "github-copilot"];
2573
- const configuredMap = {};
2574
- for (const s of statuses) configuredMap[s.provider] = s;
2575
-
2576
- const { loadConfig } = await import(path.join(__dirname, "..", "core", "config.mjs"));
2577
- const cfg = await loadConfig();
2578
- const activeProviders = cfg.providers ? Object.keys(cfg.providers) : (cfg.provider ? [cfg.provider] : []);
2579
-
2580
- const allProviders = [...new Set([...activeProviders, ...statuses.map(s => s.provider), ...KNOWN_PROVIDERS])];
2581
-
2582
- const choices = allProviders.map(p => {
2583
- const s = configuredMap[p];
2584
- const status = s ? (s.expired ? _yellow("⚠️ expired") : _green("API key ✓")) : _dim("not configured");
2585
- return {
2586
- name: `${p} — ${status}`,
2587
- value: { provider: p, configured: !!s, expired: s?.expired },
2588
- short: p,
2589
- };
2590
- });
2591
- choices.push(new Separator("──────────"));
2592
- choices.push({ name: "Add new provider auth", value: { provider: "__new__" } });
2593
- choices.push({ name: "Refresh expired tokens", value: { provider: "__refresh_all__" } });
2594
-
2595
- let answer;
2596
- try {
2597
- answer = await select({ message: "Authentication:", choices });
2598
- } catch (e) {
2599
- if (e.name === "ExitPromptError") { process.exit(130); }
2600
- throw e;
2601
- }
2602
-
2603
- if (answer.provider === "__new__") {
2604
- const { input } = await import("@inquirer/prompts");
2605
- let prov;
2606
- try { prov = await input({ message: "Provider name (e.g. github-copilot):" }); }
2607
- catch (e) { if (e.name === "ExitPromptError") { process.exit(130); } throw e; }
2608
- if (prov && prov.trim()) {
2609
- // Treat as wispy auth <provider>
2610
- const provName = prov.trim();
2611
- if (provName === "github-copilot") {
2612
- console.log(`\n🔑 ${_bold("GitHub Copilot")} — OAuth sign-in\n`);
2613
- try {
2614
- const result = await auth.oauth("github-copilot");
2615
- if (result.valid) console.log(_green(`✅ GitHub Copilot authenticated!\n`));
2616
- } catch (err) { console.error(_red(`\n❌ ${err.message}\n`)); }
2617
- } else {
2618
- console.log(_dim(` API key auth: set env var for ${provName} and run 'wispy setup'\n`));
2619
- }
2620
- }
2621
- } else if (answer.provider === "__refresh_all__") {
2622
- const expired = statuses.filter(s => s.expired);
2623
- if (expired.length === 0) {
2624
- console.log(_dim("No expired tokens found.\n"));
2625
- } else {
2626
- for (const s of expired) {
2627
- process.stdout.write(`Refreshing ${s.provider}... `);
2628
- try { await auth.refreshToken(s.provider); console.log(_green("✓")); }
2629
- catch (e) { console.log(_red(`✗ ${e.message}`)); }
2630
- }
2631
- }
2632
- } else if (answer.configured) {
2633
- let authAction;
2634
- try {
2635
- authAction = await select({
2636
- message: `${answer.provider}:`,
2637
- choices: [
2638
- { name: "Refresh token", value: "refresh" },
2639
- { name: "Revoke / remove", value: "revoke" },
2640
- ],
2641
- });
2642
- } catch (e) {
2643
- if (e.name === "ExitPromptError") { process.exit(130); }
2644
- throw e;
2645
- }
2646
- if (authAction === "refresh") {
2647
- try { await auth.refreshToken(answer.provider); console.log(_green(`✅ Token refreshed for ${answer.provider}\n`)); }
2648
- catch (err) { console.error(_red(`❌ ${err.message}\n`)); }
2649
- } else if (authAction === "revoke") {
2650
- await auth.revokeToken(answer.provider);
2651
- console.log(_green(`✅ Revoked auth for ${answer.provider}\n`));
2652
- }
2653
- } else {
2654
- // Setup flow for unconfigured provider
2655
- if (answer.provider === "github-copilot") {
2656
- console.log(`\n🔑 ${_bold("GitHub Copilot")} — OAuth sign-in\n`);
2657
- try {
2658
- const result = await auth.oauth("github-copilot");
2659
- if (result.valid) console.log(_green(`✅ GitHub Copilot authenticated!\n`));
2660
- } catch (err) { console.error(_red(`\n❌ ${err.message}\n`)); }
2661
- } else {
2662
- console.log(_dim(`\n Set env var for ${answer.provider} and run 'wispy setup provider'\n`));
2663
- }
2664
- }
2665
- process.exit(0);
2666
- } catch (e) {
2667
- if (e.name === "ExitPromptError") { process.exit(130); }
2668
- // Fallback: show status
2669
- const statuses = await auth.allStatus().catch(() => []);
2670
- if (statuses.length === 0) {
2671
- console.log(_dim("\n No saved auth tokens. Run: wispy auth <provider>\n"));
2672
- } else {
2673
- console.log(`\n${_bold("🔑 Auth Status")}\n`);
2674
- for (const s of statuses) {
2675
- const expiryStr = s.expiresAt ? _dim(` (expires ${new Date(s.expiresAt).toLocaleString()})`) : "";
2676
- const statusIcon = s.expired ? _yellow("⚠️ expired") : _green("✅ valid");
2677
- console.log(` ${_cyan(s.provider.padEnd(20))} ${s.type.padEnd(8)} ${statusIcon}${expiryStr}`);
2678
- }
2679
- console.log("");
2680
- }
2681
- process.exit(0);
2682
- }
2683
- }
2684
-
2685
- // wispy auth refresh <provider>
2686
- if (sub === "refresh" && args[2]) {
2687
- const provider = args[2];
2688
- console.log(`\n🔄 Refreshing token for ${_cyan(provider)}...`);
2689
- try {
2690
- await auth.refreshToken(provider);
2691
- console.log(_green(`✅ Token refreshed for ${provider}\n`));
2692
- } catch (err) {
2693
- console.error(_red(`❌ ${err.message}\n`));
2694
- process.exit(1);
2695
- }
2696
- process.exit(0);
2697
- }
2698
-
2699
- // wispy auth revoke <provider>
2700
- if (sub === "revoke" && args[2]) {
2701
- const provider = args[2];
2702
- await auth.revokeToken(provider);
2703
- console.log(_green(`✅ Revoked auth for ${provider}\n`));
2704
- process.exit(0);
2705
- }
2706
-
2707
- // wispy auth <provider> — run OAuth or re-authenticate
2708
- if (sub && sub !== "refresh" && sub !== "revoke") {
2709
- const provider = sub;
2710
-
2711
- if (provider === "github-copilot") {
2712
- console.log(`\n🔑 ${_bold("GitHub Copilot")} — OAuth sign-in\n`);
2713
- try {
2714
- const result = await auth.oauth("github-copilot");
2715
- if (result.valid) {
2716
- console.log(_green(`✅ GitHub Copilot authenticated!\n`));
2717
- console.log(_dim(" Token saved to ~/.wispy/auth/github-copilot.json"));
2718
- console.log(_dim(" Run: wispy auth to verify status\n"));
2719
- }
2720
- } catch (err) {
2721
- console.error(_red(`\n❌ ${err.message}\n`));
2722
- process.exit(1);
2723
- }
2724
- } else {
2725
- console.error(_red(`\n❌ Unknown provider for auth: ${provider}`));
2726
- console.log(_dim(` Currently OAuth is supported for: github-copilot\n`));
2727
- process.exit(1);
2728
- }
2729
- process.exit(0);
2730
- }
2731
-
2732
- // Help
2733
- console.log(`
2734
- ${_bold("🔑 Wispy Auth Commands")}
2735
-
2736
- ${_cyan("wispy auth")} — show auth status for all providers
2737
- ${_cyan("wispy auth github-copilot")} — sign in with GitHub (Copilot OAuth)
2738
- ${_cyan("wispy auth refresh <provider>")} — refresh expired token
2739
- ${_cyan("wispy auth revoke <provider>")} — remove saved auth
2740
-
2741
- ${_bold("Examples:")}
2742
- wispy auth github-copilot
2743
- wispy auth refresh github-copilot
2744
- wispy auth revoke github-copilot
2745
- `);
2746
- process.exit(0);
2747
- }
2748
-
2749
- // ── Unknown command detection ─────────────────────────────────────────────────
2750
- // Any non-flag argument that wasn't matched above is an unknown command
2751
- const _KNOWN_COMMANDS = new Set([
2752
- "ws", "trust", "where", "handoff", "skill", "teach", "improve", "dry",
2753
- "setup", "init", "update", "status", "connect", "disconnect", "sync",
2754
- "deploy", "migrate", "cron", "audit", "log", "server", "node", "channel",
2755
- "auth", "config", "model", "tui", "help", "doctor", "completion", "version",
2756
- // serve flags (handled below)
2757
- "--serve", "--telegram", "--discord", "--slack", "--server",
2758
- "--help", "-h", "--version", "-v", "--debug", "--tui",
2759
- // workstream flags
2760
- "-w", "--workstream",
2761
- ]);
2762
- const _firstArg = args[0];
2763
- if (_firstArg && _firstArg[0] !== "-" && !_KNOWN_COMMANDS.has(_firstArg)) {
2764
- // Not a known command — but could be a one-shot message (no quotes needed)
2765
- // Heuristic: if it looks like a real command word (no spaces, short), warn.
2766
- // If it looks like a natural language sentence, fall through to REPL one-shot mode.
2767
- // Only show "unknown command" if it looks very much like a mistyped subcommand.
2768
- // Common typos/misspellings of real commands (2-char levenshtein distance).
2769
- // Everything else falls through to one-shot message mode.
2770
- const CLOSE_COMMANDS = ["seutp", "set", "stat", "statu", "doc", "wss", "trus", "depliy", "sycn", "cran"];
2771
- const looksLikeCommand = CLOSE_COMMANDS.includes(_firstArg) || (_firstArg.length <= 3 && /^[a-z]+$/.test(_firstArg) && !["hi", "hey", "yo", "sup", "thx", "ty", "ok", "no", "yes", "ya", "hmm"].includes(_firstArg));
2772
- if (looksLikeCommand) {
2773
- // Show unknown command error
2774
- const suggestions = [
2775
- { cmd: "setup", desc: "configure wispy" },
2776
- { cmd: "tui", desc: "workspace UI" },
2777
- { cmd: "ws", desc: "workstream management" },
2778
- { cmd: "doctor", desc: "check system health" },
2779
- { cmd: "help", desc: "show detailed help" },
2780
- ];
2781
- console.error(`\n${_red(`❌ Unknown command: ${_firstArg}`)}`);
2782
- console.error(`\n${_bold("Did you mean one of these?")}`);
2783
- for (const { cmd, desc } of suggestions) {
2784
- console.error(` ${_cyan("wispy " + cmd.padEnd(12))}— ${desc}`);
2785
- }
2786
- console.error(`\n${_dim("Run wispy --help for all commands.")}\n`);
2787
- process.exit(2);
2788
- }
2789
- // Otherwise fall through to one-shot message mode in REPL
2790
- }
2791
-
2792
- // ── Bot / serve modes ─────────────────────────────────────────────────────────
2793
- const serveMode = args.includes("--serve");
2794
- const telegramMode = args.includes("--telegram");
2795
- const discordMode = args.includes("--discord");
2796
- const slackMode = args.includes("--slack");
2797
-
2798
- if (serveMode || telegramMode || discordMode || slackMode) {
2799
- const { ChannelManager } = await import(
2800
- path.join(__dirname, "..", "lib", "channels", "index.mjs")
2801
- );
2802
-
2803
- const manager = new ChannelManager();
2804
-
2805
- const only = [];
2806
- if (telegramMode) only.push("telegram");
2807
- if (discordMode) only.push("discord");
2808
- if (slackMode) only.push("slack");
2809
- // serveMode → only stays empty → all channels started
2810
-
2811
- await manager.startAll(only);
2812
-
2813
- // Start cron scheduler if in serve mode
2814
- let cronManager = null;
2815
- if (serveMode) {
2816
- try {
2817
- const { WispyEngine, CronManager, WISPY_DIR } = await import(
2818
- path.join(__dirname, "..", "core", "index.mjs")
2819
- );
2820
- const engine = new WispyEngine();
2821
- await engine.init({ skipMcp: true });
2822
- cronManager = new CronManager(WISPY_DIR, engine);
2823
- await cronManager.init();
2824
- cronManager.start();
2825
- console.error("[wispy] Cron scheduler started");
2826
- } catch (err) {
2827
- console.error("[wispy] Failed to start cron scheduler:", err.message);
2828
- }
2829
- }
2830
-
2831
- // Keep process alive and stop cleanly on Ctrl+C
2832
- process.on("SIGINT", async () => {
2833
- if (cronManager) cronManager.stop();
2834
- await manager.stopAll();
2835
- process.exit(0);
2836
- });
2837
- process.on("SIGTERM", async () => {
2838
- if (cronManager) cronManager.stop();
2839
- await manager.stopAll();
2840
- process.exit(0);
2841
- });
2842
-
2843
- // Prevent Node from exiting
2844
- setInterval(() => {}, 60_000);
2845
- // eslint-disable-next-line no-constant-condition
2846
- await new Promise(() => {}); // keep alive
2847
- }
2848
-
2849
- // ── First-run detection (before TUI or REPL) ──────────────────────────────────
2850
- // Only trigger onboarding for interactive modes (not flags like --serve, channel, etc.)
2851
- const isInteractiveStart = !args.some(a =>
2852
- ["--serve", "--telegram", "--discord", "--slack", "--server",
2853
- "status", "setup", "init", "connect", "disconnect", "deploy",
2854
- "cron", "audit", "log", "server", "node", "channel", "sync", "tui",
2855
- "ws", "trust", "where", "handoff", "skill", "teach", "improve", "dry",
2856
- "auth"].includes(a)
2857
- );
2858
-
2859
- if (isInteractiveStart) {
2860
- try {
2861
- const { isFirstRun } = await import(
2862
- path.join(__dirname, "..", "core", "config.mjs")
2863
- );
2864
- if (await isFirstRun()) {
2865
- // Show welcome interactive menu before onboarding
2866
- if (args.length === 0) {
2867
- try {
2868
- const { select } = await import("@inquirer/prompts");
2869
- let welcomeAction;
2870
- try {
2871
- welcomeAction = await select({
2872
- message: "Welcome to Wispy! What would you like to do?",
2873
- choices: [
2874
- { name: "Start chatting (REPL)", value: "repl" },
2875
- { name: "Open workspace (TUI)", value: "tui" },
2876
- { name: "Set up wispy", value: "setup" },
2877
- { name: "Run diagnostics", value: "doctor" },
2878
- ],
2879
- });
2880
- } catch (e) {
2881
- if (e.name === "ExitPromptError") { process.exit(130); }
2882
- throw e;
2883
- }
2884
- if (welcomeAction === "tui") {
2885
- args.push("tui");
2886
- } else if (welcomeAction === "setup") {
2887
- const { OnboardingWizard } = await import(path.join(__dirname, "..", "core", "onboarding.mjs"));
2888
- const wizard = new OnboardingWizard();
2889
- await wizard.run();
2890
- process.exit(0);
2891
- } else if (welcomeAction === "doctor") {
2892
- // Re-exec with doctor
2893
- const { spawn: sp } = await import("node:child_process");
2894
- sp(process.execPath, [process.argv[1], "doctor"], { stdio: "inherit" })
2895
- .on("exit", code => process.exit(code ?? 0));
2896
- await new Promise(() => {});
2897
- }
2898
- // "repl" falls through to REPL below
2899
- } catch (e) {
2900
- if (e.name !== "ExitPromptError") {
2901
- // Run normal onboarding if welcome menu fails
2902
- const { OnboardingWizard } = await import(path.join(__dirname, "..", "core", "onboarding.mjs"));
2903
- const wizard = new OnboardingWizard();
2904
- await wizard.run();
2905
- }
2906
- }
2907
- } else {
2908
- const { OnboardingWizard } = await import(
2909
- path.join(__dirname, "..", "core", "onboarding.mjs")
2910
- );
2911
- const wizard = new OnboardingWizard();
2912
- await wizard.run();
2913
- // After onboarding, continue to REPL or TUI
2914
- }
2915
- }
2916
- } catch {
2917
- // If onboarding fails for any reason, continue normally
2918
- }
2919
- }
2920
-
2921
- // Validate config before starting interactive modes
2922
- await validateConfigOnStartup();
2923
-
2924
- // ── TUI mode ──────────────────────────────────────────────────────────────────
2925
- // `wispy ui` is the primary way; `--tui` is kept as a hidden backwards-compat alias
2926
- const tuiMode = args[0] === "tui" || args.includes("--tui");
2927
-
2928
- if (tuiMode) {
2929
- // Override SIGINT for clean TUI exit
2930
- process.removeAllListeners("SIGINT");
2931
- process.on("SIGINT", () => { process.exit(130); });
2932
-
2933
- const newArgs = args.filter(a => a !== "--tui" && a !== "tui");
2934
- process.argv = [process.argv[0], process.argv[1], ...newArgs];
2935
- const tuiScript = path.join(__dirname, "..", "lib", "wispy-tui.mjs");
2936
- try {
2937
- await import(tuiScript);
2938
- } catch (e) { friendlyError(e); }
2939
- } else {
2940
- // Default: interactive REPL
2941
- const mainScript = path.join(__dirname, "..", "lib", "wispy-repl.mjs");
2942
- try {
2943
- await import(mainScript);
2944
- } catch (e) { friendlyError(e); }
2945
- }
49
+ await handleCommand(args[0] === '--help' ? 'help' : args[0]);
50
+ })();