wispy-cli 1.2.2 → 1.3.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 +180 -0
- package/core/config.mjs +12 -0
- package/core/engine.mjs +11 -0
- package/core/index.mjs +3 -1
- package/core/memory.mjs +10 -1
- package/core/onboarding.mjs +751 -0
- package/core/server.mjs +152 -2
- package/core/session.mjs +8 -0
- package/core/sync.mjs +682 -0
- package/lib/wispy-repl.mjs +9 -1
- package/package.json +1 -1
package/bin/wispy.mjs
CHANGED
|
@@ -22,8 +22,33 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
22
22
|
|
|
23
23
|
const args = process.argv.slice(2);
|
|
24
24
|
|
|
25
|
+
// ── setup / init sub-command ──────────────────────────────────────────────────
|
|
26
|
+
if (args[0] === "setup" || args[0] === "init") {
|
|
27
|
+
const { OnboardingWizard } = await import(
|
|
28
|
+
path.join(__dirname, "..", "core", "onboarding.mjs")
|
|
29
|
+
);
|
|
30
|
+
const wizard = new OnboardingWizard();
|
|
31
|
+
const sub = args[1]; // e.g. "provider", "channels", "security"
|
|
32
|
+
if (sub && sub !== "wizard") {
|
|
33
|
+
await wizard.runStep(sub);
|
|
34
|
+
} else {
|
|
35
|
+
await wizard.run();
|
|
36
|
+
}
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
// ── status sub-command ────────────────────────────────────────────────────────
|
|
26
41
|
if (args[0] === "status") {
|
|
42
|
+
// Try the enhanced status from onboarding.mjs first
|
|
43
|
+
try {
|
|
44
|
+
const { printStatus } = await import(
|
|
45
|
+
path.join(__dirname, "..", "core", "onboarding.mjs")
|
|
46
|
+
);
|
|
47
|
+
await printStatus();
|
|
48
|
+
process.exit(0);
|
|
49
|
+
} catch {}
|
|
50
|
+
|
|
51
|
+
// Fallback: original status (remote check)
|
|
27
52
|
const { readFile } = await import("node:fs/promises");
|
|
28
53
|
const { homedir } = await import("node:os");
|
|
29
54
|
const { join } = await import("node:path");
|
|
@@ -131,6 +156,135 @@ if (args[0] === "disconnect") {
|
|
|
131
156
|
process.exit(0);
|
|
132
157
|
}
|
|
133
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
|
+
|
|
134
288
|
// ── deploy sub-command ────────────────────────────────────────────────────────
|
|
135
289
|
if (args[0] === "deploy") {
|
|
136
290
|
const { DeployManager } = await import(
|
|
@@ -775,6 +929,32 @@ if (serveMode || telegramMode || discordMode || slackMode) {
|
|
|
775
929
|
await new Promise(() => {}); // keep alive
|
|
776
930
|
}
|
|
777
931
|
|
|
932
|
+
// ── First-run detection (before TUI or REPL) ──────────────────────────────────
|
|
933
|
+
// Only trigger onboarding for interactive modes (not flags like --serve, channel, etc.)
|
|
934
|
+
const isInteractiveStart = !args.some(a =>
|
|
935
|
+
["--serve", "--telegram", "--discord", "--slack", "--server",
|
|
936
|
+
"status", "setup", "init", "connect", "disconnect", "deploy",
|
|
937
|
+
"cron", "audit", "log", "server", "node", "channel", "sync"].includes(a)
|
|
938
|
+
);
|
|
939
|
+
|
|
940
|
+
if (isInteractiveStart) {
|
|
941
|
+
try {
|
|
942
|
+
const { isFirstRun } = await import(
|
|
943
|
+
path.join(__dirname, "..", "core", "config.mjs")
|
|
944
|
+
);
|
|
945
|
+
if (await isFirstRun()) {
|
|
946
|
+
const { OnboardingWizard } = await import(
|
|
947
|
+
path.join(__dirname, "..", "core", "onboarding.mjs")
|
|
948
|
+
);
|
|
949
|
+
const wizard = new OnboardingWizard();
|
|
950
|
+
await wizard.run();
|
|
951
|
+
// After onboarding, continue to REPL or TUI
|
|
952
|
+
}
|
|
953
|
+
} catch {
|
|
954
|
+
// If onboarding fails for any reason, continue normally
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
778
958
|
// ── TUI mode ──────────────────────────────────────────────────────────────────
|
|
779
959
|
const tuiMode = args.includes("--tui");
|
|
780
960
|
|
package/core/config.mjs
CHANGED
|
@@ -58,6 +58,18 @@ export async function saveConfig(config) {
|
|
|
58
58
|
await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Returns true if no config exists or onboarded flag is not set.
|
|
63
|
+
*/
|
|
64
|
+
export async function isFirstRun() {
|
|
65
|
+
try {
|
|
66
|
+
const cfg = await loadConfig();
|
|
67
|
+
return !cfg.onboarded;
|
|
68
|
+
} catch {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
61
73
|
export async function detectProvider() {
|
|
62
74
|
// 1. WISPY_PROVIDER env override
|
|
63
75
|
const forced = process.env.WISPY_PROVIDER;
|
package/core/engine.mjs
CHANGED
|
@@ -24,6 +24,7 @@ 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";
|
|
27
28
|
|
|
28
29
|
const MAX_TOOL_ROUNDS = 10;
|
|
29
30
|
const MAX_CONTEXT_CHARS = 40_000;
|
|
@@ -40,6 +41,7 @@ export class WispyEngine {
|
|
|
40
41
|
this.permissions = new PermissionManager(config.permissions ?? {});
|
|
41
42
|
this.audit = new AuditLog(WISPY_DIR);
|
|
42
43
|
this.harness = new Harness(this.tools, this.permissions, this.audit, config);
|
|
44
|
+
this.sync = null; // SyncManager, initialized lazily
|
|
43
45
|
this._initialized = false;
|
|
44
46
|
this._workMdContent = null;
|
|
45
47
|
this._workMdLoaded = false;
|
|
@@ -93,6 +95,15 @@ export class WispyEngine {
|
|
|
93
95
|
this.tools.registerMCP(this.mcpManager);
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
// Initialize sync manager (if configured)
|
|
99
|
+
try {
|
|
100
|
+
this.sync = await getSyncManager();
|
|
101
|
+
// Auto-sync on startup: pull any new data from remote
|
|
102
|
+
if (this.sync?.auto && this.sync.remoteUrl) {
|
|
103
|
+
this.sync.pull(this.sync.remoteUrl, this.sync.token).catch(() => {});
|
|
104
|
+
}
|
|
105
|
+
} catch { /* sync is optional */ }
|
|
106
|
+
|
|
96
107
|
this._initialized = true;
|
|
97
108
|
return this;
|
|
98
109
|
}
|
package/core/index.mjs
CHANGED
|
@@ -9,7 +9,8 @@ export { SessionManager, Session, sessionManager } from "./session.mjs";
|
|
|
9
9
|
export { ProviderRegistry } from "./providers.mjs";
|
|
10
10
|
export { ToolRegistry } from "./tools.mjs";
|
|
11
11
|
export { MCPClient, MCPManager, ensureDefaultMcpConfig } from "./mcp.mjs";
|
|
12
|
-
export { loadConfig, saveConfig, detectProvider, PROVIDERS, WISPY_DIR, MCP_CONFIG_PATH, SESSIONS_DIR, CONVERSATIONS_DIR, MEMORY_DIR } from "./config.mjs";
|
|
12
|
+
export { loadConfig, saveConfig, detectProvider, isFirstRun, PROVIDERS, WISPY_DIR, CONFIG_PATH, MCP_CONFIG_PATH, SESSIONS_DIR, CONVERSATIONS_DIR, MEMORY_DIR } from "./config.mjs";
|
|
13
|
+
export { OnboardingWizard, isFirstRun as checkFirstRun, printStatus } from "./onboarding.mjs";
|
|
13
14
|
export { MemoryManager } from "./memory.mjs";
|
|
14
15
|
export { CronManager } from "./cron.mjs";
|
|
15
16
|
export { SubAgentManager, SubAgent } from "./subagents.mjs";
|
|
@@ -19,3 +20,4 @@ export { WispyServer } from "./server.mjs";
|
|
|
19
20
|
export { NodeManager, CAPABILITIES } from "./nodes.mjs";
|
|
20
21
|
export { Harness, Receipt, HarnessResult, computeUnifiedDiff } from "./harness.mjs";
|
|
21
22
|
export { DeployManager } from "./deploy.mjs";
|
|
23
|
+
export { SyncManager, getSyncManager, sha256 } from "./sync.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
|
|