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.
- package/bin/wispy.mjs +34 -2929
- 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
|
-
*
|
|
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
|
-
|
|
20
|
-
import path from "node:path";
|
|
21
|
-
import { readFileSync } from "node:fs";
|
|
12
|
+
const args = process.argv.slice(2);
|
|
22
13
|
|
|
23
|
-
|
|
14
|
+
async function handleCommand(command) {
|
|
15
|
+
const inquirer = (await import('inquirer')).default;
|
|
24
16
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// ── Global helpers ─────────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
// Color helpers (reused throughout)
|
|
30
|
-
const _green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
31
|
-
const _red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
32
|
-
const _yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
33
|
-
const _cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
34
|
-
const _bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
35
|
-
const _dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
36
|
-
|
|
37
|
-
// Debug mode (--debug flag or WISPY_DEBUG env)
|
|
38
|
-
const DEBUG = args.includes("--debug") || process.env.WISPY_DEBUG === "1";
|
|
39
|
-
if (DEBUG) {
|
|
40
|
-
process.env.WISPY_DEBUG = "1";
|
|
41
|
-
// Remove --debug from args so it doesn't confuse subcommands
|
|
42
|
-
const idx = args.indexOf("--debug");
|
|
43
|
-
if (idx !== -1) args.splice(idx, 1);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Friendly error display
|
|
47
|
-
function friendlyError(err, exitCode = 1) {
|
|
48
|
-
if (DEBUG) {
|
|
49
|
-
console.error(_red(`\n❌ Error: ${err.message}`));
|
|
50
|
-
console.error(_dim(err.stack));
|
|
51
|
-
} else {
|
|
52
|
-
console.error(_red(`\n❌ Error: ${err.message}`));
|
|
53
|
-
console.error(_dim(" Run with --debug for more details."));
|
|
54
|
-
}
|
|
55
|
-
process.exit(exitCode);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Read version once
|
|
59
|
-
let _version;
|
|
60
|
-
function getVersion() {
|
|
61
|
-
if (_version) return _version;
|
|
62
|
-
try {
|
|
63
|
-
_version = JSON.parse(readFileSync(path.join(__dirname, "..", "package.json"), "utf8")).version;
|
|
64
|
-
} catch { _version = "?.?.?"; }
|
|
65
|
-
return _version;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Global SIGINT handler (individual commands may override this)
|
|
69
|
-
process.on("SIGINT", () => {
|
|
70
|
-
console.log(_dim("\n\nInterrupted."));
|
|
71
|
-
process.exit(130);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// ── --version / -v / version ───────────────────────────────────────────────────
|
|
75
|
-
if (args[0] === "--version" || args[0] === "-v" || args[0] === "version") {
|
|
76
|
-
console.log(`wispy-cli v${getVersion()}`);
|
|
77
|
-
process.exit(0);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ── --help / -h ────────────────────────────────────────────────────────────────
|
|
81
|
-
if (args[0] === "--help" || args[0] === "-h") {
|
|
82
|
-
const v = getVersion();
|
|
83
|
-
console.log(`
|
|
84
|
-
${_bold("🌿 Wispy")} ${_dim(`v${v}`)} — AI workspace assistant
|
|
85
|
-
|
|
86
|
-
${_bold("Usage:")} wispy [command] [options]
|
|
87
|
-
|
|
88
|
-
${_bold("AI Interaction:")}
|
|
89
|
-
${_cyan("wispy")} Start interactive REPL
|
|
90
|
-
${_cyan('wispy "message"')} One-shot message
|
|
91
|
-
${_cyan("wispy tui")} Workspace TUI
|
|
92
|
-
${_cyan("wispy dry <cmd>")} Run in dry-run mode
|
|
93
|
-
|
|
94
|
-
${_bold("Workstreams:")}
|
|
95
|
-
${_cyan("wispy ws")} List workstreams
|
|
96
|
-
${_cyan("wispy ws new <name>")} Create workstream
|
|
97
|
-
${_cyan("wispy ws switch <name>")} Switch workstream
|
|
98
|
-
${_cyan("wispy ws archive <name>")} Archive workstream
|
|
99
|
-
${_cyan("wispy ws delete <name>")} Delete workstream
|
|
100
|
-
|
|
101
|
-
${_bold("Trust & Security:")}
|
|
102
|
-
${_cyan("wispy trust")} Show trust level & policies
|
|
103
|
-
${_cyan("wispy trust <level>")} Set trust (careful/balanced/yolo)
|
|
104
|
-
${_cyan("wispy audit")} View audit log
|
|
105
|
-
|
|
106
|
-
${_bold("Continuity:")}
|
|
107
|
-
${_cyan("wispy where")} Show current mode
|
|
108
|
-
${_cyan("wispy handoff")} Generate handoff summary
|
|
109
|
-
|
|
110
|
-
${_bold("Skills:")}
|
|
111
|
-
${_cyan("wispy skill list")} List skills
|
|
112
|
-
${_cyan("wispy skill show <name>")} Show skill details
|
|
113
|
-
${_cyan("wispy teach <name>")} Create skill from conversation
|
|
114
|
-
${_cyan("wispy improve <name> [notes]")} Improve a skill
|
|
115
|
-
|
|
116
|
-
${_bold("Channels & Bots:")}
|
|
117
|
-
${_cyan("wispy channel setup <name>")} Setup channel (telegram/discord/slack/…)
|
|
118
|
-
${_cyan("wispy channel list")} List configured channels
|
|
119
|
-
${_cyan("wispy channel test <name>")} Test channel connection
|
|
120
|
-
${_cyan("wispy --serve")} Start all channel bots
|
|
121
|
-
|
|
122
|
-
${_bold("Server & Deploy:")}
|
|
123
|
-
${_cyan("wispy server")} Start API server
|
|
124
|
-
${_cyan("wispy deploy")} Deploy help & config generators
|
|
125
|
-
${_cyan("wispy node")} Node (multi-machine) management
|
|
126
|
-
${_cyan("wispy sync")} Sync with remote server
|
|
127
|
-
|
|
128
|
-
${_bold("Cron & Automation:")}
|
|
129
|
-
${_cyan("wispy cron list")} List cron jobs
|
|
130
|
-
${_cyan("wispy cron add")} Add a cron job
|
|
131
|
-
${_cyan("wispy cron start")} Start scheduler
|
|
132
|
-
|
|
133
|
-
${_bold("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
|
-
|
|
1642
|
-
|
|
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:
|
|
1997
|
-
{ name:
|
|
1998
|
-
{ name:
|
|
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
|
-
|
|
2277
|
-
|
|
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
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
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
|
-
|
|
2389
|
-
|
|
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
|
+
})();
|