wispy-cli 1.2.3 → 1.4.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 +219 -1
- package/core/deploy.mjs +51 -0
- package/core/engine.mjs +132 -0
- package/core/index.mjs +4 -0
- package/core/memory.mjs +10 -1
- package/core/migrate.mjs +357 -0
- package/core/server.mjs +152 -2
- package/core/session.mjs +8 -0
- package/core/skills.mjs +339 -0
- package/core/sync.mjs +682 -0
- package/core/user-model.mjs +302 -0
- package/lib/channels/email.mjs +187 -0
- package/lib/channels/index.mjs +66 -9
- package/lib/channels/signal.mjs +151 -0
- package/lib/channels/whatsapp.mjs +141 -0
- package/lib/wispy-repl.mjs +102 -24
- package/lib/wispy-tui.mjs +964 -380
- package/package.json +18 -2
package/bin/wispy.mjs
CHANGED
|
@@ -156,6 +156,135 @@ if (args[0] === "disconnect") {
|
|
|
156
156
|
process.exit(0);
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
// ── sync sub-command ──────────────────────────────────────────────────────────
|
|
160
|
+
if (args[0] === "sync") {
|
|
161
|
+
const { SyncManager } = await import(
|
|
162
|
+
path.join(__dirname, "..", "core", "sync.mjs")
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const sub = args[1]; // push | pull | status | auto | (undefined = bidirectional)
|
|
166
|
+
|
|
167
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
168
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
169
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
170
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
171
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
172
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
173
|
+
|
|
174
|
+
// Parse flags
|
|
175
|
+
const strategyIdx = args.indexOf("--strategy");
|
|
176
|
+
const strategy = strategyIdx !== -1 ? args[strategyIdx + 1] : null;
|
|
177
|
+
const memoryOnly = args.includes("--memory-only");
|
|
178
|
+
const sessionsOnly = args.includes("--sessions-only");
|
|
179
|
+
const remoteUrlIdx = args.indexOf("--remote");
|
|
180
|
+
const remoteUrlArg = remoteUrlIdx !== -1 ? args[remoteUrlIdx + 1] : null;
|
|
181
|
+
const tokenIdx = args.indexOf("--token");
|
|
182
|
+
const tokenArg = tokenIdx !== -1 ? args[tokenIdx + 1] : null;
|
|
183
|
+
|
|
184
|
+
// Load config (or use overrides)
|
|
185
|
+
const cfg = await SyncManager.loadConfig();
|
|
186
|
+
const remoteUrl = remoteUrlArg ?? cfg.remoteUrl;
|
|
187
|
+
const token = tokenArg ?? cfg.token;
|
|
188
|
+
|
|
189
|
+
// ── auto enable/disable ──
|
|
190
|
+
if (sub === "auto") {
|
|
191
|
+
const enable = !args.includes("--off");
|
|
192
|
+
const disable = args.includes("--off");
|
|
193
|
+
if (disable) {
|
|
194
|
+
await SyncManager.disableAuto();
|
|
195
|
+
console.log(`\n${yellow("⏸")} Auto-sync ${bold("disabled")}.\n`);
|
|
196
|
+
} else if (!remoteUrl) {
|
|
197
|
+
console.log(`\n${red("✗")} No remote URL configured.`);
|
|
198
|
+
console.log(dim(` Use: wispy sync auto --remote https://vps.com:18790 --token <token>\n`));
|
|
199
|
+
process.exit(1);
|
|
200
|
+
} else {
|
|
201
|
+
await SyncManager.enableAuto(remoteUrl, token ?? "");
|
|
202
|
+
console.log(`\n${green("✓")} Auto-sync ${bold("enabled")}.`);
|
|
203
|
+
console.log(` Remote: ${cyan(remoteUrl)}`);
|
|
204
|
+
console.log(dim(" Sessions and memory will be synced automatically.\n"));
|
|
205
|
+
}
|
|
206
|
+
process.exit(0);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── status ──
|
|
210
|
+
if (sub === "status") {
|
|
211
|
+
if (!remoteUrl) {
|
|
212
|
+
console.log(`\n${red("✗")} No remote configured. Set via sync.json or --remote flag.\n`);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
console.log(`\n🔄 ${bold("Sync Status")}\n`);
|
|
216
|
+
console.log(` Remote: ${cyan(remoteUrl)}`);
|
|
217
|
+
const mgr = new SyncManager({ remoteUrl, token, strategy: strategy ?? "newer-wins" });
|
|
218
|
+
const s = await mgr.status(remoteUrl, token);
|
|
219
|
+
console.log(` Connection: ${s.reachable ? green("✅ reachable") : red("✗ unreachable")}\n`);
|
|
220
|
+
|
|
221
|
+
const fmt = (label, info) => {
|
|
222
|
+
const parts = [];
|
|
223
|
+
if (info.localOnly > 0) parts.push(`${yellow(info.localOnly + " local only")}`);
|
|
224
|
+
if (info.remoteOnly > 0) parts.push(`${cyan(info.remoteOnly + " remote only")}`);
|
|
225
|
+
if (info.inSync > 0) parts.push(`${green(info.inSync + " in sync")}`);
|
|
226
|
+
console.log(` ${bold(label.padEnd(14))} ${parts.join(", ") || dim("(empty)")}`);
|
|
227
|
+
};
|
|
228
|
+
fmt("Memory:", s.memory);
|
|
229
|
+
fmt("Sessions:", s.sessions);
|
|
230
|
+
fmt("Cron:", s.cron);
|
|
231
|
+
fmt("Workstreams:",s.workstreams);
|
|
232
|
+
fmt("Permissions:",s.permissions);
|
|
233
|
+
|
|
234
|
+
const needPull = (s.memory.remoteOnly + s.sessions.remoteOnly + s.cron.remoteOnly + s.workstreams.remoteOnly + s.permissions.remoteOnly);
|
|
235
|
+
const needPush = (s.memory.localOnly + s.sessions.localOnly + s.cron.localOnly + s.workstreams.localOnly + s.permissions.localOnly);
|
|
236
|
+
if (needPull > 0 || needPush > 0) {
|
|
237
|
+
console.log(`\n Action needed: ${needPull > 0 ? `pull ${yellow(needPull)} file(s)` : ""} ${needPush > 0 ? `push ${yellow(needPush)} file(s)` : ""}`.trimEnd());
|
|
238
|
+
console.log(dim(" Run 'wispy sync' to synchronize."));
|
|
239
|
+
} else {
|
|
240
|
+
console.log(`\n ${green("✓")} Everything in sync!`);
|
|
241
|
+
}
|
|
242
|
+
console.log("");
|
|
243
|
+
process.exit(0);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// For push/pull/sync we need a remote URL
|
|
247
|
+
if (!remoteUrl) {
|
|
248
|
+
console.log(`\n${red("✗")} No remote configured.`);
|
|
249
|
+
console.log(dim(" Set remoteUrl in ~/.wispy/sync.json or use --remote <url>\n"));
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const opts = { strategy: strategy ?? cfg.strategy ?? "newer-wins", memoryOnly, sessionsOnly };
|
|
254
|
+
const mgr = new SyncManager({ remoteUrl, token, ...opts });
|
|
255
|
+
|
|
256
|
+
if (sub === "push") {
|
|
257
|
+
console.log(`\n📤 ${bold("Pushing")} to ${cyan(remoteUrl)}...`);
|
|
258
|
+
const result = await mgr.push(remoteUrl, token, opts);
|
|
259
|
+
console.log(` Pushed: ${green(result.pushed)}`);
|
|
260
|
+
console.log(` Skipped: ${dim(result.skipped)}`);
|
|
261
|
+
if (result.errors.length) result.errors.forEach(e => console.log(` ${red("✗")} ${e}`));
|
|
262
|
+
console.log(`\n${result.errors.length === 0 ? green("✓ Done") : yellow("⚠ Done with errors")}.\n`);
|
|
263
|
+
|
|
264
|
+
} else if (sub === "pull") {
|
|
265
|
+
console.log(`\n📥 ${bold("Pulling")} from ${cyan(remoteUrl)}...`);
|
|
266
|
+
const result = await mgr.pull(remoteUrl, token, opts);
|
|
267
|
+
console.log(` Pulled: ${green(result.pulled)}`);
|
|
268
|
+
console.log(` Skipped: ${dim(result.skipped)}`);
|
|
269
|
+
if (result.conflicts) console.log(` Conflicts: ${yellow(result.conflicts)} (saved as .conflict-* files)`);
|
|
270
|
+
if (result.errors.length) result.errors.forEach(e => console.log(` ${red("✗")} ${e}`));
|
|
271
|
+
console.log(`\n${result.errors.length === 0 ? green("✓ Done") : yellow("⚠ Done with errors")}.\n`);
|
|
272
|
+
|
|
273
|
+
} else {
|
|
274
|
+
// Bidirectional sync (default)
|
|
275
|
+
console.log(`\n🔄 ${bold("Syncing")} with ${cyan(remoteUrl)}...`);
|
|
276
|
+
const result = await mgr.sync(remoteUrl, token, opts);
|
|
277
|
+
console.log(` Pushed: ${green(result.pushed)}`);
|
|
278
|
+
console.log(` Pulled: ${green(result.pulled)}`);
|
|
279
|
+
console.log(` Skipped: ${dim(result.skipped)}`);
|
|
280
|
+
if (result.conflicts) console.log(` Conflicts: ${yellow(result.conflicts)} (saved as .conflict-* files)`);
|
|
281
|
+
if (result.errors.length) result.errors.forEach(e => console.log(` ${red("✗")} ${e}`));
|
|
282
|
+
console.log(`\n${result.errors.length === 0 ? green("✓ Done") : yellow("⚠ Done with errors")}.\n`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
process.exit(0);
|
|
286
|
+
}
|
|
287
|
+
|
|
159
288
|
// ── deploy sub-command ────────────────────────────────────────────────────────
|
|
160
289
|
if (args[0] === "deploy") {
|
|
161
290
|
const { DeployManager } = await import(
|
|
@@ -244,6 +373,29 @@ if (args[0] === "deploy") {
|
|
|
244
373
|
process.exit(0);
|
|
245
374
|
}
|
|
246
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
|
+
|
|
247
399
|
// Help
|
|
248
400
|
console.log(`
|
|
249
401
|
🚀 ${bold("Wispy Deploy Commands")}
|
|
@@ -256,6 +408,8 @@ if (args[0] === "deploy") {
|
|
|
256
408
|
wispy deploy railway — print railway.json
|
|
257
409
|
wispy deploy fly — print fly.toml
|
|
258
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)
|
|
259
413
|
|
|
260
414
|
${cyan("Deploy:")}
|
|
261
415
|
wispy deploy vps user@host — SSH deploy: install + systemd setup
|
|
@@ -272,6 +426,67 @@ if (args[0] === "deploy") {
|
|
|
272
426
|
process.exit(0);
|
|
273
427
|
}
|
|
274
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
|
+
|
|
275
490
|
// ── cron sub-command ──────────────────────────────────────────────────────────
|
|
276
491
|
if (args[0] === "cron") {
|
|
277
492
|
const { WispyEngine, CronManager, WISPY_DIR } = await import(
|
|
@@ -731,6 +946,9 @@ if (args[0] === "channel") {
|
|
|
731
946
|
wispy channel setup telegram — interactive Telegram bot setup
|
|
732
947
|
wispy channel setup discord — interactive Discord bot setup
|
|
733
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)
|
|
734
952
|
wispy channel list — show configured channels
|
|
735
953
|
wispy channel test <name> — test a channel connection
|
|
736
954
|
|
|
@@ -805,7 +1023,7 @@ if (serveMode || telegramMode || discordMode || slackMode) {
|
|
|
805
1023
|
const isInteractiveStart = !args.some(a =>
|
|
806
1024
|
["--serve", "--telegram", "--discord", "--slack", "--server",
|
|
807
1025
|
"status", "setup", "init", "connect", "disconnect", "deploy",
|
|
808
|
-
"cron", "audit", "log", "server", "node", "channel"].includes(a)
|
|
1026
|
+
"cron", "audit", "log", "server", "node", "channel", "sync"].includes(a)
|
|
809
1027
|
);
|
|
810
1028
|
|
|
811
1029
|
if (isInteractiveStart) {
|
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
|
@@ -24,6 +24,9 @@ import { SubAgentManager } from "./subagents.mjs";
|
|
|
24
24
|
import { PermissionManager } from "./permissions.mjs";
|
|
25
25
|
import { AuditLog, EVENT_TYPES } from "./audit.mjs";
|
|
26
26
|
import { Harness } from "./harness.mjs";
|
|
27
|
+
import { SyncManager, getSyncManager } from "./sync.mjs";
|
|
28
|
+
import { SkillManager } from "./skills.mjs";
|
|
29
|
+
import { UserModel } from "./user-model.mjs";
|
|
27
30
|
|
|
28
31
|
const MAX_TOOL_ROUNDS = 10;
|
|
29
32
|
const MAX_CONTEXT_CHARS = 40_000;
|
|
@@ -40,6 +43,9 @@ export class WispyEngine {
|
|
|
40
43
|
this.permissions = new PermissionManager(config.permissions ?? {});
|
|
41
44
|
this.audit = new AuditLog(WISPY_DIR);
|
|
42
45
|
this.harness = new Harness(this.tools, this.permissions, this.audit, config);
|
|
46
|
+
this.sync = null; // SyncManager, initialized lazily
|
|
47
|
+
this.skills = new SkillManager(WISPY_DIR, this);
|
|
48
|
+
this.userModel = new UserModel(WISPY_DIR, this);
|
|
43
49
|
this._initialized = false;
|
|
44
50
|
this._workMdContent = null;
|
|
45
51
|
this._workMdLoaded = false;
|
|
@@ -78,6 +84,9 @@ export class WispyEngine {
|
|
|
78
84
|
// Register node tools
|
|
79
85
|
this._registerNodeTools();
|
|
80
86
|
|
|
87
|
+
// Register skill tools
|
|
88
|
+
this._registerSkillTools();
|
|
89
|
+
|
|
81
90
|
// Re-wire harness after tools are registered
|
|
82
91
|
this.harness = new Harness(this.tools, this.permissions, this.audit, this.config);
|
|
83
92
|
|
|
@@ -93,6 +102,15 @@ export class WispyEngine {
|
|
|
93
102
|
this.tools.registerMCP(this.mcpManager);
|
|
94
103
|
}
|
|
95
104
|
|
|
105
|
+
// Initialize sync manager (if configured)
|
|
106
|
+
try {
|
|
107
|
+
this.sync = await getSyncManager();
|
|
108
|
+
// Auto-sync on startup: pull any new data from remote
|
|
109
|
+
if (this.sync?.auto && this.sync.remoteUrl) {
|
|
110
|
+
this.sync.pull(this.sync.remoteUrl, this.sync.token).catch(() => {});
|
|
111
|
+
}
|
|
112
|
+
} catch { /* sync is optional */ }
|
|
113
|
+
|
|
96
114
|
this._initialized = true;
|
|
97
115
|
return this;
|
|
98
116
|
}
|
|
@@ -196,6 +214,21 @@ export class WispyEngine {
|
|
|
196
214
|
this.sessions.save(session.id).catch(() => {});
|
|
197
215
|
}
|
|
198
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
|
+
|
|
199
232
|
return {
|
|
200
233
|
role: "assistant",
|
|
201
234
|
content: responseText,
|
|
@@ -313,6 +346,7 @@ export class WispyEngine {
|
|
|
313
346
|
"kill_subagent", "steer_subagent",
|
|
314
347
|
"node_list", "node_status", "node_execute",
|
|
315
348
|
"update_work_context",
|
|
349
|
+
"run_skill", "list_skills", "delete_skill",
|
|
316
350
|
]);
|
|
317
351
|
|
|
318
352
|
const harnessResult = await this.harness.execute(name, args, {
|
|
@@ -381,6 +415,13 @@ export class WispyEngine {
|
|
|
381
415
|
return this._toolNodeStatus();
|
|
382
416
|
case "node_execute":
|
|
383
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);
|
|
384
425
|
default:
|
|
385
426
|
return this.tools.execute(name, args);
|
|
386
427
|
}
|
|
@@ -731,6 +772,14 @@ export class WispyEngine {
|
|
|
731
772
|
"",
|
|
732
773
|
];
|
|
733
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
|
+
|
|
734
783
|
// Load WISPY.md context
|
|
735
784
|
const wispyMd = await this._loadWispyMd();
|
|
736
785
|
if (wispyMd) {
|
|
@@ -1059,6 +1108,89 @@ export class WispyEngine {
|
|
|
1059
1108
|
}
|
|
1060
1109
|
}
|
|
1061
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
|
+
|
|
1062
1194
|
// ── Cleanup ──────────────────────────────────────────────────────────────────
|
|
1063
1195
|
|
|
1064
1196
|
destroy() {
|
package/core/index.mjs
CHANGED
|
@@ -20,3 +20,7 @@ export { WispyServer } from "./server.mjs";
|
|
|
20
20
|
export { NodeManager, CAPABILITIES } from "./nodes.mjs";
|
|
21
21
|
export { Harness, Receipt, HarnessResult, computeUnifiedDiff } from "./harness.mjs";
|
|
22
22
|
export { DeployManager } from "./deploy.mjs";
|
|
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";
|
package/core/memory.mjs
CHANGED
|
@@ -39,7 +39,8 @@ export class MemoryManager {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Save (overwrite) a memory file
|
|
42
|
+
* Save (overwrite) a memory file.
|
|
43
|
+
* If auto-sync is enabled, pushes the file to remote (non-blocking).
|
|
43
44
|
*/
|
|
44
45
|
async save(key, content, metadata = {}) {
|
|
45
46
|
await this._ensureDir();
|
|
@@ -55,6 +56,14 @@ export class MemoryManager {
|
|
|
55
56
|
: `_Last updated: ${ts}_\n\n`;
|
|
56
57
|
|
|
57
58
|
await writeFile(filePath, header + content, "utf8");
|
|
59
|
+
|
|
60
|
+
// Auto-sync: push to remote if configured (fire-and-forget)
|
|
61
|
+
try {
|
|
62
|
+
const { getSyncManager } = await import("./sync.mjs");
|
|
63
|
+
const syncMgr = await getSyncManager();
|
|
64
|
+
if (syncMgr?.auto) syncMgr.pushFile(filePath);
|
|
65
|
+
} catch { /* sync is optional */ }
|
|
66
|
+
|
|
58
67
|
return { key, path: filePath };
|
|
59
68
|
}
|
|
60
69
|
|