wispy-cli 2.6.3 → 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.
package/bin/wispy.mjs CHANGED
@@ -1,2219 +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
  */
18
-
19
- import { fileURLToPath } from "node:url";
20
- import path from "node:path";
21
- import { readFileSync } from "node:fs";
22
-
23
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
-
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) — show current
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
- if (configuredProviders.length > 1) {
829
- console.log(` Configured providers: ${configuredProviders.join(", ")}`);
830
- console.log(` ${_dim("Use 'wispy model list' to see all options")}`);
831
- console.log(` ${_dim("Use 'wispy model set <provider:model>' to switch")}\n`);
832
- }
833
- process.exit(0);
834
- }
835
-
836
- // wispy model list
837
- if (sub === "list") {
838
- console.log(`\n🤖 ${_bold("Available models")}\n`);
839
- for (const p of configuredProviders) {
840
- const currentModel = config.providers?.[p]?.model ?? PROVIDERS[p]?.defaultModel ?? "";
841
- const models = KNOWN_MODELS[p] ?? [currentModel];
842
- console.log(` ${_cyan(p)}:`);
843
- for (const m of models) {
844
- const isCurrent = m === currentModel;
845
- console.log(` ${isCurrent ? _green("●") : " "} ${m}${isCurrent ? _dim(" (current)") : ""}`);
846
- }
847
- }
848
- console.log(`\n ${_dim("Switch with: wispy model set <provider:model>")}\n`);
849
- process.exit(0);
850
- }
851
-
852
- // wispy model set <provider:model>
853
- if (sub === "set") {
854
- const spec = args[2];
855
- if (!spec || !spec.includes(":")) {
856
- console.error(_red(`\n❌ Usage: wispy model set <provider:model>\n Example: wispy model set xai:grok-3\n`));
857
- process.exit(1);
858
- }
859
- const colonIdx = spec.indexOf(":");
860
- const provName = spec.slice(0, colonIdx);
861
- const modelName = spec.slice(colonIdx + 1);
862
-
863
- if (!PROVIDERS[provName]) {
864
- console.error(_red(`\n❌ Unknown provider: ${provName}\n Available: ${Object.keys(PROVIDERS).join(", ")}\n`));
865
- process.exit(1);
866
- }
867
-
868
- // Update config
869
- if (!config.providers) config.providers = {};
870
- if (!config.providers[provName]) config.providers[provName] = {};
871
- config.providers[provName].model = modelName;
872
- if (!config.defaultProvider) config.defaultProvider = provName;
873
-
874
- await saveConfig(config);
875
- console.log(_green(`\n✅ Switched to ${_cyan(provName)}:${_bold(modelName)}\n`));
876
- process.exit(0);
877
- }
878
-
879
- console.error(_red(`\n❌ Unknown subcommand: wispy model ${sub}\n`));
880
- console.log(_dim(" Usage: wispy model | wispy model list | wispy model set <provider:model>\n"));
881
- process.exit(1);
882
- }
883
-
884
- // ── config sub-command ────────────────────────────────────────────────────────
885
- if (args[0] === "config") {
886
- const { loadConfig, saveConfig, WISPY_DIR, CONFIG_PATH } = await import(
887
- path.join(__dirname, "..", "core", "config.mjs")
888
- );
889
- const sub = args[1];
890
- const config = await loadConfig();
891
-
892
- if (!sub || sub === "show") {
893
- // wispy config — interactive config menu
894
- const { select: cfgSelect } = await import("@inquirer/prompts");
895
-
896
- // Show current status first
897
- const providers = config.providers ? Object.keys(config.providers) : (config.provider ? [config.provider] : []);
898
- const providerStr = providers.length > 0 ? providers.join(", ") : _dim("not set");
899
- const defaultProv = config.defaultProvider ?? config.provider ?? _dim("not set");
900
- const security = config.security ?? config.securityLevel ?? _dim("not set");
901
- const language = config.language ?? _dim("auto");
902
- const wsName = config.workstream ?? "default";
903
-
904
- console.log(`\n${_bold("Wispy Config")}\n`);
905
- console.log(` Providers: ${providerStr}`);
906
- console.log(` Default: ${defaultProv}`);
907
- console.log(` Security: ${security}`);
908
- console.log(` Language: ${language}`);
909
- console.log(` Workstream: ${wsName}`);
910
- console.log(` Config file: ${_dim(CONFIG_PATH)}`);
911
- console.log("");
912
-
913
- const action = await cfgSelect({
914
- message: "What do you want to change?",
915
- choices: [
916
- { name: "Providers — add/remove AI providers", value: "provider" },
917
- { name: "Security — change trust level", value: "security" },
918
- { name: "Language — set preferred language", value: "language" },
919
- { name: "Channels — configure messaging bots", value: "channels" },
920
- { name: "Server — cloud/server settings", value: "server" },
921
- { name: "View raw config (JSON)", value: "raw" },
922
- { name: "Reset everything", value: "reset" },
923
- { name: "Done", value: "done" },
924
- ],
925
- });
926
-
927
- if (action === "done") {
928
- process.exit(0);
929
- } else if (action === "raw") {
930
- const display = JSON.parse(JSON.stringify(config));
931
- if (display.providers) {
932
- for (const [k, v] of Object.entries(display.providers)) {
933
- if (v.apiKey) v.apiKey = v.apiKey.slice(0, 6) + "..." + v.apiKey.slice(-4);
934
- }
935
- }
936
- console.log(JSON.stringify(display, null, 2));
937
- } else if (action === "reset") {
938
- const { confirm: cfgConfirm } = await import("@inquirer/prompts");
939
- const yes = await cfgConfirm({ message: "Reset all configuration?", default: false });
940
- if (yes) {
941
- const { writeFile: wf } = await import("node:fs/promises");
942
- await wf(CONFIG_PATH, "{}\n");
943
- console.log(`${_green("✓")} Config reset. Run 'wispy setup' to reconfigure.`);
944
- }
945
- } else {
946
- // Delegate to setup wizard step
947
- const { OnboardingWizard } = await import(
948
- path.join(__dirname, "..", "core", "onboarding.mjs")
949
- );
950
- const wizard = new OnboardingWizard();
951
- await wizard.runStep(action);
952
- }
953
-
954
- } else if (sub === "get") {
955
- // wispy config get <key>
956
- const key = args[2];
957
- if (!key) { console.error(_red("Usage: wispy config get <key>")); process.exit(2); }
958
- const val = key.split(".").reduce((o, k) => o?.[k], config);
959
- if (val === undefined) {
960
- console.log(_dim(`(not set)`));
961
- } else {
962
- console.log(typeof val === "object" ? JSON.stringify(val, null, 2) : String(val));
963
- }
964
-
965
- } else if (sub === "set") {
966
- // wispy config set <key> <value>
967
- const key = args[2];
968
- const value = args.slice(3).join(" ");
969
- if (!key || !value) { console.error(_red("Usage: wispy config set <key> <value>")); process.exit(2); }
970
-
971
- // Parse value
972
- let parsed = value;
973
- if (value === "true") parsed = true;
974
- else if (value === "false") parsed = false;
975
- else if (/^\d+$/.test(value)) parsed = parseInt(value);
976
-
977
- // Set nested key
978
- const keys = key.split(".");
979
- let obj = config;
980
- for (let i = 0; i < keys.length - 1; i++) {
981
- if (!obj[keys[i]]) obj[keys[i]] = {};
982
- obj = obj[keys[i]];
983
- }
984
- obj[keys[keys.length - 1]] = parsed;
985
- await saveConfig(config);
986
- console.log(`${_green("✓")} ${key} = ${parsed}`);
987
-
988
- } else if (sub === "delete" || sub === "unset") {
989
- // wispy config delete <key>
990
- const key = args[2];
991
- if (!key) { console.error(_red("Usage: wispy config delete <key>")); process.exit(2); }
992
- const keys = key.split(".");
993
- let obj = config;
994
- for (let i = 0; i < keys.length - 1; i++) {
995
- if (!obj[keys[i]]) break;
996
- obj = obj[keys[i]];
997
- }
998
- delete obj[keys[keys.length - 1]];
999
- await saveConfig(config);
1000
- console.log(`${_green("✓")} ${key} removed`);
1001
-
1002
- } else if (sub === "reset") {
1003
- // wispy config reset
1004
- const { confirm } = await import("@inquirer/prompts");
1005
- const yes = await confirm({ message: "Reset all configuration? This cannot be undone.", default: false });
1006
- if (yes) {
1007
- const { writeFile } = await import("node:fs/promises");
1008
- await writeFile(CONFIG_PATH, "{}\n");
1009
- console.log(`${_green("✓")} Config reset. Run 'wispy setup' to reconfigure.`);
1010
- } else {
1011
- console.log(_dim("Cancelled."));
1012
- }
1013
-
1014
- } else if (sub === "path") {
1015
- // wispy config path
1016
- console.log(CONFIG_PATH);
1017
-
1018
- } else if (sub === "edit") {
1019
- // wispy config edit — open in $EDITOR
1020
- const editor = process.env.EDITOR ?? process.env.VISUAL ?? "nano";
1021
- const { spawn: sp } = await import("node:child_process");
1022
- sp(editor, [CONFIG_PATH], { stdio: "inherit" });
1023
-
1024
- } else {
1025
- console.log(`
1026
- ${_bold("wispy config")} — manage configuration
1027
-
1028
- ${_cyan("wispy config")} Show current config (keys masked)
1029
- ${_cyan("wispy config get <key>")} Get a specific value (dot notation)
1030
- ${_cyan("wispy config set <key> <val>")} Set a value
1031
- ${_cyan("wispy config delete <key>")} Remove a key
1032
- ${_cyan("wispy config reset")} Reset to defaults
1033
- ${_cyan("wispy config path")} Show config file path
1034
- ${_cyan("wispy config edit")} Open in $EDITOR
1035
-
1036
- ${_dim("Examples:")}
1037
- ${_dim("wispy config get defaultProvider")}
1038
- ${_dim("wispy config set security careful")}
1039
- ${_dim("wispy config set language ko")}
1040
- `);
1041
- }
1042
- process.exit(0);
1043
- }
1044
-
1045
- // ── status sub-command ────────────────────────────────────────────────────────
1046
- if (args[0] === "status") {
1047
- // Try the enhanced status from onboarding.mjs first
1048
- try {
1049
- const { printStatus } = await import(
1050
- path.join(__dirname, "..", "core", "onboarding.mjs")
1051
- );
1052
- await printStatus();
1053
- process.exit(0);
1054
- } catch (e) { if (DEBUG) console.error(e); }
1055
-
1056
- // Fallback: original status (remote check)
1057
- const { readFile } = await import("node:fs/promises");
1058
- const { homedir } = await import("node:os");
1059
- const { join } = await import("node:path");
1060
- const { DeployManager } = await import(
1061
- path.join(__dirname, "..", "core", "deploy.mjs")
1062
- );
1063
-
1064
- const remotePath = join(homedir(), ".wispy", "remote.json");
1065
- let remote = null;
1066
- try {
1067
- remote = JSON.parse(await readFile(remotePath, "utf8"));
1068
- } catch {}
1069
-
1070
- const bold = (s) => `\x1b[1m${s}\x1b[0m`;
1071
- const green = (s) => `\x1b[32m${s}\x1b[0m`;
1072
- const dim = (s) => `\x1b[2m${s}\x1b[0m`;
1073
- const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
1074
- const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
1075
-
1076
- console.log(`\n🌿 ${bold("Wispy Status")}\n`);
1077
-
1078
- if (remote?.url) {
1079
- console.log(` Mode: ${yellow("remote")}`);
1080
- console.log(` Server: ${cyan(remote.url)}`);
1081
- console.log(` Token: ${dim(remote.token ? remote.token.slice(0, 8) + "..." : "none")}`);
1082
-
1083
- const dm = new DeployManager();
1084
- process.stdout.write(" Health: checking... ");
1085
- const status = await dm.checkRemote(remote.url);
1086
- if (status.alive) {
1087
- console.log(green("✓ alive"));
1088
- if (status.version) console.log(` Version: ${status.version}`);
1089
- if (status.uptime != null) console.log(` Uptime: ${Math.floor(status.uptime)}s`);
1090
- if (status.latency) console.log(` Latency: ${status.latency}ms`);
1091
- } else {
1092
- console.log(`\x1b[31m✗ unreachable\x1b[0m`);
1093
- if (status.error) console.log(` Error: ${dim(status.error)}`);
1094
- }
1095
- } else {
1096
- console.log(` Mode: ${green("local")}`);
1097
- console.log(` Server: http://localhost:18790 ${dim("(when running wispy server)")}`);
1098
- console.log(dim("\n Tip: use `wispy connect <url> --token <token>` to use a remote server"));
1099
- }
1100
- console.log("");
1101
- process.exit(0);
1102
- }
1103
-
1104
- // ── connect sub-command ───────────────────────────────────────────────────────
1105
- if (args[0] === "connect" && args[1]) {
1106
- const { writeFile, mkdir } = await import("node:fs/promises");
1107
- const { homedir } = await import("node:os");
1108
- const { join } = await import("node:path");
1109
- const { DeployManager } = await import(
1110
- path.join(__dirname, "..", "core", "deploy.mjs")
1111
- );
1112
-
1113
- const url = args[1].replace(/\/$/, "");
1114
- const tokenIdx = args.indexOf("--token");
1115
- const token = tokenIdx !== -1 ? args[tokenIdx + 1] : "";
1116
-
1117
- const green = (s) => `\x1b[32m${s}\x1b[0m`;
1118
- const red = (s) => `\x1b[31m${s}\x1b[0m`;
1119
- const dim = (s) => `\x1b[2m${s}\x1b[0m`;
1120
-
1121
- process.stdout.write(`\n🔗 Checking ${url}... `);
1122
- const dm = new DeployManager();
1123
- const status = await dm.checkRemote(url);
1124
-
1125
- if (!status.alive) {
1126
- console.log(red("unreachable"));
1127
- if (status.error) console.log(dim(` ${status.error}`));
1128
- console.log(red("\n❌ Could not connect to remote wispy server."));
1129
- process.exit(1);
1130
- }
1131
-
1132
- console.log(green("✓ alive"));
1133
-
1134
- const wispyDir = join(homedir(), ".wispy");
1135
- await mkdir(wispyDir, { recursive: true });
1136
- await writeFile(
1137
- join(wispyDir, "remote.json"),
1138
- JSON.stringify({ url, token, connectedAt: new Date().toISOString() }, null, 2),
1139
- "utf8"
1140
- );
1141
-
1142
- console.log(green(`\n✅ Connected to ${url}`));
1143
- console.log(dim(" Local wispy will now proxy to the remote server."));
1144
- console.log(dim(" Run `wispy disconnect` to go back to local mode.\n"));
1145
- process.exit(0);
1146
- }
1147
-
1148
- // ── disconnect sub-command ────────────────────────────────────────────────────
1149
- if (args[0] === "disconnect") {
1150
- const { unlink } = await import("node:fs/promises");
1151
- const { homedir } = await import("node:os");
1152
- const { join } = await import("node:path");
1153
-
1154
- const remotePath = join(homedir(), ".wispy", "remote.json");
1155
- try {
1156
- await unlink(remotePath);
1157
- console.log("\n✅ Disconnected. Wispy is back in local mode.\n");
1158
- } catch {
1159
- console.log("\n🌿 Already in local mode.\n");
1160
- }
1161
- process.exit(0);
1162
- }
1163
-
1164
- // ── sync sub-command ──────────────────────────────────────────────────────────
1165
- if (args[0] === "sync") {
1166
- const { SyncManager } = await import(
1167
- path.join(__dirname, "..", "core", "sync.mjs")
1168
- );
1169
-
1170
- const sub = args[1]; // push | pull | status | auto | (undefined = bidirectional)
1171
-
1172
- const green = (s) => `\x1b[32m${s}\x1b[0m`;
1173
- const red = (s) => `\x1b[31m${s}\x1b[0m`;
1174
- const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
1175
- const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
1176
- const bold = (s) => `\x1b[1m${s}\x1b[0m`;
1177
- const dim = (s) => `\x1b[2m${s}\x1b[0m`;
1178
-
1179
- // Parse flags
1180
- const strategyIdx = args.indexOf("--strategy");
1181
- const strategy = strategyIdx !== -1 ? args[strategyIdx + 1] : null;
1182
- const memoryOnly = args.includes("--memory-only");
1183
- const sessionsOnly = args.includes("--sessions-only");
1184
- const remoteUrlIdx = args.indexOf("--remote");
1185
- const remoteUrlArg = remoteUrlIdx !== -1 ? args[remoteUrlIdx + 1] : null;
1186
- const tokenIdx = args.indexOf("--token");
1187
- const tokenArg = tokenIdx !== -1 ? args[tokenIdx + 1] : null;
1188
-
1189
- // Load config (or use overrides)
1190
- const cfg = await SyncManager.loadConfig();
1191
- const remoteUrl = remoteUrlArg ?? cfg.remoteUrl;
1192
- const token = tokenArg ?? cfg.token;
1193
-
1194
- // ── auto enable/disable ──
1195
- if (sub === "auto") {
1196
- const enable = !args.includes("--off");
1197
- const disable = args.includes("--off");
1198
- if (disable) {
1199
- await SyncManager.disableAuto();
1200
- console.log(`\n${yellow("⏸")} Auto-sync ${bold("disabled")}.\n`);
1201
- } else if (!remoteUrl) {
1202
- console.log(`\n${red("✗")} No remote URL configured.`);
1203
- console.log(dim(` Use: wispy sync auto --remote https://vps.com:18790 --token <token>\n`));
1204
- process.exit(1);
1205
- } else {
1206
- await SyncManager.enableAuto(remoteUrl, token ?? "");
1207
- console.log(`\n${green("✓")} Auto-sync ${bold("enabled")}.`);
1208
- console.log(` Remote: ${cyan(remoteUrl)}`);
1209
- console.log(dim(" Sessions and memory will be synced automatically.\n"));
1210
- }
1211
- process.exit(0);
1212
- }
1213
-
1214
- // ── status ──
1215
- if (sub === "status") {
1216
- if (!remoteUrl) {
1217
- console.log(`\n${red("✗")} No remote configured. Set via sync.json or --remote flag.\n`);
1218
- process.exit(1);
1219
- }
1220
- console.log(`\n🔄 ${bold("Sync Status")}\n`);
1221
- console.log(` Remote: ${cyan(remoteUrl)}`);
1222
- const mgr = new SyncManager({ remoteUrl, token, strategy: strategy ?? "newer-wins" });
1223
- const s = await mgr.status(remoteUrl, token);
1224
- console.log(` Connection: ${s.reachable ? green("✅ reachable") : red("✗ unreachable")}\n`);
1225
-
1226
- const fmt = (label, info) => {
1227
- const parts = [];
1228
- if (info.localOnly > 0) parts.push(`${yellow(info.localOnly + " local only")}`);
1229
- if (info.remoteOnly > 0) parts.push(`${cyan(info.remoteOnly + " remote only")}`);
1230
- if (info.inSync > 0) parts.push(`${green(info.inSync + " in sync")}`);
1231
- console.log(` ${bold(label.padEnd(14))} ${parts.join(", ") || dim("(empty)")}`);
1232
- };
1233
- fmt("Memory:", s.memory);
1234
- fmt("Sessions:", s.sessions);
1235
- fmt("Cron:", s.cron);
1236
- fmt("Workstreams:",s.workstreams);
1237
- fmt("Permissions:",s.permissions);
1238
-
1239
- const needPull = (s.memory.remoteOnly + s.sessions.remoteOnly + s.cron.remoteOnly + s.workstreams.remoteOnly + s.permissions.remoteOnly);
1240
- const needPush = (s.memory.localOnly + s.sessions.localOnly + s.cron.localOnly + s.workstreams.localOnly + s.permissions.localOnly);
1241
- if (needPull > 0 || needPush > 0) {
1242
- console.log(`\n Action needed: ${needPull > 0 ? `pull ${yellow(needPull)} file(s)` : ""} ${needPush > 0 ? `push ${yellow(needPush)} file(s)` : ""}`.trimEnd());
1243
- console.log(dim(" Run 'wispy sync' to synchronize."));
1244
- } else {
1245
- console.log(`\n ${green("✓")} Everything in sync!`);
1246
- }
1247
- console.log("");
1248
- process.exit(0);
1249
- }
1250
-
1251
- // For push/pull/sync we need a remote URL
1252
- if (!remoteUrl) {
1253
- console.log(`\n${red("✗")} No remote configured.`);
1254
- console.log(dim(" Set remoteUrl in ~/.wispy/sync.json or use --remote <url>\n"));
1255
- process.exit(1);
1256
- }
1257
-
1258
- const opts = { strategy: strategy ?? cfg.strategy ?? "newer-wins", memoryOnly, sessionsOnly };
1259
- const mgr = new SyncManager({ remoteUrl, token, ...opts });
1260
-
1261
- if (sub === "push") {
1262
- console.log(`\n📤 ${bold("Pushing")} to ${cyan(remoteUrl)}...`);
1263
- const result = await mgr.push(remoteUrl, token, opts);
1264
- console.log(` Pushed: ${green(result.pushed)}`);
1265
- console.log(` Skipped: ${dim(result.skipped)}`);
1266
- if (result.errors.length) result.errors.forEach(e => console.log(` ${red("✗")} ${e}`));
1267
- console.log(`\n${result.errors.length === 0 ? green("✓ Done") : yellow("⚠ Done with errors")}.\n`);
1268
-
1269
- } else if (sub === "pull") {
1270
- console.log(`\n📥 ${bold("Pulling")} from ${cyan(remoteUrl)}...`);
1271
- const result = await mgr.pull(remoteUrl, token, opts);
1272
- console.log(` Pulled: ${green(result.pulled)}`);
1273
- console.log(` Skipped: ${dim(result.skipped)}`);
1274
- if (result.conflicts) console.log(` Conflicts: ${yellow(result.conflicts)} (saved as .conflict-* files)`);
1275
- if (result.errors.length) result.errors.forEach(e => console.log(` ${red("✗")} ${e}`));
1276
- console.log(`\n${result.errors.length === 0 ? green("✓ Done") : yellow("⚠ Done with errors")}.\n`);
1277
-
1278
- } else {
1279
- // Bidirectional sync (default)
1280
- console.log(`\n🔄 ${bold("Syncing")} with ${cyan(remoteUrl)}...`);
1281
- const result = await mgr.sync(remoteUrl, token, opts);
1282
- console.log(` Pushed: ${green(result.pushed)}`);
1283
- console.log(` Pulled: ${green(result.pulled)}`);
1284
- console.log(` Skipped: ${dim(result.skipped)}`);
1285
- if (result.conflicts) console.log(` Conflicts: ${yellow(result.conflicts)} (saved as .conflict-* files)`);
1286
- if (result.errors.length) result.errors.forEach(e => console.log(` ${red("✗")} ${e}`));
1287
- console.log(`\n${result.errors.length === 0 ? green("✓ Done") : yellow("⚠ Done with errors")}.\n`);
1288
- }
1289
-
1290
- process.exit(0);
1291
- }
1292
-
1293
- // ── deploy sub-command ────────────────────────────────────────────────────────
1294
- if (args[0] === "deploy") {
1295
- const { DeployManager } = await import(
1296
- path.join(__dirname, "..", "core", "deploy.mjs")
1297
- );
1298
-
1299
- const sub = args[1];
1300
- const dm = new DeployManager();
1301
-
1302
- const green = (s) => `\x1b[32m${s}\x1b[0m`;
1303
- const dim = (s) => `\x1b[2m${s}\x1b[0m`;
1304
- const bold = (s) => `\x1b[1m${s}\x1b[0m`;
1305
- const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
1306
-
1307
- if (sub === "dockerfile") {
1308
- process.stdout.write(dm.generateDockerfile());
1309
- process.exit(0);
1310
- }
1311
-
1312
- if (sub === "compose") {
1313
- process.stdout.write(dm.generateDockerCompose());
1314
- process.exit(0);
1315
- }
1316
-
1317
- if (sub === "systemd") {
1318
- process.stdout.write(dm.generateSystemd());
1319
- process.exit(0);
1320
- }
1321
-
1322
- if (sub === "railway") {
1323
- process.stdout.write(dm.generateRailwayConfig() + "\n");
1324
- process.exit(0);
1325
- }
1326
-
1327
- if (sub === "fly") {
1328
- process.stdout.write(dm.generateFlyConfig());
1329
- process.exit(0);
1330
- }
1331
-
1332
- if (sub === "render") {
1333
- process.stdout.write(dm.generateRenderConfig());
1334
- process.exit(0);
1335
- }
1336
-
1337
- if (sub === "init") {
1338
- console.log("\n🌿 Initializing wispy deploy configs...\n");
1339
- const created = await dm.init(process.cwd());
1340
- for (const f of created) {
1341
- console.log(` ${f.includes("skipped") ? "⏭️ " : "✅"} ${f}`);
1342
- }
1343
- console.log(dim("\n Next steps:"));
1344
- console.log(dim(" 1. Copy .env.example → .env and fill in your API keys"));
1345
- console.log(dim(" 2. docker-compose up -d — for Docker"));
1346
- console.log(dim(" 3. wispy deploy vps user@host — for raw VPS (no Docker)\n"));
1347
- process.exit(0);
1348
- }
1349
-
1350
- if (sub === "vps" && args[2]) {
1351
- const target = args[2];
1352
- const envIdx = args.indexOf("--env");
1353
- const envFile = envIdx !== -1 ? args[envIdx + 1] : null;
1354
-
1355
- try {
1356
- await dm.deployVPS({ target, envFile });
1357
- } catch (err) {
1358
- console.error(`\n❌ Deploy failed: ${err.message}`);
1359
- process.exit(1);
1360
- }
1361
- process.exit(0);
1362
- }
1363
-
1364
- if (sub === "status" && args[2]) {
1365
- const url = args[2];
1366
- process.stdout.write(`\n📡 Checking ${cyan(url)}... `);
1367
- const status = await dm.checkRemote(url);
1368
- if (status.alive) {
1369
- console.log(green("✓ alive"));
1370
- if (status.version) console.log(` Version: ${status.version}`);
1371
- if (status.uptime != null) console.log(` Uptime: ${Math.floor(status.uptime)}s`);
1372
- if (status.latency) console.log(` Latency: ${status.latency}ms`);
1373
- } else {
1374
- console.log(`\x1b[31m✗ unreachable\x1b[0m`);
1375
- if (status.error) console.log(` Error: ${dim(status.error)}`);
1376
- }
1377
- console.log("");
1378
- process.exit(0);
1379
- }
1380
-
1381
- if (sub === "modal") {
1382
- process.stdout.write(dm.generateModalConfig());
1383
- console.log(dim("\n# Save as modal_app.py, then: pip install modal && modal run modal_app.py"));
1384
- process.exit(0);
1385
- }
1386
-
1387
- if (sub === "daytona") {
1388
- const { mkdir: mkdirDaytona, writeFile: writeDaytona } = await import("node:fs/promises");
1389
- const daytonaDir = path.join(process.cwd(), ".daytona");
1390
- await mkdirDaytona(daytonaDir, { recursive: true });
1391
- const daytonaConfigPath = path.join(daytonaDir, "config.yaml");
1392
- let exists = false;
1393
- try { await (await import("node:fs/promises")).access(daytonaConfigPath); exists = true; } catch {}
1394
- if (!exists) {
1395
- await writeDaytona(daytonaConfigPath, dm.generateDaytonaConfig(), "utf8");
1396
- console.log(green(`✅ Created .daytona/config.yaml`));
1397
- } else {
1398
- console.log(yellow(`⏭️ .daytona/config.yaml already exists (skipped)`));
1399
- }
1400
- console.log(dim(" Push to your repo and connect via Daytona workspace."));
1401
- process.exit(0);
1402
- }
1403
-
1404
- // Help
1405
- console.log(`
1406
- 🚀 ${bold("Wispy Deploy Commands")}
1407
-
1408
- ${cyan("Config generators:")}
1409
- wispy deploy init — generate Dockerfile + compose + .env.example
1410
- wispy deploy dockerfile — print Dockerfile to stdout
1411
- wispy deploy compose — print docker-compose.yml
1412
- wispy deploy systemd — print systemd unit file
1413
- wispy deploy railway — print railway.json
1414
- wispy deploy fly — print fly.toml
1415
- wispy deploy render — print render.yaml
1416
- wispy deploy modal — generate Modal serverless config (modal_app.py)
1417
- wispy deploy daytona — generate Daytona workspace config (.daytona/config.yaml)
1418
-
1419
- ${cyan("Deploy:")}
1420
- wispy deploy vps user@host — SSH deploy: install + systemd setup
1421
- wispy deploy vps user@host --env .env — include env file
1422
-
1423
- ${cyan("Status:")}
1424
- wispy deploy status https://my.vps — check if remote wispy is alive
1425
-
1426
- ${cyan("Remote connect:")}
1427
- wispy connect https://my.vps:18790 --token <token> — use remote server
1428
- wispy disconnect — go back to local
1429
- wispy status — show current mode
1430
- `);
1431
- process.exit(0);
1432
- }
1433
-
1434
- // ── migrate sub-command ───────────────────────────────────────────────────────
1435
- if (args[0] === "migrate") {
1436
- const { OpenClawMigrator, WISPY_DIR } = await import(
1437
- path.join(__dirname, "..", "core", "index.mjs")
1438
- );
1439
-
1440
- const sub = args[1]; // "openclaw" (only supported source for now)
1441
- const dryRun = args.includes("--dry-run");
1442
- const memoryOnly = args.includes("--memory-only");
1443
-
1444
- const green = (s) => `\x1b[32m${s}\x1b[0m`;
1445
- const red = (s) => `\x1b[31m${s}\x1b[0m`;
1446
- const bold = (s) => `\x1b[1m${s}\x1b[0m`;
1447
- const dim = (s) => `\x1b[2m${s}\x1b[0m`;
1448
- const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
1449
-
1450
- if (!sub || sub === "openclaw") {
1451
- console.log(`\n🌿 ${bold("Wispy Migration Tool")} ${dim("— from OpenClaw")}\n`);
1452
- if (dryRun) console.log(yellow(" [DRY RUN — no files will be written]\n"));
1453
- if (memoryOnly) console.log(dim(" [memory-only mode]\n"));
1454
-
1455
- const migrator = new OpenClawMigrator(WISPY_DIR);
1456
- const result = await migrator.migrate({ dryRun, memoryOnly });
1457
-
1458
- console.log(migrator.formatReport());
1459
-
1460
- if (result.success) {
1461
- if (dryRun) {
1462
- console.log(dim("\nRun without --dry-run to apply changes.\n"));
1463
- } else {
1464
- const counts = [
1465
- result.report.memories.length > 0 && `${result.report.memories.length} memory files`,
1466
- result.report.userModel.length > 0 && `${result.report.userModel.length} profile files`,
1467
- result.report.cronJobs.length > 0 && `${result.report.cronJobs.length} cron jobs`,
1468
- result.report.channels.length > 0 && `${result.report.channels.length} channels`,
1469
- ].filter(Boolean);
1470
-
1471
- if (counts.length > 0) {
1472
- console.log(`\n${green("✅ Migration complete!")} Imported: ${counts.join(", ")}`);
1473
- } else {
1474
- console.log(`\n${dim("Nothing new to import (already migrated or empty).")}`);
1475
- }
1476
- console.log(dim("\nTip: run `wispy` to start chatting with your imported context.\n"));
1477
- }
1478
- } else {
1479
- console.error(`\n${red("❌ Migration failed:")} ${result.error}\n`);
1480
- process.exit(1);
1481
- }
1482
- } else {
1483
- console.log(`
1484
- 🔀 ${bold("Wispy Migrate Commands")}
1485
-
1486
- wispy migrate openclaw — import from OpenClaw (~/.openclaw/)
1487
- wispy migrate openclaw --dry-run — preview what would be imported
1488
- wispy migrate openclaw --memory-only — only import memories
1489
- `);
1490
- }
1491
-
1492
- process.exit(0);
1493
- }
1494
-
1495
- // ── cron sub-command ──────────────────────────────────────────────────────────
1496
- if (args[0] === "cron") {
1497
- const { WispyEngine, CronManager, WISPY_DIR } = await import(
1498
- path.join(__dirname, "..", "core", "index.mjs")
1499
- );
1500
- const { createInterface } = await import("node:readline");
1501
-
1502
- const sub = args[1];
1503
-
1504
- // Init engine for cron commands that need it
1505
- const engine = new WispyEngine();
1506
- await engine.init({ skipMcp: true });
1507
- const cron = new CronManager(WISPY_DIR, engine);
1508
- await cron.init();
1509
-
1510
- if (!sub || sub === "list") {
1511
- const jobs = cron.list();
1512
- if (jobs.length === 0) {
1513
- console.log("No cron jobs configured. Use: wispy cron add");
1514
- } else {
1515
- console.log(`\n🕐 Cron Jobs (${jobs.length}):\n`);
1516
- for (const j of jobs) {
1517
- const status = j.enabled ? "✅" : "⏸️ ";
1518
- const schedStr = j.schedule.kind === "cron" ? j.schedule.expr
1519
- : j.schedule.kind === "every" ? `every ${j.schedule.ms / 60000}min`
1520
- : `at ${j.schedule.time}`;
1521
- console.log(` ${status} ${j.id.slice(0, 8)} ${j.name.padEnd(20)} ${schedStr}`);
1522
- console.log(` Task: ${j.task.slice(0, 60)}${j.task.length > 60 ? "..." : ""}`);
1523
- if (j.channel) console.log(` Channel: ${j.channel}`);
1524
- if (j.nextRun) console.log(` Next run: ${new Date(j.nextRun).toLocaleString()}`);
1525
- console.log("");
1526
- }
1527
- }
1528
- process.exit(0);
1529
- }
1530
-
1531
- if (sub === "add") {
1532
- const rl = createInterface({ input: process.stdin, output: process.stdout });
1533
- const ask = (q) => new Promise(r => rl.question(q, r));
1534
-
1535
- console.log("\n🕐 Add Cron Job\n");
1536
- const name = await ask(" Job name: ");
1537
- const task = await ask(" Task (what to do): ");
1538
- const schedKind = await ask(" Schedule type (cron/every/at) [cron]: ") || "cron";
1539
- let schedule = { kind: schedKind };
1540
-
1541
- if (schedKind === "cron") {
1542
- const expr = await ask(" Cron expression (e.g. '0 9 * * *'): ");
1543
- const tz = await ask(" Timezone [Asia/Seoul]: ") || "Asia/Seoul";
1544
- schedule = { kind: "cron", expr: expr.trim(), tz: tz.trim() };
1545
- } else if (schedKind === "every") {
1546
- const mins = await ask(" Interval in minutes: ");
1547
- schedule = { kind: "every", ms: parseFloat(mins) * 60_000 };
1548
- } else if (schedKind === "at") {
1549
- const time = await ask(" Run at (ISO datetime, e.g. 2025-01-01T09:00:00): ");
1550
- schedule = { kind: "at", time: time.trim() };
1551
- }
1552
-
1553
- const channel = await ask(" Channel (e.g. telegram:12345, or leave empty): ");
1554
- rl.close();
1555
-
1556
- const job = await cron.add({
1557
- name: name.trim(),
1558
- task: task.trim(),
1559
- schedule,
1560
- channel: channel.trim() || null,
1561
- enabled: true,
1562
- });
1563
-
1564
- console.log(`\n✅ Job created: ${job.id}`);
1565
- console.log(` Next run: ${job.nextRun ? new Date(job.nextRun).toLocaleString() : "N/A"}`);
1566
- process.exit(0);
1567
- }
1568
-
1569
- if (sub === "remove" && args[2]) {
1570
- const id = args[2];
1571
- // Support partial ID match
1572
- const all = cron.list();
1573
- const match = all.find(j => j.id === id || j.id.startsWith(id));
1574
- if (!match) {
1575
- console.error(`Job not found: ${id}`);
1576
- process.exit(1);
1577
- }
1578
- await cron.remove(match.id);
1579
- console.log(`✅ Removed job: ${match.name} (${match.id})`);
1580
- process.exit(0);
1581
- }
1582
-
1583
- if (sub === "run" && args[2]) {
1584
- const id = args[2];
1585
- const all = cron.list();
1586
- const match = all.find(j => j.id === id || j.id.startsWith(id));
1587
- if (!match) {
1588
- console.error(`Job not found: ${id}`);
1589
- process.exit(1);
1590
- }
1591
- console.log(`🌿 Running job: ${match.name}...`);
1592
- const result = await cron.runNow(match.id);
1593
- console.log(result.output ?? result.error);
1594
- process.exit(0);
1595
- }
1596
-
1597
- if (sub === "history" && args[2]) {
1598
- const id = args[2];
1599
- const all = cron.list();
1600
- const match = all.find(j => j.id === id || j.id.startsWith(id));
1601
- if (!match) {
1602
- console.error(`Job not found: ${id}`);
1603
- process.exit(1);
1604
- }
1605
- const history = await cron.getHistory(match.id);
1606
- console.log(`\n📋 History for "${match.name}" (last ${history.length} runs):\n`);
1607
- for (const h of history) {
1608
- const icon = h.status === "success" ? "✅" : "❌";
1609
- console.log(` ${icon} ${new Date(h.startedAt).toLocaleString()}`);
1610
- console.log(` ${h.output?.slice(0, 100) ?? h.error ?? ""}`);
1611
- console.log("");
1612
- }
1613
- if (history.length === 0) console.log(" No runs yet.");
1614
- process.exit(0);
1615
- }
1616
-
1617
- if (sub === "start") {
1618
- console.log("🌿 Starting cron scheduler... (Ctrl+C to stop)\n");
1619
- cron.start();
1620
- process.on("SIGINT", () => { cron.stop(); process.exit(0); });
1621
- process.on("SIGTERM", () => { cron.stop(); process.exit(0); });
1622
- setInterval(() => {}, 60_000);
1623
- await new Promise(() => {});
1624
- }
1625
-
1626
- console.log(`
1627
- 🕐 Wispy Cron Commands:
1628
-
1629
- wispy cron list — list all jobs
1630
- wispy cron add — interactive job creation
1631
- wispy cron remove <id> — delete a job
1632
- wispy cron run <id> — trigger immediately
1633
- wispy cron history <id> — show past runs
1634
- wispy cron start — start scheduler (foreground)
1635
- `);
1636
- process.exit(0);
1637
- }
1638
-
1639
- // ── audit / log sub-command ───────────────────────────────────────────────────
1640
- if (args[0] === "audit" || args[0] === "log") {
1641
- const { AuditLog, WISPY_DIR } = await import(
1642
- path.join(__dirname, "..", "core", "index.mjs")
1643
- );
1644
- const { writeFile } = await import("node:fs/promises");
1645
-
1646
- const audit = new AuditLog(WISPY_DIR);
1647
- const sub = args[1];
1648
-
1649
- const dim = (s) => `\x1b[2m${s}\x1b[0m`;
1650
- const bold = (s) => `\x1b[1m${s}\x1b[0m`;
1651
- const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
1652
- const green = (s) => `\x1b[32m${s}\x1b[0m`;
1653
- const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
1654
- const red = (s) => `\x1b[31m${s}\x1b[0m`;
1655
-
1656
- function formatEvent(evt) {
1657
- const ts = new Date(evt.timestamp).toLocaleTimeString();
1658
- const icons = {
1659
- tool_call: "🔧",
1660
- tool_result: "✅",
1661
- approval_requested: "⚠️ ",
1662
- approval_granted: "✅",
1663
- approval_denied: "❌",
1664
- message_sent: "🌿",
1665
- message_received: "👤",
1666
- error: "🚨",
1667
- subagent_spawned: "🤖",
1668
- subagent_completed: "🎉",
1669
- cron_executed: "🕐",
1670
- };
1671
- const icon = icons[evt.type] ?? "•";
1672
- let detail = "";
1673
- if (evt.tool) detail += ` ${cyan(evt.tool)}`;
1674
- if (evt.content) detail += ` ${dim(evt.content.slice(0, 60))}`;
1675
- if (evt.message) detail += ` ${dim(evt.message.slice(0, 60))}`;
1676
- if (evt.label) detail += ` ${dim(evt.label)}`;
1677
- const sid = evt.sessionId ? dim(` [${evt.sessionId.slice(-8)}]`) : "";
1678
- return ` ${dim(ts)} ${icon} ${evt.type}${detail}${sid}`;
1679
- }
1680
-
1681
- if (sub === "replay" && args[2]) {
1682
- const sessionId = args[2];
1683
- const steps = await audit.getReplayTrace(sessionId);
1684
- if (steps.length === 0) {
1685
- console.log(dim(`No events found for session: ${sessionId}`));
1686
- } else {
1687
- console.log(`\n${bold("🎬 Replay:")} ${cyan(sessionId)}\n`);
1688
- for (const step of steps) {
1689
- const ts = new Date(step.timestamp).toLocaleTimeString();
1690
- const icons = {
1691
- user_message: "👤",
1692
- assistant_message: "🌿",
1693
- tool_call: "🔧",
1694
- tool_result: "✅",
1695
- approval_requested: "⚠️ ",
1696
- approval_granted: "✅",
1697
- approval_denied: "❌",
1698
- subagent_spawned: "🤖",
1699
- subagent_completed: "🎉",
1700
- };
1701
- const icon = icons[step.type] ?? "•";
1702
- let detail = "";
1703
- if (step.content) detail = dim(step.content.slice(0, 100));
1704
- if (step.tool) detail = `${cyan(step.tool)} ${dim(JSON.stringify(step.args ?? {}).slice(0, 60))}`;
1705
- console.log(` ${bold(`Step ${step.step}`)} ${dim(ts)} ${icon} ${detail}`);
1706
- }
1707
- }
1708
- process.exit(0);
1709
- }
1710
-
1711
- if (sub === "export") {
1712
- const format = args.includes("--format") ? args[args.indexOf("--format") + 1] : "json";
1713
- const outputIdx = args.indexOf("--output");
1714
- const output = outputIdx !== -1 ? args[outputIdx + 1] : null;
1715
-
1716
- let content;
1717
- if (format === "md" || format === "markdown") {
1718
- content = await audit.exportMarkdown();
1719
- } else {
1720
- content = await audit.exportJson();
1721
- }
1722
-
1723
- if (output) {
1724
- await writeFile(output, content, "utf8");
1725
- console.log(green(`✅ Exported to ${output}`));
1726
- } else {
1727
- console.log(content);
1728
- }
1729
- process.exit(0);
1730
- }
1731
-
1732
- // Build filter from flags
1733
- const filter = {};
1734
- const sessionIdx = args.indexOf("--session");
1735
- if (sessionIdx !== -1) filter.sessionId = args[sessionIdx + 1];
1736
- const toolIdx = args.indexOf("--tool");
1737
- if (toolIdx !== -1) filter.tool = args[toolIdx + 1];
1738
- if (args.includes("--today")) filter.date = new Date().toISOString().slice(0, 10);
1739
- const limitIdx = args.indexOf("--limit");
1740
- filter.limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1]) : 30;
1741
-
1742
- const events = await audit.search(filter);
1743
-
1744
- if (events.length === 0) {
1745
- console.log(dim("No audit events found."));
1746
- } else {
1747
- console.log(`\n${bold("📋 Audit Log")} ${dim(`(${events.length} events)`)}\n`);
1748
- for (const evt of events) {
1749
- console.log(formatEvent(evt));
1750
- }
1751
- console.log("");
1752
- }
1753
-
1754
- if (!sub) {
1755
- console.log(dim(`
1756
- wispy audit — show recent events
1757
- wispy audit --session <id> — filter by session
1758
- wispy audit --today — today's events
1759
- wispy audit --tool <name> — filter by tool
1760
- wispy audit replay <sessionId> — step-by-step replay
1761
- wispy audit export --format md — export as markdown
1762
- wispy audit export --output file.md — save to file
1763
- `));
1764
- }
1765
-
1766
- process.exit(0);
1767
- }
1768
-
1769
- // ── server sub-command ────────────────────────────────────────────────────────
1770
- if (args[0] === "server" || args.includes("--server")) {
1771
- const { WispyEngine, WispyServer } = await import(
1772
- path.join(__dirname, "..", "core", "index.mjs")
1773
- );
1774
-
1775
- const portIdx = args.indexOf("--port");
1776
- const hostIdx = args.indexOf("--host");
1777
- const port = portIdx !== -1 ? parseInt(args[portIdx + 1]) : undefined;
1778
- const host = hostIdx !== -1 ? args[hostIdx + 1] : undefined;
1779
-
1780
- console.log("🌿 Starting Wispy API server...");
1781
-
1782
- const engine = new WispyEngine();
1783
- const initResult = await engine.init();
1784
- if (!initResult) {
1785
- console.error("❌ No AI provider configured. Run `wispy` first to set up.");
1786
- process.exit(1);
1787
- }
1788
-
1789
- const server = new WispyServer(engine, { port, host });
1790
- await server.start();
1791
-
1792
- process.on("SIGINT", () => { server.stop(); process.exit(0); });
1793
- process.on("SIGTERM", () => { server.stop(); process.exit(0); });
1794
-
1795
- // Keep alive
1796
- setInterval(() => {}, 60_000);
1797
- await new Promise(() => {});
1798
- }
1799
-
1800
- // ── node sub-command ──────────────────────────────────────────────────────────
1801
- if (args[0] === "node") {
1802
- const { NodeManager, WISPY_DIR, CAPABILITIES } = await import(
1803
- path.join(__dirname, "..", "core", "index.mjs")
1804
- );
1805
- const { createInterface } = await import("node:readline");
1806
-
1807
- const sub = args[1];
1808
- const nodes = new NodeManager(WISPY_DIR);
1809
-
1810
- const dim = (s) => `\x1b[2m${s}\x1b[0m`;
1811
- const bold = (s) => `\x1b[1m${s}\x1b[0m`;
1812
- const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
1813
- const green = (s) => `\x1b[32m${s}\x1b[0m`;
1814
- const red = (s) => `\x1b[31m${s}\x1b[0m`;
1815
- const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
1816
-
1817
- if (sub === "pair") {
1818
- const code = await nodes.generatePairCode();
1819
- console.log(`\n🔗 Pairing Code: ${bold(green(code))}\n`);
1820
- console.log(` This code expires in 1 hour.`);
1821
- console.log(`\n On the remote machine, run:`);
1822
- console.log(` ${cyan(`wispy node connect ${code} --url http://localhost:18790`)}\n`);
1823
- process.exit(0);
1824
- }
1825
-
1826
- if (sub === "connect" && args[2]) {
1827
- const code = args[2];
1828
- const urlIdx = args.indexOf("--url");
1829
- const serverUrl = urlIdx !== -1 ? args[urlIdx + 1] : "http://localhost:18790";
1830
-
1831
- const rl = createInterface({ input: process.stdin, output: process.stdout });
1832
- const ask = (q) => new Promise(r => rl.question(q, r));
1833
-
1834
- console.log(`\n🔗 Connecting to Wispy at ${serverUrl}\n`);
1835
- const name = (await ask(" Node name (e.g. my-laptop): ")).trim() || `node-${Date.now().toString(36)}`;
1836
-
1837
- console.log(`\nAvailable capabilities:`);
1838
- const capList = Object.entries(CAPABILITIES);
1839
- capList.forEach(([k, v], i) => console.log(` ${i + 1}. ${k.padEnd(15)} — ${dim(v)}`));
1840
- const capInput = (await ask("\n Select capabilities (comma-separated numbers, e.g. 1,3): ")).trim();
1841
- rl.close();
1842
-
1843
- const selectedCaps = capInput
1844
- ? capInput.split(",").map(n => capList[parseInt(n.trim()) - 1]?.[0]).filter(Boolean)
1845
- : [];
1846
-
1847
- // Call server to confirm pair
1848
- try {
1849
- const tokenIdx = args.indexOf("--token");
1850
- const token = tokenIdx !== -1 ? args[tokenIdx + 1] : "";
1851
- const resp = await fetch(`${serverUrl}/api/nodes/pair`, {
1852
- method: "POST",
1853
- headers: { "Content-Type": "application/json", ...(token ? { "Authorization": `Bearer ${token}` } : {}) },
1854
- body: JSON.stringify({ code, name, capabilities: selectedCaps, host: "localhost", port: 18791 }),
1855
- });
1856
- if (!resp.ok) {
1857
- const err = await resp.json().catch(() => ({ error: resp.statusText }));
1858
- console.error(red(`❌ Pairing failed: ${err.error ?? resp.statusText}`));
1859
- process.exit(1);
1860
- }
1861
- const result = await resp.json();
1862
- console.log(green(`\n✅ Paired! Node ID: ${result.id}`));
1863
- } catch (err) {
1864
- console.error(red(`❌ Connection failed: ${err.message}`));
1865
- console.log(dim(" Make sure `wispy server` is running on the main machine."));
1866
- process.exit(1);
1867
- }
1868
- process.exit(0);
1869
- }
1870
-
1871
- if (sub === "list") {
1872
- const nodeList = await nodes.list();
1873
- if (nodeList.length === 0) {
1874
- console.log(dim("No nodes registered. Use: wispy node pair"));
1875
- } else {
1876
- console.log(`\n${bold("🌐 Registered Nodes:")}\n`);
1877
- for (const n of nodeList) {
1878
- const caps = n.capabilities.join(", ") || dim("none");
1879
- const last = n.lastSeen ? dim(new Date(n.lastSeen).toLocaleString()) : dim("never");
1880
- console.log(` ${green(n.id.slice(-12))} ${bold(n.name.padEnd(20))} ${n.host}:${n.port}`);
1881
- console.log(` Caps: ${caps} Last seen: ${last}`);
1882
- console.log("");
1883
- }
1884
- }
1885
- process.exit(0);
1886
- }
1887
-
1888
- if (sub === "status") {
1889
- const nodeList = await nodes.list();
1890
- if (nodeList.length === 0) {
1891
- console.log(dim("No nodes registered."));
1892
- process.exit(0);
1893
- }
1894
- console.log(`\n${bold("📡 Node Status:")}\n`);
1895
- const results = await nodes.status();
1896
- for (const r of results) {
1897
- const statusIcon = r.alive ? green("●") : red("●");
1898
- const latency = r.latency ? dim(` ${r.latency}ms`) : "";
1899
- const err = r.error ? red(` (${r.error})`) : "";
1900
- console.log(` ${statusIcon} ${r.name.padEnd(20)} ${r.host}:${r.port}${latency}${err}`);
1901
- }
1902
- console.log("");
1903
- process.exit(0);
1904
- }
1905
-
1906
- if (sub === "remove" && args[2]) {
1907
- const id = args[2];
1908
- const nodeList = await nodes.list();
1909
- const match = nodeList.find(n => n.id === id || n.id.endsWith(id));
1910
- if (!match) {
1911
- console.error(red(`Node not found: ${id}`));
1912
- process.exit(1);
1913
- }
1914
- await nodes.remove(match.id);
1915
- console.log(green(`✅ Removed node: ${match.name} (${match.id})`));
1916
- process.exit(0);
1917
- }
1918
-
1919
- // Help
1920
- console.log(`
1921
- 🌐 Wispy Node Commands:
1922
-
1923
- wispy node pair — generate pairing code
1924
- wispy node connect <code> --url <url> — connect as a node
1925
- wispy node list — show registered nodes
1926
- wispy node status — ping all nodes
1927
- wispy node remove <id> — unregister a node
1928
- `);
1929
- process.exit(0);
1930
- }
1931
-
1932
- // ── channel sub-command ───────────────────────────────────────────────────────
1933
- if (args[0] === "channel") {
1934
- const { channelSetup, channelList, channelTest } = await import(
1935
- path.join(__dirname, "..", "lib", "channels", "index.mjs")
1936
- );
1937
-
1938
- const sub = args[1];
1939
- const name = args[2];
1940
-
1941
- if (sub === "setup" && name) {
1942
- await channelSetup(name);
1943
- } else if (sub === "list") {
1944
- await channelList();
1945
- } else if (sub === "test" && name) {
1946
- await channelTest(name);
1947
- } else {
1948
- console.log(`
1949
- 🌿 Wispy Channel Commands:
1950
-
1951
- wispy channel setup telegram — interactive Telegram bot setup
1952
- wispy channel setup discord — interactive Discord bot setup
1953
- wispy channel setup slack — interactive Slack bot setup
1954
- wispy channel setup whatsapp — WhatsApp setup (requires: npm install whatsapp-web.js qrcode-terminal)
1955
- wispy channel setup signal — Signal setup (requires: signal-cli)
1956
- wispy channel setup email — Email setup (requires: npm install nodemailer imapflow)
1957
- wispy channel list — show configured channels
1958
- wispy channel test <name> — test a channel connection
1959
-
1960
- wispy --serve — start all configured channel bots
1961
- wispy --telegram — start Telegram bot only
1962
- wispy --discord — start Discord bot only
1963
- wispy --slack — start Slack bot only
1964
- `);
1965
- }
1966
- process.exit(0);
1967
- }
1968
-
1969
- // ── auth sub-command ──────────────────────────────────────────────────────────
1970
- if (args[0] === "auth") {
1971
- const { AuthManager } = await import(
1972
- path.join(__dirname, "..", "core", "auth.mjs")
1973
- );
1974
- const { WISPY_DIR } = await import(
1975
- path.join(__dirname, "..", "core", "config.mjs")
1976
- );
1977
-
1978
- const sub = args[1]; // provider name | "refresh" | "revoke" | undefined
1979
-
1980
- const auth = new AuthManager(WISPY_DIR);
1981
-
1982
- // wispy auth — show status for all providers
1983
- if (!sub) {
1984
- const statuses = await auth.allStatus();
1985
- if (statuses.length === 0) {
1986
- console.log(_dim("\n No saved auth tokens. Run: wispy auth <provider>\n"));
1987
- } else {
1988
- console.log(`\n${_bold("🔑 Auth Status")}\n`);
1989
- for (const s of statuses) {
1990
- const expiryStr = s.expiresAt ? _dim(` (expires ${new Date(s.expiresAt).toLocaleString()})`) : "";
1991
- const statusIcon = s.expired ? _yellow("⚠️ expired") : _green("✅ valid");
1992
- console.log(` ${_cyan(s.provider.padEnd(20))} ${s.type.padEnd(8)} ${statusIcon}${expiryStr}`);
1993
- }
1994
- console.log("");
1995
- console.log(_dim(" wispy auth <provider> — re-authenticate"));
1996
- console.log(_dim(" wispy auth refresh <provider> — refresh token"));
1997
- console.log(_dim(" wispy auth revoke <provider> — remove saved token"));
1998
- }
1999
- console.log("");
2000
- process.exit(0);
2001
- }
2002
-
2003
- // wispy auth refresh <provider>
2004
- if (sub === "refresh" && args[2]) {
2005
- const provider = args[2];
2006
- console.log(`\n🔄 Refreshing token for ${_cyan(provider)}...`);
2007
- try {
2008
- await auth.refreshToken(provider);
2009
- console.log(_green(`✅ Token refreshed for ${provider}\n`));
2010
- } catch (err) {
2011
- console.error(_red(`❌ ${err.message}\n`));
2012
- process.exit(1);
2013
- }
2014
- process.exit(0);
2015
- }
2016
-
2017
- // wispy auth revoke <provider>
2018
- if (sub === "revoke" && args[2]) {
2019
- const provider = args[2];
2020
- await auth.revokeToken(provider);
2021
- console.log(_green(`✅ Revoked auth for ${provider}\n`));
2022
- process.exit(0);
2023
- }
2024
-
2025
- // wispy auth <provider> — run OAuth or re-authenticate
2026
- if (sub && sub !== "refresh" && sub !== "revoke") {
2027
- const provider = sub;
2028
-
2029
- if (provider === "github-copilot") {
2030
- console.log(`\n🔑 ${_bold("GitHub Copilot")} — OAuth sign-in\n`);
2031
- try {
2032
- const result = await auth.oauth("github-copilot");
2033
- if (result.valid) {
2034
- console.log(_green(`✅ GitHub Copilot authenticated!\n`));
2035
- console.log(_dim(" Token saved to ~/.wispy/auth/github-copilot.json"));
2036
- console.log(_dim(" Run: wispy auth to verify status\n"));
7
+ (async () => {
8
+ const { fileURLToPath } = await import('url');
9
+ const { dirname } = await import('path');
10
+ const baseDir = dirname(fileURLToPath(import.meta.url));
11
+
12
+ const args = process.argv.slice(2);
13
+
14
+ async function handleCommand(command) {
15
+ const inquirer = (await import('inquirer')).default;
16
+
17
+ if (!command) {
18
+ const answers = await inquirer.prompt([
19
+ {
20
+ type: 'list',
21
+ name: 'selectedCommand',
22
+ message: 'What would you like to do?',
23
+ choices: [
24
+ { name: 'Run WebSocket command', value: 'ws' },
25
+ { name: 'Get help', value: 'help' },
26
+ { name: 'Exit', value: null }
27
+ ],
2037
28
  }
2038
- } catch (err) {
2039
- console.error(_red(`\n❌ ${err.message}\n`));
2040
- process.exit(1);
2041
- }
2042
- } else {
2043
- console.error(_red(`\n❌ Unknown provider for auth: ${provider}`));
2044
- console.log(_dim(` Currently OAuth is supported for: github-copilot\n`));
2045
- process.exit(1);
2046
- }
2047
- process.exit(0);
2048
- }
2049
-
2050
- // Help
2051
- console.log(`
2052
- ${_bold("🔑 Wispy Auth Commands")}
2053
-
2054
- ${_cyan("wispy auth")} — show auth status for all providers
2055
- ${_cyan("wispy auth github-copilot")} — sign in with GitHub (Copilot OAuth)
2056
- ${_cyan("wispy auth refresh <provider>")} — refresh expired token
2057
- ${_cyan("wispy auth revoke <provider>")} — remove saved auth
2058
-
2059
- ${_bold("Examples:")}
2060
- wispy auth github-copilot
2061
- wispy auth refresh github-copilot
2062
- wispy auth revoke github-copilot
2063
- `);
2064
- process.exit(0);
2065
- }
2066
-
2067
- // ── Unknown command detection ─────────────────────────────────────────────────
2068
- // Any non-flag argument that wasn't matched above is an unknown command
2069
- const _KNOWN_COMMANDS = new Set([
2070
- "ws", "trust", "where", "handoff", "skill", "teach", "improve", "dry",
2071
- "setup", "init", "update", "status", "connect", "disconnect", "sync",
2072
- "deploy", "migrate", "cron", "audit", "log", "server", "node", "channel",
2073
- "auth", "config", "model", "tui", "help", "doctor", "completion", "version",
2074
- // serve flags (handled below)
2075
- "--serve", "--telegram", "--discord", "--slack", "--server",
2076
- "--help", "-h", "--version", "-v", "--debug", "--tui",
2077
- // workstream flags
2078
- "-w", "--workstream",
2079
- ]);
2080
- const _firstArg = args[0];
2081
- if (_firstArg && _firstArg[0] !== "-" && !_KNOWN_COMMANDS.has(_firstArg)) {
2082
- // Not a known command — but could be a one-shot message (no quotes needed)
2083
- // Heuristic: if it looks like a real command word (no spaces, short), warn.
2084
- // If it looks like a natural language sentence, fall through to REPL one-shot mode.
2085
- // Only show "unknown command" if it looks very much like a mistyped subcommand.
2086
- // Common typos/misspellings of real commands (2-char levenshtein distance).
2087
- // Everything else falls through to one-shot message mode.
2088
- const CLOSE_COMMANDS = ["seutp", "set", "stat", "statu", "doc", "wss", "trus", "depliy", "sycn", "cran"];
2089
- 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));
2090
- if (looksLikeCommand) {
2091
- // Show unknown command error
2092
- const suggestions = [
2093
- { cmd: "setup", desc: "configure wispy" },
2094
- { cmd: "tui", desc: "workspace UI" },
2095
- { cmd: "ws", desc: "workstream management" },
2096
- { cmd: "doctor", desc: "check system health" },
2097
- { cmd: "help", desc: "show detailed help" },
2098
- ];
2099
- console.error(`\n${_red(`❌ Unknown command: ${_firstArg}`)}`);
2100
- console.error(`\n${_bold("Did you mean one of these?")}`);
2101
- for (const { cmd, desc } of suggestions) {
2102
- console.error(` ${_cyan("wispy " + cmd.padEnd(12))}— ${desc}`);
2103
- }
2104
- console.error(`\n${_dim("Run wispy --help for all commands.")}\n`);
2105
- process.exit(2);
2106
- }
2107
- // Otherwise fall through to one-shot message mode in REPL
2108
- }
2109
-
2110
- // ── Bot / serve modes ─────────────────────────────────────────────────────────
2111
- const serveMode = args.includes("--serve");
2112
- const telegramMode = args.includes("--telegram");
2113
- const discordMode = args.includes("--discord");
2114
- const slackMode = args.includes("--slack");
2115
-
2116
- if (serveMode || telegramMode || discordMode || slackMode) {
2117
- const { ChannelManager } = await import(
2118
- path.join(__dirname, "..", "lib", "channels", "index.mjs")
2119
- );
2120
-
2121
- const manager = new ChannelManager();
2122
-
2123
- const only = [];
2124
- if (telegramMode) only.push("telegram");
2125
- if (discordMode) only.push("discord");
2126
- if (slackMode) only.push("slack");
2127
- // serveMode → only stays empty → all channels started
2128
-
2129
- await manager.startAll(only);
2130
-
2131
- // Start cron scheduler if in serve mode
2132
- let cronManager = null;
2133
- if (serveMode) {
2134
- try {
2135
- const { WispyEngine, CronManager, WISPY_DIR } = await import(
2136
- path.join(__dirname, "..", "core", "index.mjs")
2137
- );
2138
- const engine = new WispyEngine();
2139
- await engine.init({ skipMcp: true });
2140
- cronManager = new CronManager(WISPY_DIR, engine);
2141
- await cronManager.init();
2142
- cronManager.start();
2143
- console.error("[wispy] Cron scheduler started");
2144
- } catch (err) {
2145
- console.error("[wispy] Failed to start cron scheduler:", err.message);
29
+ ]);
30
+ command = answers.selectedCommand;
2146
31
  }
2147
- }
2148
-
2149
- // Keep process alive and stop cleanly on Ctrl+C
2150
- process.on("SIGINT", async () => {
2151
- if (cronManager) cronManager.stop();
2152
- await manager.stopAll();
2153
- process.exit(0);
2154
- });
2155
- process.on("SIGTERM", async () => {
2156
- if (cronManager) cronManager.stop();
2157
- await manager.stopAll();
2158
- process.exit(0);
2159
- });
2160
32
 
2161
- // Prevent Node from exiting
2162
- setInterval(() => {}, 60_000);
2163
- // eslint-disable-next-line no-constant-condition
2164
- await new Promise(() => {}); // keep alive
2165
- }
2166
-
2167
- // ── First-run detection (before TUI or REPL) ──────────────────────────────────
2168
- // Only trigger onboarding for interactive modes (not flags like --serve, channel, etc.)
2169
- const isInteractiveStart = !args.some(a =>
2170
- ["--serve", "--telegram", "--discord", "--slack", "--server",
2171
- "status", "setup", "init", "connect", "disconnect", "deploy",
2172
- "cron", "audit", "log", "server", "node", "channel", "sync", "tui",
2173
- "ws", "trust", "where", "handoff", "skill", "teach", "improve", "dry",
2174
- "auth"].includes(a)
2175
- );
2176
-
2177
- if (isInteractiveStart) {
2178
- try {
2179
- const { isFirstRun } = await import(
2180
- path.join(__dirname, "..", "core", "config.mjs")
2181
- );
2182
- if (await isFirstRun()) {
2183
- const { OnboardingWizard } = await import(
2184
- path.join(__dirname, "..", "core", "onboarding.mjs")
2185
- );
2186
- const wizard = new OnboardingWizard();
2187
- await wizard.run();
2188
- // After onboarding, continue to REPL or TUI
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");
2189
46
  }
2190
- } catch {
2191
- // If onboarding fails for any reason, continue normally
2192
47
  }
2193
- }
2194
-
2195
- // Validate config before starting interactive modes
2196
- await validateConfigOnStartup();
2197
-
2198
- // ── TUI mode ──────────────────────────────────────────────────────────────────
2199
- // `wispy ui` is the primary way; `--tui` is kept as a hidden backwards-compat alias
2200
- const tuiMode = args[0] === "tui" || args.includes("--tui");
2201
-
2202
- if (tuiMode) {
2203
- // Override SIGINT for clean TUI exit
2204
- process.removeAllListeners("SIGINT");
2205
- process.on("SIGINT", () => { process.exit(130); });
2206
48
 
2207
- const newArgs = args.filter(a => a !== "--tui" && a !== "tui");
2208
- process.argv = [process.argv[0], process.argv[1], ...newArgs];
2209
- const tuiScript = path.join(__dirname, "..", "lib", "wispy-tui.mjs");
2210
- try {
2211
- await import(tuiScript);
2212
- } catch (e) { friendlyError(e); }
2213
- } else {
2214
- // Default: interactive REPL
2215
- const mainScript = path.join(__dirname, "..", "lib", "wispy-repl.mjs");
2216
- try {
2217
- await import(mainScript);
2218
- } catch (e) { friendlyError(e); }
2219
- }
49
+ await handleCommand(args[0] === '--help' ? 'help' : args[0]);
50
+ })();