wispy-cli 2.6.3 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/wispy.mjs +40 -2209
- package/lib/commands/skills-cmd.mjs +117 -0
- package/lib/commands/trust.mjs +79 -1
- package/lib/commands/ws.mjs +81 -1
- package/lib/wispy-tui-clean.mjs +82 -0
- package/package.json +2 -1
package/bin/wispy.mjs
CHANGED
|
@@ -1,2219 +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
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (DEBUG) {
|
|
40
|
-
process.env.WISPY_DEBUG = "1";
|
|
41
|
-
// Remove --debug from args so it doesn't confuse subcommands
|
|
42
|
-
const idx = args.indexOf("--debug");
|
|
43
|
-
if (idx !== -1) args.splice(idx, 1);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Friendly error display
|
|
47
|
-
function friendlyError(err, exitCode = 1) {
|
|
48
|
-
if (DEBUG) {
|
|
49
|
-
console.error(_red(`\n❌ Error: ${err.message}`));
|
|
50
|
-
console.error(_dim(err.stack));
|
|
51
|
-
} else {
|
|
52
|
-
console.error(_red(`\n❌ Error: ${err.message}`));
|
|
53
|
-
console.error(_dim(" Run with --debug for more details."));
|
|
54
|
-
}
|
|
55
|
-
process.exit(exitCode);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Read version once
|
|
59
|
-
let _version;
|
|
60
|
-
function getVersion() {
|
|
61
|
-
if (_version) return _version;
|
|
62
|
-
try {
|
|
63
|
-
_version = JSON.parse(readFileSync(path.join(__dirname, "..", "package.json"), "utf8")).version;
|
|
64
|
-
} catch { _version = "?.?.?"; }
|
|
65
|
-
return _version;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Global SIGINT handler (individual commands may override this)
|
|
69
|
-
process.on("SIGINT", () => {
|
|
70
|
-
console.log(_dim("\n\nInterrupted."));
|
|
71
|
-
process.exit(130);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// ── --version / -v / version ───────────────────────────────────────────────────
|
|
75
|
-
if (args[0] === "--version" || args[0] === "-v" || args[0] === "version") {
|
|
76
|
-
console.log(`wispy-cli v${getVersion()}`);
|
|
77
|
-
process.exit(0);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ── --help / -h ────────────────────────────────────────────────────────────────
|
|
81
|
-
if (args[0] === "--help" || args[0] === "-h") {
|
|
82
|
-
const v = getVersion();
|
|
83
|
-
console.log(`
|
|
84
|
-
${_bold("🌿 Wispy")} ${_dim(`v${v}`)} — AI workspace assistant
|
|
85
|
-
|
|
86
|
-
${_bold("Usage:")} wispy [command] [options]
|
|
87
|
-
|
|
88
|
-
${_bold("AI Interaction:")}
|
|
89
|
-
${_cyan("wispy")} Start interactive REPL
|
|
90
|
-
${_cyan('wispy "message"')} One-shot message
|
|
91
|
-
${_cyan("wispy tui")} Workspace TUI
|
|
92
|
-
${_cyan("wispy dry <cmd>")} Run in dry-run mode
|
|
93
|
-
|
|
94
|
-
${_bold("Workstreams:")}
|
|
95
|
-
${_cyan("wispy ws")} List workstreams
|
|
96
|
-
${_cyan("wispy ws new <name>")} Create workstream
|
|
97
|
-
${_cyan("wispy ws switch <name>")} Switch workstream
|
|
98
|
-
${_cyan("wispy ws archive <name>")} Archive workstream
|
|
99
|
-
${_cyan("wispy ws delete <name>")} Delete workstream
|
|
100
|
-
|
|
101
|
-
${_bold("Trust & Security:")}
|
|
102
|
-
${_cyan("wispy trust")} Show trust level & policies
|
|
103
|
-
${_cyan("wispy trust <level>")} Set trust (careful/balanced/yolo)
|
|
104
|
-
${_cyan("wispy audit")} View audit log
|
|
105
|
-
|
|
106
|
-
${_bold("Continuity:")}
|
|
107
|
-
${_cyan("wispy where")} Show current mode
|
|
108
|
-
${_cyan("wispy handoff")} Generate handoff summary
|
|
109
|
-
|
|
110
|
-
${_bold("Skills:")}
|
|
111
|
-
${_cyan("wispy skill list")} List skills
|
|
112
|
-
${_cyan("wispy skill show <name>")} Show skill details
|
|
113
|
-
${_cyan("wispy teach <name>")} Create skill from conversation
|
|
114
|
-
${_cyan("wispy improve <name> [notes]")} Improve a skill
|
|
115
|
-
|
|
116
|
-
${_bold("Channels & Bots:")}
|
|
117
|
-
${_cyan("wispy channel setup <name>")} Setup channel (telegram/discord/slack/…)
|
|
118
|
-
${_cyan("wispy channel list")} List configured channels
|
|
119
|
-
${_cyan("wispy channel test <name>")} Test channel connection
|
|
120
|
-
${_cyan("wispy --serve")} Start all channel bots
|
|
121
|
-
|
|
122
|
-
${_bold("Server & Deploy:")}
|
|
123
|
-
${_cyan("wispy server")} Start API server
|
|
124
|
-
${_cyan("wispy deploy")} Deploy help & config generators
|
|
125
|
-
${_cyan("wispy node")} Node (multi-machine) management
|
|
126
|
-
${_cyan("wispy sync")} Sync with remote server
|
|
127
|
-
|
|
128
|
-
${_bold("Cron & Automation:")}
|
|
129
|
-
${_cyan("wispy cron list")} List cron jobs
|
|
130
|
-
${_cyan("wispy cron add")} Add a cron job
|
|
131
|
-
${_cyan("wispy cron start")} Start scheduler
|
|
132
|
-
|
|
133
|
-
${_bold("Auth:")}
|
|
134
|
-
${_cyan("wispy auth")} Show auth status for all providers
|
|
135
|
-
${_cyan("wispy auth github-copilot")} Sign in with GitHub (Copilot OAuth)
|
|
136
|
-
${_cyan("wispy auth refresh <provider>")} Refresh expired OAuth token
|
|
137
|
-
${_cyan("wispy auth revoke <provider>")} Remove saved auth token
|
|
138
|
-
|
|
139
|
-
${_bold("Config & Maintenance:")}
|
|
140
|
-
${_cyan("wispy setup")} Configure wispy interactively ${_dim("(30 AI providers)")}
|
|
141
|
-
${_cyan("wispy model")} Show / switch AI model
|
|
142
|
-
${_cyan("wispy model list")} List available models per provider
|
|
143
|
-
${_cyan("wispy model set <p:model>")} Switch model (e.g. xai:grok-3)
|
|
144
|
-
${_cyan("wispy update")} Update to latest version
|
|
145
|
-
${_cyan("wispy migrate")} Import from OpenClaw
|
|
146
|
-
${_cyan("wispy doctor")} Check system health
|
|
147
|
-
${_cyan("wispy version")} Show version
|
|
148
|
-
|
|
149
|
-
${_bold("Shell Completions:")}
|
|
150
|
-
${_cyan("wispy completion bash")} Bash completion script
|
|
151
|
-
${_cyan("wispy completion zsh")} Zsh completion script
|
|
152
|
-
|
|
153
|
-
${_bold("Flags:")}
|
|
154
|
-
${_cyan("--help, -h")} Show this help
|
|
155
|
-
${_cyan("--version, -v")} Show version
|
|
156
|
-
${_cyan("--debug")} Verbose logs & stack traces
|
|
157
|
-
${_cyan("WISPY_DEBUG=1")} Same as --debug via env
|
|
158
|
-
|
|
159
|
-
${_dim("Run 'wispy help <command>' for detailed help on a specific command.")}
|
|
160
|
-
`);
|
|
161
|
-
process.exit(0);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// ── help sub-command (detailed per-command help) ──────────────────────────────
|
|
165
|
-
if (args[0] === "help") {
|
|
166
|
-
const topic = args[1];
|
|
167
|
-
const helpTexts = {
|
|
168
|
-
ws: `
|
|
169
|
-
${_bold("wispy ws")} — Workstream management
|
|
170
|
-
|
|
171
|
-
${_bold("Usage:")}
|
|
172
|
-
wispy ws List all workstreams
|
|
173
|
-
wispy ws new <name> Create a new workstream
|
|
174
|
-
wispy ws switch <name> Switch to a workstream
|
|
175
|
-
wispy ws archive <name> Archive a workstream (move sessions/memory)
|
|
176
|
-
wispy ws delete <name> Permanently delete a workstream
|
|
177
|
-
wispy ws status Status overview of all workstreams
|
|
178
|
-
wispy ws search <query> Search across all workstreams
|
|
179
|
-
|
|
180
|
-
${_bold("Examples:")}
|
|
181
|
-
wispy ws new project-x
|
|
182
|
-
wispy ws switch project-x
|
|
183
|
-
wispy ws archive old-project
|
|
184
|
-
`,
|
|
185
|
-
trust: `
|
|
186
|
-
${_bold("wispy trust")} — Trust levels and security
|
|
187
|
-
|
|
188
|
-
${_bold("Usage:")}
|
|
189
|
-
wispy trust Show current trust level and policies
|
|
190
|
-
wispy trust careful Require approval for everything
|
|
191
|
-
wispy trust balanced Approve only risky operations
|
|
192
|
-
wispy trust yolo Auto-approve everything
|
|
193
|
-
|
|
194
|
-
${_bold("Trust levels:")}
|
|
195
|
-
careful — Review every tool call before execution
|
|
196
|
-
balanced — Auto-approve safe ops, review destructive ones
|
|
197
|
-
yolo — Full automation (use with care!)
|
|
198
|
-
`,
|
|
199
|
-
deploy: `
|
|
200
|
-
${_bold("wispy deploy")} — Deployment helpers
|
|
201
|
-
|
|
202
|
-
${_bold("Usage:")}
|
|
203
|
-
wispy deploy init Generate Dockerfile + compose + .env.example
|
|
204
|
-
wispy deploy dockerfile Print Dockerfile
|
|
205
|
-
wispy deploy compose Print docker-compose.yml
|
|
206
|
-
wispy deploy systemd Print systemd unit
|
|
207
|
-
wispy deploy railway Print railway.json
|
|
208
|
-
wispy deploy fly Print fly.toml
|
|
209
|
-
wispy deploy render Print render.yaml
|
|
210
|
-
wispy deploy modal Generate Modal serverless config
|
|
211
|
-
wispy deploy daytona Generate Daytona workspace config
|
|
212
|
-
wispy deploy vps user@host SSH deploy to VPS
|
|
213
|
-
wispy deploy status <url> Check remote server health
|
|
214
|
-
|
|
215
|
-
${_bold("Examples:")}
|
|
216
|
-
wispy deploy init
|
|
217
|
-
wispy deploy vps root@my.vps
|
|
218
|
-
wispy connect https://my.vps:18790 --token <token>
|
|
219
|
-
`,
|
|
220
|
-
cron: `
|
|
221
|
-
${_bold("wispy cron")} — Scheduled task management
|
|
222
|
-
|
|
223
|
-
${_bold("Usage:")}
|
|
224
|
-
wispy cron list List all cron jobs
|
|
225
|
-
wispy cron add Interactive job creation
|
|
226
|
-
wispy cron remove <id> Delete a cron job
|
|
227
|
-
wispy cron run <id> Run a job immediately
|
|
228
|
-
wispy cron history <id> Show past runs
|
|
229
|
-
wispy cron start Start scheduler in foreground
|
|
230
|
-
|
|
231
|
-
${_bold("Schedule types:")}
|
|
232
|
-
cron — standard cron expression (e.g. "0 9 * * *")
|
|
233
|
-
every — interval in minutes
|
|
234
|
-
at — one-time ISO datetime
|
|
235
|
-
`,
|
|
236
|
-
channel: `
|
|
237
|
-
${_bold("wispy channel")} — Messaging channel setup
|
|
238
|
-
|
|
239
|
-
${_bold("Usage:")}
|
|
240
|
-
wispy channel setup telegram Telegram bot setup
|
|
241
|
-
wispy channel setup discord Discord bot setup
|
|
242
|
-
wispy channel setup slack Slack bot setup
|
|
243
|
-
wispy channel setup whatsapp WhatsApp setup
|
|
244
|
-
wispy channel setup email Email setup
|
|
245
|
-
wispy channel list List configured channels
|
|
246
|
-
wispy channel test <name> Test channel connection
|
|
247
|
-
|
|
248
|
-
${_bold("Running bots:")}
|
|
249
|
-
wispy --serve Start all configured bots
|
|
250
|
-
wispy --telegram Start Telegram bot only
|
|
251
|
-
wispy --discord Start Discord bot only
|
|
252
|
-
`,
|
|
253
|
-
sync: `
|
|
254
|
-
${_bold("wispy sync")} — Remote sync
|
|
255
|
-
|
|
256
|
-
${_bold("Usage:")}
|
|
257
|
-
wispy sync Bidirectional sync
|
|
258
|
-
wispy sync push Push local → remote
|
|
259
|
-
wispy sync pull Pull remote → local
|
|
260
|
-
wispy sync status Show sync status
|
|
261
|
-
wispy sync auto Enable auto-sync
|
|
262
|
-
wispy sync auto --off Disable auto-sync
|
|
263
|
-
|
|
264
|
-
${_bold("Flags:")}
|
|
265
|
-
--remote <url> Remote server URL
|
|
266
|
-
--token <token> Auth token
|
|
267
|
-
--strategy newer-wins Conflict resolution (newer-wins|local-wins|remote-wins)
|
|
268
|
-
--memory-only Only sync memory files
|
|
269
|
-
--sessions-only Only sync sessions
|
|
270
|
-
`,
|
|
271
|
-
skill: `
|
|
272
|
-
${_bold("wispy skill")} — Skill management
|
|
273
|
-
|
|
274
|
-
${_bold("Usage:")}
|
|
275
|
-
wispy skill list List all installed skills
|
|
276
|
-
wispy skill show <name> Show skill details
|
|
277
|
-
wispy teach <name> Create a skill from current conversation
|
|
278
|
-
wispy improve <name> Improve an existing skill
|
|
279
|
-
|
|
280
|
-
${_bold("In REPL:")}
|
|
281
|
-
/skills List skills
|
|
282
|
-
/teach <name> Teach from conversation
|
|
283
|
-
/<skill-name> Invoke any skill
|
|
284
|
-
`,
|
|
285
|
-
doctor: `
|
|
286
|
-
${_bold("wispy doctor")} — System health check
|
|
287
|
-
|
|
288
|
-
Checks Node.js version, config, API keys, directory permissions,
|
|
289
|
-
optional dependencies, and remote server connectivity.
|
|
290
|
-
|
|
291
|
-
${_bold("Usage:")}
|
|
292
|
-
wispy doctor
|
|
293
|
-
`,
|
|
294
|
-
node: `
|
|
295
|
-
${_bold("wispy node")} — Multi-machine node management
|
|
296
|
-
|
|
297
|
-
${_bold("Usage:")}
|
|
298
|
-
wispy node pair Generate pairing code
|
|
299
|
-
wispy node connect <code> --url <url> Connect as a node
|
|
300
|
-
wispy node list Show registered nodes
|
|
301
|
-
wispy node status Ping all nodes
|
|
302
|
-
wispy node remove <id> Unregister a node
|
|
303
|
-
`,
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
if (topic && helpTexts[topic]) {
|
|
307
|
-
console.log(helpTexts[topic]);
|
|
308
|
-
} else if (topic) {
|
|
309
|
-
console.log(_yellow(`\n⚠️ No detailed help for '${topic}'. Try 'wispy --help' for the full command list.\n`));
|
|
310
|
-
} else {
|
|
311
|
-
// Generic help — same as --help
|
|
312
|
-
args[0] = "--help";
|
|
313
|
-
// Re-trigger help by falling through (not possible here, just print it)
|
|
314
|
-
console.log(`Run ${_cyan("wispy --help")} for the full command list.`);
|
|
315
|
-
console.log(`Run ${_cyan("wispy help <command>")} for per-command help.`);
|
|
316
|
-
console.log(`\nAvailable topics: ${Object.keys(helpTexts).map(k => _cyan(k)).join(", ")}`);
|
|
317
|
-
}
|
|
318
|
-
process.exit(0);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// ── completion sub-command ────────────────────────────────────────────────────
|
|
322
|
-
if (args[0] === "completion") {
|
|
323
|
-
const shell = args[1] ?? "bash";
|
|
324
|
-
|
|
325
|
-
// Commands with descriptions for rich completions
|
|
326
|
-
const cmdSpecs = [
|
|
327
|
-
{ cmd: "ws", desc: "Workstream management (list/new/switch/archive/delete/status/search)" },
|
|
328
|
-
{ cmd: "trust", desc: "Trust level & security policies (careful/balanced/yolo)" },
|
|
329
|
-
{ cmd: "where", desc: "Show current mode & context" },
|
|
330
|
-
{ cmd: "handoff", desc: "Generate handoff summary & sync" },
|
|
331
|
-
{ cmd: "skill", desc: "Skill management (list/show)" },
|
|
332
|
-
{ cmd: "teach", desc: "Create skill from current conversation" },
|
|
333
|
-
{ cmd: "improve", desc: "Improve an existing skill" },
|
|
334
|
-
{ cmd: "dry", desc: "Run next command in dry-run (preview-only) mode" },
|
|
335
|
-
{ cmd: "deploy", desc: "Deployment helpers (Dockerfile/compose/systemd/vps/…)" },
|
|
336
|
-
{ cmd: "server", desc: "Start the Wispy API server" },
|
|
337
|
-
{ cmd: "node", desc: "Multi-machine node management (pair/connect/list/status)" },
|
|
338
|
-
{ cmd: "channel", desc: "Messaging channel setup (telegram/discord/slack/email/…)" },
|
|
339
|
-
{ cmd: "cron", desc: "Scheduled tasks (list/add/remove/run/history/start)" },
|
|
340
|
-
{ cmd: "audit", desc: "View audit log & replay sessions" },
|
|
341
|
-
{ cmd: "log", desc: "Alias for audit" },
|
|
342
|
-
{ cmd: "sync", desc: "Sync with remote server (push/pull/status/auto)" },
|
|
343
|
-
{ cmd: "setup", desc: "Interactive setup wizard" },
|
|
344
|
-
{ cmd: "init", desc: "Alias for setup" },
|
|
345
|
-
{ cmd: "update", desc: "Update wispy-cli to latest version" },
|
|
346
|
-
{ cmd: "tui", desc: "Launch the workspace TUI" },
|
|
347
|
-
{ cmd: "migrate", desc: "Import from OpenClaw or other sources" },
|
|
348
|
-
{ cmd: "version", desc: "Show version" },
|
|
349
|
-
{ cmd: "doctor", desc: "Check system health & API keys" },
|
|
350
|
-
{ cmd: "help", desc: "Show help (optionally for a specific command)" },
|
|
351
|
-
{ cmd: "completion", desc: "Print shell completion script (bash/zsh/fish)" },
|
|
352
|
-
{ cmd: "status", desc: "Show wispy status & remote connection info" },
|
|
353
|
-
{ cmd: "connect", desc: "Connect to a remote Wispy server" },
|
|
354
|
-
{ cmd: "disconnect", desc: "Disconnect from remote server (go back to local)" },
|
|
355
|
-
{ cmd: "auth", desc: "OAuth auth management (github-copilot, refresh, revoke)" },
|
|
356
|
-
];
|
|
357
|
-
|
|
358
|
-
// Sub-command completions for nested commands
|
|
359
|
-
const subCmds = {
|
|
360
|
-
ws: ["new", "switch", "archive", "delete", "status", "search"],
|
|
361
|
-
trust: ["careful", "balanced", "yolo", "log"],
|
|
362
|
-
skill: ["list", "show"],
|
|
363
|
-
deploy: ["init", "dockerfile", "compose", "systemd", "railway", "fly", "render", "modal", "daytona", "vps", "status"],
|
|
364
|
-
cron: ["list", "add", "remove", "run", "history", "start"],
|
|
365
|
-
audit: ["replay", "export", "--today", "--session", "--tool", "--limit"],
|
|
366
|
-
channel: ["setup", "list", "test"],
|
|
367
|
-
node: ["pair", "connect", "list", "status", "remove"],
|
|
368
|
-
sync: ["push", "pull", "status", "auto"],
|
|
369
|
-
auth: ["github-copilot", "refresh", "revoke"],
|
|
370
|
-
server: ["start", "stop", "status"],
|
|
371
|
-
completion: ["bash", "zsh", "fish"],
|
|
372
|
-
help: cmdSpecs.map(c => c.cmd),
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
const cmds = cmdSpecs.map(c => c.cmd);
|
|
376
|
-
const flags = ["--help", "--version", "--debug", "--serve", "--telegram", "--discord", "--slack"];
|
|
377
|
-
|
|
378
|
-
if (shell === "bash") {
|
|
379
|
-
const subCmdsStr = Object.entries(subCmds)
|
|
380
|
-
.map(([k, v]) => ` ${k}) COMPREPLY=( $(compgen -W "${v.join(' ')}" -- "\${cur}") ) ;;`)
|
|
381
|
-
.join('\n');
|
|
382
|
-
|
|
383
|
-
console.log(`# wispy bash completion
|
|
384
|
-
# Add to ~/.bashrc: eval "$(wispy completion bash)"
|
|
385
|
-
_wispy_completion() {
|
|
386
|
-
local cur prev words
|
|
387
|
-
COMPREPLY=()
|
|
388
|
-
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
389
|
-
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
390
|
-
local commands="${cmds.join(" ")}"
|
|
391
|
-
local flags="${flags.join(" ")}"
|
|
392
|
-
|
|
393
|
-
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
394
|
-
COMPREPLY=( $(compgen -W "\${commands} \${flags}" -- "\${cur}") )
|
|
395
|
-
elif [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
396
|
-
case "\${prev}" in
|
|
397
|
-
${subCmdsStr}
|
|
398
|
-
esac
|
|
399
|
-
fi
|
|
400
|
-
}
|
|
401
|
-
complete -F _wispy_completion wispy`);
|
|
402
|
-
|
|
403
|
-
} else if (shell === "zsh") {
|
|
404
|
-
const cmdDescLines = cmdSpecs.map(c => ` '${c.cmd}:${c.desc.replace(/'/g, "''")}'`).join('\n');
|
|
405
|
-
const subCmdLines = Object.entries(subCmds)
|
|
406
|
-
.map(([k, v]) => ` (${k}) _values '${k} commands' ${v.map(s => `'${s}'`).join(' ')} ;;`)
|
|
407
|
-
.join('\n');
|
|
408
|
-
|
|
409
|
-
console.log(`# wispy zsh completion
|
|
410
|
-
# Add to ~/.zshrc: eval "$(wispy completion zsh)"
|
|
411
|
-
# Or for permanent use: wispy completion zsh > /usr/local/share/zsh/site-functions/_wispy
|
|
412
|
-
_wispy() {
|
|
413
|
-
local context state state_descr line
|
|
414
|
-
typeset -A opt_args
|
|
415
|
-
|
|
416
|
-
local -a commands
|
|
417
|
-
commands=(
|
|
418
|
-
${cmdDescLines}
|
|
419
|
-
)
|
|
420
|
-
|
|
421
|
-
_arguments -C \\
|
|
422
|
-
'(- *)'{-h,--help}'[Show help]' \\
|
|
423
|
-
'(- *)'{-v,--version}'[Show version]' \\
|
|
424
|
-
'--debug[Enable verbose logs]' \\
|
|
425
|
-
'--serve[Start all channel bots]' \\
|
|
426
|
-
'--telegram[Start Telegram bot]' \\
|
|
427
|
-
'--discord[Start Discord bot]' \\
|
|
428
|
-
'--slack[Start Slack bot]' \\
|
|
429
|
-
'1:command:->cmd' \\
|
|
430
|
-
'*::arg:->args'
|
|
431
|
-
|
|
432
|
-
case $state in
|
|
433
|
-
cmd)
|
|
434
|
-
_describe 'wispy commands' commands
|
|
435
|
-
;;
|
|
436
|
-
args)
|
|
437
|
-
case $line[1] in
|
|
438
|
-
${subCmdLines}
|
|
439
|
-
esac
|
|
440
|
-
;;
|
|
441
|
-
esac
|
|
442
|
-
}
|
|
443
|
-
compdef _wispy wispy`);
|
|
444
|
-
|
|
445
|
-
} else if (shell === "fish") {
|
|
446
|
-
const mainComps = cmdSpecs
|
|
447
|
-
.map(c => `complete -c wispy -f -n '__fish_use_subcommand' -a '${c.cmd}' -d '${c.desc.replace(/'/g, "\\'")}'`)
|
|
448
|
-
.join("\n");
|
|
449
|
-
const subComps = Object.entries(subCmds)
|
|
450
|
-
.flatMap(([k, v]) => v.map(s => `complete -c wispy -f -n '__fish_seen_subcommand_from ${k}' -a '${s}'`))
|
|
451
|
-
.join("\n");
|
|
452
|
-
|
|
453
|
-
console.log(`# wispy fish completion
|
|
454
|
-
# Save to ~/.config/fish/completions/wispy.fish
|
|
455
|
-
${mainComps}
|
|
456
|
-
${subComps}`);
|
|
457
|
-
|
|
458
|
-
} else {
|
|
459
|
-
console.error(_red(`❌ Unknown shell: ${shell}. Use: bash, zsh, or fish`));
|
|
460
|
-
process.exit(2);
|
|
461
|
-
}
|
|
462
|
-
process.exit(0);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// ── doctor sub-command ────────────────────────────────────────────────────────
|
|
466
|
-
if (args[0] === "doctor") {
|
|
467
|
-
const { access, readFile: rf, stat } = await import("node:fs/promises");
|
|
468
|
-
const { homedir } = await import("node:os");
|
|
469
|
-
const wispyDir = path.join(homedir(), ".wispy");
|
|
470
|
-
const configPath = path.join(wispyDir, "config.json");
|
|
471
|
-
const memoryDir = path.join(wispyDir, "memory");
|
|
472
|
-
const sessionsDir = path.join(wispyDir, "sessions");
|
|
473
|
-
|
|
474
|
-
console.log(`\n${_bold("🩺 Wispy Doctor")} ${_dim(`v${getVersion()}`)}\n`);
|
|
475
|
-
|
|
476
|
-
let allOk = true;
|
|
477
|
-
const issues = [];
|
|
478
|
-
|
|
479
|
-
function check(label, ok, detail = "") {
|
|
480
|
-
if (ok) {
|
|
481
|
-
console.log(` ${_green("✅")} ${label}${detail ? _dim(" " + detail) : ""}`);
|
|
482
|
-
} else {
|
|
483
|
-
console.log(` ${_red("❌")} ${label}${detail ? _dim(" " + detail) : ""}`);
|
|
484
|
-
allOk = false;
|
|
485
|
-
issues.push(label);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function info(label, detail) {
|
|
490
|
-
console.log(` ${_cyan("ℹ️ ")} ${label}${detail ? _dim(" " + detail) : ""}`);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// 1. Node.js version
|
|
494
|
-
const [major] = process.version.replace("v", "").split(".").map(Number);
|
|
495
|
-
check("Node.js version", major >= 18, `${process.version} (required: >= 18)`);
|
|
496
|
-
|
|
497
|
-
// 2. Config file
|
|
498
|
-
let config = null;
|
|
499
|
-
try {
|
|
500
|
-
const raw = await rf(configPath, "utf8");
|
|
501
|
-
config = JSON.parse(raw);
|
|
502
|
-
check("Config file", true, configPath);
|
|
503
|
-
} catch (e) {
|
|
504
|
-
if (e.code === "ENOENT") {
|
|
505
|
-
check("Config file", false, `Not found at ${configPath} — run 'wispy setup'`);
|
|
506
|
-
} else {
|
|
507
|
-
check("Config file", false, `Malformed JSON — run 'wispy setup' to reconfigure`);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// 3. API key configured
|
|
512
|
-
if (config) {
|
|
513
|
-
const envMap = {
|
|
514
|
-
google: ["GOOGLE_AI_KEY", "GOOGLE_GENERATIVE_AI_KEY", "GEMINI_API_KEY"],
|
|
515
|
-
anthropic: ["ANTHROPIC_API_KEY"],
|
|
516
|
-
openai: ["OPENAI_API_KEY"],
|
|
517
|
-
groq: ["GROQ_API_KEY"],
|
|
518
|
-
openrouter: ["OPENROUTER_API_KEY"],
|
|
519
|
-
deepseek: ["DEEPSEEK_API_KEY"],
|
|
520
|
-
xai: ["XAI_API_KEY"],
|
|
521
|
-
mistral: ["MISTRAL_API_KEY"],
|
|
522
|
-
together: ["TOGETHER_API_KEY"],
|
|
523
|
-
nvidia: ["NVIDIA_API_KEY"],
|
|
524
|
-
kimi: ["MOONSHOT_API_KEY", "KIMI_API_KEY"],
|
|
525
|
-
minimax: ["MINIMAX_API_KEY"],
|
|
526
|
-
chutes: ["CHUTES_API_KEY"],
|
|
527
|
-
venice: ["VENICE_API_KEY"],
|
|
528
|
-
huggingface: ["HF_TOKEN", "HUGGINGFACE_API_KEY"],
|
|
529
|
-
cloudflare: ["CF_API_TOKEN"],
|
|
530
|
-
volcengine: ["VOLCENGINE_API_KEY", "ARK_API_KEY"],
|
|
531
|
-
byteplus: ["BYTEPLUS_API_KEY"],
|
|
532
|
-
zai: ["ZAI_API_KEY", "GLM_API_KEY", "ZHIPU_API_KEY"],
|
|
533
|
-
dashscope: ["DASHSCOPE_API_KEY"],
|
|
534
|
-
xiaomi: ["XIAOMI_API_KEY"],
|
|
535
|
-
vercelai: ["VERCEL_AI_TOKEN"],
|
|
536
|
-
litellm: ["LITELLM_API_KEY"],
|
|
537
|
-
ollama: null, // no key
|
|
538
|
-
vllm: null, // no key
|
|
539
|
-
sglang: null, // no key
|
|
540
|
-
};
|
|
541
|
-
// Support both old (config.provider) and new (config.providers) format
|
|
542
|
-
const providers = config.providers ? Object.keys(config.providers) : (config.provider ? [config.provider] : []);
|
|
543
|
-
if (providers.length === 0) {
|
|
544
|
-
check("AI provider", false, "no provider configured — run 'wispy setup provider'");
|
|
545
|
-
} else {
|
|
546
|
-
for (const provider of providers) {
|
|
547
|
-
const envKeys = envMap[provider];
|
|
548
|
-
if (envKeys === null) {
|
|
549
|
-
check(`AI provider (${provider})`, true, "no key needed");
|
|
550
|
-
} else if (envKeys) {
|
|
551
|
-
const key = config.providers?.[provider]?.apiKey || config.apiKey
|
|
552
|
-
|| envKeys.reduce((found, k) => found || process.env[k], null);
|
|
553
|
-
check(`API key (${provider})`, !!key && key.length > 8, key ? "configured" : `set env var or run 'wispy setup provider'`);
|
|
554
|
-
} else {
|
|
555
|
-
check(`AI provider (${provider})`, false, `unknown provider`);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
} else {
|
|
560
|
-
info("AI provider", "skipped (no config)");
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// 4. Memory directory writable
|
|
564
|
-
try {
|
|
565
|
-
const { mkdir: mkd, writeFile: wf, unlink: unl } = await import("node:fs/promises");
|
|
566
|
-
await mkd(memoryDir, { recursive: true });
|
|
567
|
-
const testFile = path.join(memoryDir, ".write-test");
|
|
568
|
-
await wf(testFile, "test", "utf8");
|
|
569
|
-
await unl(testFile);
|
|
570
|
-
check("Memory dir writable", true, memoryDir);
|
|
571
|
-
} catch (e) {
|
|
572
|
-
check("Memory dir writable", false, e.message);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// 5. Sessions directory writable
|
|
576
|
-
try {
|
|
577
|
-
const { mkdir: mkd, writeFile: wf, unlink: unl } = await import("node:fs/promises");
|
|
578
|
-
await mkd(sessionsDir, { recursive: true });
|
|
579
|
-
const testFile = path.join(sessionsDir, ".write-test");
|
|
580
|
-
await wf(testFile, "test", "utf8");
|
|
581
|
-
await unl(testFile);
|
|
582
|
-
check("Sessions dir writable", true, sessionsDir);
|
|
583
|
-
} catch (e) {
|
|
584
|
-
check("Sessions dir writable", false, e.message);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// 6. Optional deps
|
|
588
|
-
console.log(`\n ${_bold("Optional dependencies:")}`);
|
|
589
|
-
const optDeps = [
|
|
590
|
-
{ pkg: "grammy", label: "Telegram (grammy)" },
|
|
591
|
-
{ pkg: "discord.js", label: "Discord (discord.js)" },
|
|
592
|
-
{ pkg: "@slack/bolt", label: "Slack (@slack/bolt)" },
|
|
593
|
-
{ pkg: "whatsapp-web.js", label: "WhatsApp (whatsapp-web.js)" },
|
|
594
|
-
{ pkg: "nodemailer", label: "Email (nodemailer)" },
|
|
595
|
-
{ pkg: "imapflow", label: "Email IMAP (imapflow)" },
|
|
596
|
-
];
|
|
597
|
-
for (const dep of optDeps) {
|
|
598
|
-
try {
|
|
599
|
-
await import(dep.pkg);
|
|
600
|
-
check(dep.label, true, "installed");
|
|
601
|
-
} catch {
|
|
602
|
-
console.log(` ${_dim("–")} ${_dim(dep.label + " not installed (optional)")}`);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// 7. Remote server (if configured)
|
|
607
|
-
const remotePath = path.join(wispyDir, "remote.json");
|
|
608
|
-
try {
|
|
609
|
-
const remote = JSON.parse(await rf(remotePath, "utf8"));
|
|
610
|
-
if (remote?.url) {
|
|
611
|
-
console.log(`\n ${_bold("Remote server:")}`);
|
|
612
|
-
process.stdout.write(` ${_cyan("🔄")} Checking ${remote.url}... `);
|
|
613
|
-
try {
|
|
614
|
-
const resp = await fetch(`${remote.url}/api/health`, { signal: AbortSignal.timeout(5000) });
|
|
615
|
-
if (resp.ok) {
|
|
616
|
-
console.log(_green("✅ reachable"));
|
|
617
|
-
} else {
|
|
618
|
-
console.log(_yellow(`⚠️ HTTP ${resp.status}`));
|
|
619
|
-
allOk = false;
|
|
620
|
-
}
|
|
621
|
-
} catch {
|
|
622
|
-
console.log(_red("❌ unreachable"));
|
|
623
|
-
allOk = false;
|
|
624
|
-
issues.push("Remote server unreachable");
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
} catch {}
|
|
628
|
-
|
|
629
|
-
console.log("");
|
|
630
|
-
if (allOk) {
|
|
631
|
-
console.log(`${_green("✅ All checks passed!")}\n`);
|
|
632
|
-
} else {
|
|
633
|
-
console.log(`${_yellow("⚠️ Issues found:")} ${issues.join(", ")}`);
|
|
634
|
-
console.log(_dim(" Run 'wispy setup' to fix configuration issues.\n"));
|
|
635
|
-
}
|
|
636
|
-
process.exit(allOk ? 0 : 1);
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// ── config validation helper ───────────────────────────────────────────────────
|
|
640
|
-
async function validateConfigOnStartup() {
|
|
641
|
-
const { homedir } = await import("node:os");
|
|
642
|
-
const { readFile: rf } = await import("node:fs/promises");
|
|
643
|
-
const configPath = path.join(homedir(), ".wispy", "config.json");
|
|
644
|
-
try {
|
|
645
|
-
const raw = await rf(configPath, "utf8");
|
|
646
|
-
JSON.parse(raw);
|
|
647
|
-
} catch (e) {
|
|
648
|
-
if (e instanceof SyntaxError) {
|
|
649
|
-
console.error(_red("❌ Config file corrupted. Run 'wispy setup' to reconfigure."));
|
|
650
|
-
process.exit(1);
|
|
651
|
-
}
|
|
652
|
-
// File not found is OK (first run)
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// ── ws sub-command ────────────────────────────────────────────────────────────
|
|
657
|
-
if (args[0] === "ws") {
|
|
658
|
-
try {
|
|
659
|
-
const { handleWsCommand } = await import(
|
|
660
|
-
path.join(__dirname, "..", "lib", "commands", "ws.mjs")
|
|
661
|
-
);
|
|
662
|
-
await handleWsCommand(args);
|
|
663
|
-
} catch (e) { friendlyError(e); }
|
|
664
|
-
process.exit(0);
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// ── trust sub-command ─────────────────────────────────────────────────────────
|
|
668
|
-
if (args[0] === "trust") {
|
|
669
|
-
try {
|
|
670
|
-
const { handleTrustCommand } = await import(
|
|
671
|
-
path.join(__dirname, "..", "lib", "commands", "trust.mjs")
|
|
672
|
-
);
|
|
673
|
-
await handleTrustCommand(args);
|
|
674
|
-
} catch (e) { friendlyError(e); }
|
|
675
|
-
process.exit(0);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
// ── where sub-command ─────────────────────────────────────────────────────────
|
|
679
|
-
if (args[0] === "where") {
|
|
680
|
-
try {
|
|
681
|
-
const { cmdWhere } = await import(
|
|
682
|
-
path.join(__dirname, "..", "lib", "commands", "continuity.mjs")
|
|
683
|
-
);
|
|
684
|
-
await cmdWhere();
|
|
685
|
-
} catch (e) { friendlyError(e); }
|
|
686
|
-
process.exit(0);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// ── handoff sub-command ───────────────────────────────────────────────────────
|
|
690
|
-
if (args[0] === "handoff") {
|
|
691
|
-
try {
|
|
692
|
-
const { handleContinuityCommand } = await import(
|
|
693
|
-
path.join(__dirname, "..", "lib", "commands", "continuity.mjs")
|
|
694
|
-
);
|
|
695
|
-
await handleContinuityCommand(args);
|
|
696
|
-
} catch (e) { friendlyError(e); }
|
|
697
|
-
process.exit(0);
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// ── skill sub-command ─────────────────────────────────────────────────────────
|
|
701
|
-
if (args[0] === "skill") {
|
|
702
|
-
try {
|
|
703
|
-
const { handleSkillCommand } = await import(
|
|
704
|
-
path.join(__dirname, "..", "lib", "commands", "skills-cmd.mjs")
|
|
705
|
-
);
|
|
706
|
-
await handleSkillCommand(args);
|
|
707
|
-
} catch (e) { friendlyError(e); }
|
|
708
|
-
process.exit(0);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// ── teach sub-command ─────────────────────────────────────────────────────────
|
|
712
|
-
if (args[0] === "teach") {
|
|
713
|
-
try {
|
|
714
|
-
const { cmdTeach } = await import(
|
|
715
|
-
path.join(__dirname, "..", "lib", "commands", "skills-cmd.mjs")
|
|
716
|
-
);
|
|
717
|
-
await cmdTeach(args[1]);
|
|
718
|
-
} catch (e) { friendlyError(e); }
|
|
719
|
-
process.exit(0);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// ── improve sub-command ───────────────────────────────────────────────────────
|
|
723
|
-
if (args[0] === "improve") {
|
|
724
|
-
try {
|
|
725
|
-
const { cmdImproveSkill } = await import(
|
|
726
|
-
path.join(__dirname, "..", "lib", "commands", "skills-cmd.mjs")
|
|
727
|
-
);
|
|
728
|
-
const name = args[1];
|
|
729
|
-
const feedback = args.slice(2).join(" ").replace(/^["']|["']$/g, "");
|
|
730
|
-
await cmdImproveSkill(name, feedback);
|
|
731
|
-
} catch (e) { friendlyError(e); }
|
|
732
|
-
process.exit(0);
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// ── dry sub-command ───────────────────────────────────────────────────────────
|
|
736
|
-
if (args[0] === "dry") {
|
|
737
|
-
// Re-launch wispy with DRY_RUN env set, passing remaining args
|
|
738
|
-
const { spawn } = await import("node:child_process");
|
|
739
|
-
const remaining = args.slice(1);
|
|
740
|
-
const child = spawn(process.execPath, [process.argv[1], ...remaining], {
|
|
741
|
-
stdio: "inherit",
|
|
742
|
-
env: { ...process.env, WISPY_DRY_RUN: "1" },
|
|
743
|
-
});
|
|
744
|
-
child.on("exit", (code) => process.exit(code ?? 0));
|
|
745
|
-
await new Promise(() => {}); // keep alive until child exits
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
// ── setup / init sub-command ──────────────────────────────────────────────────
|
|
749
|
-
if (args[0] === "setup" || args[0] === "init") {
|
|
750
|
-
// Handle Ctrl+C gracefully in setup
|
|
751
|
-
process.removeAllListeners("SIGINT");
|
|
752
|
-
process.on("SIGINT", () => { console.log(_dim("\nSetup cancelled.")); process.exit(130); });
|
|
753
|
-
try {
|
|
754
|
-
const { OnboardingWizard } = await import(
|
|
755
|
-
path.join(__dirname, "..", "core", "onboarding.mjs")
|
|
756
|
-
);
|
|
757
|
-
const wizard = new OnboardingWizard();
|
|
758
|
-
const sub = args[1]; // e.g. "provider", "channels", "security"
|
|
759
|
-
if (sub && sub !== "wizard") {
|
|
760
|
-
await wizard.runStep(sub);
|
|
761
|
-
} else {
|
|
762
|
-
await wizard.run();
|
|
763
|
-
}
|
|
764
|
-
} catch (e) { friendlyError(e); }
|
|
765
|
-
|
|
766
|
-
// Shell completion tip
|
|
767
|
-
console.log(_dim(`
|
|
768
|
-
💡 Tip: Enable tab completion in your shell:
|
|
769
|
-
eval "$(wispy completion zsh)" # zsh — add to ~/.zshrc for permanent
|
|
770
|
-
eval "$(wispy completion bash)" # bash — add to ~/.bashrc for permanent
|
|
771
|
-
`));
|
|
772
|
-
process.exit(0);
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
// ── update sub-command ────────────────────────────────────────────────────────
|
|
776
|
-
if (args[0] === "update") {
|
|
777
|
-
try {
|
|
778
|
-
const { execSync } = await import("node:child_process");
|
|
779
|
-
console.log(_cyan("🔄 Checking for updates..."));
|
|
780
|
-
const current = getVersion();
|
|
781
|
-
const latest = execSync("npm info wispy-cli version", { encoding: "utf8" }).trim();
|
|
782
|
-
if (current === latest) {
|
|
783
|
-
console.log(_green(`✅ Already on latest version (${current})`));
|
|
784
|
-
} else {
|
|
785
|
-
console.log(`📦 ${_dim(current)} → ${_bold(latest)}`);
|
|
786
|
-
console.log(_cyan("🔄 Updating..."));
|
|
787
|
-
execSync("npm install -g wispy-cli@latest", { stdio: "inherit" });
|
|
788
|
-
console.log(_green(`\n✅ Updated to ${latest}`));
|
|
789
|
-
}
|
|
790
|
-
} catch (e) { friendlyError(e); }
|
|
791
|
-
process.exit(0);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
// ── model sub-command ─────────────────────────────────────────────────────────
|
|
795
|
-
if (args[0] === "model") {
|
|
796
|
-
const { loadConfig, saveConfig, PROVIDERS } = await import(
|
|
797
|
-
path.join(__dirname, "..", "core", "config.mjs")
|
|
798
|
-
);
|
|
799
|
-
|
|
800
|
-
const KNOWN_MODELS = {
|
|
801
|
-
google: ["gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash", "gemini-1.5-pro"],
|
|
802
|
-
anthropic: ["claude-sonnet-4-20250514", "claude-opus-4-6", "claude-haiku-3.5", "claude-3-5-sonnet-20241022"],
|
|
803
|
-
openai: ["gpt-4o", "gpt-4o-mini", "gpt-4.1", "gpt-4.1-mini", "o4-mini", "o3"],
|
|
804
|
-
xai: ["grok-3", "grok-3-mini", "grok-2-1212"],
|
|
805
|
-
mistral: ["mistral-large-latest", "mistral-small-latest", "codestral-latest", "open-mistral-nemo"],
|
|
806
|
-
groq: ["llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768", "gemma2-9b-it"],
|
|
807
|
-
deepseek: ["deepseek-chat", "deepseek-reasoner"],
|
|
808
|
-
together: ["meta-llama/Llama-3.3-70B-Instruct-Turbo", "Qwen/Qwen2.5-72B-Instruct-Turbo"],
|
|
809
|
-
openrouter: ["anthropic/claude-sonnet-4-20250514", "openai/gpt-4o", "google/gemini-2.5-flash"],
|
|
810
|
-
chutes: ["deepseek-ai/DeepSeek-V3-0324", "deepseek-ai/DeepSeek-R1"],
|
|
811
|
-
nvidia: ["meta/llama-3.3-70b-instruct", "nvidia/llama-3.1-nemotron-70b-instruct"],
|
|
812
|
-
kimi: ["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"],
|
|
813
|
-
zai: ["glm-4-flash", "glm-4-plus", "glm-z1-flash"],
|
|
814
|
-
dashscope: ["qwen-max", "qwen-plus", "qwen-turbo"],
|
|
815
|
-
volcengine: ["doubao-pro-32k", "doubao-lite-32k"],
|
|
816
|
-
ollama: ["llama3.2", "llama3.1", "qwen2.5", "phi3", "mistral"],
|
|
817
|
-
};
|
|
818
|
-
|
|
819
|
-
const sub = args[1];
|
|
820
|
-
const config = await loadConfig();
|
|
821
|
-
const configuredProviders = config.providers ? Object.keys(config.providers) : (config.provider ? [config.provider] : []);
|
|
822
|
-
|
|
823
|
-
// wispy model (no args) — show current
|
|
824
|
-
if (!sub) {
|
|
825
|
-
const defaultP = config.defaultProvider ?? configuredProviders[0];
|
|
826
|
-
const currentModel = config.providers?.[defaultP]?.model ?? config.model ?? PROVIDERS[defaultP]?.defaultModel ?? "unknown";
|
|
827
|
-
console.log(`\n🤖 ${_bold("Current model")}: ${_cyan(defaultP)}:${_bold(currentModel)}\n`);
|
|
828
|
-
if (configuredProviders.length > 1) {
|
|
829
|
-
console.log(` Configured providers: ${configuredProviders.join(", ")}`);
|
|
830
|
-
console.log(` ${_dim("Use 'wispy model list' to see all options")}`);
|
|
831
|
-
console.log(` ${_dim("Use 'wispy model set <provider:model>' to switch")}\n`);
|
|
832
|
-
}
|
|
833
|
-
process.exit(0);
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
// wispy model list
|
|
837
|
-
if (sub === "list") {
|
|
838
|
-
console.log(`\n🤖 ${_bold("Available models")}\n`);
|
|
839
|
-
for (const p of configuredProviders) {
|
|
840
|
-
const currentModel = config.providers?.[p]?.model ?? PROVIDERS[p]?.defaultModel ?? "";
|
|
841
|
-
const models = KNOWN_MODELS[p] ?? [currentModel];
|
|
842
|
-
console.log(` ${_cyan(p)}:`);
|
|
843
|
-
for (const m of models) {
|
|
844
|
-
const isCurrent = m === currentModel;
|
|
845
|
-
console.log(` ${isCurrent ? _green("●") : " "} ${m}${isCurrent ? _dim(" (current)") : ""}`);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
console.log(`\n ${_dim("Switch with: wispy model set <provider:model>")}\n`);
|
|
849
|
-
process.exit(0);
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
// wispy model set <provider:model>
|
|
853
|
-
if (sub === "set") {
|
|
854
|
-
const spec = args[2];
|
|
855
|
-
if (!spec || !spec.includes(":")) {
|
|
856
|
-
console.error(_red(`\n❌ Usage: wispy model set <provider:model>\n Example: wispy model set xai:grok-3\n`));
|
|
857
|
-
process.exit(1);
|
|
858
|
-
}
|
|
859
|
-
const colonIdx = spec.indexOf(":");
|
|
860
|
-
const provName = spec.slice(0, colonIdx);
|
|
861
|
-
const modelName = spec.slice(colonIdx + 1);
|
|
862
|
-
|
|
863
|
-
if (!PROVIDERS[provName]) {
|
|
864
|
-
console.error(_red(`\n❌ Unknown provider: ${provName}\n Available: ${Object.keys(PROVIDERS).join(", ")}\n`));
|
|
865
|
-
process.exit(1);
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
// Update config
|
|
869
|
-
if (!config.providers) config.providers = {};
|
|
870
|
-
if (!config.providers[provName]) config.providers[provName] = {};
|
|
871
|
-
config.providers[provName].model = modelName;
|
|
872
|
-
if (!config.defaultProvider) config.defaultProvider = provName;
|
|
873
|
-
|
|
874
|
-
await saveConfig(config);
|
|
875
|
-
console.log(_green(`\n✅ Switched to ${_cyan(provName)}:${_bold(modelName)}\n`));
|
|
876
|
-
process.exit(0);
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
console.error(_red(`\n❌ Unknown subcommand: wispy model ${sub}\n`));
|
|
880
|
-
console.log(_dim(" Usage: wispy model | wispy model list | wispy model set <provider:model>\n"));
|
|
881
|
-
process.exit(1);
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
// ── config sub-command ────────────────────────────────────────────────────────
|
|
885
|
-
if (args[0] === "config") {
|
|
886
|
-
const { loadConfig, saveConfig, WISPY_DIR, CONFIG_PATH } = await import(
|
|
887
|
-
path.join(__dirname, "..", "core", "config.mjs")
|
|
888
|
-
);
|
|
889
|
-
const sub = args[1];
|
|
890
|
-
const config = await loadConfig();
|
|
891
|
-
|
|
892
|
-
if (!sub || sub === "show") {
|
|
893
|
-
// wispy config — interactive config menu
|
|
894
|
-
const { select: cfgSelect } = await import("@inquirer/prompts");
|
|
895
|
-
|
|
896
|
-
// Show current status first
|
|
897
|
-
const providers = config.providers ? Object.keys(config.providers) : (config.provider ? [config.provider] : []);
|
|
898
|
-
const providerStr = providers.length > 0 ? providers.join(", ") : _dim("not set");
|
|
899
|
-
const defaultProv = config.defaultProvider ?? config.provider ?? _dim("not set");
|
|
900
|
-
const security = config.security ?? config.securityLevel ?? _dim("not set");
|
|
901
|
-
const language = config.language ?? _dim("auto");
|
|
902
|
-
const wsName = config.workstream ?? "default";
|
|
903
|
-
|
|
904
|
-
console.log(`\n${_bold("Wispy Config")}\n`);
|
|
905
|
-
console.log(` Providers: ${providerStr}`);
|
|
906
|
-
console.log(` Default: ${defaultProv}`);
|
|
907
|
-
console.log(` Security: ${security}`);
|
|
908
|
-
console.log(` Language: ${language}`);
|
|
909
|
-
console.log(` Workstream: ${wsName}`);
|
|
910
|
-
console.log(` Config file: ${_dim(CONFIG_PATH)}`);
|
|
911
|
-
console.log("");
|
|
912
|
-
|
|
913
|
-
const action = await cfgSelect({
|
|
914
|
-
message: "What do you want to change?",
|
|
915
|
-
choices: [
|
|
916
|
-
{ name: "Providers — add/remove AI providers", value: "provider" },
|
|
917
|
-
{ name: "Security — change trust level", value: "security" },
|
|
918
|
-
{ name: "Language — set preferred language", value: "language" },
|
|
919
|
-
{ name: "Channels — configure messaging bots", value: "channels" },
|
|
920
|
-
{ name: "Server — cloud/server settings", value: "server" },
|
|
921
|
-
{ name: "View raw config (JSON)", value: "raw" },
|
|
922
|
-
{ name: "Reset everything", value: "reset" },
|
|
923
|
-
{ name: "Done", value: "done" },
|
|
924
|
-
],
|
|
925
|
-
});
|
|
926
|
-
|
|
927
|
-
if (action === "done") {
|
|
928
|
-
process.exit(0);
|
|
929
|
-
} else if (action === "raw") {
|
|
930
|
-
const display = JSON.parse(JSON.stringify(config));
|
|
931
|
-
if (display.providers) {
|
|
932
|
-
for (const [k, v] of Object.entries(display.providers)) {
|
|
933
|
-
if (v.apiKey) v.apiKey = v.apiKey.slice(0, 6) + "..." + v.apiKey.slice(-4);
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
console.log(JSON.stringify(display, null, 2));
|
|
937
|
-
} else if (action === "reset") {
|
|
938
|
-
const { confirm: cfgConfirm } = await import("@inquirer/prompts");
|
|
939
|
-
const yes = await cfgConfirm({ message: "Reset all configuration?", default: false });
|
|
940
|
-
if (yes) {
|
|
941
|
-
const { writeFile: wf } = await import("node:fs/promises");
|
|
942
|
-
await wf(CONFIG_PATH, "{}\n");
|
|
943
|
-
console.log(`${_green("✓")} Config reset. Run 'wispy setup' to reconfigure.`);
|
|
944
|
-
}
|
|
945
|
-
} else {
|
|
946
|
-
// Delegate to setup wizard step
|
|
947
|
-
const { OnboardingWizard } = await import(
|
|
948
|
-
path.join(__dirname, "..", "core", "onboarding.mjs")
|
|
949
|
-
);
|
|
950
|
-
const wizard = new OnboardingWizard();
|
|
951
|
-
await wizard.runStep(action);
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
} else if (sub === "get") {
|
|
955
|
-
// wispy config get <key>
|
|
956
|
-
const key = args[2];
|
|
957
|
-
if (!key) { console.error(_red("Usage: wispy config get <key>")); process.exit(2); }
|
|
958
|
-
const val = key.split(".").reduce((o, k) => o?.[k], config);
|
|
959
|
-
if (val === undefined) {
|
|
960
|
-
console.log(_dim(`(not set)`));
|
|
961
|
-
} else {
|
|
962
|
-
console.log(typeof val === "object" ? JSON.stringify(val, null, 2) : String(val));
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
} else if (sub === "set") {
|
|
966
|
-
// wispy config set <key> <value>
|
|
967
|
-
const key = args[2];
|
|
968
|
-
const value = args.slice(3).join(" ");
|
|
969
|
-
if (!key || !value) { console.error(_red("Usage: wispy config set <key> <value>")); process.exit(2); }
|
|
970
|
-
|
|
971
|
-
// Parse value
|
|
972
|
-
let parsed = value;
|
|
973
|
-
if (value === "true") parsed = true;
|
|
974
|
-
else if (value === "false") parsed = false;
|
|
975
|
-
else if (/^\d+$/.test(value)) parsed = parseInt(value);
|
|
976
|
-
|
|
977
|
-
// Set nested key
|
|
978
|
-
const keys = key.split(".");
|
|
979
|
-
let obj = config;
|
|
980
|
-
for (let i = 0; i < keys.length - 1; i++) {
|
|
981
|
-
if (!obj[keys[i]]) obj[keys[i]] = {};
|
|
982
|
-
obj = obj[keys[i]];
|
|
983
|
-
}
|
|
984
|
-
obj[keys[keys.length - 1]] = parsed;
|
|
985
|
-
await saveConfig(config);
|
|
986
|
-
console.log(`${_green("✓")} ${key} = ${parsed}`);
|
|
987
|
-
|
|
988
|
-
} else if (sub === "delete" || sub === "unset") {
|
|
989
|
-
// wispy config delete <key>
|
|
990
|
-
const key = args[2];
|
|
991
|
-
if (!key) { console.error(_red("Usage: wispy config delete <key>")); process.exit(2); }
|
|
992
|
-
const keys = key.split(".");
|
|
993
|
-
let obj = config;
|
|
994
|
-
for (let i = 0; i < keys.length - 1; i++) {
|
|
995
|
-
if (!obj[keys[i]]) break;
|
|
996
|
-
obj = obj[keys[i]];
|
|
997
|
-
}
|
|
998
|
-
delete obj[keys[keys.length - 1]];
|
|
999
|
-
await saveConfig(config);
|
|
1000
|
-
console.log(`${_green("✓")} ${key} removed`);
|
|
1001
|
-
|
|
1002
|
-
} else if (sub === "reset") {
|
|
1003
|
-
// wispy config reset
|
|
1004
|
-
const { confirm } = await import("@inquirer/prompts");
|
|
1005
|
-
const yes = await confirm({ message: "Reset all configuration? This cannot be undone.", default: false });
|
|
1006
|
-
if (yes) {
|
|
1007
|
-
const { writeFile } = await import("node:fs/promises");
|
|
1008
|
-
await writeFile(CONFIG_PATH, "{}\n");
|
|
1009
|
-
console.log(`${_green("✓")} Config reset. Run 'wispy setup' to reconfigure.`);
|
|
1010
|
-
} else {
|
|
1011
|
-
console.log(_dim("Cancelled."));
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
} else if (sub === "path") {
|
|
1015
|
-
// wispy config path
|
|
1016
|
-
console.log(CONFIG_PATH);
|
|
1017
|
-
|
|
1018
|
-
} else if (sub === "edit") {
|
|
1019
|
-
// wispy config edit — open in $EDITOR
|
|
1020
|
-
const editor = process.env.EDITOR ?? process.env.VISUAL ?? "nano";
|
|
1021
|
-
const { spawn: sp } = await import("node:child_process");
|
|
1022
|
-
sp(editor, [CONFIG_PATH], { stdio: "inherit" });
|
|
1023
|
-
|
|
1024
|
-
} else {
|
|
1025
|
-
console.log(`
|
|
1026
|
-
${_bold("wispy config")} — manage configuration
|
|
1027
|
-
|
|
1028
|
-
${_cyan("wispy config")} Show current config (keys masked)
|
|
1029
|
-
${_cyan("wispy config get <key>")} Get a specific value (dot notation)
|
|
1030
|
-
${_cyan("wispy config set <key> <val>")} Set a value
|
|
1031
|
-
${_cyan("wispy config delete <key>")} Remove a key
|
|
1032
|
-
${_cyan("wispy config reset")} Reset to defaults
|
|
1033
|
-
${_cyan("wispy config path")} Show config file path
|
|
1034
|
-
${_cyan("wispy config edit")} Open in $EDITOR
|
|
1035
|
-
|
|
1036
|
-
${_dim("Examples:")}
|
|
1037
|
-
${_dim("wispy config get defaultProvider")}
|
|
1038
|
-
${_dim("wispy config set security careful")}
|
|
1039
|
-
${_dim("wispy config set language ko")}
|
|
1040
|
-
`);
|
|
1041
|
-
}
|
|
1042
|
-
process.exit(0);
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
// ── status sub-command ────────────────────────────────────────────────────────
|
|
1046
|
-
if (args[0] === "status") {
|
|
1047
|
-
// Try the enhanced status from onboarding.mjs first
|
|
1048
|
-
try {
|
|
1049
|
-
const { printStatus } = await import(
|
|
1050
|
-
path.join(__dirname, "..", "core", "onboarding.mjs")
|
|
1051
|
-
);
|
|
1052
|
-
await printStatus();
|
|
1053
|
-
process.exit(0);
|
|
1054
|
-
} catch (e) { if (DEBUG) console.error(e); }
|
|
1055
|
-
|
|
1056
|
-
// Fallback: original status (remote check)
|
|
1057
|
-
const { readFile } = await import("node:fs/promises");
|
|
1058
|
-
const { homedir } = await import("node:os");
|
|
1059
|
-
const { join } = await import("node:path");
|
|
1060
|
-
const { DeployManager } = await import(
|
|
1061
|
-
path.join(__dirname, "..", "core", "deploy.mjs")
|
|
1062
|
-
);
|
|
1063
|
-
|
|
1064
|
-
const remotePath = join(homedir(), ".wispy", "remote.json");
|
|
1065
|
-
let remote = null;
|
|
1066
|
-
try {
|
|
1067
|
-
remote = JSON.parse(await readFile(remotePath, "utf8"));
|
|
1068
|
-
} catch {}
|
|
1069
|
-
|
|
1070
|
-
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
1071
|
-
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
1072
|
-
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
1073
|
-
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
1074
|
-
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
1075
|
-
|
|
1076
|
-
console.log(`\n🌿 ${bold("Wispy Status")}\n`);
|
|
1077
|
-
|
|
1078
|
-
if (remote?.url) {
|
|
1079
|
-
console.log(` Mode: ${yellow("remote")}`);
|
|
1080
|
-
console.log(` Server: ${cyan(remote.url)}`);
|
|
1081
|
-
console.log(` Token: ${dim(remote.token ? remote.token.slice(0, 8) + "..." : "none")}`);
|
|
1082
|
-
|
|
1083
|
-
const dm = new DeployManager();
|
|
1084
|
-
process.stdout.write(" Health: checking... ");
|
|
1085
|
-
const status = await dm.checkRemote(remote.url);
|
|
1086
|
-
if (status.alive) {
|
|
1087
|
-
console.log(green("✓ alive"));
|
|
1088
|
-
if (status.version) console.log(` Version: ${status.version}`);
|
|
1089
|
-
if (status.uptime != null) console.log(` Uptime: ${Math.floor(status.uptime)}s`);
|
|
1090
|
-
if (status.latency) console.log(` Latency: ${status.latency}ms`);
|
|
1091
|
-
} else {
|
|
1092
|
-
console.log(`\x1b[31m✗ unreachable\x1b[0m`);
|
|
1093
|
-
if (status.error) console.log(` Error: ${dim(status.error)}`);
|
|
1094
|
-
}
|
|
1095
|
-
} else {
|
|
1096
|
-
console.log(` Mode: ${green("local")}`);
|
|
1097
|
-
console.log(` Server: http://localhost:18790 ${dim("(when running wispy server)")}`);
|
|
1098
|
-
console.log(dim("\n Tip: use `wispy connect <url> --token <token>` to use a remote server"));
|
|
1099
|
-
}
|
|
1100
|
-
console.log("");
|
|
1101
|
-
process.exit(0);
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
// ── connect sub-command ───────────────────────────────────────────────────────
|
|
1105
|
-
if (args[0] === "connect" && args[1]) {
|
|
1106
|
-
const { writeFile, mkdir } = await import("node:fs/promises");
|
|
1107
|
-
const { homedir } = await import("node:os");
|
|
1108
|
-
const { join } = await import("node:path");
|
|
1109
|
-
const { DeployManager } = await import(
|
|
1110
|
-
path.join(__dirname, "..", "core", "deploy.mjs")
|
|
1111
|
-
);
|
|
1112
|
-
|
|
1113
|
-
const url = args[1].replace(/\/$/, "");
|
|
1114
|
-
const tokenIdx = args.indexOf("--token");
|
|
1115
|
-
const token = tokenIdx !== -1 ? args[tokenIdx + 1] : "";
|
|
1116
|
-
|
|
1117
|
-
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
1118
|
-
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
1119
|
-
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
1120
|
-
|
|
1121
|
-
process.stdout.write(`\n🔗 Checking ${url}... `);
|
|
1122
|
-
const dm = new DeployManager();
|
|
1123
|
-
const status = await dm.checkRemote(url);
|
|
1124
|
-
|
|
1125
|
-
if (!status.alive) {
|
|
1126
|
-
console.log(red("unreachable"));
|
|
1127
|
-
if (status.error) console.log(dim(` ${status.error}`));
|
|
1128
|
-
console.log(red("\n❌ Could not connect to remote wispy server."));
|
|
1129
|
-
process.exit(1);
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
console.log(green("✓ alive"));
|
|
1133
|
-
|
|
1134
|
-
const wispyDir = join(homedir(), ".wispy");
|
|
1135
|
-
await mkdir(wispyDir, { recursive: true });
|
|
1136
|
-
await writeFile(
|
|
1137
|
-
join(wispyDir, "remote.json"),
|
|
1138
|
-
JSON.stringify({ url, token, connectedAt: new Date().toISOString() }, null, 2),
|
|
1139
|
-
"utf8"
|
|
1140
|
-
);
|
|
1141
|
-
|
|
1142
|
-
console.log(green(`\n✅ Connected to ${url}`));
|
|
1143
|
-
console.log(dim(" Local wispy will now proxy to the remote server."));
|
|
1144
|
-
console.log(dim(" Run `wispy disconnect` to go back to local mode.\n"));
|
|
1145
|
-
process.exit(0);
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
// ── disconnect sub-command ────────────────────────────────────────────────────
|
|
1149
|
-
if (args[0] === "disconnect") {
|
|
1150
|
-
const { unlink } = await import("node:fs/promises");
|
|
1151
|
-
const { homedir } = await import("node:os");
|
|
1152
|
-
const { join } = await import("node:path");
|
|
1153
|
-
|
|
1154
|
-
const remotePath = join(homedir(), ".wispy", "remote.json");
|
|
1155
|
-
try {
|
|
1156
|
-
await unlink(remotePath);
|
|
1157
|
-
console.log("\n✅ Disconnected. Wispy is back in local mode.\n");
|
|
1158
|
-
} catch {
|
|
1159
|
-
console.log("\n🌿 Already in local mode.\n");
|
|
1160
|
-
}
|
|
1161
|
-
process.exit(0);
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
// ── sync sub-command ──────────────────────────────────────────────────────────
|
|
1165
|
-
if (args[0] === "sync") {
|
|
1166
|
-
const { SyncManager } = await import(
|
|
1167
|
-
path.join(__dirname, "..", "core", "sync.mjs")
|
|
1168
|
-
);
|
|
1169
|
-
|
|
1170
|
-
const sub = args[1]; // push | pull | status | auto | (undefined = bidirectional)
|
|
1171
|
-
|
|
1172
|
-
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
1173
|
-
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
1174
|
-
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
1175
|
-
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
1176
|
-
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
1177
|
-
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
1178
|
-
|
|
1179
|
-
// Parse flags
|
|
1180
|
-
const strategyIdx = args.indexOf("--strategy");
|
|
1181
|
-
const strategy = strategyIdx !== -1 ? args[strategyIdx + 1] : null;
|
|
1182
|
-
const memoryOnly = args.includes("--memory-only");
|
|
1183
|
-
const sessionsOnly = args.includes("--sessions-only");
|
|
1184
|
-
const remoteUrlIdx = args.indexOf("--remote");
|
|
1185
|
-
const remoteUrlArg = remoteUrlIdx !== -1 ? args[remoteUrlIdx + 1] : null;
|
|
1186
|
-
const tokenIdx = args.indexOf("--token");
|
|
1187
|
-
const tokenArg = tokenIdx !== -1 ? args[tokenIdx + 1] : null;
|
|
1188
|
-
|
|
1189
|
-
// Load config (or use overrides)
|
|
1190
|
-
const cfg = await SyncManager.loadConfig();
|
|
1191
|
-
const remoteUrl = remoteUrlArg ?? cfg.remoteUrl;
|
|
1192
|
-
const token = tokenArg ?? cfg.token;
|
|
1193
|
-
|
|
1194
|
-
// ── auto enable/disable ──
|
|
1195
|
-
if (sub === "auto") {
|
|
1196
|
-
const enable = !args.includes("--off");
|
|
1197
|
-
const disable = args.includes("--off");
|
|
1198
|
-
if (disable) {
|
|
1199
|
-
await SyncManager.disableAuto();
|
|
1200
|
-
console.log(`\n${yellow("⏸")} Auto-sync ${bold("disabled")}.\n`);
|
|
1201
|
-
} else if (!remoteUrl) {
|
|
1202
|
-
console.log(`\n${red("✗")} No remote URL configured.`);
|
|
1203
|
-
console.log(dim(` Use: wispy sync auto --remote https://vps.com:18790 --token <token>\n`));
|
|
1204
|
-
process.exit(1);
|
|
1205
|
-
} else {
|
|
1206
|
-
await SyncManager.enableAuto(remoteUrl, token ?? "");
|
|
1207
|
-
console.log(`\n${green("✓")} Auto-sync ${bold("enabled")}.`);
|
|
1208
|
-
console.log(` Remote: ${cyan(remoteUrl)}`);
|
|
1209
|
-
console.log(dim(" Sessions and memory will be synced automatically.\n"));
|
|
1210
|
-
}
|
|
1211
|
-
process.exit(0);
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
// ── status ──
|
|
1215
|
-
if (sub === "status") {
|
|
1216
|
-
if (!remoteUrl) {
|
|
1217
|
-
console.log(`\n${red("✗")} No remote configured. Set via sync.json or --remote flag.\n`);
|
|
1218
|
-
process.exit(1);
|
|
1219
|
-
}
|
|
1220
|
-
console.log(`\n🔄 ${bold("Sync Status")}\n`);
|
|
1221
|
-
console.log(` Remote: ${cyan(remoteUrl)}`);
|
|
1222
|
-
const mgr = new SyncManager({ remoteUrl, token, strategy: strategy ?? "newer-wins" });
|
|
1223
|
-
const s = await mgr.status(remoteUrl, token);
|
|
1224
|
-
console.log(` Connection: ${s.reachable ? green("✅ reachable") : red("✗ unreachable")}\n`);
|
|
1225
|
-
|
|
1226
|
-
const fmt = (label, info) => {
|
|
1227
|
-
const parts = [];
|
|
1228
|
-
if (info.localOnly > 0) parts.push(`${yellow(info.localOnly + " local only")}`);
|
|
1229
|
-
if (info.remoteOnly > 0) parts.push(`${cyan(info.remoteOnly + " remote only")}`);
|
|
1230
|
-
if (info.inSync > 0) parts.push(`${green(info.inSync + " in sync")}`);
|
|
1231
|
-
console.log(` ${bold(label.padEnd(14))} ${parts.join(", ") || dim("(empty)")}`);
|
|
1232
|
-
};
|
|
1233
|
-
fmt("Memory:", s.memory);
|
|
1234
|
-
fmt("Sessions:", s.sessions);
|
|
1235
|
-
fmt("Cron:", s.cron);
|
|
1236
|
-
fmt("Workstreams:",s.workstreams);
|
|
1237
|
-
fmt("Permissions:",s.permissions);
|
|
1238
|
-
|
|
1239
|
-
const needPull = (s.memory.remoteOnly + s.sessions.remoteOnly + s.cron.remoteOnly + s.workstreams.remoteOnly + s.permissions.remoteOnly);
|
|
1240
|
-
const needPush = (s.memory.localOnly + s.sessions.localOnly + s.cron.localOnly + s.workstreams.localOnly + s.permissions.localOnly);
|
|
1241
|
-
if (needPull > 0 || needPush > 0) {
|
|
1242
|
-
console.log(`\n Action needed: ${needPull > 0 ? `pull ${yellow(needPull)} file(s)` : ""} ${needPush > 0 ? `push ${yellow(needPush)} file(s)` : ""}`.trimEnd());
|
|
1243
|
-
console.log(dim(" Run 'wispy sync' to synchronize."));
|
|
1244
|
-
} else {
|
|
1245
|
-
console.log(`\n ${green("✓")} Everything in sync!`);
|
|
1246
|
-
}
|
|
1247
|
-
console.log("");
|
|
1248
|
-
process.exit(0);
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
// For push/pull/sync we need a remote URL
|
|
1252
|
-
if (!remoteUrl) {
|
|
1253
|
-
console.log(`\n${red("✗")} No remote configured.`);
|
|
1254
|
-
console.log(dim(" Set remoteUrl in ~/.wispy/sync.json or use --remote <url>\n"));
|
|
1255
|
-
process.exit(1);
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
const opts = { strategy: strategy ?? cfg.strategy ?? "newer-wins", memoryOnly, sessionsOnly };
|
|
1259
|
-
const mgr = new SyncManager({ remoteUrl, token, ...opts });
|
|
1260
|
-
|
|
1261
|
-
if (sub === "push") {
|
|
1262
|
-
console.log(`\n📤 ${bold("Pushing")} to ${cyan(remoteUrl)}...`);
|
|
1263
|
-
const result = await mgr.push(remoteUrl, token, opts);
|
|
1264
|
-
console.log(` Pushed: ${green(result.pushed)}`);
|
|
1265
|
-
console.log(` Skipped: ${dim(result.skipped)}`);
|
|
1266
|
-
if (result.errors.length) result.errors.forEach(e => console.log(` ${red("✗")} ${e}`));
|
|
1267
|
-
console.log(`\n${result.errors.length === 0 ? green("✓ Done") : yellow("⚠ Done with errors")}.\n`);
|
|
1268
|
-
|
|
1269
|
-
} else if (sub === "pull") {
|
|
1270
|
-
console.log(`\n📥 ${bold("Pulling")} from ${cyan(remoteUrl)}...`);
|
|
1271
|
-
const result = await mgr.pull(remoteUrl, token, opts);
|
|
1272
|
-
console.log(` Pulled: ${green(result.pulled)}`);
|
|
1273
|
-
console.log(` Skipped: ${dim(result.skipped)}`);
|
|
1274
|
-
if (result.conflicts) console.log(` Conflicts: ${yellow(result.conflicts)} (saved as .conflict-* files)`);
|
|
1275
|
-
if (result.errors.length) result.errors.forEach(e => console.log(` ${red("✗")} ${e}`));
|
|
1276
|
-
console.log(`\n${result.errors.length === 0 ? green("✓ Done") : yellow("⚠ Done with errors")}.\n`);
|
|
1277
|
-
|
|
1278
|
-
} else {
|
|
1279
|
-
// Bidirectional sync (default)
|
|
1280
|
-
console.log(`\n🔄 ${bold("Syncing")} with ${cyan(remoteUrl)}...`);
|
|
1281
|
-
const result = await mgr.sync(remoteUrl, token, opts);
|
|
1282
|
-
console.log(` Pushed: ${green(result.pushed)}`);
|
|
1283
|
-
console.log(` Pulled: ${green(result.pulled)}`);
|
|
1284
|
-
console.log(` Skipped: ${dim(result.skipped)}`);
|
|
1285
|
-
if (result.conflicts) console.log(` Conflicts: ${yellow(result.conflicts)} (saved as .conflict-* files)`);
|
|
1286
|
-
if (result.errors.length) result.errors.forEach(e => console.log(` ${red("✗")} ${e}`));
|
|
1287
|
-
console.log(`\n${result.errors.length === 0 ? green("✓ Done") : yellow("⚠ Done with errors")}.\n`);
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
process.exit(0);
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
// ── deploy sub-command ────────────────────────────────────────────────────────
|
|
1294
|
-
if (args[0] === "deploy") {
|
|
1295
|
-
const { DeployManager } = await import(
|
|
1296
|
-
path.join(__dirname, "..", "core", "deploy.mjs")
|
|
1297
|
-
);
|
|
1298
|
-
|
|
1299
|
-
const sub = args[1];
|
|
1300
|
-
const dm = new DeployManager();
|
|
1301
|
-
|
|
1302
|
-
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
1303
|
-
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
1304
|
-
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
1305
|
-
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
1306
|
-
|
|
1307
|
-
if (sub === "dockerfile") {
|
|
1308
|
-
process.stdout.write(dm.generateDockerfile());
|
|
1309
|
-
process.exit(0);
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
if (sub === "compose") {
|
|
1313
|
-
process.stdout.write(dm.generateDockerCompose());
|
|
1314
|
-
process.exit(0);
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
if (sub === "systemd") {
|
|
1318
|
-
process.stdout.write(dm.generateSystemd());
|
|
1319
|
-
process.exit(0);
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
if (sub === "railway") {
|
|
1323
|
-
process.stdout.write(dm.generateRailwayConfig() + "\n");
|
|
1324
|
-
process.exit(0);
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
if (sub === "fly") {
|
|
1328
|
-
process.stdout.write(dm.generateFlyConfig());
|
|
1329
|
-
process.exit(0);
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
if (sub === "render") {
|
|
1333
|
-
process.stdout.write(dm.generateRenderConfig());
|
|
1334
|
-
process.exit(0);
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
if (sub === "init") {
|
|
1338
|
-
console.log("\n🌿 Initializing wispy deploy configs...\n");
|
|
1339
|
-
const created = await dm.init(process.cwd());
|
|
1340
|
-
for (const f of created) {
|
|
1341
|
-
console.log(` ${f.includes("skipped") ? "⏭️ " : "✅"} ${f}`);
|
|
1342
|
-
}
|
|
1343
|
-
console.log(dim("\n Next steps:"));
|
|
1344
|
-
console.log(dim(" 1. Copy .env.example → .env and fill in your API keys"));
|
|
1345
|
-
console.log(dim(" 2. docker-compose up -d — for Docker"));
|
|
1346
|
-
console.log(dim(" 3. wispy deploy vps user@host — for raw VPS (no Docker)\n"));
|
|
1347
|
-
process.exit(0);
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
if (sub === "vps" && args[2]) {
|
|
1351
|
-
const target = args[2];
|
|
1352
|
-
const envIdx = args.indexOf("--env");
|
|
1353
|
-
const envFile = envIdx !== -1 ? args[envIdx + 1] : null;
|
|
1354
|
-
|
|
1355
|
-
try {
|
|
1356
|
-
await dm.deployVPS({ target, envFile });
|
|
1357
|
-
} catch (err) {
|
|
1358
|
-
console.error(`\n❌ Deploy failed: ${err.message}`);
|
|
1359
|
-
process.exit(1);
|
|
1360
|
-
}
|
|
1361
|
-
process.exit(0);
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
if (sub === "status" && args[2]) {
|
|
1365
|
-
const url = args[2];
|
|
1366
|
-
process.stdout.write(`\n📡 Checking ${cyan(url)}... `);
|
|
1367
|
-
const status = await dm.checkRemote(url);
|
|
1368
|
-
if (status.alive) {
|
|
1369
|
-
console.log(green("✓ alive"));
|
|
1370
|
-
if (status.version) console.log(` Version: ${status.version}`);
|
|
1371
|
-
if (status.uptime != null) console.log(` Uptime: ${Math.floor(status.uptime)}s`);
|
|
1372
|
-
if (status.latency) console.log(` Latency: ${status.latency}ms`);
|
|
1373
|
-
} else {
|
|
1374
|
-
console.log(`\x1b[31m✗ unreachable\x1b[0m`);
|
|
1375
|
-
if (status.error) console.log(` Error: ${dim(status.error)}`);
|
|
1376
|
-
}
|
|
1377
|
-
console.log("");
|
|
1378
|
-
process.exit(0);
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
if (sub === "modal") {
|
|
1382
|
-
process.stdout.write(dm.generateModalConfig());
|
|
1383
|
-
console.log(dim("\n# Save as modal_app.py, then: pip install modal && modal run modal_app.py"));
|
|
1384
|
-
process.exit(0);
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
if (sub === "daytona") {
|
|
1388
|
-
const { mkdir: mkdirDaytona, writeFile: writeDaytona } = await import("node:fs/promises");
|
|
1389
|
-
const daytonaDir = path.join(process.cwd(), ".daytona");
|
|
1390
|
-
await mkdirDaytona(daytonaDir, { recursive: true });
|
|
1391
|
-
const daytonaConfigPath = path.join(daytonaDir, "config.yaml");
|
|
1392
|
-
let exists = false;
|
|
1393
|
-
try { await (await import("node:fs/promises")).access(daytonaConfigPath); exists = true; } catch {}
|
|
1394
|
-
if (!exists) {
|
|
1395
|
-
await writeDaytona(daytonaConfigPath, dm.generateDaytonaConfig(), "utf8");
|
|
1396
|
-
console.log(green(`✅ Created .daytona/config.yaml`));
|
|
1397
|
-
} else {
|
|
1398
|
-
console.log(yellow(`⏭️ .daytona/config.yaml already exists (skipped)`));
|
|
1399
|
-
}
|
|
1400
|
-
console.log(dim(" Push to your repo and connect via Daytona workspace."));
|
|
1401
|
-
process.exit(0);
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
// Help
|
|
1405
|
-
console.log(`
|
|
1406
|
-
🚀 ${bold("Wispy Deploy Commands")}
|
|
1407
|
-
|
|
1408
|
-
${cyan("Config generators:")}
|
|
1409
|
-
wispy deploy init — generate Dockerfile + compose + .env.example
|
|
1410
|
-
wispy deploy dockerfile — print Dockerfile to stdout
|
|
1411
|
-
wispy deploy compose — print docker-compose.yml
|
|
1412
|
-
wispy deploy systemd — print systemd unit file
|
|
1413
|
-
wispy deploy railway — print railway.json
|
|
1414
|
-
wispy deploy fly — print fly.toml
|
|
1415
|
-
wispy deploy render — print render.yaml
|
|
1416
|
-
wispy deploy modal — generate Modal serverless config (modal_app.py)
|
|
1417
|
-
wispy deploy daytona — generate Daytona workspace config (.daytona/config.yaml)
|
|
1418
|
-
|
|
1419
|
-
${cyan("Deploy:")}
|
|
1420
|
-
wispy deploy vps user@host — SSH deploy: install + systemd setup
|
|
1421
|
-
wispy deploy vps user@host --env .env — include env file
|
|
1422
|
-
|
|
1423
|
-
${cyan("Status:")}
|
|
1424
|
-
wispy deploy status https://my.vps — check if remote wispy is alive
|
|
1425
|
-
|
|
1426
|
-
${cyan("Remote connect:")}
|
|
1427
|
-
wispy connect https://my.vps:18790 --token <token> — use remote server
|
|
1428
|
-
wispy disconnect — go back to local
|
|
1429
|
-
wispy status — show current mode
|
|
1430
|
-
`);
|
|
1431
|
-
process.exit(0);
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
// ── migrate sub-command ───────────────────────────────────────────────────────
|
|
1435
|
-
if (args[0] === "migrate") {
|
|
1436
|
-
const { OpenClawMigrator, WISPY_DIR } = await import(
|
|
1437
|
-
path.join(__dirname, "..", "core", "index.mjs")
|
|
1438
|
-
);
|
|
1439
|
-
|
|
1440
|
-
const sub = args[1]; // "openclaw" (only supported source for now)
|
|
1441
|
-
const dryRun = args.includes("--dry-run");
|
|
1442
|
-
const memoryOnly = args.includes("--memory-only");
|
|
1443
|
-
|
|
1444
|
-
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
1445
|
-
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
1446
|
-
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
1447
|
-
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
1448
|
-
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
1449
|
-
|
|
1450
|
-
if (!sub || sub === "openclaw") {
|
|
1451
|
-
console.log(`\n🌿 ${bold("Wispy Migration Tool")} ${dim("— from OpenClaw")}\n`);
|
|
1452
|
-
if (dryRun) console.log(yellow(" [DRY RUN — no files will be written]\n"));
|
|
1453
|
-
if (memoryOnly) console.log(dim(" [memory-only mode]\n"));
|
|
1454
|
-
|
|
1455
|
-
const migrator = new OpenClawMigrator(WISPY_DIR);
|
|
1456
|
-
const result = await migrator.migrate({ dryRun, memoryOnly });
|
|
1457
|
-
|
|
1458
|
-
console.log(migrator.formatReport());
|
|
1459
|
-
|
|
1460
|
-
if (result.success) {
|
|
1461
|
-
if (dryRun) {
|
|
1462
|
-
console.log(dim("\nRun without --dry-run to apply changes.\n"));
|
|
1463
|
-
} else {
|
|
1464
|
-
const counts = [
|
|
1465
|
-
result.report.memories.length > 0 && `${result.report.memories.length} memory files`,
|
|
1466
|
-
result.report.userModel.length > 0 && `${result.report.userModel.length} profile files`,
|
|
1467
|
-
result.report.cronJobs.length > 0 && `${result.report.cronJobs.length} cron jobs`,
|
|
1468
|
-
result.report.channels.length > 0 && `${result.report.channels.length} channels`,
|
|
1469
|
-
].filter(Boolean);
|
|
1470
|
-
|
|
1471
|
-
if (counts.length > 0) {
|
|
1472
|
-
console.log(`\n${green("✅ Migration complete!")} Imported: ${counts.join(", ")}`);
|
|
1473
|
-
} else {
|
|
1474
|
-
console.log(`\n${dim("Nothing new to import (already migrated or empty).")}`);
|
|
1475
|
-
}
|
|
1476
|
-
console.log(dim("\nTip: run `wispy` to start chatting with your imported context.\n"));
|
|
1477
|
-
}
|
|
1478
|
-
} else {
|
|
1479
|
-
console.error(`\n${red("❌ Migration failed:")} ${result.error}\n`);
|
|
1480
|
-
process.exit(1);
|
|
1481
|
-
}
|
|
1482
|
-
} else {
|
|
1483
|
-
console.log(`
|
|
1484
|
-
🔀 ${bold("Wispy Migrate Commands")}
|
|
1485
|
-
|
|
1486
|
-
wispy migrate openclaw — import from OpenClaw (~/.openclaw/)
|
|
1487
|
-
wispy migrate openclaw --dry-run — preview what would be imported
|
|
1488
|
-
wispy migrate openclaw --memory-only — only import memories
|
|
1489
|
-
`);
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
process.exit(0);
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
// ── cron sub-command ──────────────────────────────────────────────────────────
|
|
1496
|
-
if (args[0] === "cron") {
|
|
1497
|
-
const { WispyEngine, CronManager, WISPY_DIR } = await import(
|
|
1498
|
-
path.join(__dirname, "..", "core", "index.mjs")
|
|
1499
|
-
);
|
|
1500
|
-
const { createInterface } = await import("node:readline");
|
|
1501
|
-
|
|
1502
|
-
const sub = args[1];
|
|
1503
|
-
|
|
1504
|
-
// Init engine for cron commands that need it
|
|
1505
|
-
const engine = new WispyEngine();
|
|
1506
|
-
await engine.init({ skipMcp: true });
|
|
1507
|
-
const cron = new CronManager(WISPY_DIR, engine);
|
|
1508
|
-
await cron.init();
|
|
1509
|
-
|
|
1510
|
-
if (!sub || sub === "list") {
|
|
1511
|
-
const jobs = cron.list();
|
|
1512
|
-
if (jobs.length === 0) {
|
|
1513
|
-
console.log("No cron jobs configured. Use: wispy cron add");
|
|
1514
|
-
} else {
|
|
1515
|
-
console.log(`\n🕐 Cron Jobs (${jobs.length}):\n`);
|
|
1516
|
-
for (const j of jobs) {
|
|
1517
|
-
const status = j.enabled ? "✅" : "⏸️ ";
|
|
1518
|
-
const schedStr = j.schedule.kind === "cron" ? j.schedule.expr
|
|
1519
|
-
: j.schedule.kind === "every" ? `every ${j.schedule.ms / 60000}min`
|
|
1520
|
-
: `at ${j.schedule.time}`;
|
|
1521
|
-
console.log(` ${status} ${j.id.slice(0, 8)} ${j.name.padEnd(20)} ${schedStr}`);
|
|
1522
|
-
console.log(` Task: ${j.task.slice(0, 60)}${j.task.length > 60 ? "..." : ""}`);
|
|
1523
|
-
if (j.channel) console.log(` Channel: ${j.channel}`);
|
|
1524
|
-
if (j.nextRun) console.log(` Next run: ${new Date(j.nextRun).toLocaleString()}`);
|
|
1525
|
-
console.log("");
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
process.exit(0);
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
if (sub === "add") {
|
|
1532
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1533
|
-
const ask = (q) => new Promise(r => rl.question(q, r));
|
|
1534
|
-
|
|
1535
|
-
console.log("\n🕐 Add Cron Job\n");
|
|
1536
|
-
const name = await ask(" Job name: ");
|
|
1537
|
-
const task = await ask(" Task (what to do): ");
|
|
1538
|
-
const schedKind = await ask(" Schedule type (cron/every/at) [cron]: ") || "cron";
|
|
1539
|
-
let schedule = { kind: schedKind };
|
|
1540
|
-
|
|
1541
|
-
if (schedKind === "cron") {
|
|
1542
|
-
const expr = await ask(" Cron expression (e.g. '0 9 * * *'): ");
|
|
1543
|
-
const tz = await ask(" Timezone [Asia/Seoul]: ") || "Asia/Seoul";
|
|
1544
|
-
schedule = { kind: "cron", expr: expr.trim(), tz: tz.trim() };
|
|
1545
|
-
} else if (schedKind === "every") {
|
|
1546
|
-
const mins = await ask(" Interval in minutes: ");
|
|
1547
|
-
schedule = { kind: "every", ms: parseFloat(mins) * 60_000 };
|
|
1548
|
-
} else if (schedKind === "at") {
|
|
1549
|
-
const time = await ask(" Run at (ISO datetime, e.g. 2025-01-01T09:00:00): ");
|
|
1550
|
-
schedule = { kind: "at", time: time.trim() };
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
const channel = await ask(" Channel (e.g. telegram:12345, or leave empty): ");
|
|
1554
|
-
rl.close();
|
|
1555
|
-
|
|
1556
|
-
const job = await cron.add({
|
|
1557
|
-
name: name.trim(),
|
|
1558
|
-
task: task.trim(),
|
|
1559
|
-
schedule,
|
|
1560
|
-
channel: channel.trim() || null,
|
|
1561
|
-
enabled: true,
|
|
1562
|
-
});
|
|
1563
|
-
|
|
1564
|
-
console.log(`\n✅ Job created: ${job.id}`);
|
|
1565
|
-
console.log(` Next run: ${job.nextRun ? new Date(job.nextRun).toLocaleString() : "N/A"}`);
|
|
1566
|
-
process.exit(0);
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
if (sub === "remove" && args[2]) {
|
|
1570
|
-
const id = args[2];
|
|
1571
|
-
// Support partial ID match
|
|
1572
|
-
const all = cron.list();
|
|
1573
|
-
const match = all.find(j => j.id === id || j.id.startsWith(id));
|
|
1574
|
-
if (!match) {
|
|
1575
|
-
console.error(`Job not found: ${id}`);
|
|
1576
|
-
process.exit(1);
|
|
1577
|
-
}
|
|
1578
|
-
await cron.remove(match.id);
|
|
1579
|
-
console.log(`✅ Removed job: ${match.name} (${match.id})`);
|
|
1580
|
-
process.exit(0);
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
if (sub === "run" && args[2]) {
|
|
1584
|
-
const id = args[2];
|
|
1585
|
-
const all = cron.list();
|
|
1586
|
-
const match = all.find(j => j.id === id || j.id.startsWith(id));
|
|
1587
|
-
if (!match) {
|
|
1588
|
-
console.error(`Job not found: ${id}`);
|
|
1589
|
-
process.exit(1);
|
|
1590
|
-
}
|
|
1591
|
-
console.log(`🌿 Running job: ${match.name}...`);
|
|
1592
|
-
const result = await cron.runNow(match.id);
|
|
1593
|
-
console.log(result.output ?? result.error);
|
|
1594
|
-
process.exit(0);
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
if (sub === "history" && args[2]) {
|
|
1598
|
-
const id = args[2];
|
|
1599
|
-
const all = cron.list();
|
|
1600
|
-
const match = all.find(j => j.id === id || j.id.startsWith(id));
|
|
1601
|
-
if (!match) {
|
|
1602
|
-
console.error(`Job not found: ${id}`);
|
|
1603
|
-
process.exit(1);
|
|
1604
|
-
}
|
|
1605
|
-
const history = await cron.getHistory(match.id);
|
|
1606
|
-
console.log(`\n📋 History for "${match.name}" (last ${history.length} runs):\n`);
|
|
1607
|
-
for (const h of history) {
|
|
1608
|
-
const icon = h.status === "success" ? "✅" : "❌";
|
|
1609
|
-
console.log(` ${icon} ${new Date(h.startedAt).toLocaleString()}`);
|
|
1610
|
-
console.log(` ${h.output?.slice(0, 100) ?? h.error ?? ""}`);
|
|
1611
|
-
console.log("");
|
|
1612
|
-
}
|
|
1613
|
-
if (history.length === 0) console.log(" No runs yet.");
|
|
1614
|
-
process.exit(0);
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
if (sub === "start") {
|
|
1618
|
-
console.log("🌿 Starting cron scheduler... (Ctrl+C to stop)\n");
|
|
1619
|
-
cron.start();
|
|
1620
|
-
process.on("SIGINT", () => { cron.stop(); process.exit(0); });
|
|
1621
|
-
process.on("SIGTERM", () => { cron.stop(); process.exit(0); });
|
|
1622
|
-
setInterval(() => {}, 60_000);
|
|
1623
|
-
await new Promise(() => {});
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
console.log(`
|
|
1627
|
-
🕐 Wispy Cron Commands:
|
|
1628
|
-
|
|
1629
|
-
wispy cron list — list all jobs
|
|
1630
|
-
wispy cron add — interactive job creation
|
|
1631
|
-
wispy cron remove <id> — delete a job
|
|
1632
|
-
wispy cron run <id> — trigger immediately
|
|
1633
|
-
wispy cron history <id> — show past runs
|
|
1634
|
-
wispy cron start — start scheduler (foreground)
|
|
1635
|
-
`);
|
|
1636
|
-
process.exit(0);
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
// ── audit / log sub-command ───────────────────────────────────────────────────
|
|
1640
|
-
if (args[0] === "audit" || args[0] === "log") {
|
|
1641
|
-
const { AuditLog, WISPY_DIR } = await import(
|
|
1642
|
-
path.join(__dirname, "..", "core", "index.mjs")
|
|
1643
|
-
);
|
|
1644
|
-
const { writeFile } = await import("node:fs/promises");
|
|
1645
|
-
|
|
1646
|
-
const audit = new AuditLog(WISPY_DIR);
|
|
1647
|
-
const sub = args[1];
|
|
1648
|
-
|
|
1649
|
-
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
1650
|
-
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
1651
|
-
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
1652
|
-
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
1653
|
-
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
1654
|
-
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
1655
|
-
|
|
1656
|
-
function formatEvent(evt) {
|
|
1657
|
-
const ts = new Date(evt.timestamp).toLocaleTimeString();
|
|
1658
|
-
const icons = {
|
|
1659
|
-
tool_call: "🔧",
|
|
1660
|
-
tool_result: "✅",
|
|
1661
|
-
approval_requested: "⚠️ ",
|
|
1662
|
-
approval_granted: "✅",
|
|
1663
|
-
approval_denied: "❌",
|
|
1664
|
-
message_sent: "🌿",
|
|
1665
|
-
message_received: "👤",
|
|
1666
|
-
error: "🚨",
|
|
1667
|
-
subagent_spawned: "🤖",
|
|
1668
|
-
subagent_completed: "🎉",
|
|
1669
|
-
cron_executed: "🕐",
|
|
1670
|
-
};
|
|
1671
|
-
const icon = icons[evt.type] ?? "•";
|
|
1672
|
-
let detail = "";
|
|
1673
|
-
if (evt.tool) detail += ` ${cyan(evt.tool)}`;
|
|
1674
|
-
if (evt.content) detail += ` ${dim(evt.content.slice(0, 60))}`;
|
|
1675
|
-
if (evt.message) detail += ` ${dim(evt.message.slice(0, 60))}`;
|
|
1676
|
-
if (evt.label) detail += ` ${dim(evt.label)}`;
|
|
1677
|
-
const sid = evt.sessionId ? dim(` [${evt.sessionId.slice(-8)}]`) : "";
|
|
1678
|
-
return ` ${dim(ts)} ${icon} ${evt.type}${detail}${sid}`;
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
if (sub === "replay" && args[2]) {
|
|
1682
|
-
const sessionId = args[2];
|
|
1683
|
-
const steps = await audit.getReplayTrace(sessionId);
|
|
1684
|
-
if (steps.length === 0) {
|
|
1685
|
-
console.log(dim(`No events found for session: ${sessionId}`));
|
|
1686
|
-
} else {
|
|
1687
|
-
console.log(`\n${bold("🎬 Replay:")} ${cyan(sessionId)}\n`);
|
|
1688
|
-
for (const step of steps) {
|
|
1689
|
-
const ts = new Date(step.timestamp).toLocaleTimeString();
|
|
1690
|
-
const icons = {
|
|
1691
|
-
user_message: "👤",
|
|
1692
|
-
assistant_message: "🌿",
|
|
1693
|
-
tool_call: "🔧",
|
|
1694
|
-
tool_result: "✅",
|
|
1695
|
-
approval_requested: "⚠️ ",
|
|
1696
|
-
approval_granted: "✅",
|
|
1697
|
-
approval_denied: "❌",
|
|
1698
|
-
subagent_spawned: "🤖",
|
|
1699
|
-
subagent_completed: "🎉",
|
|
1700
|
-
};
|
|
1701
|
-
const icon = icons[step.type] ?? "•";
|
|
1702
|
-
let detail = "";
|
|
1703
|
-
if (step.content) detail = dim(step.content.slice(0, 100));
|
|
1704
|
-
if (step.tool) detail = `${cyan(step.tool)} ${dim(JSON.stringify(step.args ?? {}).slice(0, 60))}`;
|
|
1705
|
-
console.log(` ${bold(`Step ${step.step}`)} ${dim(ts)} ${icon} ${detail}`);
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
process.exit(0);
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
if (sub === "export") {
|
|
1712
|
-
const format = args.includes("--format") ? args[args.indexOf("--format") + 1] : "json";
|
|
1713
|
-
const outputIdx = args.indexOf("--output");
|
|
1714
|
-
const output = outputIdx !== -1 ? args[outputIdx + 1] : null;
|
|
1715
|
-
|
|
1716
|
-
let content;
|
|
1717
|
-
if (format === "md" || format === "markdown") {
|
|
1718
|
-
content = await audit.exportMarkdown();
|
|
1719
|
-
} else {
|
|
1720
|
-
content = await audit.exportJson();
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
if (output) {
|
|
1724
|
-
await writeFile(output, content, "utf8");
|
|
1725
|
-
console.log(green(`✅ Exported to ${output}`));
|
|
1726
|
-
} else {
|
|
1727
|
-
console.log(content);
|
|
1728
|
-
}
|
|
1729
|
-
process.exit(0);
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
// Build filter from flags
|
|
1733
|
-
const filter = {};
|
|
1734
|
-
const sessionIdx = args.indexOf("--session");
|
|
1735
|
-
if (sessionIdx !== -1) filter.sessionId = args[sessionIdx + 1];
|
|
1736
|
-
const toolIdx = args.indexOf("--tool");
|
|
1737
|
-
if (toolIdx !== -1) filter.tool = args[toolIdx + 1];
|
|
1738
|
-
if (args.includes("--today")) filter.date = new Date().toISOString().slice(0, 10);
|
|
1739
|
-
const limitIdx = args.indexOf("--limit");
|
|
1740
|
-
filter.limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1]) : 30;
|
|
1741
|
-
|
|
1742
|
-
const events = await audit.search(filter);
|
|
1743
|
-
|
|
1744
|
-
if (events.length === 0) {
|
|
1745
|
-
console.log(dim("No audit events found."));
|
|
1746
|
-
} else {
|
|
1747
|
-
console.log(`\n${bold("📋 Audit Log")} ${dim(`(${events.length} events)`)}\n`);
|
|
1748
|
-
for (const evt of events) {
|
|
1749
|
-
console.log(formatEvent(evt));
|
|
1750
|
-
}
|
|
1751
|
-
console.log("");
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
if (!sub) {
|
|
1755
|
-
console.log(dim(`
|
|
1756
|
-
wispy audit — show recent events
|
|
1757
|
-
wispy audit --session <id> — filter by session
|
|
1758
|
-
wispy audit --today — today's events
|
|
1759
|
-
wispy audit --tool <name> — filter by tool
|
|
1760
|
-
wispy audit replay <sessionId> — step-by-step replay
|
|
1761
|
-
wispy audit export --format md — export as markdown
|
|
1762
|
-
wispy audit export --output file.md — save to file
|
|
1763
|
-
`));
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
process.exit(0);
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
// ── server sub-command ────────────────────────────────────────────────────────
|
|
1770
|
-
if (args[0] === "server" || args.includes("--server")) {
|
|
1771
|
-
const { WispyEngine, WispyServer } = await import(
|
|
1772
|
-
path.join(__dirname, "..", "core", "index.mjs")
|
|
1773
|
-
);
|
|
1774
|
-
|
|
1775
|
-
const portIdx = args.indexOf("--port");
|
|
1776
|
-
const hostIdx = args.indexOf("--host");
|
|
1777
|
-
const port = portIdx !== -1 ? parseInt(args[portIdx + 1]) : undefined;
|
|
1778
|
-
const host = hostIdx !== -1 ? args[hostIdx + 1] : undefined;
|
|
1779
|
-
|
|
1780
|
-
console.log("🌿 Starting Wispy API server...");
|
|
1781
|
-
|
|
1782
|
-
const engine = new WispyEngine();
|
|
1783
|
-
const initResult = await engine.init();
|
|
1784
|
-
if (!initResult) {
|
|
1785
|
-
console.error("❌ No AI provider configured. Run `wispy` first to set up.");
|
|
1786
|
-
process.exit(1);
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
const server = new WispyServer(engine, { port, host });
|
|
1790
|
-
await server.start();
|
|
1791
|
-
|
|
1792
|
-
process.on("SIGINT", () => { server.stop(); process.exit(0); });
|
|
1793
|
-
process.on("SIGTERM", () => { server.stop(); process.exit(0); });
|
|
1794
|
-
|
|
1795
|
-
// Keep alive
|
|
1796
|
-
setInterval(() => {}, 60_000);
|
|
1797
|
-
await new Promise(() => {});
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
// ── node sub-command ──────────────────────────────────────────────────────────
|
|
1801
|
-
if (args[0] === "node") {
|
|
1802
|
-
const { NodeManager, WISPY_DIR, CAPABILITIES } = await import(
|
|
1803
|
-
path.join(__dirname, "..", "core", "index.mjs")
|
|
1804
|
-
);
|
|
1805
|
-
const { createInterface } = await import("node:readline");
|
|
1806
|
-
|
|
1807
|
-
const sub = args[1];
|
|
1808
|
-
const nodes = new NodeManager(WISPY_DIR);
|
|
1809
|
-
|
|
1810
|
-
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
1811
|
-
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
1812
|
-
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
1813
|
-
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
1814
|
-
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
1815
|
-
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
1816
|
-
|
|
1817
|
-
if (sub === "pair") {
|
|
1818
|
-
const code = await nodes.generatePairCode();
|
|
1819
|
-
console.log(`\n🔗 Pairing Code: ${bold(green(code))}\n`);
|
|
1820
|
-
console.log(` This code expires in 1 hour.`);
|
|
1821
|
-
console.log(`\n On the remote machine, run:`);
|
|
1822
|
-
console.log(` ${cyan(`wispy node connect ${code} --url http://localhost:18790`)}\n`);
|
|
1823
|
-
process.exit(0);
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
if (sub === "connect" && args[2]) {
|
|
1827
|
-
const code = args[2];
|
|
1828
|
-
const urlIdx = args.indexOf("--url");
|
|
1829
|
-
const serverUrl = urlIdx !== -1 ? args[urlIdx + 1] : "http://localhost:18790";
|
|
1830
|
-
|
|
1831
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1832
|
-
const ask = (q) => new Promise(r => rl.question(q, r));
|
|
1833
|
-
|
|
1834
|
-
console.log(`\n🔗 Connecting to Wispy at ${serverUrl}\n`);
|
|
1835
|
-
const name = (await ask(" Node name (e.g. my-laptop): ")).trim() || `node-${Date.now().toString(36)}`;
|
|
1836
|
-
|
|
1837
|
-
console.log(`\nAvailable capabilities:`);
|
|
1838
|
-
const capList = Object.entries(CAPABILITIES);
|
|
1839
|
-
capList.forEach(([k, v], i) => console.log(` ${i + 1}. ${k.padEnd(15)} — ${dim(v)}`));
|
|
1840
|
-
const capInput = (await ask("\n Select capabilities (comma-separated numbers, e.g. 1,3): ")).trim();
|
|
1841
|
-
rl.close();
|
|
1842
|
-
|
|
1843
|
-
const selectedCaps = capInput
|
|
1844
|
-
? capInput.split(",").map(n => capList[parseInt(n.trim()) - 1]?.[0]).filter(Boolean)
|
|
1845
|
-
: [];
|
|
1846
|
-
|
|
1847
|
-
// Call server to confirm pair
|
|
1848
|
-
try {
|
|
1849
|
-
const tokenIdx = args.indexOf("--token");
|
|
1850
|
-
const token = tokenIdx !== -1 ? args[tokenIdx + 1] : "";
|
|
1851
|
-
const resp = await fetch(`${serverUrl}/api/nodes/pair`, {
|
|
1852
|
-
method: "POST",
|
|
1853
|
-
headers: { "Content-Type": "application/json", ...(token ? { "Authorization": `Bearer ${token}` } : {}) },
|
|
1854
|
-
body: JSON.stringify({ code, name, capabilities: selectedCaps, host: "localhost", port: 18791 }),
|
|
1855
|
-
});
|
|
1856
|
-
if (!resp.ok) {
|
|
1857
|
-
const err = await resp.json().catch(() => ({ error: resp.statusText }));
|
|
1858
|
-
console.error(red(`❌ Pairing failed: ${err.error ?? resp.statusText}`));
|
|
1859
|
-
process.exit(1);
|
|
1860
|
-
}
|
|
1861
|
-
const result = await resp.json();
|
|
1862
|
-
console.log(green(`\n✅ Paired! Node ID: ${result.id}`));
|
|
1863
|
-
} catch (err) {
|
|
1864
|
-
console.error(red(`❌ Connection failed: ${err.message}`));
|
|
1865
|
-
console.log(dim(" Make sure `wispy server` is running on the main machine."));
|
|
1866
|
-
process.exit(1);
|
|
1867
|
-
}
|
|
1868
|
-
process.exit(0);
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
if (sub === "list") {
|
|
1872
|
-
const nodeList = await nodes.list();
|
|
1873
|
-
if (nodeList.length === 0) {
|
|
1874
|
-
console.log(dim("No nodes registered. Use: wispy node pair"));
|
|
1875
|
-
} else {
|
|
1876
|
-
console.log(`\n${bold("🌐 Registered Nodes:")}\n`);
|
|
1877
|
-
for (const n of nodeList) {
|
|
1878
|
-
const caps = n.capabilities.join(", ") || dim("none");
|
|
1879
|
-
const last = n.lastSeen ? dim(new Date(n.lastSeen).toLocaleString()) : dim("never");
|
|
1880
|
-
console.log(` ${green(n.id.slice(-12))} ${bold(n.name.padEnd(20))} ${n.host}:${n.port}`);
|
|
1881
|
-
console.log(` Caps: ${caps} Last seen: ${last}`);
|
|
1882
|
-
console.log("");
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
process.exit(0);
|
|
1886
|
-
}
|
|
1887
|
-
|
|
1888
|
-
if (sub === "status") {
|
|
1889
|
-
const nodeList = await nodes.list();
|
|
1890
|
-
if (nodeList.length === 0) {
|
|
1891
|
-
console.log(dim("No nodes registered."));
|
|
1892
|
-
process.exit(0);
|
|
1893
|
-
}
|
|
1894
|
-
console.log(`\n${bold("📡 Node Status:")}\n`);
|
|
1895
|
-
const results = await nodes.status();
|
|
1896
|
-
for (const r of results) {
|
|
1897
|
-
const statusIcon = r.alive ? green("●") : red("●");
|
|
1898
|
-
const latency = r.latency ? dim(` ${r.latency}ms`) : "";
|
|
1899
|
-
const err = r.error ? red(` (${r.error})`) : "";
|
|
1900
|
-
console.log(` ${statusIcon} ${r.name.padEnd(20)} ${r.host}:${r.port}${latency}${err}`);
|
|
1901
|
-
}
|
|
1902
|
-
console.log("");
|
|
1903
|
-
process.exit(0);
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
if (sub === "remove" && args[2]) {
|
|
1907
|
-
const id = args[2];
|
|
1908
|
-
const nodeList = await nodes.list();
|
|
1909
|
-
const match = nodeList.find(n => n.id === id || n.id.endsWith(id));
|
|
1910
|
-
if (!match) {
|
|
1911
|
-
console.error(red(`Node not found: ${id}`));
|
|
1912
|
-
process.exit(1);
|
|
1913
|
-
}
|
|
1914
|
-
await nodes.remove(match.id);
|
|
1915
|
-
console.log(green(`✅ Removed node: ${match.name} (${match.id})`));
|
|
1916
|
-
process.exit(0);
|
|
1917
|
-
}
|
|
1918
|
-
|
|
1919
|
-
// Help
|
|
1920
|
-
console.log(`
|
|
1921
|
-
🌐 Wispy Node Commands:
|
|
1922
|
-
|
|
1923
|
-
wispy node pair — generate pairing code
|
|
1924
|
-
wispy node connect <code> --url <url> — connect as a node
|
|
1925
|
-
wispy node list — show registered nodes
|
|
1926
|
-
wispy node status — ping all nodes
|
|
1927
|
-
wispy node remove <id> — unregister a node
|
|
1928
|
-
`);
|
|
1929
|
-
process.exit(0);
|
|
1930
|
-
}
|
|
1931
|
-
|
|
1932
|
-
// ── channel sub-command ───────────────────────────────────────────────────────
|
|
1933
|
-
if (args[0] === "channel") {
|
|
1934
|
-
const { channelSetup, channelList, channelTest } = await import(
|
|
1935
|
-
path.join(__dirname, "..", "lib", "channels", "index.mjs")
|
|
1936
|
-
);
|
|
1937
|
-
|
|
1938
|
-
const sub = args[1];
|
|
1939
|
-
const name = args[2];
|
|
1940
|
-
|
|
1941
|
-
if (sub === "setup" && name) {
|
|
1942
|
-
await channelSetup(name);
|
|
1943
|
-
} else if (sub === "list") {
|
|
1944
|
-
await channelList();
|
|
1945
|
-
} else if (sub === "test" && name) {
|
|
1946
|
-
await channelTest(name);
|
|
1947
|
-
} else {
|
|
1948
|
-
console.log(`
|
|
1949
|
-
🌿 Wispy Channel Commands:
|
|
1950
|
-
|
|
1951
|
-
wispy channel setup telegram — interactive Telegram bot setup
|
|
1952
|
-
wispy channel setup discord — interactive Discord bot setup
|
|
1953
|
-
wispy channel setup slack — interactive Slack bot setup
|
|
1954
|
-
wispy channel setup whatsapp — WhatsApp setup (requires: npm install whatsapp-web.js qrcode-terminal)
|
|
1955
|
-
wispy channel setup signal — Signal setup (requires: signal-cli)
|
|
1956
|
-
wispy channel setup email — Email setup (requires: npm install nodemailer imapflow)
|
|
1957
|
-
wispy channel list — show configured channels
|
|
1958
|
-
wispy channel test <name> — test a channel connection
|
|
1959
|
-
|
|
1960
|
-
wispy --serve — start all configured channel bots
|
|
1961
|
-
wispy --telegram — start Telegram bot only
|
|
1962
|
-
wispy --discord — start Discord bot only
|
|
1963
|
-
wispy --slack — start Slack bot only
|
|
1964
|
-
`);
|
|
1965
|
-
}
|
|
1966
|
-
process.exit(0);
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
// ── auth sub-command ──────────────────────────────────────────────────────────
|
|
1970
|
-
if (args[0] === "auth") {
|
|
1971
|
-
const { AuthManager } = await import(
|
|
1972
|
-
path.join(__dirname, "..", "core", "auth.mjs")
|
|
1973
|
-
);
|
|
1974
|
-
const { WISPY_DIR } = await import(
|
|
1975
|
-
path.join(__dirname, "..", "core", "config.mjs")
|
|
1976
|
-
);
|
|
1977
|
-
|
|
1978
|
-
const sub = args[1]; // provider name | "refresh" | "revoke" | undefined
|
|
1979
|
-
|
|
1980
|
-
const auth = new AuthManager(WISPY_DIR);
|
|
1981
|
-
|
|
1982
|
-
// wispy auth — show status for all providers
|
|
1983
|
-
if (!sub) {
|
|
1984
|
-
const statuses = await auth.allStatus();
|
|
1985
|
-
if (statuses.length === 0) {
|
|
1986
|
-
console.log(_dim("\n No saved auth tokens. Run: wispy auth <provider>\n"));
|
|
1987
|
-
} else {
|
|
1988
|
-
console.log(`\n${_bold("🔑 Auth Status")}\n`);
|
|
1989
|
-
for (const s of statuses) {
|
|
1990
|
-
const expiryStr = s.expiresAt ? _dim(` (expires ${new Date(s.expiresAt).toLocaleString()})`) : "";
|
|
1991
|
-
const statusIcon = s.expired ? _yellow("⚠️ expired") : _green("✅ valid");
|
|
1992
|
-
console.log(` ${_cyan(s.provider.padEnd(20))} ${s.type.padEnd(8)} ${statusIcon}${expiryStr}`);
|
|
1993
|
-
}
|
|
1994
|
-
console.log("");
|
|
1995
|
-
console.log(_dim(" wispy auth <provider> — re-authenticate"));
|
|
1996
|
-
console.log(_dim(" wispy auth refresh <provider> — refresh token"));
|
|
1997
|
-
console.log(_dim(" wispy auth revoke <provider> — remove saved token"));
|
|
1998
|
-
}
|
|
1999
|
-
console.log("");
|
|
2000
|
-
process.exit(0);
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
|
-
// wispy auth refresh <provider>
|
|
2004
|
-
if (sub === "refresh" && args[2]) {
|
|
2005
|
-
const provider = args[2];
|
|
2006
|
-
console.log(`\n🔄 Refreshing token for ${_cyan(provider)}...`);
|
|
2007
|
-
try {
|
|
2008
|
-
await auth.refreshToken(provider);
|
|
2009
|
-
console.log(_green(`✅ Token refreshed for ${provider}\n`));
|
|
2010
|
-
} catch (err) {
|
|
2011
|
-
console.error(_red(`❌ ${err.message}\n`));
|
|
2012
|
-
process.exit(1);
|
|
2013
|
-
}
|
|
2014
|
-
process.exit(0);
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
// wispy auth revoke <provider>
|
|
2018
|
-
if (sub === "revoke" && args[2]) {
|
|
2019
|
-
const provider = args[2];
|
|
2020
|
-
await auth.revokeToken(provider);
|
|
2021
|
-
console.log(_green(`✅ Revoked auth for ${provider}\n`));
|
|
2022
|
-
process.exit(0);
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
|
-
// wispy auth <provider> — run OAuth or re-authenticate
|
|
2026
|
-
if (sub && sub !== "refresh" && sub !== "revoke") {
|
|
2027
|
-
const provider = sub;
|
|
2028
|
-
|
|
2029
|
-
if (provider === "github-copilot") {
|
|
2030
|
-
console.log(`\n🔑 ${_bold("GitHub Copilot")} — OAuth sign-in\n`);
|
|
2031
|
-
try {
|
|
2032
|
-
const result = await auth.oauth("github-copilot");
|
|
2033
|
-
if (result.valid) {
|
|
2034
|
-
console.log(_green(`✅ GitHub Copilot authenticated!\n`));
|
|
2035
|
-
console.log(_dim(" Token saved to ~/.wispy/auth/github-copilot.json"));
|
|
2036
|
-
console.log(_dim(" Run: wispy auth to verify status\n"));
|
|
7
|
+
(async () => {
|
|
8
|
+
const { fileURLToPath } = await import('url');
|
|
9
|
+
const { dirname } = await import('path');
|
|
10
|
+
const baseDir = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
|
|
14
|
+
async function handleCommand(command) {
|
|
15
|
+
const inquirer = (await import('inquirer')).default;
|
|
16
|
+
|
|
17
|
+
if (!command) {
|
|
18
|
+
const answers = await inquirer.prompt([
|
|
19
|
+
{
|
|
20
|
+
type: 'list',
|
|
21
|
+
name: 'selectedCommand',
|
|
22
|
+
message: 'What would you like to do?',
|
|
23
|
+
choices: [
|
|
24
|
+
{ name: 'Run WebSocket command', value: 'ws' },
|
|
25
|
+
{ name: 'Get help', value: 'help' },
|
|
26
|
+
{ name: 'Exit', value: null }
|
|
27
|
+
],
|
|
2037
28
|
}
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
process.exit(1);
|
|
2041
|
-
}
|
|
2042
|
-
} else {
|
|
2043
|
-
console.error(_red(`\n❌ Unknown provider for auth: ${provider}`));
|
|
2044
|
-
console.log(_dim(` Currently OAuth is supported for: github-copilot\n`));
|
|
2045
|
-
process.exit(1);
|
|
2046
|
-
}
|
|
2047
|
-
process.exit(0);
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
// Help
|
|
2051
|
-
console.log(`
|
|
2052
|
-
${_bold("🔑 Wispy Auth Commands")}
|
|
2053
|
-
|
|
2054
|
-
${_cyan("wispy auth")} — show auth status for all providers
|
|
2055
|
-
${_cyan("wispy auth github-copilot")} — sign in with GitHub (Copilot OAuth)
|
|
2056
|
-
${_cyan("wispy auth refresh <provider>")} — refresh expired token
|
|
2057
|
-
${_cyan("wispy auth revoke <provider>")} — remove saved auth
|
|
2058
|
-
|
|
2059
|
-
${_bold("Examples:")}
|
|
2060
|
-
wispy auth github-copilot
|
|
2061
|
-
wispy auth refresh github-copilot
|
|
2062
|
-
wispy auth revoke github-copilot
|
|
2063
|
-
`);
|
|
2064
|
-
process.exit(0);
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
// ── Unknown command detection ─────────────────────────────────────────────────
|
|
2068
|
-
// Any non-flag argument that wasn't matched above is an unknown command
|
|
2069
|
-
const _KNOWN_COMMANDS = new Set([
|
|
2070
|
-
"ws", "trust", "where", "handoff", "skill", "teach", "improve", "dry",
|
|
2071
|
-
"setup", "init", "update", "status", "connect", "disconnect", "sync",
|
|
2072
|
-
"deploy", "migrate", "cron", "audit", "log", "server", "node", "channel",
|
|
2073
|
-
"auth", "config", "model", "tui", "help", "doctor", "completion", "version",
|
|
2074
|
-
// serve flags (handled below)
|
|
2075
|
-
"--serve", "--telegram", "--discord", "--slack", "--server",
|
|
2076
|
-
"--help", "-h", "--version", "-v", "--debug", "--tui",
|
|
2077
|
-
// workstream flags
|
|
2078
|
-
"-w", "--workstream",
|
|
2079
|
-
]);
|
|
2080
|
-
const _firstArg = args[0];
|
|
2081
|
-
if (_firstArg && _firstArg[0] !== "-" && !_KNOWN_COMMANDS.has(_firstArg)) {
|
|
2082
|
-
// Not a known command — but could be a one-shot message (no quotes needed)
|
|
2083
|
-
// Heuristic: if it looks like a real command word (no spaces, short), warn.
|
|
2084
|
-
// If it looks like a natural language sentence, fall through to REPL one-shot mode.
|
|
2085
|
-
// Only show "unknown command" if it looks very much like a mistyped subcommand.
|
|
2086
|
-
// Common typos/misspellings of real commands (2-char levenshtein distance).
|
|
2087
|
-
// Everything else falls through to one-shot message mode.
|
|
2088
|
-
const CLOSE_COMMANDS = ["seutp", "set", "stat", "statu", "doc", "wss", "trus", "depliy", "sycn", "cran"];
|
|
2089
|
-
const looksLikeCommand = CLOSE_COMMANDS.includes(_firstArg) || (_firstArg.length <= 3 && /^[a-z]+$/.test(_firstArg) && !["hi", "hey", "yo", "sup", "thx", "ty", "ok", "no", "yes", "ya", "hmm"].includes(_firstArg));
|
|
2090
|
-
if (looksLikeCommand) {
|
|
2091
|
-
// Show unknown command error
|
|
2092
|
-
const suggestions = [
|
|
2093
|
-
{ cmd: "setup", desc: "configure wispy" },
|
|
2094
|
-
{ cmd: "tui", desc: "workspace UI" },
|
|
2095
|
-
{ cmd: "ws", desc: "workstream management" },
|
|
2096
|
-
{ cmd: "doctor", desc: "check system health" },
|
|
2097
|
-
{ cmd: "help", desc: "show detailed help" },
|
|
2098
|
-
];
|
|
2099
|
-
console.error(`\n${_red(`❌ Unknown command: ${_firstArg}`)}`);
|
|
2100
|
-
console.error(`\n${_bold("Did you mean one of these?")}`);
|
|
2101
|
-
for (const { cmd, desc } of suggestions) {
|
|
2102
|
-
console.error(` ${_cyan("wispy " + cmd.padEnd(12))}— ${desc}`);
|
|
2103
|
-
}
|
|
2104
|
-
console.error(`\n${_dim("Run wispy --help for all commands.")}\n`);
|
|
2105
|
-
process.exit(2);
|
|
2106
|
-
}
|
|
2107
|
-
// Otherwise fall through to one-shot message mode in REPL
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
// ── Bot / serve modes ─────────────────────────────────────────────────────────
|
|
2111
|
-
const serveMode = args.includes("--serve");
|
|
2112
|
-
const telegramMode = args.includes("--telegram");
|
|
2113
|
-
const discordMode = args.includes("--discord");
|
|
2114
|
-
const slackMode = args.includes("--slack");
|
|
2115
|
-
|
|
2116
|
-
if (serveMode || telegramMode || discordMode || slackMode) {
|
|
2117
|
-
const { ChannelManager } = await import(
|
|
2118
|
-
path.join(__dirname, "..", "lib", "channels", "index.mjs")
|
|
2119
|
-
);
|
|
2120
|
-
|
|
2121
|
-
const manager = new ChannelManager();
|
|
2122
|
-
|
|
2123
|
-
const only = [];
|
|
2124
|
-
if (telegramMode) only.push("telegram");
|
|
2125
|
-
if (discordMode) only.push("discord");
|
|
2126
|
-
if (slackMode) only.push("slack");
|
|
2127
|
-
// serveMode → only stays empty → all channels started
|
|
2128
|
-
|
|
2129
|
-
await manager.startAll(only);
|
|
2130
|
-
|
|
2131
|
-
// Start cron scheduler if in serve mode
|
|
2132
|
-
let cronManager = null;
|
|
2133
|
-
if (serveMode) {
|
|
2134
|
-
try {
|
|
2135
|
-
const { WispyEngine, CronManager, WISPY_DIR } = await import(
|
|
2136
|
-
path.join(__dirname, "..", "core", "index.mjs")
|
|
2137
|
-
);
|
|
2138
|
-
const engine = new WispyEngine();
|
|
2139
|
-
await engine.init({ skipMcp: true });
|
|
2140
|
-
cronManager = new CronManager(WISPY_DIR, engine);
|
|
2141
|
-
await cronManager.init();
|
|
2142
|
-
cronManager.start();
|
|
2143
|
-
console.error("[wispy] Cron scheduler started");
|
|
2144
|
-
} catch (err) {
|
|
2145
|
-
console.error("[wispy] Failed to start cron scheduler:", err.message);
|
|
29
|
+
]);
|
|
30
|
+
command = answers.selectedCommand;
|
|
2146
31
|
}
|
|
2147
|
-
}
|
|
2148
|
-
|
|
2149
|
-
// Keep process alive and stop cleanly on Ctrl+C
|
|
2150
|
-
process.on("SIGINT", async () => {
|
|
2151
|
-
if (cronManager) cronManager.stop();
|
|
2152
|
-
await manager.stopAll();
|
|
2153
|
-
process.exit(0);
|
|
2154
|
-
});
|
|
2155
|
-
process.on("SIGTERM", async () => {
|
|
2156
|
-
if (cronManager) cronManager.stop();
|
|
2157
|
-
await manager.stopAll();
|
|
2158
|
-
process.exit(0);
|
|
2159
|
-
});
|
|
2160
32
|
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
"auth"].includes(a)
|
|
2175
|
-
);
|
|
2176
|
-
|
|
2177
|
-
if (isInteractiveStart) {
|
|
2178
|
-
try {
|
|
2179
|
-
const { isFirstRun } = await import(
|
|
2180
|
-
path.join(__dirname, "..", "core", "config.mjs")
|
|
2181
|
-
);
|
|
2182
|
-
if (await isFirstRun()) {
|
|
2183
|
-
const { OnboardingWizard } = await import(
|
|
2184
|
-
path.join(__dirname, "..", "core", "onboarding.mjs")
|
|
2185
|
-
);
|
|
2186
|
-
const wizard = new OnboardingWizard();
|
|
2187
|
-
await wizard.run();
|
|
2188
|
-
// After onboarding, continue to REPL or TUI
|
|
33
|
+
switch (command) {
|
|
34
|
+
case "ws":
|
|
35
|
+
const { handleWsCommand } = await import(baseDir + '/../lib/commands/ws.mjs');
|
|
36
|
+
return await handleWsCommand(args);
|
|
37
|
+
case "help":
|
|
38
|
+
console.log("Available commands: ws, help");
|
|
39
|
+
break;
|
|
40
|
+
case null:
|
|
41
|
+
console.log("Goodbye!");
|
|
42
|
+
break;
|
|
43
|
+
default:
|
|
44
|
+
console.error(`Unknown command: ${command}`);
|
|
45
|
+
console.log("Try one of the available commands: ws, help");
|
|
2189
46
|
}
|
|
2190
|
-
} catch {
|
|
2191
|
-
// If onboarding fails for any reason, continue normally
|
|
2192
47
|
}
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
// Validate config before starting interactive modes
|
|
2196
|
-
await validateConfigOnStartup();
|
|
2197
|
-
|
|
2198
|
-
// ── TUI mode ──────────────────────────────────────────────────────────────────
|
|
2199
|
-
// `wispy ui` is the primary way; `--tui` is kept as a hidden backwards-compat alias
|
|
2200
|
-
const tuiMode = args[0] === "tui" || args.includes("--tui");
|
|
2201
|
-
|
|
2202
|
-
if (tuiMode) {
|
|
2203
|
-
// Override SIGINT for clean TUI exit
|
|
2204
|
-
process.removeAllListeners("SIGINT");
|
|
2205
|
-
process.on("SIGINT", () => { process.exit(130); });
|
|
2206
48
|
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
const tuiScript = path.join(__dirname, "..", "lib", "wispy-tui.mjs");
|
|
2210
|
-
try {
|
|
2211
|
-
await import(tuiScript);
|
|
2212
|
-
} catch (e) { friendlyError(e); }
|
|
2213
|
-
} else {
|
|
2214
|
-
// Default: interactive REPL
|
|
2215
|
-
const mainScript = path.join(__dirname, "..", "lib", "wispy-repl.mjs");
|
|
2216
|
-
try {
|
|
2217
|
-
await import(mainScript);
|
|
2218
|
-
} catch (e) { friendlyError(e); }
|
|
2219
|
-
}
|
|
49
|
+
await handleCommand(args[0] === '--help' ? 'help' : args[0]);
|
|
50
|
+
})();
|