wispy-cli 1.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/wispy.mjs CHANGED
@@ -373,6 +373,29 @@ if (args[0] === "deploy") {
373
373
  process.exit(0);
374
374
  }
375
375
 
376
+ if (sub === "modal") {
377
+ process.stdout.write(dm.generateModalConfig());
378
+ console.log(dim("\n# Save as modal_app.py, then: pip install modal && modal run modal_app.py"));
379
+ process.exit(0);
380
+ }
381
+
382
+ if (sub === "daytona") {
383
+ const { mkdir: mkdirDaytona, writeFile: writeDaytona } = await import("node:fs/promises");
384
+ const daytonaDir = path.join(process.cwd(), ".daytona");
385
+ await mkdirDaytona(daytonaDir, { recursive: true });
386
+ const daytonaConfigPath = path.join(daytonaDir, "config.yaml");
387
+ let exists = false;
388
+ try { await (await import("node:fs/promises")).access(daytonaConfigPath); exists = true; } catch {}
389
+ if (!exists) {
390
+ await writeDaytona(daytonaConfigPath, dm.generateDaytonaConfig(), "utf8");
391
+ console.log(green(`✅ Created .daytona/config.yaml`));
392
+ } else {
393
+ console.log(yellow(`⏭️ .daytona/config.yaml already exists (skipped)`));
394
+ }
395
+ console.log(dim(" Push to your repo and connect via Daytona workspace."));
396
+ process.exit(0);
397
+ }
398
+
376
399
  // Help
377
400
  console.log(`
378
401
  🚀 ${bold("Wispy Deploy Commands")}
@@ -385,6 +408,8 @@ if (args[0] === "deploy") {
385
408
  wispy deploy railway — print railway.json
386
409
  wispy deploy fly — print fly.toml
387
410
  wispy deploy render — print render.yaml
411
+ wispy deploy modal — generate Modal serverless config (modal_app.py)
412
+ wispy deploy daytona — generate Daytona workspace config (.daytona/config.yaml)
388
413
 
389
414
  ${cyan("Deploy:")}
390
415
  wispy deploy vps user@host — SSH deploy: install + systemd setup
@@ -401,6 +426,67 @@ if (args[0] === "deploy") {
401
426
  process.exit(0);
402
427
  }
403
428
 
429
+ // ── migrate sub-command ───────────────────────────────────────────────────────
430
+ if (args[0] === "migrate") {
431
+ const { OpenClawMigrator, WISPY_DIR } = await import(
432
+ path.join(__dirname, "..", "core", "index.mjs")
433
+ );
434
+
435
+ const sub = args[1]; // "openclaw" (only supported source for now)
436
+ const dryRun = args.includes("--dry-run");
437
+ const memoryOnly = args.includes("--memory-only");
438
+
439
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
440
+ const red = (s) => `\x1b[31m${s}\x1b[0m`;
441
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
442
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
443
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
444
+
445
+ if (!sub || sub === "openclaw") {
446
+ console.log(`\n🌿 ${bold("Wispy Migration Tool")} ${dim("— from OpenClaw")}\n`);
447
+ if (dryRun) console.log(yellow(" [DRY RUN — no files will be written]\n"));
448
+ if (memoryOnly) console.log(dim(" [memory-only mode]\n"));
449
+
450
+ const migrator = new OpenClawMigrator(WISPY_DIR);
451
+ const result = await migrator.migrate({ dryRun, memoryOnly });
452
+
453
+ console.log(migrator.formatReport());
454
+
455
+ if (result.success) {
456
+ if (dryRun) {
457
+ console.log(dim("\nRun without --dry-run to apply changes.\n"));
458
+ } else {
459
+ const counts = [
460
+ result.report.memories.length > 0 && `${result.report.memories.length} memory files`,
461
+ result.report.userModel.length > 0 && `${result.report.userModel.length} profile files`,
462
+ result.report.cronJobs.length > 0 && `${result.report.cronJobs.length} cron jobs`,
463
+ result.report.channels.length > 0 && `${result.report.channels.length} channels`,
464
+ ].filter(Boolean);
465
+
466
+ if (counts.length > 0) {
467
+ console.log(`\n${green("✅ Migration complete!")} Imported: ${counts.join(", ")}`);
468
+ } else {
469
+ console.log(`\n${dim("Nothing new to import (already migrated or empty).")}`);
470
+ }
471
+ console.log(dim("\nTip: run `wispy` to start chatting with your imported context.\n"));
472
+ }
473
+ } else {
474
+ console.error(`\n${red("❌ Migration failed:")} ${result.error}\n`);
475
+ process.exit(1);
476
+ }
477
+ } else {
478
+ console.log(`
479
+ 🔀 ${bold("Wispy Migrate Commands")}
480
+
481
+ wispy migrate openclaw — import from OpenClaw (~/.openclaw/)
482
+ wispy migrate openclaw --dry-run — preview what would be imported
483
+ wispy migrate openclaw --memory-only — only import memories
484
+ `);
485
+ }
486
+
487
+ process.exit(0);
488
+ }
489
+
404
490
  // ── cron sub-command ──────────────────────────────────────────────────────────
405
491
  if (args[0] === "cron") {
406
492
  const { WispyEngine, CronManager, WISPY_DIR } = await import(
@@ -860,6 +946,9 @@ if (args[0] === "channel") {
860
946
  wispy channel setup telegram — interactive Telegram bot setup
861
947
  wispy channel setup discord — interactive Discord bot setup
862
948
  wispy channel setup slack — interactive Slack bot setup
949
+ wispy channel setup whatsapp — WhatsApp setup (requires: npm install whatsapp-web.js qrcode-terminal)
950
+ wispy channel setup signal — Signal setup (requires: signal-cli)
951
+ wispy channel setup email — Email setup (requires: npm install nodemailer imapflow)
863
952
  wispy channel list — show configured channels
864
953
  wispy channel test <name> — test a channel connection
865
954
 
package/core/deploy.mjs CHANGED
@@ -121,6 +121,57 @@ WantedBy=multi-user.target
121
121
  `;
122
122
  }
123
123
 
124
+ generateModalConfig() {
125
+ const app = this.appName;
126
+ return `# modal_app.py — auto-generated by wispy deploy modal
127
+ # Install: pip install modal
128
+ # Run: modal run modal_app.py
129
+ import modal
130
+ import subprocess
131
+
132
+ app = modal.App("${app}")
133
+
134
+ image = (
135
+ modal.Image.debian_slim()
136
+ .pip_install("nodeenv")
137
+ .run_commands(
138
+ "nodeenv /opt/node",
139
+ "/opt/node/bin/npm install -g wispy-cli",
140
+ )
141
+ )
142
+
143
+
144
+ @app.function(
145
+ image=image,
146
+ secrets=[modal.Secret.from_name("wispy-secrets")],
147
+ timeout=3600,
148
+ keep_warm=1,
149
+ )
150
+ def serve():
151
+ subprocess.run(
152
+ ["/opt/node/bin/wispy", "server", "--host", "0.0.0.0", "--port", "18790"],
153
+ check=True,
154
+ )
155
+
156
+
157
+ @app.local_entrypoint()
158
+ def main():
159
+ serve.remote()
160
+ `;
161
+ }
162
+
163
+ generateDaytonaConfig() {
164
+ return `# .daytona/config.yaml — auto-generated by wispy deploy daytona
165
+ # Daytona workspace config for wispy server
166
+ name: ${this.appName}
167
+ image: node:20-slim
168
+ onCreateCommand: npm install -g wispy-cli
169
+ postStartCommand: wispy server --host 0.0.0.0 --port \${PORT:-${this.port}}
170
+ # Set secrets in Daytona workspace environment variables:
171
+ # WISPY_SERVER_TOKEN, GOOGLE_AI_KEY (or other provider key)
172
+ `;
173
+ }
174
+
124
175
  generateEnvExample() {
125
176
  const token = randomBytes(24).toString("hex");
126
177
  return `# Wispy Server Configuration
package/core/engine.mjs CHANGED
@@ -25,6 +25,8 @@ import { PermissionManager } from "./permissions.mjs";
25
25
  import { AuditLog, EVENT_TYPES } from "./audit.mjs";
26
26
  import { Harness } from "./harness.mjs";
27
27
  import { SyncManager, getSyncManager } from "./sync.mjs";
28
+ import { SkillManager } from "./skills.mjs";
29
+ import { UserModel } from "./user-model.mjs";
28
30
 
29
31
  const MAX_TOOL_ROUNDS = 10;
30
32
  const MAX_CONTEXT_CHARS = 40_000;
@@ -42,6 +44,8 @@ export class WispyEngine {
42
44
  this.audit = new AuditLog(WISPY_DIR);
43
45
  this.harness = new Harness(this.tools, this.permissions, this.audit, config);
44
46
  this.sync = null; // SyncManager, initialized lazily
47
+ this.skills = new SkillManager(WISPY_DIR, this);
48
+ this.userModel = new UserModel(WISPY_DIR, this);
45
49
  this._initialized = false;
46
50
  this._workMdContent = null;
47
51
  this._workMdLoaded = false;
@@ -80,6 +84,9 @@ export class WispyEngine {
80
84
  // Register node tools
81
85
  this._registerNodeTools();
82
86
 
87
+ // Register skill tools
88
+ this._registerSkillTools();
89
+
83
90
  // Re-wire harness after tools are registered
84
91
  this.harness = new Harness(this.tools, this.permissions, this.audit, this.config);
85
92
 
@@ -207,6 +214,21 @@ export class WispyEngine {
207
214
  this.sessions.save(session.id).catch(() => {});
208
215
  }
209
216
 
217
+ // Skills auto-capture (non-blocking fire-and-forget)
218
+ const currentMessages = session.messages ?? [];
219
+ if (currentMessages.length > 1 && !opts.skipSkillCapture) {
220
+ this.skills.autoCapture(currentMessages, session.id).then(skill => {
221
+ if (skill && opts.onSkillLearned) {
222
+ opts.onSkillLearned(skill);
223
+ }
224
+ }).catch(() => {});
225
+ }
226
+
227
+ // User model observation (non-blocking, every 10 messages)
228
+ if (currentMessages.length > 0 && !opts.skipUserModel) {
229
+ this.userModel.observe(currentMessages).catch(() => {});
230
+ }
231
+
210
232
  return {
211
233
  role: "assistant",
212
234
  content: responseText,
@@ -324,6 +346,7 @@ export class WispyEngine {
324
346
  "kill_subagent", "steer_subagent",
325
347
  "node_list", "node_status", "node_execute",
326
348
  "update_work_context",
349
+ "run_skill", "list_skills", "delete_skill",
327
350
  ]);
328
351
 
329
352
  const harnessResult = await this.harness.execute(name, args, {
@@ -392,6 +415,13 @@ export class WispyEngine {
392
415
  return this._toolNodeStatus();
393
416
  case "node_execute":
394
417
  return this._toolNodeExecute(args);
418
+ // Skill tools (v1.4)
419
+ case "run_skill":
420
+ return this._toolRunSkill(args, session);
421
+ case "list_skills":
422
+ return this._toolListSkills(args);
423
+ case "delete_skill":
424
+ return this._toolDeleteSkill(args);
395
425
  default:
396
426
  return this.tools.execute(name, args);
397
427
  }
@@ -742,6 +772,14 @@ export class WispyEngine {
742
772
  "",
743
773
  ];
744
774
 
775
+ // Load user model personalization
776
+ try {
777
+ const userModelAddition = await this.userModel.getSystemPromptAddition();
778
+ if (userModelAddition) {
779
+ parts.push(userModelAddition, "");
780
+ }
781
+ } catch { /* ignore */ }
782
+
745
783
  // Load WISPY.md context
746
784
  const wispyMd = await this._loadWispyMd();
747
785
  if (wispyMd) {
@@ -1070,6 +1108,89 @@ export class WispyEngine {
1070
1108
  }
1071
1109
  }
1072
1110
 
1111
+ // ── Skill tools (v1.4) ───────────────────────────────────────────────────────
1112
+
1113
+ _registerSkillTools() {
1114
+ const skillTools = [
1115
+ {
1116
+ name: "run_skill",
1117
+ description: "Execute a saved skill by name. Skills are reusable task patterns learned from previous conversations.",
1118
+ parameters: {
1119
+ type: "object",
1120
+ properties: {
1121
+ name: { type: "string", description: "Skill name (e.g., 'deploy-next-app')" },
1122
+ args: { type: "object", description: "Optional args to template into the skill prompt ({{key}} placeholders)" },
1123
+ },
1124
+ required: ["name"],
1125
+ },
1126
+ },
1127
+ {
1128
+ name: "list_skills",
1129
+ description: "List all available skills or search for specific ones.",
1130
+ parameters: {
1131
+ type: "object",
1132
+ properties: {
1133
+ query: { type: "string", description: "Optional search query to filter skills" },
1134
+ },
1135
+ },
1136
+ },
1137
+ {
1138
+ name: "delete_skill",
1139
+ description: "Delete a saved skill by name.",
1140
+ parameters: {
1141
+ type: "object",
1142
+ properties: {
1143
+ name: { type: "string", description: "Skill name to delete" },
1144
+ },
1145
+ required: ["name"],
1146
+ },
1147
+ },
1148
+ ];
1149
+
1150
+ for (const tool of skillTools) {
1151
+ this.tools._definitions.set(tool.name, tool);
1152
+ }
1153
+ }
1154
+
1155
+ async _toolRunSkill(args, session) {
1156
+ try {
1157
+ const skill = await this.skills.get(args.name);
1158
+ if (!skill) {
1159
+ const available = (await this.skills.list()).map(s => s.name);
1160
+ return { success: false, error: `Skill '${args.name}' not found`, available };
1161
+ }
1162
+ const result = await this.skills.execute(args.name, args.args ?? {}, session?.id ?? null);
1163
+ return { success: true, skill: args.name, result: result?.content ?? result };
1164
+ } catch (err) {
1165
+ return { success: false, error: err.message };
1166
+ }
1167
+ }
1168
+
1169
+ async _toolListSkills(args) {
1170
+ try {
1171
+ const skills = args?.query
1172
+ ? await this.skills.search(args.query)
1173
+ : await this.skills.list();
1174
+ return {
1175
+ success: true,
1176
+ skills: skills.map(s => ({
1177
+ name: s.name,
1178
+ description: s.description,
1179
+ tags: s.tags,
1180
+ timesUsed: s.timesUsed ?? 0,
1181
+ version: s.version ?? 1,
1182
+ })),
1183
+ total: skills.length,
1184
+ };
1185
+ } catch (err) {
1186
+ return { success: false, error: err.message };
1187
+ }
1188
+ }
1189
+
1190
+ async _toolDeleteSkill(args) {
1191
+ return this.skills.delete(args.name);
1192
+ }
1193
+
1073
1194
  // ── Cleanup ──────────────────────────────────────────────────────────────────
1074
1195
 
1075
1196
  destroy() {
package/core/index.mjs CHANGED
@@ -21,3 +21,6 @@ export { NodeManager, CAPABILITIES } from "./nodes.mjs";
21
21
  export { Harness, Receipt, HarnessResult, computeUnifiedDiff } from "./harness.mjs";
22
22
  export { DeployManager } from "./deploy.mjs";
23
23
  export { SyncManager, getSyncManager, sha256 } from "./sync.mjs";
24
+ export { SkillManager } from "./skills.mjs";
25
+ export { UserModel } from "./user-model.mjs";
26
+ export { OpenClawMigrator } from "./migrate.mjs";