mover-os 4.6.3 → 4.6.5

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.
Files changed (3) hide show
  1. package/README.md +19 -17
  2. package/install.js +297 -118
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  Mover OS turns Obsidian into an AI-powered execution engine. It audits your behavior against your stated strategy, extracts reusable knowledge from daily work, and evolves based on your corrections.
8
8
 
9
- **Works best with Claude Code.** Supports 14 AI coding agents.
9
+ **Works best with Claude Code.** Supports 15 AI coding agents.
10
10
 
11
11
  **Version:** 4.3 | **Status:** Production
12
12
 
@@ -91,22 +91,24 @@ bash src/install/link.sh
91
91
 
92
92
  ## Supported Agents
93
93
 
94
- Works with **14 AI coding agents.** The installer auto-detects and configures each one:
94
+ Works with **15 AI coding agents.** The installer auto-detects and configures each one:
95
95
 
96
96
  | Agent | What Gets Installed |
97
97
  |-------|---------------------|
98
- | Claude Code | Rules + 22 commands + 61 skills + 6 hooks |
99
- | Cursor | Rules + 22 commands + 61 skills |
100
- | Cline | Rules + 61 skills |
101
- | Windsurf | Rules + 61 skills |
102
- | Gemini CLI | Rules + 22 workflows + 61 skills |
103
- | GitHub Copilot | Rules + 61 skills |
104
- | Codex CLI | Rules + 61 skills |
105
- | Codex (Cloud) | Rules + 61 skills |
106
- | Antigravity | Rules + 22 workflows |
107
- | OpenClaw | Rules + 61 skills |
108
- | Roo Code | Rules + 61 skills |
109
- | Amp | Rules + 61 skills |
98
+ | Claude Code | Rules + 23 commands + 62 skills + 18 hooks |
99
+ | Cursor | Rules + 23 commands + 62 skills |
100
+ | Cline | Rules + 62 skills |
101
+ | Windsurf | Rules + 62 skills |
102
+ | Gemini CLI | Rules + 23 workflows + 62 skills |
103
+ | GitHub Copilot | Rules + 62 skills |
104
+ | Codex | Rules + 62 skills |
105
+ | Antigravity | Rules + 23 workflows |
106
+ | Amazon Q | Rules + 62 skills |
107
+ | OpenCode | Rules + 62 skills |
108
+ | Kilo Code | Rules + 62 skills |
109
+ | Amp | Rules + 62 skills |
110
+ | Roo Code | Rules + 62 skills |
111
+ | Continue.dev | Rules + 62 skills |
110
112
  | Aider | Rules |
111
113
 
112
114
  > Claude Code gets the deepest integration — hooks, native slash commands, and the full correction lifecycle. Every other agent gets the core system.
@@ -196,10 +198,10 @@ Your `02_Areas/Engine/` folder is the brain. Core files that the AI reads before
196
198
  ```
197
199
  Mover OS Bundle/
198
200
  src/
199
- workflows/ # 22 AI command handlers (markdown)
201
+ workflows/ # 23 AI command handlers (markdown)
200
202
  system/ # Global Rules + Install Manifest
201
- hooks/ # 6 Claude Code lifecycle hooks
202
- skills/ # 61 curated skill packs
203
+ hooks/ # 18 hook files (11 registered in settings.json)
204
+ skills/ # 62 curated skill packs
203
205
  install/ # link.sh (hard-link re-linker)
204
206
  structure/ # Vault template (PARA + Engine)
205
207
  ```
package/install.js CHANGED
@@ -17,6 +17,39 @@ const { execSync } = require("child_process");
17
17
 
18
18
  const VERSION = "4";
19
19
 
20
+ // ─── JSON output helper ──────────────────────────────────────────────────────
21
+ function jsonOut(command, data, ok = true) {
22
+ const envelope = {
23
+ ok,
24
+ command,
25
+ version: require("./package.json").version,
26
+ timestamp: new Date().toISOString(),
27
+ data,
28
+ };
29
+ if (!ok && data.error) envelope.error = data.error;
30
+ process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
31
+ }
32
+
33
+ // ─── Output budget (30K chars = Anthropic bash tool truncation limit) ────────
34
+ const MAX_OUTPUT_CHARS = 28000; // Leave 2K buffer below 30K limit
35
+ let _outputCharCount = 0;
36
+ const _origWrite = process.stdout.write.bind(process.stdout);
37
+ if (!IS_TTY) {
38
+ // In non-TTY (agent) mode, track output size and warn before truncation
39
+ process.stdout.write = function(chunk, ...args) {
40
+ const str = typeof chunk === "string" ? chunk : chunk.toString();
41
+ _outputCharCount += str.length;
42
+ if (_outputCharCount > MAX_OUTPUT_CHARS && !_outputCharCount._warned) {
43
+ _origWrite("\n... output truncated (approaching 30K char limit) ...\n");
44
+ _outputCharCount._warned = true;
45
+ }
46
+ return _origWrite(chunk, ...args);
47
+ };
48
+ }
49
+
50
+ // ─── Exit codes (semantic, for agent parsing) ────────────────────────────────
51
+ const EXIT = { OK: 0, ERROR: 1, USAGE: 2, NOT_FOUND: 3, PERMISSION: 4, CONFLICT: 5, NETWORK: 6 };
52
+
20
53
  // ─── ANSI ────────────────────────────────────────────────────────────────────
21
54
  const IS_TTY = process.stdout.isTTY && process.stdin.isTTY;
22
55
  const S = IS_TTY
@@ -342,6 +375,25 @@ async function shimmerText(text, duration = 400) {
342
375
  w(`\r\x1b[2K ${text}\n`);
343
376
  }
344
377
 
378
+ async function installProgress(steps) {
379
+ const total = steps.length;
380
+ for (let i = 0; i < total; i++) {
381
+ const step = steps[i];
382
+ w(`\x1b[2K\r ${progressBar(i, total, 30)} ${S.dim}${step.label}${S.reset}`);
383
+ await step.fn();
384
+ w(`\x1b[2K\r ${progressBar(i + 1, total, 30)} ${S.green}\u2713${S.reset} ${step.label}\n`);
385
+ await sleep(60);
386
+ }
387
+ }
388
+
389
+ async function successAnimation(message) {
390
+ if (!IS_TTY) { ln(`\n ${message}\n`); return; }
391
+ ln();
392
+ await shimmerText(green(" \u2713"), 150);
393
+ await shimmerText(bold(message), 300);
394
+ ln();
395
+ }
396
+
345
397
  // ─── TUI: waitForEsc ────────────────────────────────────────────────────────
346
398
  function waitForEsc() {
347
399
  return new Promise((r) => {
@@ -363,37 +415,6 @@ function waitForEsc() {
363
415
  });
364
416
  }
365
417
 
366
- // ─── TUI: Animated progress for install/update ──────────────────────────────
367
- async function installProgress(steps) {
368
- const total = steps.length;
369
- for (let i = 0; i < total; i++) {
370
- const step = steps[i];
371
- w(`\x1b[2K\r ${progressBar(i, total, 30)} ${S.dim}${step.label}${S.reset}`);
372
- await step.fn();
373
- w(`\x1b[2K\r ${progressBar(i + 1, total, 30)} ${S.green}\u2713${S.reset} ${step.label}\n`);
374
- await sleep(60);
375
- }
376
- }
377
-
378
- // ─── TUI: Success animation ─────────────────────────────────────────────────
379
- async function successAnimation(message) {
380
- if (!IS_TTY) { ln(message); return; }
381
- const check = [
382
- " \u2713",
383
- " \u2713",
384
- " \u2713",
385
- " \u2713 \u2713",
386
- " \u2713",
387
- ];
388
- ln();
389
- for (const line of check) {
390
- await shimmerText(green(line), 200);
391
- }
392
- ln();
393
- await shimmerText(bold(message), 400);
394
- ln();
395
- }
396
-
397
418
  // ─── Clack-style frame ──────────────────────────────────────────────────────
398
419
  const BAR_COLOR = S.cyan;
399
420
  const bar = () => w(`${BAR_COLOR}│${S.reset}`);
@@ -583,8 +604,8 @@ function interactiveSelect(items, { multi = false, preSelected = [], defaultInde
583
604
  lines++;
584
605
 
585
606
  const hint = multi
586
- ? dim(" ↑↓ navigate space select a all enter confirm esc back")
587
- : dim(" ↑↓ navigate enter select esc back");
607
+ ? dim(` ↑↓ move space toggle a all enter done esc cancel${selected.size > 0 ? ` (${selected.size} selected)` : ""}`)
608
+ : dim(" ↑↓ move enter select esc cancel");
588
609
  w(`\x1b[2K${BAR_COLOR}│${S.reset}${hint}\n`);
589
610
  lines++;
590
611
 
@@ -658,7 +679,7 @@ function interactiveSelect(items, { multi = false, preSelected = [], defaultInde
658
679
  // No hardcoded keys — every key must be a real Polar license.
659
680
 
660
681
  async function validateKey(key) {
661
- if (!key) return false;
682
+ if (!key) return { valid: false, reason: "empty" };
662
683
  const k = key.trim();
663
684
 
664
685
  // Polar license key validation
@@ -679,15 +700,19 @@ async function validateKey(key) {
679
700
  try { resolve(JSON.parse(data)); } catch { reject(new Error("Invalid response")); }
680
701
  });
681
702
  });
682
- req.on("error", reject);
683
- req.on("timeout", () => { req.destroy(); reject(new Error("Timeout")); });
703
+ req.on("error", (err) => reject(Object.assign(err, { _isNetwork: true })));
704
+ req.on("timeout", () => { req.destroy(); reject(Object.assign(new Error("Timeout"), { _isNetwork: true })); });
684
705
  req.write(body);
685
706
  req.end();
686
707
  });
687
- return result.status === "granted";
688
- } catch {
689
- // Network error cannot validate without Polar
690
- return false;
708
+ return result.status === "granted"
709
+ ? { valid: true }
710
+ : { valid: false, reason: "rejected" };
711
+ } catch (err) {
712
+ if (err._isNetwork || err.code === "ENOTFOUND" || err.code === "ECONNREFUSED" || err.code === "ETIMEDOUT") {
713
+ return { valid: false, reason: "network", error: err.message };
714
+ }
715
+ return { valid: false, reason: "rejected" };
691
716
  }
692
717
  }
693
718
 
@@ -877,6 +902,7 @@ const CLI_COMMANDS = {
877
902
  backup: { desc: "Manual backup wizard", alias: [] },
878
903
  restore: { desc: "Restore from backup", alias: [] },
879
904
  prayer: { desc: "Manage prayer times", alias: [] },
905
+ status: { desc: "Full system state (vault, agents, version)", alias: [] },
880
906
  help: { desc: "Interactive guide to Mover OS", alias: ["-h"] },
881
907
  test: { desc: "Run integration tests (dev)", alias: [], hidden: true },
882
908
  };
@@ -893,6 +919,10 @@ function parseArgs() {
893
919
  // Backward compat: --update / -u → command 'update'
894
920
  if (a === "--update" || a === "-u") { opts.command = "update"; continue; }
895
921
  if (a === "--_self-updated") { opts._selfUpdated = true; continue; }
922
+ if (a === "--yes" || a === "-y") { opts.yes = true; continue; }
923
+ if (a === "--json") { opts.json = true; continue; }
924
+ if (a === "--quiet" || a === "-q") { opts.quiet = true; continue; }
925
+ if (a === "--dry-run") { opts.dryRun = true; continue; }
896
926
  if (a === "--help" || a === "-h") {
897
927
  ln();
898
928
  ln(` ${bold("moveros")} ${dim("— the Mover OS companion CLI")}`);
@@ -908,6 +938,10 @@ function parseArgs() {
908
938
  ln(` ${dim("Options")}`);
909
939
  ln(` --key KEY License key (skip interactive prompt)`);
910
940
  ln(` --vault PATH Obsidian vault path (skip detection)`);
941
+ ln(` --yes, -y Accept defaults (non-interactive mode)`);
942
+ ln(` --json Structured JSON output (for status command)`);
943
+ ln(` --quiet, -q Minimal output, one value per line`);
944
+ ln(` --dry-run Show what would change without doing it`);
911
945
  ln(` --update, -u Quick update (backward compat)`);
912
946
  ln();
913
947
  ln(` ${dim("Run")} moveros ${dim("with no args for interactive menu")}`);
@@ -2401,9 +2435,8 @@ function writeMoverConfig(vaultPath, agentIds, licenseKey, opts = {}) {
2401
2435
  vaultPath: vaultPath,
2402
2436
  agents: agentIds,
2403
2437
  feedbackWebhook: "https://moveros.dev/api/feedback",
2404
- track_food: true,
2405
- track_sleep: true,
2406
2438
  installedAt: new Date().toISOString(),
2439
+ settings: { track_food: true, track_sleep: true, friction_level: 3, review_day: "Sunday" },
2407
2440
  };
2408
2441
  if (licenseKey) config.licenseKey = licenseKey;
2409
2442
  // If config exists, preserve existing values
@@ -2413,10 +2446,15 @@ function writeMoverConfig(vaultPath, agentIds, licenseKey, opts = {}) {
2413
2446
  if (existing.installedAt) config.installedAt = existing.installedAt;
2414
2447
  if (existing.licenseKey && !licenseKey) config.licenseKey = existing.licenseKey;
2415
2448
  if (existing.feedbackWebhook) config.feedbackWebhook = existing.feedbackWebhook;
2416
- if (existing.track_food !== undefined) config.track_food = existing.track_food;
2417
- if (existing.track_sleep !== undefined) config.track_sleep = existing.track_sleep;
2449
+ // Migrate root-level track_food/track_sleep to settings block (legacy compat)
2450
+ if (existing.track_food !== undefined && !existing.settings?.track_food) {
2451
+ config.settings.track_food = existing.track_food;
2452
+ }
2453
+ if (existing.track_sleep !== undefined && !existing.settings?.track_sleep) {
2454
+ config.settings.track_sleep = existing.track_sleep;
2455
+ }
2418
2456
  // Preserve settings block (prayer times, review_day, etc.)
2419
- if (existing.settings) config.settings = { ...existing.settings };
2457
+ if (existing.settings) config.settings = { ...config.settings, ...existing.settings };
2420
2458
  // Preserve prayer_times fallback
2421
2459
  if (existing.prayer_times) config.prayer_times = existing.prayer_times;
2422
2460
  // Preserve frecency data
@@ -3300,6 +3338,7 @@ const CLI_HANDLERS = {
3300
3338
  settings: async (opts) => { await cmdSettings(opts); },
3301
3339
  backup: async (opts) => { await cmdBackup(opts); },
3302
3340
  restore: async (opts) => { await cmdRestore(opts); },
3341
+ status: async (opts) => { await cmdStatus(opts); },
3303
3342
  doctor: async (opts) => { await cmdDoctor(opts); },
3304
3343
  prayer: async (opts) => { await cmdPrayer(opts); },
3305
3344
  help: async (opts) => { await cmdHelp(opts); },
@@ -3307,11 +3346,54 @@ const CLI_HANDLERS = {
3307
3346
  test: async (opts) => { await cmdTest(opts); },
3308
3347
  };
3309
3348
 
3349
+ // ─── moveros status ──────────────────────────────────────────────────────────
3350
+ async function cmdStatus(opts) {
3351
+ const vault = requireVault(opts.vault);
3352
+ if (!vault) return;
3353
+ const home = os.homedir();
3354
+ const cfgPath = path.join(home, ".mover", "config.json");
3355
+ let cfg = {};
3356
+ if (fs.existsSync(cfgPath)) { try { cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8")); } catch {} }
3357
+ const agents = cfg.agents || [];
3358
+ const ver = fs.existsSync(path.join(vault, ".mover-version")) ? fs.readFileSync(path.join(vault, ".mover-version"), "utf8").trim() : "unknown";
3359
+ const engineDir = path.join(vault, "02_Areas", "Engine");
3360
+ const engineFiles = ["Identity_Prime.md", "Strategy.md", "Active_Context.md", "Goals.md", "Mover_Dossier.md", "Auto_Learnings.md"];
3361
+ const engineOk = engineFiles.filter((f) => fs.existsSync(path.join(engineDir, f))).length;
3362
+
3363
+ const data = {
3364
+ vault: vault,
3365
+ version: ver,
3366
+ agents: agents,
3367
+ agentCount: agents.length,
3368
+ engineFiles: { present: engineOk, total: engineFiles.length },
3369
+ settings: cfg.settings || {},
3370
+ licenseKey: cfg.licenseKey ? cfg.licenseKey.substring(0, 12) + "..." : null,
3371
+ hasManifest: fs.existsSync(path.join(home, ".mover", "manifest.json")),
3372
+ };
3373
+
3374
+ if (opts.json) {
3375
+ jsonOut("status", data);
3376
+ return;
3377
+ }
3378
+
3379
+ barLn(bold(" System Status"));
3380
+ barLn();
3381
+ barLn(` ${bold("Vault:")} ${vault}`);
3382
+ barLn(` ${bold("Version:")} ${ver}`);
3383
+ barLn(` ${bold("Agents:")} ${agents.length} (${agents.join(", ")})`);
3384
+ barLn(` ${bold("Engine:")} ${engineOk}/${engineFiles.length} files present`);
3385
+ barLn(` ${bold("License:")} ${data.licenseKey || dim("not set")}`);
3386
+ barLn(` ${bold("Manifest:")} ${data.hasManifest ? green("present") : yellow("missing (legacy install)")}`);
3387
+ barLn(` ${bold("Settings:")} friction=${cfg.settings?.friction_level || 3}, review=${cfg.settings?.review_day || "Sunday"}, food=${cfg.settings?.track_food !== false ? "on" : "off"}, sleep=${cfg.settings?.track_sleep !== false ? "on" : "off"}`);
3388
+ barLn();
3389
+ barLn(dim(" Run moveros doctor for detailed health check."));
3390
+ }
3391
+
3310
3392
  // ─── moveros doctor ─────────────────────────────────────────────────────────
3311
3393
  async function cmdDoctor(opts) {
3312
3394
  const vault = resolveVaultPath(opts.vault);
3313
3395
  if (!vault) {
3314
- barLn(red("No Mover OS vault found. Use: moveros doctor --vault /path"));
3396
+ // handled by requireVault()
3315
3397
  return;
3316
3398
  }
3317
3399
  const home = os.homedir();
@@ -3395,13 +3477,16 @@ async function cmdDoctor(opts) {
3395
3477
 
3396
3478
  barLn();
3397
3479
  barLn(dim(" Run moveros install or moveros update to fix any issues."));
3480
+
3481
+ barLn();
3482
+ barLn(dim(" Fix any issues above, then run /mover-check in your AI agent for deeper validation."));
3398
3483
  }
3399
3484
 
3400
3485
  // ─── moveros pulse ──────────────────────────────────────────────────────────
3401
3486
  async function cmdPulse(opts) {
3402
3487
  const vault = resolveVaultPath(opts.vault);
3403
3488
  if (!vault) {
3404
- barLn(red("No vault found. Use: moveros pulse --vault /path"));
3489
+ // handled by requireVault()
3405
3490
  return;
3406
3491
  }
3407
3492
  const engineDir = path.join(vault, "02_Areas", "Engine");
@@ -3507,14 +3592,16 @@ async function cmdPulse(opts) {
3507
3592
  } catch {}
3508
3593
  }
3509
3594
  barLn();
3595
+
3596
+ barLn();
3597
+ barLn(dim(" Run /morning for full session primer or moveros capture for quick capture."));
3510
3598
  }
3511
3599
 
3512
3600
  // cmdWarm removed — hooks + rules + /morning handle session priming
3513
3601
 
3514
3602
  // ─── moveros capture ────────────────────────────────────────────────────────
3515
3603
  async function cmdCapture(opts) {
3516
- const vault = resolveVaultPath(opts.vault);
3517
- if (!vault) { barLn(red("No vault found.")); return; }
3604
+ const vault = requireVault(opts.vault);
3518
3605
 
3519
3606
  const now = new Date();
3520
3607
  const ymd = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
@@ -3590,8 +3677,7 @@ async function cmdCapture(opts) {
3590
3677
 
3591
3678
  // ─── moveros who ────────────────────────────────────────────────────────────
3592
3679
  async function cmdWho(opts) {
3593
- const vault = resolveVaultPath(opts.vault);
3594
- if (!vault) { barLn(red("No vault found.")); return; }
3680
+ const vault = requireVault(opts.vault);
3595
3681
 
3596
3682
  const name = opts.rest.join(" ").trim();
3597
3683
  if (!name) { barLn(yellow("Usage: moveros who <name>")); return; }
@@ -3638,12 +3724,14 @@ async function cmdWho(opts) {
3638
3724
  for (const l of lines) barLn(` ${l}`);
3639
3725
  barLn();
3640
3726
  }
3727
+
3728
+ barLn();
3729
+ barLn(dim(" Entities live in 03_Library/Entities/. Run /harvest to extract people from sessions."));
3641
3730
  }
3642
3731
 
3643
3732
  // ─── moveros diff ───────────────────────────────────────────────────────────
3644
3733
  async function cmdDiff(opts) {
3645
- const vault = resolveVaultPath(opts.vault);
3646
- if (!vault) { barLn(red("No vault found.")); return; }
3734
+ const vault = requireVault(opts.vault);
3647
3735
  if (!cmdExists("git")) { barLn(red("Git required for moveros diff.")); return; }
3648
3736
 
3649
3737
  const target = opts.rest[0] || "strategy";
@@ -3697,12 +3785,14 @@ async function cmdDiff(opts) {
3697
3785
  barLn(dim(" Not a git repo or no history available."));
3698
3786
  }
3699
3787
  barLn();
3788
+
3789
+ barLn();
3790
+ barLn(dim(" Run /history in your AI agent for full Engine evolution analysis."));
3700
3791
  }
3701
3792
 
3702
3793
  // ─── moveros sync ───────────────────────────────────────────────────────────
3703
3794
  async function cmdSync(opts) {
3704
- const vault = resolveVaultPath(opts.vault);
3705
- if (!vault) { barLn(red("No vault found.")); return; }
3795
+ const vault = requireVault(opts.vault);
3706
3796
  const apply = opts.rest.includes("--apply");
3707
3797
  const home = os.homedir();
3708
3798
 
@@ -3775,12 +3865,14 @@ async function cmdSync(opts) {
3775
3865
  barLn(dim(` ${staleCount} agent(s) need updating. Run: moveros sync --apply`));
3776
3866
  }
3777
3867
  barLn();
3868
+
3869
+ barLn();
3870
+ barLn(dim(" Run moveros update for comprehensive update with merge support."));
3778
3871
  }
3779
3872
 
3780
3873
  // ─── moveros replay ─────────────────────────────────────────────────────────
3781
3874
  async function cmdReplay(opts) {
3782
- const vault = resolveVaultPath(opts.vault);
3783
- if (!vault) { barLn(red("No vault found.")); return; }
3875
+ const vault = requireVault(opts.vault);
3784
3876
 
3785
3877
  const engineDir = path.join(vault, "02_Areas", "Engine");
3786
3878
  let dateStr = opts.rest.find((a) => a.startsWith("--date="))?.split("=")[1];
@@ -3837,12 +3929,14 @@ async function cmdReplay(opts) {
3837
3929
  barLn(` ${progressBar(done, tasks.length, 25, "Completion")}`);
3838
3930
  barLn();
3839
3931
  }
3932
+
3933
+ barLn();
3934
+ barLn(dim(" Run /log in your AI agent for full session capture."));
3840
3935
  }
3841
3936
 
3842
3937
  // ─── moveros context ────────────────────────────────────────────────────────
3843
3938
  async function cmdContext(opts) {
3844
- const vault = resolveVaultPath(opts.vault);
3845
- if (!vault) { barLn(red("No vault found.")); return; }
3939
+ const vault = requireVault(opts.vault);
3846
3940
 
3847
3941
  const target = opts.rest[0];
3848
3942
  const home = os.homedir();
@@ -4230,23 +4324,57 @@ async function cmdSettings(opts) {
4230
4324
  };
4231
4325
  });
4232
4326
 
4327
+ items.push({ id: "_reset", name: dim("Reset all to defaults"), tier: "Resets every setting to its default value" });
4233
4328
  question(dim(" Select a setting to edit (esc to go back)"));
4234
4329
  barLn();
4235
4330
  const picked = await interactiveSelect(items);
4236
4331
  if (!picked) return;
4237
4332
 
4333
+ if (picked === "_reset") {
4334
+ cfg.settings = {};
4335
+ for (const [k, m] of Object.entries(KNOWN_SETTINGS)) {
4336
+ cfg.settings[k] = m.defaults;
4337
+ }
4338
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
4339
+ statusLine("ok", "All settings", "reset to defaults");
4340
+ barLn();
4341
+ return;
4342
+ }
4343
+
4238
4344
  const meta = KNOWN_SETTINGS[picked];
4239
4345
  const current = settings[picked] !== undefined ? settings[picked] : meta.defaults;
4240
4346
 
4241
4347
  if (meta.type === "boolean") {
4242
- // Toggle immediately
4243
4348
  const newVal = !current;
4244
4349
  if (!cfg.settings) cfg.settings = {};
4245
4350
  cfg.settings[picked] = newVal;
4246
4351
  fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
4247
4352
  statusLine("ok", picked, newVal ? "on" : "off");
4353
+ } else if (picked === "friction_level") {
4354
+ const flItems = [
4355
+ { id: "1", name: "1 — Gentle", tier: "Surface conflicts, easy to override" },
4356
+ { id: "2", name: "2 — Moderate", tier: "Ask for justification on off-plan work" },
4357
+ { id: "3", name: "3 — Firm", tier: "Hold the line, identify avoidance (recommended)" },
4358
+ { id: "4", name: "4 — Hard block", tier: "Refuse destructive actions without confirmation" },
4359
+ ];
4360
+ const flPick = await interactiveSelect(flItems, { defaultIndex: (current || 3) - 1 });
4361
+ if (flPick) {
4362
+ if (!cfg.settings) cfg.settings = {};
4363
+ cfg.settings[picked] = parseInt(flPick, 10);
4364
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
4365
+ statusLine("ok", picked, flPick);
4366
+ }
4367
+ } else if (picked === "review_day") {
4368
+ const days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
4369
+ const dayItems = days.map((d) => ({ id: d, name: d, tier: d === "Sunday" ? "Default — end-of-week reflection" : "" }));
4370
+ const dayPick = await interactiveSelect(dayItems, { defaultIndex: days.indexOf(current || "Sunday") });
4371
+ if (dayPick) {
4372
+ if (!cfg.settings) cfg.settings = {};
4373
+ cfg.settings[picked] = dayPick;
4374
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
4375
+ statusLine("ok", picked, dayPick);
4376
+ }
4248
4377
  } else {
4249
- // Prompt for new value
4250
4378
  const answer = await textInput({ label: `${picked}`, initial: String(current) });
4251
4379
  if (answer === null || answer.trim() === "" || answer === String(current)) return;
4252
4380
  let val = answer.trim();
@@ -4261,8 +4389,7 @@ async function cmdSettings(opts) {
4261
4389
 
4262
4390
  // ─── moveros backup ─────────────────────────────────────────────────────────
4263
4391
  async function cmdBackup(opts) {
4264
- const vault = resolveVaultPath(opts.vault);
4265
- if (!vault) { barLn(red("No vault found.")); return; }
4392
+ const vault = requireVault(opts.vault);
4266
4393
  const home = os.homedir();
4267
4394
 
4268
4395
  barLn(bold(" Backup"));
@@ -4359,8 +4486,7 @@ async function cmdBackup(opts) {
4359
4486
 
4360
4487
  // ─── moveros restore ────────────────────────────────────────────────────────
4361
4488
  async function cmdRestore(opts) {
4362
- const vault = resolveVaultPath(opts.vault);
4363
- if (!vault) { barLn(red("No vault found.")); return; }
4489
+ const vault = requireVault(opts.vault);
4364
4490
 
4365
4491
  const archivesDir = path.join(vault, "04_Archives");
4366
4492
  if (!fs.existsSync(archivesDir)) { barLn(yellow(" No archives found.")); return; }
@@ -4957,6 +5083,16 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
4957
5083
  const npmVer = execSync("npm view mover-os version", { encoding: "utf8", timeout: 10000 }).trim();
4958
5084
  if (npmVer && npmVer !== localVer && compareVersions(npmVer, localVer) > 0) {
4959
5085
  barLn(`${yellow("CLI update available:")} ${dim(localVer)} ${dim("\u2192")} ${green(npmVer)}`);
5086
+ const selfUpdateChoice = IS_TTY ? await interactiveSelect(
5087
+ [
5088
+ { id: "yes", name: "Update CLI first", tier: `Downloads v${npmVer} before proceeding` },
5089
+ { id: "no", name: "Skip, continue with current", tier: `Stay on v${localVer}` },
5090
+ ],
5091
+ { multi: false, defaultIndex: 0 }
5092
+ ) : "yes";
5093
+ if (selfUpdateChoice === "no" || !selfUpdateChoice) {
5094
+ statusLine("ok", "CLI", `${localVer} (update skipped)`);
5095
+ } else {
4960
5096
  const sp = spinner("Updating CLI");
4961
5097
  try {
4962
5098
  try {
@@ -4979,6 +5115,7 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
4979
5115
  sp.stop(yellow(`CLI self-update failed: ${e.message}`));
4980
5116
  barLn(dim(" Try: sudo npm i -g mover-os"));
4981
5117
  }
5118
+ } // end selfUpdateChoice === "yes"
4982
5119
  } else {
4983
5120
  statusLine("ok", "CLI", `up to date (${localVer})`);
4984
5121
  }
@@ -5010,13 +5147,20 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
5010
5147
  if (!updateKey) return;
5011
5148
  }
5012
5149
  const sp1 = spinner("Validating license");
5013
- const keyValid = await validateKey(updateKey);
5014
- if (!keyValid && keyFromConfig) {
5150
+ const keyResult = await validateKey(updateKey);
5151
+ if (!keyResult.valid && keyResult.reason === "network" && keyFromConfig) {
5015
5152
  sp1.stop(dim("License check skipped (offline — using stored key)"));
5016
- } else if (!keyValid) {
5017
- sp1.stop(red("Invalid key"));
5018
- outro(red("Valid license key required."));
5153
+ } else if (!keyResult.valid && keyResult.reason === "network") {
5154
+ sp1.stop(yellow("Could not connect"));
5155
+ barLn(yellow(" Could not reach license server. Check your internet connection and try again."));
5156
+ outro("Update cancelled.");
5019
5157
  process.exit(1);
5158
+ } else if (!keyResult.valid) {
5159
+ sp1.stop(red("Key not recognized"));
5160
+ barLn(red(" This license key was not recognized. Check your email from Polar for the correct key."));
5161
+ barLn(dim(" Purchase at https://moveros.dev if you need a key."));
5162
+ outro("Update cancelled.");
5163
+ process.exit(EXIT.PERMISSION);
5020
5164
  } else {
5021
5165
  sp1.stop(green("License verified"));
5022
5166
  }
@@ -5055,7 +5199,7 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
5055
5199
 
5056
5200
  // Quick mode: force-apply (CI/headless — no user customizations to protect)
5057
5201
  if (isQuick) {
5058
- if (detectedAgents.length === 0) { outro(red("No AI agents detected.")); process.exit(1); }
5202
+ if (detectedAgents.length === 0) { outro(red("No AI agents detected.")); process.exit(EXIT.NOT_FOUND); }
5059
5203
  const changes = detectChanges(bundleDir, vaultPath, selectedIds);
5060
5204
  const totalChanged = countChanges(changes);
5061
5205
  displayChangeSummary(changes, installedVer, newVer);
@@ -5085,7 +5229,7 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
5085
5229
  writeMoverConfig(vaultPath, selectedIds);
5086
5230
  barLn();
5087
5231
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
5088
- outro(`${green("Done.")} ${totalChanged} files updated in ${elapsed}s.`);
5232
+ await successAnimation(`${totalChanged} files updated in ${elapsed}s.`);
5089
5233
  return;
5090
5234
  }
5091
5235
 
@@ -5394,6 +5538,16 @@ function resolveVaultPath(explicitVault) {
5394
5538
  return obsVaults.find((p) => fs.existsSync(path.join(p, ".mover-version"))) || null;
5395
5539
  }
5396
5540
 
5541
+ function requireVault(explicitVault) {
5542
+ const v = resolveVaultPath(explicitVault);
5543
+ if (!v) {
5544
+ barLn(red(" No Mover OS vault found."));
5545
+ barLn(dim(" Use --vault /path or run moveros install to set up."));
5546
+ return null;
5547
+ }
5548
+ return v;
5549
+ }
5550
+
5397
5551
  // ─── Main ───────────────────────────────────────────────────────────────────
5398
5552
  async function main() {
5399
5553
  const opts = parseArgs();
@@ -5489,6 +5643,7 @@ async function main() {
5489
5643
  }
5490
5644
 
5491
5645
  if (!key) {
5646
+ barLn(dim(" Purchased at moveros.dev — check your email for the key."));
5492
5647
  let validated = false;
5493
5648
  let attempts = 0;
5494
5649
  while (attempts < 3) {
@@ -5500,15 +5655,21 @@ async function main() {
5500
5655
  if (key === null) return;
5501
5656
 
5502
5657
  const sp = spinner("Validating...");
5503
- const valid = await validateKey(key);
5504
- if (valid) {
5658
+ const keyResult = await validateKey(key);
5659
+ if (keyResult.valid) {
5505
5660
  sp.stop(green("License verified"));
5506
5661
  await activateKey(key);
5507
5662
  validated = true;
5508
5663
  break;
5509
5664
  }
5510
5665
 
5511
- sp.stop(red("Invalid key"));
5666
+ if (keyResult.reason === "network") {
5667
+ sp.stop(yellow("Could not connect"));
5668
+ barLn(yellow(" Check your internet connection and try again."));
5669
+ } else {
5670
+ sp.stop(red("Key not recognized"));
5671
+ barLn(red(" Check your email from Polar for the correct key."));
5672
+ }
5512
5673
  attempts++;
5513
5674
  if (attempts < 3) {
5514
5675
  barLn(red("Try again."));
@@ -5526,15 +5687,19 @@ async function main() {
5526
5687
  }
5527
5688
  } else {
5528
5689
  const sp = spinner("Validating license...");
5529
- if (!await validateKey(key)) {
5530
- sp.stop(red("Invalid key"));
5690
+ const storedKeyResult = await validateKey(key);
5691
+ if (!storedKeyResult.valid && storedKeyResult.reason === "network") {
5692
+ sp.stop(dim("Offline — using stored key"));
5693
+ } else if (!storedKeyResult.valid) {
5694
+ sp.stop(red("Stored key not recognized"));
5531
5695
  barLn();
5532
5696
  barLn(dim("Get a key at https://moveros.dev"));
5533
5697
  outro("Cancelled.");
5534
5698
  process.exit(1);
5699
+ } else {
5700
+ sp.stop(green("License verified"));
5701
+ await activateKey(key);
5535
5702
  }
5536
- sp.stop(green("License verified"));
5537
- await activateKey(key);
5538
5703
  barLn();
5539
5704
  }
5540
5705
 
@@ -5547,9 +5712,26 @@ async function main() {
5547
5712
  dlSp.stop(green("Downloaded"));
5548
5713
  } catch (err) {
5549
5714
  dlSp.stop(red("Download failed"));
5550
- barLn(red(err.message));
5551
- barLn();
5552
- barLn(dim("Check your connection and try again."));
5715
+ const msg = err.message || "";
5716
+ if (msg.includes("Timeout") || msg.includes("timeout")) {
5717
+ barLn(red(" Connection timed out."));
5718
+ barLn(dim(" Check your internet connection and try again."));
5719
+ } else if (msg.includes("401") || msg.includes("rejected")) {
5720
+ barLn(red(" License key rejected by server."));
5721
+ barLn(dim(" Has your key expired? Check polar.sh or email support@moveros.dev."));
5722
+ } else if (msg.includes("500") || msg.includes("502") || msg.includes("503")) {
5723
+ barLn(red(" Server error."));
5724
+ barLn(dim(" Try again in a few minutes. If it persists, email support@moveros.dev."));
5725
+ } else if (msg.includes("extract") || msg.includes("tar")) {
5726
+ barLn(red(" Failed to extract payload."));
5727
+ barLn(dim(" Ensure 'tar' is available. On Windows, install Git Bash."));
5728
+ } else if (msg.includes("Untrusted")) {
5729
+ barLn(red(" Unexpected download redirect."));
5730
+ barLn(dim(" This may be a bug. Report at github.com/azkhh/Mover-OS/issues."));
5731
+ } else {
5732
+ barLn(red(` ${msg}`));
5733
+ barLn(dim(" Check your internet connection and try again."));
5734
+ }
5553
5735
  outro("Cancelled.");
5554
5736
  process.exit(1);
5555
5737
  }
@@ -5642,7 +5824,7 @@ async function main() {
5642
5824
  barLn(dim("You need at least one to use Mover OS. Recommended: Claude Code (claude.ai/code)"));
5643
5825
  barLn();
5644
5826
  }
5645
- question(`Select your AI agents${detectedIds.length > 0 ? dim(" (detected agents pre-selected)") : ""}`);
5827
+ question(`[1/4] Select your AI agents${detectedIds.length > 0 ? dim(" (detected agents pre-selected)") : ""}`);
5646
5828
  barLn();
5647
5829
 
5648
5830
  const selectedIds = await interactiveSelect(agentItems, {
@@ -5674,7 +5856,7 @@ async function main() {
5674
5856
  catCounts[sk.category] = (catCounts[sk.category] || 0) + 1;
5675
5857
  }
5676
5858
 
5677
- question(`${bold(String(allSkills.length))} skill packs available. Select categories:`);
5859
+ question(`[2/4] ${bold(String(allSkills.length))} skill packs available. Select categories:`);
5678
5860
  barLn();
5679
5861
 
5680
5862
  const categoryItems = CATEGORY_META.map((c) => ({
@@ -5705,7 +5887,7 @@ async function main() {
5705
5887
  let installStatusLine = false;
5706
5888
  if (selectedIds.includes("claude-code")) {
5707
5889
  barLn();
5708
- question("Install Claude Code status line?");
5890
+ question("[3/4] Install Claude Code status line?");
5709
5891
  barLn(dim(" Live status bar with model, context %, project, session cost, and Mover OS data."));
5710
5892
  barLn(dim(" Example: Opus 4.6 · 24% · my-project (main) · 2h14m · $12.50"));
5711
5893
  barLn(dim(" ▸ next task · 2/5 done · Sleep by 22:00 · logged 30m ago"));
@@ -5722,24 +5904,10 @@ async function main() {
5722
5904
  installStatusLine = slChoice === "yes";
5723
5905
  }
5724
5906
 
5725
- // ── Prayer times (optional) ──
5907
+ // ── Prayer times — moved out of install flow (available via: moveros prayer) ──
5726
5908
  let prayerSetup = false;
5727
- {
5728
- barLn();
5729
- question("Would you like prayer time reminders?");
5730
- barLn(dim(" Shows next prayer time in the status line and Daily Notes."));
5731
- barLn(dim(" Designed for Muslims — skip if not relevant to you."));
5732
- barLn();
5733
-
5734
- const ptChoice = await interactiveSelect(
5735
- [
5736
- { id: "yes", name: "Yes, set up prayer times", tier: "You can paste your mosque's timetable or fetch by city" },
5737
- { id: "no", name: "No, skip", tier: "You can enable later with: moveros prayer" },
5738
- ],
5739
- { multi: false, defaultIndex: 1 }
5740
- );
5741
- if (!ptChoice || ptChoice === "no") { /* skip prayer setup */ }
5742
- else if (ptChoice === "yes") {
5909
+ if (false) { // Prayer setup deferred to post-install: moveros prayer
5910
+ {
5743
5911
  prayerSetup = true;
5744
5912
  barLn();
5745
5913
  question("How would you like to set up prayer times?");
@@ -5840,7 +6008,7 @@ async function main() {
5840
6008
  let confirmed = false;
5841
6009
  while (!confirmed) {
5842
6010
  barLn();
5843
- question(bold("Review your selections") + dim(" (enter to change, esc to cancel)"));
6011
+ question(bold("[4/4] Review your selections") + dim(" (enter to change, esc to cancel)"));
5844
6012
  barLn();
5845
6013
 
5846
6014
  const agentNames = selectedAgents.map((a) => a.name).join(", ");
@@ -5925,12 +6093,20 @@ async function main() {
5925
6093
  sCfg.settings[sPick] = !(sCfg.settings[sPick] !== undefined ? sCfg.settings[sPick] : sMeta.defaults);
5926
6094
  } else if (sPick === "friction_level") {
5927
6095
  const cur = sCfg.settings[sPick] || sMeta.defaults;
5928
- sCfg.settings[sPick] = cur >= 4 ? 1 : cur + 1;
6096
+ const flItems = [
6097
+ { id: "1", name: "1 — Gentle", tier: "Surface conflicts, easy to override" },
6098
+ { id: "2", name: "2 — Moderate", tier: "Ask for justification on off-plan work" },
6099
+ { id: "3", name: "3 — Firm", tier: "Hold the line, identify avoidance (recommended)" },
6100
+ { id: "4", name: "4 — Hard block", tier: "Refuse destructive actions without confirmation" },
6101
+ ];
6102
+ const flPick = await interactiveSelect(flItems, { defaultIndex: cur - 1 });
6103
+ if (flPick) sCfg.settings[sPick] = parseInt(flPick, 10);
5929
6104
  } else if (sPick === "review_day") {
5930
6105
  const days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
5931
6106
  const cur = sCfg.settings[sPick] || sMeta.defaults;
5932
- const idx = days.indexOf(cur);
5933
- sCfg.settings[sPick] = days[(idx + 1) % days.length];
6107
+ const dayItems = days.map((d) => ({ id: d, name: d, tier: d === "Sunday" ? "Default — end-of-week reflection" : "" }));
6108
+ const dayPick = await interactiveSelect(dayItems, { defaultIndex: days.indexOf(cur) });
6109
+ if (dayPick) sCfg.settings[sPick] = dayPick;
5934
6110
  }
5935
6111
  fs.mkdirSync(path.dirname(sCfgPath), { recursive: true });
5936
6112
  fs.writeFileSync(sCfgPath, JSON.stringify(sCfg, null, 2), "utf8");
@@ -6106,7 +6282,7 @@ async function main() {
6106
6282
 
6107
6283
  // ── Done ──
6108
6284
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
6109
- outro(`${green("Done.")} Mover OS v${VERSION} installed. ${dim(`${totalSteps} steps in ${elapsed}s`)}`);
6285
+ await successAnimation(`Mover OS v${VERSION} installed. ${dim(`${totalSteps} steps in ${elapsed}s`)}`);
6110
6286
 
6111
6287
  // Size check on installed rules files
6112
6288
  const rulesFile = path.join(bundleDir, "src", "system", "Mover_Global_Rules.md");
@@ -6128,25 +6304,27 @@ async function main() {
6128
6304
  ln(` ${bold("What was installed")}`);
6129
6305
  ln();
6130
6306
  ln(` ${green("▸")} ${bold("23")} workflows ${dim("slash commands for daily rhythm, projects, strategy")}`);
6131
- ln(` ${green("▸")} ${bold("63")} skills ${dim("curated packs for dev, marketing, CRO, design")}`);
6307
+ ln(` ${green("▸")} ${bold("62")} skills ${dim("curated packs for dev, marketing, CRO, design")}`);
6132
6308
  if (selectedIds.includes("claude-code")) {
6133
- ln(` ${green("▸")} ${bold("6")} hooks ${dim("lifecycle guards (engine protection, git safety)")}`);
6309
+ ln(` ${green("▸")} ${bold("18")} hooks ${dim("lifecycle guards (engine protection, git safety)")}`);
6134
6310
  }
6135
6311
  ln(` ${green("▸")} ${bold(String(selectedAgents.length))} agent${selectedAgents.length > 1 ? "s" : ""} ${dim(agentNames.join(", "))}`);
6136
6312
  ln(` ${green("▸")} PARA vault ${dim("folders, templates, Engine scaffold")}`);
6137
6313
  ln();
6138
6314
 
6139
- // ── Next steps ──
6315
+ // ── Next steps (animated slide-in) ──
6140
6316
  ln(gray(" ─────────────────────────────────────────────"));
6141
6317
  ln();
6142
- ln(` ${bold("Next steps")}`);
6143
- ln();
6144
- ln(` ${cyan("1")} Open your vault in ${bold("Obsidian")}`);
6145
- ln(` ${dim("This is where you view and browse your files")}`);
6146
- ln();
6147
- ln(` ${cyan("2")} Open the vault folder in your AI agent`);
6148
- ln(` ${dim("Installed: " + agentNames.join(", "))}`);
6149
- ln();
6318
+ await slideIn([
6319
+ ` ${bold("Next steps")}`,
6320
+ "",
6321
+ ` ${cyan("1")} Open your vault in ${bold("Obsidian")}`,
6322
+ ` ${dim("This is where you view and browse your files")}`,
6323
+ "",
6324
+ ` ${cyan("2")} Open the vault folder in your AI agent`,
6325
+ ` ${dim("Installed: " + agentNames.join(", "))}`,
6326
+ "",
6327
+ ]);
6150
6328
  ln(` ${cyan("3")} Enable the Obsidian theme`);
6151
6329
  ln(` ${dim("Settings → Appearance → CSS snippets → minimal-theme")}`);
6152
6330
  ln();
@@ -6167,6 +6345,7 @@ async function main() {
6167
6345
  ln(` ${green("▸")} ${bold("moveros pulse")} ${dim("Dashboard — energy, tasks, streaks")}`);
6168
6346
  ln(` ${green("▸")} ${bold("moveros doctor")} ${dim("Health check across all agents")}`);
6169
6347
  ln(` ${green("▸")} ${bold("moveros capture")} ${dim("Quick inbox — tasks, links, ideas")}`);
6348
+ ln(` ${green("▸")} ${bold("moveros prayer")} ${dim("Set up prayer time reminders")}`);
6170
6349
  ln(` ${green("▸")} ${bold("moveros update")} ${dim("Update agents, rules, and skills")}`);
6171
6350
  ln(` ${green("▸")} ${bold("moveros")} ${dim("Full menu with all commands")}`);
6172
6351
  ln();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mover-os",
3
- "version": "4.6.3",
3
+ "version": "4.6.5",
4
4
  "description": "The self-improving OS for AI agents. Turns Obsidian into an execution engine.",
5
5
  "bin": {
6
6
  "moveros": "install.js"