mover-os 4.4.2 → 4.5.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/install.js +1067 -780
  3. package/package.json +1 -1
package/install.js CHANGED
@@ -89,8 +89,28 @@ function waveGradient(text, frame, totalFrames) {
89
89
  }).join("") + S.reset;
90
90
  }
91
91
 
92
+ // ─── TUI: Alternate screen buffer ────────────────────────────────────────────
93
+ let _inAltScreen = false;
94
+ function enterAltScreen() {
95
+ if (!IS_TTY || _inAltScreen) return;
96
+ w("\x1b[?1049h"); // Enter alternate screen
97
+ w("\x1b[2J\x1b[H"); // Clear + cursor home
98
+ w(S.hide);
99
+ _inAltScreen = true;
100
+ }
101
+ function exitAltScreen() {
102
+ if (!IS_TTY || !_inAltScreen) return;
103
+ w("\x1b[?1049l"); // Restore original screen
104
+ w(S.show);
105
+ _inAltScreen = false;
106
+ }
107
+ function clearContent() {
108
+ w("\x1b[2J\x1b[H"); // Clear screen + cursor to top
109
+ }
110
+
92
111
  // ─── Terminal cleanup ────────────────────────────────────────────────────────
93
112
  function cleanup() {
113
+ exitAltScreen();
94
114
  w(S.show);
95
115
  try { if (process.stdin.isTTY && process.stdin.isRaw) process.stdin.setRawMode(false); } catch {}
96
116
  }
@@ -182,7 +202,17 @@ async function printHeader(animate = IS_TTY) {
182
202
  }
183
203
  } catch {}
184
204
 
185
- ln(` ${dim(`v${VERSION}`)} ${gray("the agentic operating system for obsidian")}${infoRight ? ` ${infoRight}` : ""}`);
205
+ const pkgVer = require("./package.json").version;
206
+ ln(` ${dim(`v${pkgVer}`)} ${gray("the agentic operating system for obsidian")}${infoRight ? ` ${infoRight}` : ""}`);
207
+
208
+ // Non-blocking update check
209
+ try {
210
+ const latest = execSync("npm view mover-os version", { encoding: "utf8", timeout: 5000 }).trim();
211
+ if (latest && latest !== pkgVer && compareVersions(latest, pkgVer) > 0) {
212
+ ln(` ${yellow(`Update available: v${pkgVer} → v${latest}`)} ${dim(`run ${bold("moveros update")}`)}`);
213
+ }
214
+ } catch {}
215
+
186
216
  ln();
187
217
  ln(gray(" ─────────────────────────────────────────────"));
188
218
  ln();
@@ -242,6 +272,127 @@ function statusLine(icon, label, detail = "") {
242
272
  barLn(`${iconMap[icon] || icon} ${label}${detail ? ` ${dim(detail)}` : ""}`);
243
273
  }
244
274
 
275
+ // ─── TUI: Compact header ────────────────────────────────────────────────────
276
+ function compactHeader() {
277
+ const pkgVer = require("./package.json").version;
278
+ let info = "";
279
+ try {
280
+ const cfgPath = path.join(os.homedir(), ".mover", "config.json");
281
+ if (fs.existsSync(cfgPath)) {
282
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
283
+ const parts = [];
284
+ if (cfg.vaultPath) parts.push(path.basename(cfg.vaultPath));
285
+ if (cfg.agents?.length) parts.push(`${cfg.agents.length} agents`);
286
+ info = parts.length ? `${S.gray} · ${parts.join(" · ")}${S.reset}` : "";
287
+ }
288
+ } catch {}
289
+ return ` ${S.bold}moveros${S.reset} ${S.dim}v${pkgVer}${S.reset}${info}`;
290
+ }
291
+
292
+ // ─── TUI: Screen transitions ────────────────────────────────────────────────
293
+ async function wipeDown(lineCount) {
294
+ if (!IS_TTY) return;
295
+ const rows = process.stdout.rows || 24;
296
+ const count = Math.min(lineCount || rows, rows);
297
+ w("\x1b[H");
298
+ for (let i = 0; i < count; i++) {
299
+ w(`\x1b[${i + 1};1H\x1b[2K`);
300
+ await sleep(8);
301
+ }
302
+ }
303
+
304
+ async function fadeOut() {
305
+ if (!IS_TTY) return;
306
+ const rows = process.stdout.rows || 24;
307
+ for (let i = 1; i <= rows; i++) {
308
+ w(`\x1b[${i};1H\x1b[2K`);
309
+ if (i % 3 === 0) await sleep(6);
310
+ }
311
+ }
312
+
313
+ async function slideIn(lines) {
314
+ if (!IS_TTY) { lines.forEach(l => ln(l)); return; }
315
+ for (const line of lines) {
316
+ ln(line);
317
+ await sleep(12);
318
+ }
319
+ }
320
+
321
+ async function glowPulse(text, x, y) {
322
+ if (!IS_TTY) { ln(text); return; }
323
+ const frames = [255, 252, 255, 250, 248, 246];
324
+ for (const brightness of frames) {
325
+ if (y) w(`\x1b[${y};${x}H`);
326
+ w(`\x1b[2K${S.fg(brightness)}${strip(text)}${S.reset}`);
327
+ await sleep(40);
328
+ }
329
+ if (y) w(`\x1b[${y};${x}H`);
330
+ w(`\x1b[2K${text}\n`);
331
+ }
332
+
333
+ async function shimmerText(text, duration = 400) {
334
+ if (!IS_TTY) { ln(text); return; }
335
+ const totalFrames = Math.floor(duration / 20);
336
+ const raw = strip(text);
337
+ for (let f = 0; f <= totalFrames; f++) {
338
+ w(`\r\x1b[2K ${waveGradient(raw, f, totalFrames)}`);
339
+ await sleep(20);
340
+ }
341
+ w(`\r\x1b[2K ${text}\n`);
342
+ }
343
+
344
+ // ─── TUI: waitForEsc ────────────────────────────────────────────────────────
345
+ function waitForEsc() {
346
+ return new Promise((r) => {
347
+ if (!IS_TTY) { r(); return; }
348
+ const { stdin } = process;
349
+ stdin.setRawMode(true);
350
+ stdin.resume();
351
+ stdin.setEncoding("utf8");
352
+ const h = (data) => {
353
+ if (data === "\x1b" || data === "\r" || data === "\n" || data === "\x03") {
354
+ stdin.removeListener("data", h);
355
+ stdin.setRawMode(false);
356
+ stdin.pause();
357
+ if (data === "\x03") { cleanup(); ln(); process.exit(0); }
358
+ r();
359
+ }
360
+ };
361
+ stdin.on("data", h);
362
+ });
363
+ }
364
+
365
+ // ─── TUI: Animated progress for install/update ──────────────────────────────
366
+ async function installProgress(steps) {
367
+ const total = steps.length;
368
+ for (let i = 0; i < total; i++) {
369
+ const step = steps[i];
370
+ w(`\x1b[2K\r ${progressBar(i, total, 30)} ${S.dim}${step.label}${S.reset}`);
371
+ await step.fn();
372
+ w(`\x1b[2K\r ${progressBar(i + 1, total, 30)} ${S.green}\u2713${S.reset} ${step.label}\n`);
373
+ await sleep(60);
374
+ }
375
+ }
376
+
377
+ // ─── TUI: Success animation ─────────────────────────────────────────────────
378
+ async function successAnimation(message) {
379
+ if (!IS_TTY) { ln(message); return; }
380
+ const check = [
381
+ " \u2713",
382
+ " \u2713",
383
+ " \u2713",
384
+ " \u2713 \u2713",
385
+ " \u2713",
386
+ ];
387
+ ln();
388
+ for (const line of check) {
389
+ await shimmerText(green(line), 200);
390
+ }
391
+ ln();
392
+ await shimmerText(bold(message), 400);
393
+ ln();
394
+ }
395
+
245
396
  // ─── Clack-style frame ──────────────────────────────────────────────────────
246
397
  const BAR_COLOR = S.cyan;
247
398
  const bar = () => w(`${BAR_COLOR}│${S.reset}`);
@@ -411,10 +562,12 @@ function interactiveSelect(items, { multi = false, preSelected = [], defaultInde
411
562
 
412
563
  const icon = multi
413
564
  ? (checked ? green("◼") : dim("◻"))
414
- : (active ? cyan("●") : dim(""));
415
- const label = active ? bold(item.name) : item.name;
565
+ : (active ? `${S.bold}${S.fg(255)}›${S.reset}` : dim(" "));
566
+ const label = active
567
+ ? `${S.bold}${S.fg(255)}${item.name}${S.reset}`
568
+ : `${S.fg(245)}${item.name}${S.reset}`;
416
569
  const padded = strip(label).padEnd(20);
417
- const styledPadded = active ? bold(padded) : padded;
570
+ const styledPadded = active ? `${S.bold}${S.fg(255)}${padded}${S.reset}` : `${S.fg(245)}${padded}${S.reset}`;
418
571
  const tag = item._detected ? dim("(detected)") : "";
419
572
 
420
573
  w(`\x1b[2K${BAR_COLOR}│${S.reset} ${icon} ${styledPadded}${tag}\n`);
@@ -645,14 +798,69 @@ async function downloadPayload(key) {
645
798
  return tmpDir;
646
799
  }
647
800
 
801
+ // ─── Path helpers ────────────────────────────────────────────────────────────
802
+ function getSessionDate() {
803
+ const now = new Date();
804
+ if (now.getHours() < 5) return new Date(now.getTime() - 86400000);
805
+ return now;
806
+ }
807
+
808
+ function resolveDailyNotePath(engineDir, date) {
809
+ if (!date) date = getSessionDate();
810
+ const yyyy = date.getFullYear();
811
+ const mm = String(date.getMonth() + 1).padStart(2, "0");
812
+ const dd = String(date.getDate()).padStart(2, "0");
813
+ const ymd = `${yyyy}-${mm}-${dd}`;
814
+ // Primary: month subfolder (Mover OS convention)
815
+ const monthPath = path.join(engineDir, "Dailies", `${yyyy}-${mm}`, `Daily - ${ymd}.md`);
816
+ if (fs.existsSync(monthPath)) return monthPath;
817
+ // Fallback: flat Dailies folder (older vaults)
818
+ const flatPath = path.join(engineDir, "Dailies", `Daily - ${ymd}.md`);
819
+ if (fs.existsSync(flatPath)) return flatPath;
820
+ return monthPath; // default for creation
821
+ }
822
+
648
823
  // ─── CLI ─────────────────────────────────────────────────────────────────────
824
+ // ─── Frecency Tracking ──────────────────────────────────────────────────────
825
+ function loadCliUsage() {
826
+ const cfgPath = path.join(os.homedir(), ".mover", "config.json");
827
+ if (!fs.existsSync(cfgPath)) return {};
828
+ try {
829
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
830
+ return cfg.cli_usage || {};
831
+ } catch { return {}; }
832
+ }
833
+
834
+ function recordCliUsage(cmd) {
835
+ const cfgPath = path.join(os.homedir(), ".mover", "config.json");
836
+ try {
837
+ const cfg = fs.existsSync(cfgPath) ? JSON.parse(fs.readFileSync(cfgPath, "utf8")) : {};
838
+ if (!cfg.cli_usage) cfg.cli_usage = {};
839
+ const entry = cfg.cli_usage[cmd] || { count: 0, last: 0 };
840
+ entry.count++;
841
+ entry.last = Date.now();
842
+ cfg.cli_usage[cmd] = entry;
843
+ fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
844
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
845
+ } catch {}
846
+ }
847
+
848
+ function frecencyScore(usage, cmd) {
849
+ const e = usage[cmd];
850
+ if (!e || !e.count) return 0;
851
+ const ageHours = (Date.now() - (e.last || 0)) / 3600000;
852
+ const recency = ageHours < 1 ? 4 : ageHours < 24 ? 3 : ageHours < 168 ? 2 : 1;
853
+ return e.count * recency;
854
+ }
855
+
649
856
  // ─── CLI Commands ────────────────────────────────────────────────────────────
650
857
  const CLI_COMMANDS = {
651
- install: { desc: "Full interactive install", alias: [] },
652
- update: { desc: "Update all agents", alias: ["-u"] },
858
+ install: { desc: "Fresh install wizard", alias: [] },
859
+ update: { desc: "Comprehensive update agents, rules, skills", alias: ["-u"] },
860
+ uninstall: { desc: "Remove Mover OS from vault and agents", alias: [] },
653
861
  doctor: { desc: "Health check across all installed agents", alias: [] },
654
862
  pulse: { desc: "Terminal dashboard — energy, tasks, streaks",alias: [] },
655
- warm: { desc: "Pre-warm an AI session with context", alias: [] },
863
+ // warm removed hooks + rules + /morning already handle session priming
656
864
  capture: { desc: "Quick capture — tasks, links, ideas", alias: [] },
657
865
  who: { desc: "Entity memory lookup", alias: [] },
658
866
  diff: { desc: "Engine file evolution viewer", alias: [] },
@@ -733,8 +941,8 @@ function detectObsidianVaults() {
733
941
  }
734
942
 
735
943
  function compareVersions(a, b) {
736
- const pa = a.split(".").map(Number);
737
- const pb = b.split(".").map(Number);
944
+ const pa = String(a).replace(/^[vV]/, "").split(".").map(Number);
945
+ const pb = String(b).replace(/^[vV]/, "").split(".").map(Number);
738
946
  for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
739
947
  const va = pa[i] || 0, vb = pb[i] || 0;
740
948
  if (va > vb) return 1;
@@ -1563,7 +1771,7 @@ Stuck: /debug-resistance
1563
1771
 
1564
1772
  ## CLI Utility
1565
1773
 
1566
- \`moveros\` provides system-level terminal operations: pulse (dashboard), warm (pre-warm sessions), sync (update agents), doctor (health check), who (entity lookup), capture (quick inbox). Use native agent capabilities first — the CLI supplements, never replaces.
1774
+ \`moveros\` provides system-level terminal operations: pulse (dashboard), sync (update agents), doctor (health check), who (entity lookup), capture (quick inbox). Use native agent capabilities first — the CLI supplements, never replaces.
1567
1775
  `;
1568
1776
  }
1569
1777
 
@@ -2705,7 +2913,7 @@ function preflight() {
2705
2913
  // ─── CLI Command Handlers (stubs — implemented progressively) ────────────────
2706
2914
  const CLI_HANDLERS = {
2707
2915
  pulse: async (opts) => { await cmdPulse(opts); },
2708
- warm: async (opts) => { await cmdWarm(opts); },
2916
+ // warm removed
2709
2917
  capture: async (opts) => { await cmdCapture(opts); },
2710
2918
  who: async (opts) => { await cmdWho(opts); },
2711
2919
  diff: async (opts) => { await cmdDiff(opts); },
@@ -2715,10 +2923,11 @@ const CLI_HANDLERS = {
2715
2923
  settings: async (opts) => { await cmdSettings(opts); },
2716
2924
  backup: async (opts) => { await cmdBackup(opts); },
2717
2925
  restore: async (opts) => { await cmdRestore(opts); },
2718
- doctor: async (opts) => { await cmdDoctor(opts); },
2719
- prayer: async (opts) => { await cmdPrayer(opts); },
2720
- help: async (opts) => { await cmdHelp(opts); },
2721
- test: async (opts) => { await cmdTest(opts); },
2926
+ doctor: async (opts) => { await cmdDoctor(opts); },
2927
+ prayer: async (opts) => { await cmdPrayer(opts); },
2928
+ help: async (opts) => { await cmdHelp(opts); },
2929
+ uninstall: async (opts) => { await runUninstall(resolveVaultPath(opts.vault)); },
2930
+ test: async (opts) => { await cmdTest(opts); },
2722
2931
  };
2723
2932
 
2724
2933
  // ─── moveros doctor ─────────────────────────────────────────────────────────
@@ -2849,10 +3058,7 @@ async function cmdPulse(opts) {
2849
3058
 
2850
3059
  // Today's tasks from Daily Note
2851
3060
  barLn();
2852
- const now = new Date();
2853
- const ymd = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
2854
- const month = ymd.substring(0, 7);
2855
- const dailyPath = path.join(engineDir, "Dailies", month, `Daily - ${ymd}.md`);
3061
+ const dailyPath = resolveDailyNotePath(engineDir);
2856
3062
  if (fs.existsSync(dailyPath)) {
2857
3063
  const daily = fs.readFileSync(dailyPath, "utf8");
2858
3064
  const taskSection = daily.match(/##\s*Tasks[\s\S]*?(?=\n##|\n---)/i);
@@ -2907,91 +3113,25 @@ async function cmdPulse(opts) {
2907
3113
  statusLine(streak >= 3 ? "ok" : "info", "Daily streak", `${streak} day${streak !== 1 ? "s" : ""}`);
2908
3114
  }
2909
3115
 
2910
- // Last session log time
2911
- const cfgPath = path.join(os.homedir(), ".mover", "config.json");
2912
- if (fs.existsSync(cfgPath)) {
3116
+ // Last session log time — read from Active_Context.md Workflow State
3117
+ const acLogPath = path.join(engineDir, "Active_Context.md");
3118
+ if (fs.existsSync(acLogPath)) {
2913
3119
  try {
2914
- const cfgData = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
2915
- if (cfgData.lastLog) {
2916
- const ago = Math.round((Date.now() - new Date(cfgData.lastLog).getTime()) / 3600000);
2917
- statusLine(ago < 8 ? "ok" : ago < 24 ? "info" : "warn", "Last /log", `${ago}h ago`);
3120
+ const acContent = fs.readFileSync(acLogPath, "utf8");
3121
+ const logMatch = acContent.match(/log_last_run:\s*(\S+)/);
3122
+ if (logMatch) {
3123
+ const agoMs = Date.now() - new Date(logMatch[1]).getTime();
3124
+ const agoMin = Math.round(agoMs / 60000);
3125
+ const agoH = Math.round(agoMs / 3600000);
3126
+ const agoStr = agoMin < 60 ? `${agoMin}m ago` : `${agoH}h ago`;
3127
+ statusLine(agoH < 8 ? "ok" : agoH < 24 ? "info" : "warn", "Last /log", agoStr);
2918
3128
  }
2919
3129
  } catch {}
2920
3130
  }
2921
3131
  barLn();
2922
3132
  }
2923
3133
 
2924
- // ─── moveros warm ───────────────────────────────────────────────────────────
2925
- async function cmdWarm(opts) {
2926
- const vault = resolveVaultPath(opts.vault);
2927
- if (!vault) { barLn(red("No vault found.")); return; }
2928
- const agent = opts.rest[0] || "claude";
2929
- const engineDir = path.join(vault, "02_Areas", "Engine");
2930
- const home = os.homedir();
2931
-
2932
- // Build context primer
2933
- const sections = [];
2934
- sections.push("# Session Context Primer");
2935
- sections.push(`Generated: ${new Date().toISOString()}\n`);
2936
-
2937
- // Active Context snapshot
2938
- const acPath = path.join(engineDir, "Active_Context.md");
2939
- if (fs.existsSync(acPath)) {
2940
- const ac = fs.readFileSync(acPath, "utf8");
2941
- // Extract key sections (first 2000 chars)
2942
- sections.push("## Active Context\n" + ac.substring(0, 2000));
2943
- }
2944
-
2945
- // Current project state
2946
- const cwd = process.cwd();
2947
- const planPath = path.join(cwd, "dev", "plan.md");
2948
- if (fs.existsSync(planPath)) {
2949
- const plan = fs.readFileSync(planPath, "utf8");
2950
- // Find last active phase
2951
- const phases = plan.match(/### Phase \d+[\s\S]*?(?=### Phase|\Z)/g);
2952
- if (phases) {
2953
- const active = phases[phases.length - 1].substring(0, 1500);
2954
- sections.push("## Current Plan (last phase)\n" + active);
2955
- }
2956
- }
2957
-
2958
- // Today's daily note tasks
2959
- const now = new Date();
2960
- const ymd = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
2961
- const dailyPath = path.join(engineDir, "Dailies", ymd.substring(0, 7), `Daily - ${ymd}.md`);
2962
- if (fs.existsSync(dailyPath)) {
2963
- const daily = fs.readFileSync(dailyPath, "utf8");
2964
- const taskSection = daily.match(/##\s*Tasks[\s\S]*?(?=\n##)/i);
2965
- if (taskSection) sections.push("## Today's Tasks\n" + taskSection[0]);
2966
- }
2967
-
2968
- const primer = sections.join("\n\n---\n\n") + "\n";
2969
-
2970
- // Write to agent-specific location
2971
- const targets = {
2972
- claude: path.join(home, ".claude", "tmp", "session-primer.md"),
2973
- cursor: path.join(vault, ".cursor", "rules", "session-primer.mdc"),
2974
- gemini: path.join(home, ".gemini", "session-primer.md"),
2975
- codex: path.join(home, ".codex", "skills", "session-primer", "SKILL.md"),
2976
- windsurf: path.join(vault, ".windsurf", "rules", "session-primer.md"),
2977
- cline: path.join(vault, ".clinerules", "session-primer.md"),
2978
- };
2979
-
2980
- const dest = targets[agent] || targets.claude;
2981
- fs.mkdirSync(path.dirname(dest), { recursive: true });
2982
-
2983
- let content = primer;
2984
- if (agent === "cursor") {
2985
- content = `---\ndescription: "Session context primer — auto-generated by moveros warm"\nglobs:\nalwaysApply: true\n---\n\n${primer}`;
2986
- } else if (agent === "codex") {
2987
- content = `---\nname: session-primer\ndescription: "Auto-generated session context from moveros warm"\n---\n\n${primer}`;
2988
- }
2989
-
2990
- fs.writeFileSync(dest, content, "utf8");
2991
- statusLine("ok", "Warm", `${agent} primer written`);
2992
- barLn(dim(` ${dest}`));
2993
- barLn();
2994
- }
3134
+ // cmdWarm removed hooks + rules + /morning handle session priming
2995
3135
 
2996
3136
  // ─── moveros capture ────────────────────────────────────────────────────────
2997
3137
  async function cmdCapture(opts) {
@@ -3001,7 +3141,19 @@ async function cmdCapture(opts) {
3001
3141
  const now = new Date();
3002
3142
  const ymd = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
3003
3143
  const ts = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
3004
- const capturePath = path.join(vault, "00_Inbox", `Capture - ${ymd}.md`);
3144
+
3145
+ // Find existing capture file — prefer today's, fallback to most recent, create new if none
3146
+ const inboxDir = path.join(vault, "00_Inbox");
3147
+ fs.mkdirSync(inboxDir, { recursive: true });
3148
+ const existingCaptures = fs.readdirSync(inboxDir)
3149
+ .filter(f => f.toLowerCase().startsWith("capture") && f.endsWith(".md"))
3150
+ .sort();
3151
+ const todayCapture = existingCaptures.find(f => f.includes(ymd));
3152
+ const capturePath = todayCapture
3153
+ ? path.join(inboxDir, todayCapture)
3154
+ : existingCaptures.length > 0
3155
+ ? path.join(inboxDir, existingCaptures[existingCaptures.length - 1])
3156
+ : path.join(inboxDir, `Capture - ${ymd}.md`);
3005
3157
 
3006
3158
  // Determine type from flags
3007
3159
  let type = null, content = "";
@@ -3047,7 +3199,6 @@ async function cmdCapture(opts) {
3047
3199
  else entry = `- ${content} *(${ts})*`;
3048
3200
 
3049
3201
  // Append to capture file
3050
- fs.mkdirSync(path.dirname(capturePath), { recursive: true });
3051
3202
  if (!fs.existsSync(capturePath)) {
3052
3203
  fs.writeFileSync(capturePath, `# Capture — ${ymd}\n\n${entry}\n`, "utf8");
3053
3204
  } else {
@@ -3253,13 +3404,20 @@ async function cmdReplay(opts) {
3253
3404
  const vault = resolveVaultPath(opts.vault);
3254
3405
  if (!vault) { barLn(red("No vault found.")); return; }
3255
3406
 
3407
+ const engineDir = path.join(vault, "02_Areas", "Engine");
3256
3408
  let dateStr = opts.rest.find((a) => a.startsWith("--date="))?.split("=")[1];
3257
- if (!dateStr) {
3258
- const now = new Date();
3259
- dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
3409
+ let targetDate;
3410
+ if (dateStr) {
3411
+ const [y, m, d] = dateStr.split("-").map(Number);
3412
+ targetDate = new Date(y, m - 1, d);
3413
+ } else {
3414
+ targetDate = getSessionDate();
3415
+ const y = targetDate.getFullYear();
3416
+ const m = String(targetDate.getMonth() + 1).padStart(2, "0");
3417
+ const d = String(targetDate.getDate()).padStart(2, "0");
3418
+ dateStr = `${y}-${m}-${d}`;
3260
3419
  }
3261
- const month = dateStr.substring(0, 7);
3262
- const dailyPath = path.join(vault, "02_Areas", "Engine", "Dailies", month, `Daily - ${dateStr}.md`);
3420
+ const dailyPath = resolveDailyNotePath(engineDir, targetDate);
3263
3421
 
3264
3422
  barLn(bold(` Session Replay — ${dateStr}`));
3265
3423
  barLn();
@@ -3636,6 +3794,19 @@ async function cmdPrayer(opts) {
3636
3794
  }
3637
3795
 
3638
3796
  // ─── moveros settings ───────────────────────────────────────────────────────
3797
+ const KNOWN_SETTINGS = {
3798
+ review_day: { type: "string", desc: "Weekly review day", defaults: "Sunday" },
3799
+ evening_zone_start: { type: "number", desc: "Evening starts at (hour)", defaults: 18 },
3800
+ midnight_cutoff: { type: "number", desc: "Before this hour = yesterday", defaults: 5 },
3801
+ deep_work_hours: { type: "number", desc: "Daily deep work target (hours)", defaults: 8 },
3802
+ sleep_target: { type: "number", desc: "Sleep target (hours)", defaults: 8 },
3803
+ track_food: { type: "boolean", desc: "Track food in daily notes", defaults: true },
3804
+ track_sleep: { type: "boolean", desc: "Track sleep in daily notes", defaults: true },
3805
+ show_prayer_times: { type: "boolean", desc: "Show prayer times in statusline", defaults: false },
3806
+ backup_retention: { type: "number", desc: "Max backups to keep", defaults: 5 },
3807
+ friction_level: { type: "number", desc: "Max friction level (1-4)", defaults: 3 },
3808
+ };
3809
+
3639
3810
  async function cmdSettings(opts) {
3640
3811
  const cfgPath = path.join(os.homedir(), ".mover", "config.json");
3641
3812
 
@@ -3644,17 +3815,15 @@ async function cmdSettings(opts) {
3644
3815
  return;
3645
3816
  }
3646
3817
 
3647
- const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
3818
+ let cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
3648
3819
 
3649
- // moveros settings set <key> <value>
3820
+ // moveros settings set <key> <value> — CLI mode
3650
3821
  if (opts.rest[0] === "set" && opts.rest[1]) {
3651
3822
  const key = opts.rest[1];
3652
3823
  let val = opts.rest.slice(2).join(" ");
3653
- // Auto-type conversion
3654
3824
  if (val === "true") val = true;
3655
3825
  else if (val === "false") val = false;
3656
3826
  else if (/^\d+$/.test(val)) val = parseInt(val, 10);
3657
-
3658
3827
  if (!cfg.settings) cfg.settings = {};
3659
3828
  cfg.settings[key] = val;
3660
3829
  fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
@@ -3662,23 +3831,51 @@ async function cmdSettings(opts) {
3662
3831
  return;
3663
3832
  }
3664
3833
 
3665
- // Show all settings
3834
+ // Interactive mode — show settings as a selectable list
3666
3835
  barLn(bold(" Settings"));
3667
3836
  barLn();
3668
3837
  barLn(` ${dim("Vault:")} ${cfg.vaultPath || dim("not set")}`);
3669
3838
  barLn(` ${dim("Key:")} ${cfg.licenseKey ? cyan(cfg.licenseKey.substring(0, 12) + "...") : dim("not set")}`);
3670
3839
  barLn(` ${dim("Agents:")} ${(cfg.agents || []).join(", ") || dim("none")}`);
3671
- barLn(` ${dim("Version:")} ${cfg.version || dim("unknown")}`);
3672
3840
  barLn();
3673
- if (cfg.settings) {
3674
- barLn(dim(" Custom:"));
3675
- for (const [k, v] of Object.entries(cfg.settings)) {
3676
- barLn(` ${k}: ${JSON.stringify(v)}`);
3677
- }
3678
- barLn();
3841
+
3842
+ const settings = cfg.settings || {};
3843
+ const items = Object.entries(KNOWN_SETTINGS).map(([key, meta]) => {
3844
+ const current = settings[key] !== undefined ? settings[key] : meta.defaults;
3845
+ const display = typeof current === "boolean" ? (current ? green("on") : red("off")) : String(current);
3846
+ return {
3847
+ id: key,
3848
+ name: `${key.padEnd(22)} ${display.padEnd(8)} ${dim(meta.desc)}`,
3849
+ tier: `Current: ${JSON.stringify(current)} Default: ${JSON.stringify(meta.defaults)}`,
3850
+ };
3851
+ });
3852
+
3853
+ question(dim(" Select a setting to edit (esc to go back)"));
3854
+ barLn();
3855
+ const picked = await interactiveSelect(items);
3856
+ if (!picked) return;
3857
+
3858
+ const meta = KNOWN_SETTINGS[picked];
3859
+ const current = settings[picked] !== undefined ? settings[picked] : meta.defaults;
3860
+
3861
+ if (meta.type === "boolean") {
3862
+ // Toggle immediately
3863
+ const newVal = !current;
3864
+ if (!cfg.settings) cfg.settings = {};
3865
+ cfg.settings[picked] = newVal;
3866
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
3867
+ statusLine("ok", picked, newVal ? "on" : "off");
3868
+ } else {
3869
+ // Prompt for new value
3870
+ const answer = await textInput({ label: `${picked}`, initial: String(current) });
3871
+ if (answer === null || answer.trim() === "" || answer === String(current)) return;
3872
+ let val = answer.trim();
3873
+ if (meta.type === "number") val = parseInt(val, 10);
3874
+ if (!cfg.settings) cfg.settings = {};
3875
+ cfg.settings[picked] = val;
3876
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
3877
+ statusLine("ok", picked, JSON.stringify(val));
3679
3878
  }
3680
- barLn(dim(" Edit: moveros settings set <key> <value>"));
3681
- barLn(dim(` File: ${cfgPath}`));
3682
3879
  barLn();
3683
3880
  }
3684
3881
 
@@ -3847,230 +4044,267 @@ async function cmdRestore(opts) {
3847
4044
 
3848
4045
  // ─── moveros help ──────────────────────────────────────────────────────────
3849
4046
  async function cmdHelp(opts) {
3850
- // Interactive animated guide — paginated walkthrough of Mover OS
4047
+ // Animated typing helper
4048
+ const typeOut = async (text, speed = 12) => {
4049
+ if (!IS_TTY) { ln(text); return; }
4050
+ const raw = strip(text);
4051
+ let rawIdx = 0, ansiIdx = 0;
4052
+ // Build mapping of raw char positions to the styled string slices
4053
+ while (rawIdx < raw.length) {
4054
+ w(text[ansiIdx] || "");
4055
+ if (text[ansiIdx] && text[ansiIdx] !== "\x1b" && !text.slice(Math.max(0, ansiIdx - 10), ansiIdx).match(/\x1b\[[^m]*$/)) {
4056
+ rawIdx++;
4057
+ }
4058
+ ansiIdx++;
4059
+ if (rawIdx % 2 === 0) await sleep(speed);
4060
+ }
4061
+ // flush remaining ANSI codes
4062
+ while (ansiIdx < text.length) { w(text[ansiIdx]); ansiIdx++; }
4063
+ w("\n");
4064
+ };
4065
+
4066
+ const typeLine = async (text, speed = 8) => {
4067
+ if (!IS_TTY) { ln(` ${text}`); return; }
4068
+ w(" ");
4069
+ for (let i = 0; i < text.length; i++) {
4070
+ w(text[i]);
4071
+ if (text[i] !== "\x1b" && i % 3 === 0) await sleep(speed);
4072
+ }
4073
+ w("\n");
4074
+ };
4075
+
3851
4076
  const pages = [
3852
4077
  {
3853
- title: "Welcome to Mover OS",
4078
+ title: "What is Mover OS?",
3854
4079
  body: [
3855
- `${bold("The agentic operating system for Obsidian.")}`,
4080
+ `${bold("Your second brain, but it actually works.")}`,
4081
+ "",
4082
+ "Most productivity systems fail because you have to maintain them.",
4083
+ "Mover OS is different — your AI agents maintain it for you.",
3856
4084
  "",
3857
- "Mover OS turns your Obsidian vault into an AI-powered execution engine.",
3858
- "It works across 16 AI coding agents — Claude Code, Cursor, Gemini,",
3859
- "Copilot, Codex, and more. Same brain, every editor.",
4085
+ "You tell your AI who you are, what you're building, and where",
4086
+ "you're going. It remembers across every session, every editor.",
3860
4087
  "",
3861
- `${dim("How it works:")}`,
3862
- ` ${cyan("1.")} Your ${bold("Engine")} stores who you are identity, strategy, goals`,
3863
- ` ${cyan("2.")} ${bold("Workflows")} run your day plan, execute, log, analyse, repeat`,
3864
- ` ${cyan("3.")} ${bold("Skills")} give your AI agents deep domain knowledge`,
3865
- ` ${cyan("4.")} The system ${bold("learns")} from your behavior and adapts`,
4088
+ `${dim("Three things make it work:")}`,
4089
+ ` ${cyan("1.")} The ${bold("Engine")} files that store your identity, strategy, goals`,
4090
+ ` ${cyan("2.")} ${bold("Workflows")} — 23 commands that run your day (plan, build, log, repeat)`,
4091
+ ` ${cyan("3.")} ${bold("Skills")} 61 packs that make your AI genuinely useful`,
3866
4092
  "",
3867
- `${dim("This guide walks through everything. Use to navigate.")}`,
4093
+ `Works across 16 agents. Claude Code, Cursor, Gemini, all of them.`,
4094
+ `Same context, every editor. ${dim("Press → to continue.")}`,
3868
4095
  ],
3869
4096
  },
3870
4097
  {
3871
- title: "The Engine — Your Brain",
4098
+ title: "The Engine",
3872
4099
  body: [
3873
- `${dim("Location:")} 02_Areas/Engine/`,
4100
+ `${dim("Your identity, stored as markdown. Read by every AI session.")}`,
3874
4101
  "",
3875
- `${bold("Core files that define you:")}`,
4102
+ ` ${cyan("Identity_Prime.md")} Who you are values, strengths, anti-patterns`,
4103
+ ` ${cyan("Strategy.md")} What you're betting on right now`,
4104
+ ` ${cyan("Active_Context.md")} What's happening today — focus, blockers, energy`,
4105
+ ` ${cyan("Goals.md")} Where you're heading — 90 days to 10 years`,
4106
+ ` ${cyan("Mover_Dossier.md")} What you've got — skills, audience, assets`,
4107
+ ` ${cyan("Auto_Learnings.md")} What the AI has noticed about your behavior`,
3876
4108
  "",
3877
- ` ${cyan("Identity_Prime.md")} Who you are values, psychology, anti-identity`,
3878
- ` ${cyan("Strategy.md")} What you're testing current hypothesis`,
3879
- ` ${cyan("Active_Context.md")} What's happening NOW — blockers, focus, state`,
3880
- ` ${cyan("Goals.md")} Where you're going — 90d, 1yr, 10yr targets`,
3881
- ` ${cyan("Mover_Dossier.md")} What you have — skills, capital, network`,
3882
- ` ${cyan("Auto_Learnings.md")} What the AI notices — behavioral patterns`,
4109
+ "These files are yours. The system never overwrites them.",
4110
+ "Every workflow reads them. They evolve as you do.",
3883
4111
  "",
3884
- `These files are ${bold("irreplaceable")}. The system never overwrites them.`,
3885
- "Every AI session reads them. Every workflow updates them.",
3886
- `Your Engine ${bold("evolves")} as you do.`,
4112
+ `${dim("Location:")} 02_Areas/Engine/`,
3887
4113
  ],
3888
4114
  },
3889
4115
  {
3890
- title: "Daily Rhythm — The Loop",
4116
+ title: "Your Daily Loop",
3891
4117
  body: [
3892
- `${bold("The daily execution cycle:")}`,
4118
+ `${dim("The rhythm that makes it stick:")}`,
3893
4119
  "",
3894
- ` ${green("→")} ${bold("/morning")} Start your day energy check, set focus`,
3895
- ` ${green("→")} ${bold("[WORK]")} Build, ship, create`,
3896
- ` ${green("→")} ${bold("/log")} Capture what happened syncs plan + state`,
3897
- ` ${green("→")} ${bold("/analyse-day")} Brutal daily audit patterns + verdict`,
3898
- ` ${green("→")} ${bold("/plan-tomorrow")} Generate tomorrow's battle plan`,
4120
+ ` ${green("→")} ${bold("/morning")} Check in. Set your one thing for the day.`,
4121
+ ` ${green("→")} ${bold("[WORK]")} Build, ship, create.`,
4122
+ ` ${green("→")} ${bold("/log")} Capture what happened. Plan syncs automatically.`,
4123
+ ` ${green("→")} ${bold("/analyse-day")} Honest audit. What worked, what didn't.`,
4124
+ ` ${green("→")} ${bold("/plan-tomorrow")} Set up tomorrow before you close the laptop.`,
3899
4125
  "",
3900
- `${dim("Weekly:")}`,
3901
- ` ${green("→")} ${bold("/review-week")} Sunday deep review + strategy validation`,
4126
+ ` ${dim("Sundays:")} ${bold("/review-week")} — zoom out, validate strategy, clean the system.`,
3902
4127
  "",
3903
- `${dim("The rhythm is the system.")} Miss a day, patterns detect it.`,
3904
- "Miss three, /reboot triggers recovery protocol.",
3905
- `Every workflow hands off to the next — ${bold("no dead ends")}.`,
4128
+ "Miss a day, the system notices. Miss three, /reboot kicks in.",
4129
+ "Each workflow hands off to the next. No dead ends.",
3906
4130
  ],
3907
4131
  },
3908
4132
  {
3909
- title: "Workflows — 23 Commands",
4133
+ title: "23 Workflows",
3910
4134
  body: [
3911
- `${bold("Daily:")} /morning /log /analyse-day /plan-tomorrow /review-week`,
3912
- `${bold("Build:")} /ignite /overview /refactor-plan /capture /debrief`,
3913
- `${bold("Think:")} /debug-resistance /pivot-strategy /mover-ideas /screenshot`,
3914
- `${bold("Grow:")} /harvest /history /reboot`,
3915
- `${bold("Meta:")} /setup /update /walkthrough /migrate /mover-check /mover-report`,
4135
+ `${dim("Slash commands inside your AI agent. Type / and go.")}`,
3916
4136
  "",
3917
- `${dim("Key concepts:")}`,
3918
- ` ${cyan("")} ${bold("/ignite")} creates projects — brief + plan in one workflow`,
3919
- ` ${cyan("")} ${bold("/debug-resistance")} diagnoses WHY you're avoiding something`,
3920
- ` ${cyan("")} ${bold("/pivot-strategy")} formally changes direction with logging`,
3921
- ` ${cyan("")} ${bold("/harvest")} extracts permanent knowledge to your Library`,
3922
- ` ${cyan("•")} ${bold("/screenshot")} does meta-analysis of your AI session patterns`,
4137
+ `${bold("Daily:")} /morning /log /analyse-day /plan-tomorrow /review-week`,
4138
+ `${bold("Projects:")} /ignite /overview /refactor-plan /capture /debrief`,
4139
+ `${bold("Strategy:")} /debug-resistance /pivot-strategy /mover-ideas /screenshot`,
4140
+ `${bold("Growth:")} /harvest /history /reboot`,
4141
+ `${bold("System:")} /setup /update /walkthrough /migrate /mover-check`,
3923
4142
  "",
3924
- "Every command reads your Engine context and adapts to your state.",
4143
+ `${dim("Highlights:")}`,
4144
+ ` ${bold("/ignite")} Starts any project — interrogation, brief, plan`,
4145
+ ` ${bold("/debug-resistance")} Figures out WHY you're avoiding something`,
4146
+ ` ${bold("/harvest")} Turns conversations into permanent knowledge`,
4147
+ ` ${bold("/screenshot")} Meta-analysis of how you actually use AI`,
3925
4148
  ],
3926
4149
  },
3927
4150
  {
3928
- title: "Skills Domain Intelligence",
4151
+ title: "61 Skill Packs",
3929
4152
  body: [
3930
- `${bold("Skills give your AI agents specialized knowledge.")}`,
4153
+ `${dim("Specialized knowledge your AI loads automatically.")}`,
3931
4154
  "",
3932
- `${dim("Categories:")}`,
3933
- ` ${cyan("dev")} Debugging, TDD, refactoring, error handling, React`,
3934
- ` ${cyan("marketing")} Copywriting, SEO, CRO, social content, email`,
3935
- ` ${cyan("cro")} Page optimization, forms, popups, paywalls, onboarding`,
3936
- ` ${cyan("strategy")} Pricing, launch, competitors, referrals, analytics`,
3937
- ` ${cyan("seo")} Audits, schema markup, programmatic SEO, content`,
3938
- ` ${cyan("design")} UI/UX, frontend design, Obsidian markdown/canvas`,
3939
- ` ${cyan("obsidian")} JSON Canvas, Bases, Obsidian CLI, markdown`,
4155
+ ` ${cyan("dev")} TDD, debugging, refactoring, error handling, React`,
4156
+ ` ${cyan("marketing")} Copywriting, SEO, CRO, social media, email sequences`,
4157
+ ` ${cyan("strategy")} Pricing, launch strategy, competitor analysis`,
4158
+ ` ${cyan("design")} UI/UX, frontend patterns, accessibility`,
4159
+ ` ${cyan("obsidian")} Canvas, Bases, markdown, CLI automation`,
3940
4160
  "",
3941
- `${dim("System skills (always active):")}`,
3942
- ` friction-enforcer pattern-detector plan-md-guardian`,
3943
- ` mover-os-context workflow-router daily-note-writer`,
4161
+ `${dim("System skills (always watching):")}`,
4162
+ ` ${bold("friction-enforcer")} Pushes back when you drift from your plan`,
4163
+ ` ${bold("pattern-detector")} Spots recurring behavior across sessions`,
4164
+ ` ${bold("plan-md-guardian")} Protects your roadmap from corruption`,
4165
+ ` ${bold("workflow-router")} Suggests the right workflow for the moment`,
3944
4166
  "",
3945
- "Skills trigger automatically based on what you're doing.",
4167
+ "You choose categories during install. Skills activate on context.",
3946
4168
  ],
3947
4169
  },
3948
4170
  {
3949
4171
  title: "The Friction System",
3950
4172
  body: [
3951
- `${bold("Your AI pushes back when you drift from your plan.")}`,
4173
+ `${dim("Your AI has opinions. That's the point.")}`,
3952
4174
  "",
3953
- ` ${dim("Level 1")} ${cyan("Surface")} \"Your plan says X. You're working on Y.\"`,
3954
- ` ${dim("Level 2")} ${yellow("Justify")} \"Why is this more important than your Single Test?\"`,
3955
- ` ${dim("Level 3")} ${red("Earn It")} AI stops helping with off-plan work.`,
3956
- ` ${dim("Level 4")} ${red("Hard Block")} Destructive actions require explicit reason.`,
4175
+ ` ${dim("Level 1")} ${cyan("Surface")} "Your plan says X. You're doing Y. Intentional?"`,
4176
+ ` ${dim("Level 2")} ${yellow("Justify")} "Why is this more important than your Single Test?"`,
4177
+ ` ${dim("Level 3")} ${red("Earn It")} Stops helping with off-plan work entirely.`,
4178
+ ` ${dim("Level 4")} ${red("Hard Block")} Won't delete Engine files without a reason.`,
3957
4179
  "",
3958
- `You can always push through Levels 1-3. Friction creates`,
3959
- `${bold("awareness")}, not walls. But the AI won't silently comply`,
3960
- "when you're avoiding the hard thing.",
4180
+ "You can always push through. It's awareness, not a wall.",
4181
+ "But if you're avoiding the hard thing, the AI won't pretend.",
3961
4182
  "",
3962
- `${dim("Pre-Escalation Gate:")} If the work is exploration or compound`,
3963
- `value (not avoidance), it logs as ${dim("[COMPOUND]")} and doesn't escalate.`,
4183
+ `${dim("Smart gate:")} If the work is genuinely useful (compound value,`,
4184
+ `exploration), it logs ${dim("[COMPOUND]")} and doesn't escalate.`,
3964
4185
  ],
3965
4186
  },
3966
4187
  {
3967
- title: "Pattern Detection",
4188
+ title: "What is this CLI?",
3968
4189
  body: [
3969
- `${bold("The system watches your behavior across sessions.")}`,
4190
+ `${bold("Terminal utilities that don't need an AI session.")}`,
3970
4191
  "",
3971
- `${dim("How it works:")}`,
3972
- ` ${cyan("1.")} AI observes recurring behavior (3+ data points)`,
3973
- ` ${cyan("2.")} Logs to Auto_Learnings.md with confidence score (1-5)`,
3974
- ` ${cyan("3.")} Patterns surface proactively in workflows`,
3975
- ` ${cyan("4.")} You confirm or dismiss — the system adapts`,
4192
+ "Your AI agents handle the complex stuff — planning, analysis,",
4193
+ "writing. This CLI handles everything else: quick lookups, status",
4194
+ "checks, maintenance, things you want instantly.",
3976
4195
  "",
3977
- `${dim("Pattern types:")}`,
3978
- ` ${yellow("Avoidance")} Dodging specific tasks repeatedly`,
3979
- ` ${yellow("Time Drift")} Working past shutdown, skipping rest`,
3980
- ` ${yellow("System Spree")} Building tools instead of shipping`,
3981
- ` ${yellow("Scope Creep")} Tasks growing beyond plan boundaries`,
3982
- ` ${yellow("Energy Cycles")} Performance tied to sleep/food/time`,
4196
+ `${dim("How it fits together:")}`,
4197
+ ` ${cyan("Workflows")} Run inside AI agents (/morning, /log, /ignite)`,
4198
+ ` ${cyan("Skills")} Loaded by AI agents automatically`,
4199
+ ` ${cyan("CLI")} Runs in your terminal, no AI needed`,
3983
4200
  "",
3984
- `Confidence 3+ patterns route to the workflow that fixes them.`,
4201
+ "The CLI supplements your agents. It never replaces them.",
4202
+ `Think of it as ${bold("htop for your productivity")} — instant, free, always there.`,
3985
4203
  ],
3986
4204
  },
3987
4205
  {
3988
- title: "16 AI Agents One Brain",
4206
+ title: "CLI Status & Insight",
3989
4207
  body: [
3990
- `${bold("Mover OS installs to every major AI coding agent.")}`,
4208
+ `${dim("See what's happening without opening an AI session.")}`,
3991
4209
  "",
3992
- `${dim("Full tier (rules + skills + commands + hooks):")}`,
3993
- ` Claude Code Cursor Cline Windsurf Gemini CLI`,
3994
- ` Copilot Amazon Q OpenCode Kilo Code`,
4210
+ ` ${cyan("pulse")} Dashboard tasks, streaks, energy, blockers`,
4211
+ ` ${dim("Like htop for your day. One glance.")}`,
3995
4212
  "",
3996
- `${dim("Enhanced tier (rules + skills):")}`,
3997
- ` Codex Amp Roo Code Antigravity`,
4213
+ ` ${cyan("replay")} Session replay what you did, when, drift analysis`,
4214
+ ` ${dim("\"You planned 4 tasks, completed 2, drifted into 3.\"")}`,
3998
4215
  "",
3999
- `${dim("Basic tier (rules only):")}`,
4000
- ` Continue Aider`,
4216
+ ` ${cyan("diff")} Engine evolution how your strategy changed over time`,
4217
+ ` ${dim("Git-powered. Shows exactly when and why.")}`,
4218
+ "",
4219
+ ` ${cyan("context")} What each agent sees — loaded rules, skills, token count`,
4220
+ ` ${dim("Debug why an agent is behaving differently.")}`,
4221
+ "",
4222
+ ` ${cyan("doctor")} Health check — are all agents configured correctly?`,
4223
+ ],
4224
+ },
4225
+ {
4226
+ title: "CLI — Quick Actions",
4227
+ body: [
4228
+ `${dim("Do things fast without starting a session.")}`,
4001
4229
  "",
4002
- "Switch editors freely. Your identity, strategy, and patterns",
4003
- `follow you everywhere. The ${bold("Engine is the constant")}.`,
4004
- `Run ${cyan("moveros sync")} to update all agents at once.`,
4230
+ ` ${cyan("capture")} Quick capture task, idea, link, brain dump`,
4231
+ ` ${dim("moveros capture --task \"Fix the login bug\"")}`,
4232
+ "",
4233
+ ` ${cyan("who")} Entity lookup — search People, Orgs, Places`,
4234
+ ` ${dim("moveros who \"Ishaaq\" → everything you know about them")}`,
4235
+ "",
4236
+ ` ${cyan("prayer")} Mosque timetable — next prayer in your status line`,
4237
+ ` ${dim("Paste or fetch. Shows countdown. Optional.")}`,
4005
4238
  ],
4006
4239
  },
4007
4240
  {
4008
- title: "CLI Commands",
4241
+ title: "CLI — Maintenance",
4009
4242
  body: [
4010
- `${bold("Terminal utilities no AI session needed.")}`,
4243
+ `${dim("Keep everything in sync without thinking about it.")}`,
4244
+ "",
4245
+ ` ${cyan("sync")} Update all agents to latest rules and skills`,
4246
+ ` ${dim("One command updates 16 agents. Shows what changed.")}`,
4247
+ "",
4248
+ ` ${cyan("backup")} Backup Engine, Areas, or agent configs`,
4249
+ ` ${dim("With manifest. Knows what's in each backup.")}`,
4011
4250
  "",
4012
- ` ${cyan("moveros pulse")} Dashboard energy, tasks, streaks, blockers`,
4013
- ` ${cyan("moveros doctor")} Health check across all installed agents`,
4014
- ` ${cyan("moveros capture")} Quick capture — tasks, links, ideas from terminal`,
4015
- ` ${cyan("moveros who")} Entity memory lookup — people, orgs, places`,
4016
- ` ${cyan("moveros sync")} Update all agents to latest rules/skills`,
4017
- ` ${cyan("moveros context")} See what each agent loads — rules, skills, tokens`,
4018
- ` ${cyan("moveros diff")} Engine file evolution via git history`,
4019
- ` ${cyan("moveros replay")} Session replay from Daily Notes`,
4020
- ` ${cyan("moveros settings")} View/edit config — ${dim("settings set <key> <val>")}`,
4021
- ` ${cyan("moveros backup")} Manual backup wizard (engine, areas, agents)`,
4022
- ` ${cyan("moveros restore")} Restore from backup`,
4023
- ` ${cyan("moveros warm")} Pre-warm an AI session with context`,
4251
+ ` ${cyan("restore")} Restore from any backup — selective, safe`,
4252
+ ` ${dim("Creates a safety backup before restoring.")}`,
4253
+ "",
4254
+ ` ${cyan("settings")} View/edit config from terminal`,
4255
+ ` ${dim("moveros settings set review_day sunday")}`,
4256
+ "",
4257
+ ` ${cyan("update")} Update CLI + workflows + skills in one go`,
4258
+ ` ${dim("Self-updates the CLI, then pulls latest payload.")}`,
4024
4259
  ],
4025
4260
  },
4026
4261
  {
4027
- title: "The Status Line",
4262
+ title: "16 Agents, One Brain",
4028
4263
  body: [
4029
- `${bold("Your life, glanceable. Always visible in Claude Code.")}`,
4264
+ `${dim("Install once. Every agent gets the same context.")}`,
4265
+ "",
4266
+ `${dim("Full tier")} ${dim("(rules + skills + commands + hooks):")}`,
4267
+ ` Claude Code Cursor Cline Windsurf Gemini CLI`,
4268
+ ` Copilot Amazon Q OpenCode Kilo Code`,
4030
4269
  "",
4031
- ` ${dim("Line 1:")} Model · Context% · Project(branch) · Lines · Time · Cost`,
4032
- ` ${dim("Line 2:")} Next task · Progress · Vitality · Prayer · Last log`,
4033
- ` ${dim("Line 3:")} Rate limits (5hr + 7day + extra usage)`,
4270
+ `${dim("Enhanced tier")} ${dim("(rules + skills):")}`,
4271
+ ` Codex Amp Roo Code Antigravity`,
4034
4272
  "",
4035
- `${dim("Features:")}`,
4036
- ` ${cyan("•")} Next unchecked task from today's Daily Note`,
4037
- ` ${cyan("•")} Strategic task count (vitality excluded)`,
4038
- ` ${cyan("•")} Vitality slot machine — rotates reminders every minute`,
4039
- ` ${cyan("•")} Next prayer time (if ${dim("show_prayer_times: true")} in settings)`,
4040
- ` ${cyan("•")} Time since last /log — so you never forget`,
4041
- ` ${cyan("•")} Rate limit bars with reset times`,
4273
+ `${dim("Basic tier")} ${dim("(rules only):")}`,
4274
+ ` Continue Aider`,
4042
4275
  "",
4043
- `All data from your vault. ${bold("No API calls")} except rate limits.`,
4276
+ "Switch editors whenever. Your Engine follows you.",
4277
+ `${cyan("moveros sync")} keeps them all current.`,
4044
4278
  ],
4045
4279
  },
4046
4280
  {
4047
- title: "Getting Started",
4281
+ title: "Get Started",
4048
4282
  body: [
4049
- `${bold("You're ready.")}`,
4283
+ `${bold("Five minutes to a system that remembers everything.")}`,
4050
4284
  "",
4051
- ` ${cyan("1.")} Run ${bold("/setup")} in any AI agent to build your Engine`,
4285
+ ` ${cyan("1.")} Run ${bold("/setup")} in your AI agent it'll interview you`,
4052
4286
  ` ${cyan("2.")} Run ${bold("/morning")} to start your first session`,
4053
4287
  ` ${cyan("3.")} Work. Build. Ship.`,
4054
- ` ${cyan("4.")} Run ${bold("/log")} to capture what happened`,
4288
+ ` ${cyan("4.")} Run ${bold("/log")} when you're done — captures everything`,
4055
4289
  ` ${cyan("5.")} Run ${bold("/plan-tomorrow")} before bed`,
4056
4290
  "",
4057
- `${dim("Quick tips:")}`,
4058
- ` ${cyan("•")} ${bold("Single Test")} — one thing that makes the day a win`,
4059
- ` ${cyan("•")} ${bold("Sacrifice")} — what you won't do today (as important as what you will)`,
4060
- ` ${cyan("")} The system gets smarter the more you use it`,
4061
- ` ${cyan("•")} Trust the Engine files they're your memory between sessions`,
4291
+ `${dim("Two things to know:")}`,
4292
+ ` ${bold("Single Test")} the one thing that makes today a win`,
4293
+ ` ${bold("Sacrifice")} — what you won't do (just as important)`,
4294
+ "",
4295
+ "The system learns from you. The more you use it, the sharper it gets.",
4062
4296
  "",
4063
- `${dim("moveros.dev")} ${dim("·")} ${dim("$49 one-time")} ${dim("·")} ${dim("updates included")}`,
4297
+ `${dim("moveros.dev")}`,
4064
4298
  ],
4065
4299
  },
4066
4300
  ];
4067
4301
 
4068
4302
  // Paginated display with keyboard navigation
4069
4303
  let page = 0;
4304
+ const seen = new Set();
4070
4305
 
4071
- function renderPage() {
4306
+ async function renderPage(animate) {
4072
4307
  const p = pages[page];
4073
- // Clear screen area
4074
4308
  w("\x1b[2J\x1b[H"); // clear screen, cursor to top
4075
4309
  ln();
4076
4310
 
@@ -4086,9 +4320,14 @@ async function cmdHelp(opts) {
4086
4320
  ln(` ${S.cyan}└${"─".repeat(56)}┘${S.reset}`);
4087
4321
  ln();
4088
4322
 
4089
- // Body
4090
- for (const line of p.body) {
4091
- ln(` ${line}`);
4323
+ // Body — animate on first visit, instant on revisit
4324
+ if (animate && !seen.has(page)) {
4325
+ for (const line of p.body) {
4326
+ await typeLine(line, 6);
4327
+ }
4328
+ seen.add(page);
4329
+ } else {
4330
+ for (const line of p.body) ln(` ${line}`);
4092
4331
  }
4093
4332
  ln();
4094
4333
  ln();
@@ -4103,7 +4342,6 @@ async function cmdHelp(opts) {
4103
4342
 
4104
4343
  return new Promise((resolve) => {
4105
4344
  if (!IS_TTY) {
4106
- // Non-interactive: dump all pages
4107
4345
  for (const p of pages) {
4108
4346
  ln(bold(`\n## ${p.title}\n`));
4109
4347
  for (const line of p.body) ln(` ${line}`);
@@ -4118,22 +4356,32 @@ async function cmdHelp(opts) {
4118
4356
  stdin.setEncoding("utf8");
4119
4357
  w(S.hide);
4120
4358
 
4121
- renderPage();
4359
+ let navigating = false;
4360
+ const go = async (newPage) => {
4361
+ if (navigating) return;
4362
+ navigating = true;
4363
+ page = newPage;
4364
+ await renderPage(true);
4365
+ navigating = false;
4366
+ };
4367
+
4368
+ go(0); // initial render with animation
4122
4369
 
4123
4370
  const handler = (data) => {
4371
+ if (navigating) return; // ignore input during animation
4124
4372
  if (data === "\x1b[C" || data === "l" || data === " ") {
4125
- if (page < pages.length - 1) { page++; renderPage(); }
4373
+ if (page < pages.length - 1) go(page + 1);
4126
4374
  } else if (data === "\x1b[D" || data === "h") {
4127
- if (page > 0) { page--; renderPage(); }
4375
+ if (page > 0) go(page - 1);
4128
4376
  } else if (data === "q" || data === "\x1b" || data === "\x03") {
4129
4377
  stdin.removeListener("data", handler);
4130
4378
  stdin.setRawMode(false);
4131
4379
  stdin.pause();
4132
4380
  w(S.show);
4133
- w("\x1b[2J\x1b[H"); // clear
4381
+ w("\x1b[2J\x1b[H");
4134
4382
  resolve();
4135
4383
  } else if (data === "\r" || data === "\n") {
4136
- if (page < pages.length - 1) { page++; renderPage(); }
4384
+ if (page < pages.length - 1) go(page + 1);
4137
4385
  else {
4138
4386
  stdin.removeListener("data", handler);
4139
4387
  stdin.setRawMode(false);
@@ -4152,120 +4400,135 @@ async function cmdHelp(opts) {
4152
4400
  // ─── moveros test (dev only) ────────────────────────────────────────────────
4153
4401
  async function cmdTest(opts) { barLn(yellow("moveros test — not yet implemented.")); }
4154
4402
 
4155
- // ─── Interactive Main Menu ────────────────────────────────────────────────────
4156
- async function cmdMainMenu() {
4157
- const categories = [
4158
- { header: "Setup", cmds: ["install", "update"] },
4159
- { header: "Dashboard", cmds: ["pulse", "replay", "diff"] },
4160
- { header: "Agents", cmds: ["doctor", "sync", "context", "warm"] },
4161
- { header: "Capture", cmds: ["capture", "who"] },
4162
- { header: "System", cmds: ["settings", "prayer", "backup", "restore", "help"] },
4163
- ];
4403
+ // ─── What's New ──────────────────────────────────────────────────────────────
4404
+ const WHATS_NEW = {
4405
+ "4.2.0": {
4406
+ headline: "Engine Intelligence + Pattern Detection",
4407
+ features: [
4408
+ "Tiered workflow reads Active_Context as cache, deep reads only when needed",
4409
+ "Active Patterns section in Active_Context — AI-detected behavioral patterns",
4410
+ "Identity Snapshot cached in Active_Context no full file reads for light workflows",
4411
+ "Monthly review overdue detection in /review-week",
4412
+ "Critical Path Check in /analyse-day (DIRECT/PREREQUISITE/DRIFT)",
4413
+ ],
4414
+ },
4415
+ "4.3.0": {
4416
+ headline: "Soul + Friction + Skills",
4417
+ features: [
4418
+ "Soul personality — sharp observations, humor, callbacks, earned respect",
4419
+ "Level 3 friction (Earn It) — AI holds the line on avoidance",
4420
+ "Pre-Escalation Gate — compound work detection, no false positives",
4421
+ "Curated skill packs — marketing, CRO, SEO, design, obsidian",
4422
+ "Skill evaluation system — A/B testing skills with benchmarks",
4423
+ ],
4424
+ },
4425
+ "4.5.0": {
4426
+ headline: "Smart Menu + TUI + Comprehensive Update",
4427
+ features: [
4428
+ "Frecency-based menu — surfaces your most-used commands",
4429
+ "Interactive settings — toggle from the menu, no CLI flags",
4430
+ "Full-screen TUI — alternate screen buffer, clean transitions",
4431
+ "Comprehensive update — backup, agent management, What's New",
4432
+ "Install/Update separation — install is fresh only, update is interactive",
4433
+ "Uninstall as standalone command",
4434
+ ],
4435
+ },
4436
+ };
4164
4437
 
4165
- const menuItems = [];
4166
- for (const cat of categories) {
4167
- menuItems.push({ id: `_sep_${cat.header}`, name: `── ${cat.header}`, _separator: true });
4168
- for (const cmd of cat.cmds) {
4169
- const meta = CLI_COMMANDS[cmd];
4170
- if (!meta || meta.hidden) continue;
4171
- menuItems.push({
4172
- id: cmd,
4173
- name: `${cmd.padEnd(12)} ${dim(meta.desc)}`,
4174
- tier: `${cat.header}`,
4175
- });
4438
+ function showWhatsNew(fromVer, toVer) {
4439
+ if (!fromVer || !toVer) return;
4440
+ const versions = Object.keys(WHATS_NEW)
4441
+ .filter(v => compareVersions(v, fromVer) > 0 && compareVersions(v, toVer) <= 0)
4442
+ .sort(compareVersions);
4443
+ if (versions.length === 0) return;
4444
+
4445
+ for (const v of versions) {
4446
+ const wn = WHATS_NEW[v];
4447
+ question(bold(`What's New in v${v}`));
4448
+ barLn(dim(wn.headline));
4449
+ barLn();
4450
+ for (const f of wn.features) {
4451
+ barLn(` ${green("+")} ${f}`);
4176
4452
  }
4453
+ barLn();
4177
4454
  }
4178
-
4179
- question(`${bold("moveros")} ${dim("— choose a command")}`);
4180
- barLn();
4181
- const choice = await interactiveSelect(menuItems);
4182
- if (!choice) {
4183
- outro(dim("Cancelled."));
4184
- process.exit(0);
4185
- }
4186
- return choice;
4187
4455
  }
4188
4456
 
4189
- // ─── Vault Resolution Helper ─────────────────────────────────────────────────
4190
- function resolveVaultPath(explicitVault) {
4191
- if (explicitVault) {
4192
- let v = explicitVault;
4193
- if (v.startsWith("~")) v = path.join(os.homedir(), v.slice(1));
4194
- return path.resolve(v);
4195
- }
4196
- // Try config.json
4457
+ // ─── Interactive Main Menu (Frecency + Context-Aware) ─────────────────────────
4458
+ async function cmdMainMenu() {
4459
+ const usage = loadCliUsage();
4460
+ const hasVault = !!resolveVaultPath();
4197
4461
  const cfgPath = path.join(os.homedir(), ".mover", "config.json");
4198
- if (fs.existsSync(cfgPath)) {
4199
- try {
4200
- const v = JSON.parse(fs.readFileSync(cfgPath, "utf8")).vaultPath;
4201
- if (v && fs.existsSync(v)) return v;
4202
- } catch {}
4203
- }
4204
- // Try Obsidian detection
4205
- const obsVaults = detectObsidianVaults();
4206
- return obsVaults.find((p) => fs.existsSync(path.join(p, ".mover-version"))) || null;
4207
- }
4208
-
4209
- // ─── Main ───────────────────────────────────────────────────────────────────
4210
- async function main() {
4211
- const opts = parseArgs();
4212
- let bundleDir = path.resolve(__dirname);
4213
- const startTime = Date.now();
4214
-
4215
- // ── Intro ──
4216
- await printHeader();
4217
-
4218
- // ── Route: no command → interactive menu (persistent loop) ──
4219
- const lightCommands = ["pulse", "warm", "capture", "who", "diff", "sync", "replay", "context", "settings", "backup", "restore", "doctor", "prayer", "help", "test"];
4462
+ const hasConfig = fs.existsSync(cfgPath);
4463
+ const hasPrayer = hasConfig && (() => { try { return !!JSON.parse(fs.readFileSync(cfgPath, "utf8")).settings?.show_prayer_times; } catch { return false; } })();
4220
4464
 
4221
- if (!opts.command) {
4222
- // Interactive loop stay open like a real app
4223
- while (true) {
4224
- opts.command = await cmdMainMenu();
4225
- if (!opts.command) break; // user cancelled
4465
+ // All non-hidden commands
4466
+ const allCmds = Object.keys(CLI_COMMANDS).filter(c => !CLI_COMMANDS[c].hidden);
4226
4467
 
4227
- if (lightCommands.includes(opts.command)) {
4228
- const handler = CLI_HANDLERS[opts.command];
4229
- if (handler) await handler(opts);
4230
- else barLn(yellow(`Command '${opts.command}' is not yet implemented.`));
4231
- opts.command = ""; // reset for next loop
4232
- barLn(dim(" Press enter to return to menu..."));
4233
- await new Promise((r) => { process.stdin.resume(); process.stdin.once("data", () => { process.stdin.pause(); r(); }); });
4234
- continue;
4235
- }
4236
- break; // install/update break out of loop into pre-flight
4237
- }
4238
- if (!opts.command) return;
4239
- }
4468
+ // Context filter: hide irrelevant commands
4469
+ const contextFilter = (cmd) => {
4470
+ if (!hasVault && ["pulse", "replay", "diff", "sync", "capture", "who", "context", "backup", "restore", "doctor"].includes(cmd)) return false;
4471
+ if (!hasPrayer && cmd === "prayer") return false;
4472
+ return true;
4473
+ };
4240
4474
 
4241
- // ── Route: direct CLI command (non-interactive) ──
4242
- if (lightCommands.includes(opts.command)) {
4243
- const handler = CLI_HANDLERS[opts.command];
4244
- if (handler) {
4245
- await handler(opts);
4246
- } else {
4247
- barLn(yellow(`Command '${opts.command}' is not yet implemented.`));
4248
- }
4249
- return;
4475
+ const eligible = allCmds.filter(contextFilter);
4476
+
4477
+ // Pinned: always visible
4478
+ const pinned = ["settings", "help"];
4479
+ // Also pin install if no vault, update if vault exists
4480
+ if (!hasVault) pinned.unshift("install");
4481
+ else pinned.unshift("update");
4482
+
4483
+ // Score remaining by frecency
4484
+ const scored = eligible
4485
+ .filter(c => !pinned.includes(c))
4486
+ .map(c => ({ cmd: c, score: frecencyScore(usage, c) }))
4487
+ .sort((a, b) => b.score - a.score);
4488
+
4489
+ // Show top N frecency commands + pinned + "more..."
4490
+ const MAX_SURFACE = 5;
4491
+ const surfaced = scored.slice(0, MAX_SURFACE).map(s => s.cmd);
4492
+ const hidden = scored.slice(MAX_SURFACE).map(s => s.cmd);
4493
+
4494
+ // Build menu: pinned first, then frecency-surfaced, then "more..." if needed
4495
+ const topItems = [...new Set([...pinned, ...surfaced])];
4496
+ const menuItems = topItems.map(cmd => ({
4497
+ id: cmd,
4498
+ name: `${cmd.padEnd(12)} ${dim(CLI_COMMANDS[cmd].desc)}`,
4499
+ }));
4500
+ if (hidden.length > 0) {
4501
+ menuItems.push({ id: "__more__", name: `${"more...".padEnd(12)} ${dim(`${hidden.length} more commands`)}` });
4250
4502
  }
4251
4503
 
4252
- // ── Pre-flight (install + update only) ──
4253
- barLn(gray("Pre-flight"));
4254
- barLn();
4255
- const checks = preflight();
4256
- for (const c of checks) {
4257
- const icon = c.status === "ok" ? green("\u2713") : c.status === "warn" ? yellow("\u25CB") : red("\u2717");
4258
- barLn(`${icon} ${dim(`${c.label} ${c.detail}`)}`);
4259
- }
4504
+ question(`${bold("moveros")} ${dim("— choose a command")}`);
4260
4505
  barLn();
4506
+ let choice = await interactiveSelect(menuItems);
4261
4507
 
4262
- if (checks.some((c) => c.status === "fail")) {
4263
- outro(red("Pre-flight failed. Fix the issues above."));
4264
- process.exit(1);
4508
+ if (choice === "__more__") {
4509
+ // Show full list
4510
+ const fullItems = eligible
4511
+ .filter(c => !c.startsWith("__"))
4512
+ .map(cmd => ({
4513
+ id: cmd,
4514
+ name: `${cmd.padEnd(12)} ${dim(CLI_COMMANDS[cmd].desc)}`,
4515
+ }));
4516
+ barLn();
4517
+ question(`${bold("moveros")} ${dim("— all commands")}`);
4518
+ barLn();
4519
+ choice = await interactiveSelect(fullItems);
4265
4520
  }
4266
4521
 
4267
- // ── CLI self-update check ──
4268
- if (opts.command === "update" && !opts._selfUpdated) {
4522
+ if (!choice) return null;
4523
+ return choice;
4524
+ }
4525
+
4526
+ // ─── Comprehensive Update (10-step interactive) ─────────────────────────────
4527
+ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
4528
+ const isQuick = opts.rest.includes("--quick");
4529
+
4530
+ // Step 1: CLI Self-Update
4531
+ if (!opts._selfUpdated) {
4269
4532
  try {
4270
4533
  const localVer = require("./package.json").version;
4271
4534
  const npmVer = execSync("npm view mover-os version", { encoding: "utf8", timeout: 10000 }).trim();
@@ -4277,7 +4540,6 @@ async function main() {
4277
4540
  sp.stop(`CLI updated to ${npmVer}`);
4278
4541
  barLn(dim(" Re-running with updated CLI..."));
4279
4542
  barLn();
4280
- // Re-exec with new code — pass args through, add flag to prevent loop
4281
4543
  const args = process.argv.slice(2).concat("--_self-updated");
4282
4544
  const { spawnSync } = require("child_process");
4283
4545
  const result = spawnSync(process.argv[0], [process.argv[1], ...args], {
@@ -4289,7 +4551,7 @@ async function main() {
4289
4551
  barLn(dim(" Continuing with current version..."));
4290
4552
  }
4291
4553
  } else {
4292
- barLn(`${green("\u2713")} ${dim("CLI is up to date")} ${dim(`(${localVer})`)}`);
4554
+ statusLine("ok", "CLI", `up to date (${localVer})`);
4293
4555
  }
4294
4556
  } catch {
4295
4557
  barLn(dim(" Could not check for CLI updates (offline?)"));
@@ -4297,90 +4559,72 @@ async function main() {
4297
4559
  barLn();
4298
4560
  }
4299
4561
 
4300
- // ── Headless quick update ──
4301
- if (opts.command === "update") {
4302
- // Validate stored key
4303
- let updateKey = opts.key;
4304
- if (!updateKey) {
4305
- const cfgPath = path.join(os.homedir(), ".mover", "config.json");
4306
- if (fs.existsSync(cfgPath)) {
4307
- try { updateKey = JSON.parse(fs.readFileSync(cfgPath, "utf8")).licenseKey; } catch {}
4308
- }
4309
- }
4310
- if (!updateKey || !await validateKey(updateKey)) {
4311
- outro(red("Valid license key required. Use: npx moveros --update --key YOUR_KEY"));
4312
- process.exit(1);
4313
- }
4314
-
4315
- // Download payload if not bundled
4316
- const hasSrcUpdate = fs.existsSync(path.join(bundleDir, "src", "workflows"));
4317
- if (!hasSrcUpdate) {
4318
- barLn(dim("Downloading payload..."));
4319
- try {
4320
- bundleDir = await downloadPayload(updateKey);
4321
- } catch (err) {
4322
- outro(red(`Download failed: ${err.message}`));
4323
- process.exit(1);
4324
- }
4325
- }
4562
+ // Step 2: Vault Detection
4563
+ let vaultPath = resolveVaultPath(opts.vault);
4564
+ if (!vaultPath) {
4565
+ outro(red("No Mover OS vault found. Use: moveros update --vault /path"));
4566
+ process.exit(1);
4567
+ }
4568
+ statusLine("ok", "Vault", path.basename(vaultPath));
4326
4569
 
4327
- // Auto-detect vault
4328
- let vaultPath = opts.vault;
4329
- if (!vaultPath) {
4330
- const obsVaults = detectObsidianVaults();
4331
- vaultPath = obsVaults.find((p) =>
4332
- fs.existsSync(path.join(p, ".mover-version"))
4333
- );
4334
- if (!vaultPath) {
4335
- outro(red("No Mover OS vault found. Use: npx moveros --update --vault /path"));
4336
- process.exit(1);
4337
- }
4570
+ // Step 3: Key Validation
4571
+ let updateKey = opts.key;
4572
+ if (!updateKey) {
4573
+ const cfgPath = path.join(os.homedir(), ".mover", "config.json");
4574
+ if (fs.existsSync(cfgPath)) {
4575
+ try { updateKey = JSON.parse(fs.readFileSync(cfgPath, "utf8")).licenseKey; } catch {}
4338
4576
  }
4339
- if (vaultPath.startsWith("~")) vaultPath = path.join(os.homedir(), vaultPath.slice(1));
4340
- vaultPath = path.resolve(vaultPath);
4341
- barLn(dim(`Vault: ${vaultPath}`));
4577
+ }
4578
+ if (!updateKey) {
4579
+ updateKey = await textInput({ label: "License key", mask: "\u25AA", placeholder: "MOVER-XXXX-XXXX" });
4580
+ if (!updateKey) return;
4581
+ }
4582
+ const sp1 = spinner("Validating license");
4583
+ if (!await validateKey(updateKey)) {
4584
+ sp1.stop(red("Invalid key"));
4585
+ outro(red("Valid license key required."));
4586
+ process.exit(1);
4587
+ }
4588
+ sp1.stop(green("License verified"));
4589
+ barLn();
4342
4590
 
4343
- // Auto-detect agents
4344
- const detectedAgents = AGENTS.filter((a) => a.detect());
4345
- if (detectedAgents.length === 0) {
4346
- outro(red("No AI agents detected."));
4591
+ // Download payload if not bundled
4592
+ const hasSrc = fs.existsSync(path.join(bundleDir, "src", "workflows"));
4593
+ if (!hasSrc) {
4594
+ const dlSp = spinner("Downloading Mover OS");
4595
+ try {
4596
+ bundleDir = await downloadPayload(updateKey);
4597
+ dlSp.stop(green("Downloaded"));
4598
+ } catch (err) {
4599
+ dlSp.stop(red("Download failed"));
4600
+ outro(red(err.message));
4347
4601
  process.exit(1);
4348
4602
  }
4349
- const selectedIds = detectedAgents.map((a) => a.id);
4350
- barLn(dim(`Agents: ${detectedAgents.map((a) => a.name).join(", ")}`));
4351
4603
  barLn();
4604
+ }
4352
4605
 
4353
- // Detect changes
4606
+ // Read versions
4607
+ const vfPath = path.join(vaultPath, ".mover-version");
4608
+ const installedVer = fs.existsSync(vfPath) ? fs.readFileSync(vfPath, "utf8").trim() : null;
4609
+ let newVer = `V${VERSION}`;
4610
+ try {
4611
+ const pkg = JSON.parse(fs.readFileSync(path.join(bundleDir, "package.json"), "utf8"));
4612
+ newVer = pkg.version || newVer;
4613
+ } catch {}
4614
+
4615
+ // Quick mode: skip interactive steps
4616
+ if (isQuick) {
4617
+ const detectedAgents = AGENTS.filter((a) => a.detect());
4618
+ if (detectedAgents.length === 0) { outro(red("No AI agents detected.")); process.exit(1); }
4619
+ const selectedIds = detectedAgents.map((a) => a.id);
4354
4620
  const changes = detectChanges(bundleDir, vaultPath, selectedIds);
4355
4621
  const totalChanged = countChanges(changes);
4356
-
4357
- // Read versions
4358
- const vfPath = path.join(vaultPath, ".mover-version");
4359
- const installedVer = fs.existsSync(vfPath) ? fs.readFileSync(vfPath, "utf8").trim() : null;
4360
- let newVer = `V${VERSION}`;
4361
- try {
4362
- const pkg = JSON.parse(fs.readFileSync(path.join(bundleDir, "package.json"), "utf8"));
4363
- newVer = pkg.version || newVer;
4364
- } catch {}
4365
-
4366
4622
  displayChangeSummary(changes, installedVer, newVer);
4367
-
4368
- if (totalChanged === 0) {
4369
- outro(green("Already up to date."));
4370
- return;
4371
- }
4372
-
4373
- // Apply all changes
4623
+ if (totalChanged === 0) { outro(green("Already up to date.")); return; }
4374
4624
  barLn(bold("Updating..."));
4375
4625
  barLn();
4376
-
4377
- // Vault structure
4378
4626
  createVaultStructure(vaultPath);
4379
-
4380
- // Templates
4381
4627
  installTemplateFiles(bundleDir, vaultPath);
4382
-
4383
- // Per-agent installation
4384
4628
  const writtenFiles = new Set();
4385
4629
  const skillOpts = { install: true, categories: null, workflows: null };
4386
4630
  for (const agent of detectedAgents) {
@@ -4389,20 +4633,325 @@ async function main() {
4389
4633
  const sp = spinner(agent.name);
4390
4634
  const steps = fn(bundleDir, vaultPath, skillOpts, writtenFiles, agent.id);
4391
4635
  await sleep(200);
4392
- if (steps.length > 0) {
4393
- sp.stop(`${agent.name} ${dim(steps.join(", "))}`);
4394
- } else {
4395
- sp.stop(`${agent.name} ${dim("configured")}`);
4636
+ sp.stop(steps.length > 0 ? `${agent.name} ${dim(steps.join(", "))}` : `${agent.name} ${dim("configured")}`);
4637
+ }
4638
+ fs.writeFileSync(path.join(vaultPath, ".mover-version"), `${require("./package.json").version}\n`, "utf8");
4639
+ writeMoverConfig(vaultPath, selectedIds);
4640
+ barLn();
4641
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
4642
+ outro(`${green("Done.")} ${totalChanged} files updated in ${elapsed}s. Run ${bold("/update")} if version bumped.`);
4643
+ return;
4644
+ }
4645
+
4646
+ // Step 4: What's New
4647
+ showWhatsNew(installedVer, newVer);
4648
+
4649
+ // Step 5: Backup Offer
4650
+ const engine = detectEngineFiles(vaultPath);
4651
+ if (engine.exists) {
4652
+ question("Back up before updating?");
4653
+ barLn(dim(" Select what to save. Esc to skip."));
4654
+ barLn();
4655
+ const backupItems = [
4656
+ { id: "engine", name: "Engine files", tier: "Identity, Strategy, Goals, Context" },
4657
+ ];
4658
+ if (fs.existsSync(path.join(vaultPath, "02_Areas"))) {
4659
+ backupItems.push({ id: "areas", name: "Full Areas folder", tier: "Everything in 02_Areas/" });
4660
+ }
4661
+ const detectedForBackup = AGENTS.filter((a) => a.detect()).map((a) => a.id);
4662
+ if (detectedForBackup.length > 0) {
4663
+ backupItems.push({ id: "agents", name: "Agent configs", tier: `Rules, skills from ${detectedForBackup.length} agent(s)` });
4664
+ }
4665
+ const backupChoices = await interactiveSelect(backupItems, { multi: true, preSelected: ["engine"] });
4666
+ if (backupChoices && backupChoices.length > 0) {
4667
+ const now = new Date();
4668
+ const ts = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}_${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
4669
+ const archivesDir = path.join(vaultPath, "04_Archives");
4670
+ if (backupChoices.includes("engine")) {
4671
+ const engineDir = path.join(vaultPath, "02_Areas", "Engine");
4672
+ const backupDir = path.join(archivesDir, `Engine_Backup_${ts}`);
4673
+ try {
4674
+ fs.mkdirSync(backupDir, { recursive: true });
4675
+ let backed = 0;
4676
+ for (const f of fs.readdirSync(engineDir).filter(f => fs.statSync(path.join(engineDir, f)).isFile())) {
4677
+ fs.copyFileSync(path.join(engineDir, f), path.join(backupDir, f));
4678
+ backed++;
4679
+ }
4680
+ statusLine("ok", "Backed up", `${backed} Engine files`);
4681
+ } catch (err) { barLn(yellow(` Backup failed: ${err.message}`)); }
4682
+ }
4683
+ if (backupChoices.includes("areas")) {
4684
+ try {
4685
+ copyDirRecursive(path.join(vaultPath, "02_Areas"), path.join(archivesDir, `Areas_Backup_${ts}`));
4686
+ statusLine("ok", "Backed up", "Full Areas folder");
4687
+ } catch (err) { barLn(yellow(` Areas backup failed: ${err.message}`)); }
4688
+ }
4689
+ if (backupChoices.includes("agents")) {
4690
+ try {
4691
+ const agentBackupDir = path.join(archivesDir, `Agent_Backup_${ts}`);
4692
+ fs.mkdirSync(agentBackupDir, { recursive: true });
4693
+ let agentsBacked = 0;
4694
+ for (const ag of AGENTS.filter((a) => a.detect())) {
4695
+ const agDir = path.join(agentBackupDir, ag.id);
4696
+ fs.mkdirSync(agDir, { recursive: true });
4697
+ for (const cp of (ag.configPaths || [])) {
4698
+ if (fs.existsSync(cp.src)) {
4699
+ try { fs.copyFileSync(cp.src, path.join(agDir, path.basename(cp.src))); agentsBacked++; } catch {}
4700
+ }
4701
+ }
4702
+ }
4703
+ statusLine("ok", "Backed up", `${agentsBacked} agent config files`);
4704
+ } catch (err) { barLn(yellow(` Agent backup failed: ${err.message}`)); }
4396
4705
  }
4397
4706
  }
4707
+ barLn();
4708
+ }
4398
4709
 
4399
- // Update version marker + config
4400
- fs.writeFileSync(path.join(vaultPath, ".mover-version"), `V${VERSION}\n`, "utf8");
4401
- writeMoverConfig(vaultPath, selectedIds);
4710
+ // Step 6: Agent Management
4711
+ const visibleAgents = AGENTS.filter((a) => !a.hidden);
4712
+ const detectedIds = visibleAgents.filter((a) => a.detect()).map((a) => a.id);
4713
+ const cfgPath = path.join(os.homedir(), ".mover", "config.json");
4714
+ let currentAgents = [];
4715
+ if (fs.existsSync(cfgPath)) {
4716
+ try { currentAgents = JSON.parse(fs.readFileSync(cfgPath, "utf8")).agents || []; } catch {}
4717
+ }
4718
+ const preSelectedAgents = currentAgents.length > 0 ? currentAgents : detectedIds;
4719
+
4720
+ question(`Agents ${dim("(add or remove)")}`);
4721
+ barLn();
4722
+ const agentItems = visibleAgents.map((a) => ({
4723
+ ...a,
4724
+ _detected: detectedIds.includes(a.id),
4725
+ }));
4726
+ const selectedIds = await interactiveSelect(agentItems, { multi: true, preSelected: preSelectedAgents });
4727
+ if (!selectedIds || selectedIds.length === 0) return;
4728
+ const selectedAgents = AGENTS.filter((a) => selectedIds.includes(a.id));
4729
+ barLn();
4730
+
4731
+ // Step 7: Change Detection
4732
+ const changes = detectChanges(bundleDir, vaultPath, selectedIds);
4733
+ const totalChanged = countChanges(changes);
4734
+ question("Change Summary");
4735
+ barLn();
4736
+ displayChangeSummary(changes, installedVer, newVer);
4402
4737
 
4738
+ if (totalChanged === 0) {
4739
+ barLn(green(" Already up to date."));
4403
4740
  barLn();
4404
4741
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
4405
- outro(`${green("Done.")} ${totalChanged} files updated in ${elapsed}s. Run ${bold("/update")} if version bumped.`);
4742
+ outro(`${green("Done.")} No changes needed. ${dim(`(${elapsed}s)`)}`);
4743
+ return;
4744
+ }
4745
+
4746
+ const applyChoice = await interactiveSelect([
4747
+ { id: "all", name: "Yes, update all changed files", tier: "" },
4748
+ { id: "select", name: "Select individually", tier: "" },
4749
+ { id: "cancel", name: "Cancel", tier: "" },
4750
+ ], { multi: false, defaultIndex: 0 });
4751
+ if (!applyChoice || applyChoice === "cancel") { outro("Cancelled."); return; }
4752
+
4753
+ let selectedWorkflows = null;
4754
+ let skipHooks = false, skipRules = false, skipTemplates = false;
4755
+ if (applyChoice === "select") {
4756
+ const changedItems = [];
4757
+ const changedPreSelected = [];
4758
+ for (const f of changes.workflows.filter((x) => x.status !== "unchanged")) {
4759
+ const id = `wf:${f.file}`;
4760
+ changedItems.push({ id, name: `/${f.file.replace(".md", "")}`, tier: dim(f.status === "new" ? "new" : "changed") });
4761
+ changedPreSelected.push(id);
4762
+ }
4763
+ for (const f of changes.hooks.filter((x) => x.status !== "unchanged")) {
4764
+ const id = `hook:${f.file}`;
4765
+ changedItems.push({ id, name: f.file, tier: dim(f.status === "new" ? "new hook" : "hook") });
4766
+ changedPreSelected.push(id);
4767
+ }
4768
+ if (changes.rules === "changed") {
4769
+ changedItems.push({ id: "rules", name: "Global Rules", tier: dim("rules") });
4770
+ changedPreSelected.push("rules");
4771
+ }
4772
+ for (const f of changes.templates.filter((x) => x.status !== "unchanged")) {
4773
+ const id = `tmpl:${f.file}`;
4774
+ changedItems.push({ id, name: f.file.replace(/\\/g, "/"), tier: dim(f.status === "new" ? "new" : "changed") });
4775
+ changedPreSelected.push(id);
4776
+ }
4777
+ if (changedItems.length > 0) {
4778
+ question("Select files to update");
4779
+ barLn();
4780
+ const selectedFileIds = await interactiveSelect(changedItems, { multi: true, preSelected: changedPreSelected });
4781
+ if (!selectedFileIds) return;
4782
+ const selectedWfFiles = selectedFileIds.filter((id) => id.startsWith("wf:")).map((id) => id.slice(3));
4783
+ if (selectedWfFiles.length < changes.workflows.filter((x) => x.status !== "unchanged").length) {
4784
+ selectedWorkflows = new Set(selectedWfFiles);
4785
+ }
4786
+ skipHooks = !selectedFileIds.some((id) => id.startsWith("hook:"));
4787
+ skipRules = !selectedFileIds.includes("rules");
4788
+ skipTemplates = !selectedFileIds.some((id) => id.startsWith("tmpl:"));
4789
+ }
4790
+ }
4791
+
4792
+ // Step 8: Apply Changes (with progress animation)
4793
+ barLn();
4794
+ question(bold("Applying updates"));
4795
+ barLn();
4796
+
4797
+ const installSteps = [];
4798
+ installSteps.push({ label: "Vault structure", fn: async () => { createVaultStructure(vaultPath); await sleep(100); } });
4799
+ if (!skipTemplates) {
4800
+ installSteps.push({ label: "Template files", fn: async () => { installTemplateFiles(bundleDir, vaultPath); await sleep(100); } });
4801
+ }
4802
+
4803
+ const writtenFiles = new Set();
4804
+ const skillOpts = { install: true, categories: null, workflows: selectedWorkflows, skipHooks, skipRules, skipTemplates };
4805
+ for (const agent of selectedAgents) {
4806
+ const sel = AGENT_SELECTIONS.find((s) => s.id === agent.id);
4807
+ const targets = sel ? sel.targets : [agent.id];
4808
+ for (const targetId of targets) {
4809
+ const fn = AGENT_INSTALLERS[targetId];
4810
+ if (!fn) continue;
4811
+ const targetReg = AGENT_REGISTRY[targetId];
4812
+ const displayName = targetReg ? targetReg.name : agent.name;
4813
+ installSteps.push({ label: displayName, fn: async () => { fn(bundleDir, vaultPath, skillOpts, writtenFiles, targetId); await sleep(150); } });
4814
+ }
4815
+ }
4816
+
4817
+ await installProgress(installSteps);
4818
+
4819
+ // Step 9: Skills Refresh
4820
+ barLn();
4821
+ const allSkills = findSkills(bundleDir);
4822
+ if (allSkills.length > 0 && selectedAgents.some((a) => a.id !== "aider")) {
4823
+ question("Refresh skill categories?");
4824
+ barLn();
4825
+ const catCounts = {};
4826
+ for (const sk of allSkills) { catCounts[sk.category] = (catCounts[sk.category] || 0) + 1; }
4827
+ const categoryItems = CATEGORY_META.map((c) => ({
4828
+ id: c.id,
4829
+ name: `${c.name} ${dim(`(${catCounts[c.id] || 0})`)}`,
4830
+ tier: dim(c.desc),
4831
+ }));
4832
+ const selectedCatIds = await interactiveSelect(categoryItems, { multi: true, preSelected: ["development", "obsidian"] });
4833
+ if (selectedCatIds && selectedCatIds.length > 0) {
4834
+ const catSet = new Set(selectedCatIds);
4835
+ const refreshOpts = { install: true, categories: catSet, workflows: null };
4836
+ for (const agent of selectedAgents) {
4837
+ const fn = AGENT_INSTALLERS[agent.id];
4838
+ if (fn) fn(bundleDir, vaultPath, refreshOpts, writtenFiles, agent.id);
4839
+ }
4840
+ const skillCount = allSkills.filter((s) => s.category === "tools" || catSet.has(s.category)).length;
4841
+ statusLine("ok", "Skills refreshed", `${skillCount} across ${selectedAgents.length} agent(s)`);
4842
+ }
4843
+ barLn();
4844
+ }
4845
+
4846
+ // Update version marker + config
4847
+ fs.writeFileSync(path.join(vaultPath, ".mover-version"), `${require("./package.json").version}\n`, "utf8");
4848
+ writeMoverConfig(vaultPath, selectedIds, updateKey);
4849
+
4850
+ // Step 10: Summary + Success
4851
+ barLn();
4852
+ await successAnimation(`Mover OS updated — ${totalChanged} files`);
4853
+
4854
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
4855
+ outro(`${green("Done.")} ${dim(`${elapsed}s`)} Run ${bold("/update")} in your agent to sync Engine.`);
4856
+ }
4857
+
4858
+ // ─── Vault Resolution Helper ─────────────────────────────────────────────────
4859
+ function resolveVaultPath(explicitVault) {
4860
+ if (explicitVault) {
4861
+ let v = explicitVault;
4862
+ if (v.startsWith("~")) v = path.join(os.homedir(), v.slice(1));
4863
+ return path.resolve(v);
4864
+ }
4865
+ // Try config.json
4866
+ const cfgPath = path.join(os.homedir(), ".mover", "config.json");
4867
+ if (fs.existsSync(cfgPath)) {
4868
+ try {
4869
+ const v = JSON.parse(fs.readFileSync(cfgPath, "utf8")).vaultPath;
4870
+ if (v && fs.existsSync(v)) return v;
4871
+ } catch {}
4872
+ }
4873
+ // Try Obsidian detection
4874
+ const obsVaults = detectObsidianVaults();
4875
+ return obsVaults.find((p) => fs.existsSync(path.join(p, ".mover-version"))) || null;
4876
+ }
4877
+
4878
+ // ─── Main ───────────────────────────────────────────────────────────────────
4879
+ async function main() {
4880
+ const opts = parseArgs();
4881
+ let bundleDir = path.resolve(__dirname);
4882
+ const startTime = Date.now();
4883
+
4884
+ // ── TUI: Enter alternate screen ──
4885
+ enterAltScreen();
4886
+
4887
+ // ── Intro: Logo plays once ──
4888
+ await printHeader();
4889
+
4890
+ // ── Route: no command → interactive menu (persistent loop) ──
4891
+ const lightCommands = ["pulse", "capture", "who", "diff", "sync", "replay", "context", "settings", "backup", "restore", "doctor", "prayer", "help", "uninstall", "test"];
4892
+
4893
+ if (!opts.command) {
4894
+ // Interactive loop — stay open like a real app
4895
+ while (true) {
4896
+ clearContent();
4897
+ ln(compactHeader());
4898
+ ln();
4899
+
4900
+ opts.command = await cmdMainMenu();
4901
+ if (!opts.command) break; // user hit Esc — clean exit
4902
+
4903
+ if (lightCommands.includes(opts.command)) {
4904
+ await wipeDown(12);
4905
+ ln(compactHeader());
4906
+ ln();
4907
+
4908
+ recordCliUsage(opts.command);
4909
+ const handler = CLI_HANDLERS[opts.command];
4910
+ if (handler) await handler(opts);
4911
+ else barLn(yellow(`Command '${opts.command}' is not yet implemented.`));
4912
+ opts.command = ""; // reset for next loop
4913
+
4914
+ barLn(dim(" esc to go back"));
4915
+ await waitForEsc();
4916
+ continue;
4917
+ }
4918
+ // install/update — record usage and break out into pre-flight
4919
+ recordCliUsage(opts.command);
4920
+ break;
4921
+ }
4922
+ if (!opts.command) return;
4923
+ }
4924
+
4925
+ // ── Route: direct CLI command (non-interactive) ──
4926
+ if (lightCommands.includes(opts.command)) {
4927
+ recordCliUsage(opts.command);
4928
+ const handler = CLI_HANDLERS[opts.command];
4929
+ if (handler) {
4930
+ await handler(opts);
4931
+ } else {
4932
+ barLn(yellow(`Command '${opts.command}' is not yet implemented.`));
4933
+ }
4934
+ return;
4935
+ }
4936
+
4937
+ // ── Pre-flight (install + update only) ──
4938
+ barLn(gray("Pre-flight"));
4939
+ barLn();
4940
+ const checks = preflight();
4941
+ for (const c of checks) {
4942
+ const icon = c.status === "ok" ? green("\u2713") : c.status === "warn" ? yellow("\u25CB") : red("\u2717");
4943
+ barLn(`${icon} ${dim(`${c.label} ${c.detail}`)}`);
4944
+ }
4945
+ barLn();
4946
+
4947
+ if (checks.some((c) => c.status === "fail")) {
4948
+ outro(red("Pre-flight failed. Fix the issues above."));
4949
+ process.exit(1);
4950
+ }
4951
+
4952
+ // ── Comprehensive Update ──
4953
+ if (opts.command === "update") {
4954
+ await cmdUpdateComprehensive(opts, bundleDir, startTime);
4406
4955
  return;
4407
4956
  }
4408
4957
 
@@ -4542,213 +5091,21 @@ async function main() {
4542
5091
  if (vaultPath.startsWith("~")) vaultPath = path.join(os.homedir(), vaultPath.slice(1));
4543
5092
  vaultPath = path.resolve(vaultPath);
4544
5093
 
4545
- // ── Detect existing install show mode menu ──
5094
+ // ── Fresh install only redirect if existing ──
4546
5095
  const engine = detectEngineFiles(vaultPath);
4547
5096
  const versionFile = path.join(vaultPath, ".mover-version");
4548
5097
  const hasExistingInstall = fs.existsSync(versionFile) || engine.exists;
4549
5098
 
4550
- let installMode = "fresh"; // fresh | update | uninstall
4551
-
4552
5099
  if (hasExistingInstall) {
4553
- if (engine.exists) {
4554
- barLn(yellow("Existing Mover OS vault detected."));
4555
- barLn(dim(` Engine files: ${engine.files.join(", ")}`));
4556
- } else {
4557
- barLn(yellow("Mover OS installed, but no Engine data yet."));
4558
- }
5100
+ question(yellow("Mover OS is already installed in this vault."));
5101
+ barLn(dim(" Use " + bold("moveros update") + " to refresh agents, rules, and skills."));
5102
+ barLn(dim(" Use " + bold("moveros uninstall") + " to remove Mover OS."));
4559
5103
  barLn();
4560
-
4561
- question("What would you like to do?");
4562
- barLn();
4563
-
4564
- installMode = await interactiveSelect(
4565
- [
4566
- { id: "update", name: "Update", tier: "Refreshes rules, commands, and skills. Your data stays safe." },
4567
- { id: "fresh", name: "Fresh Install", tier: "Full setup from scratch. Template files will be overwritten." },
4568
- { id: "uninstall", name: "Uninstall", tier: "Remove Mover OS files from your agents and vault." },
4569
- ],
4570
- { multi: false, defaultIndex: 0 }
4571
- );
4572
- if (!installMode) return;
4573
- }
4574
-
4575
- // ── Uninstall flow ──
4576
- if (installMode === "uninstall") {
4577
- await runUninstall(vaultPath);
5104
+ outro("Use update or uninstall instead.");
4578
5105
  return;
4579
5106
  }
4580
5107
 
4581
- const updateMode = installMode === "update";
4582
-
4583
- // ── Backup (update mode only, if Engine files exist) ──
4584
- if (updateMode && engine.exists) {
4585
- barLn();
4586
- question("Back up before updating?");
4587
- barLn(dim(" Select what to save. Your data won't be overwritten, but backups are always safer."));
4588
- barLn();
4589
-
4590
- const backupItems = [
4591
- { id: "engine", name: "Engine files", tier: "Identity, Strategy, Goals, and all Engine data" },
4592
- ];
4593
-
4594
- const areasDir = path.join(vaultPath, "02_Areas");
4595
- if (fs.existsSync(areasDir)) {
4596
- backupItems.push({ id: "areas", name: "Full Areas folder", tier: "Everything in 02_Areas/ including Dailies and Reviews" });
4597
- }
4598
-
4599
- // Only offer agent config backup if any agents are detected
4600
- const detectedForBackup = AGENTS.filter((a) => a.detect()).map((a) => a.id);
4601
- if (detectedForBackup.length > 0) {
4602
- backupItems.push({ id: "agents", name: "Agent configs", tier: `Current rules, skills, and commands from ${detectedForBackup.length} detected agent(s)` });
4603
- }
4604
-
4605
- backupItems.push({ id: "skip", name: "Skip backup", tier: "Continue without backing up" });
4606
-
4607
- const backupChoices = await interactiveSelect(backupItems, {
4608
- multi: true,
4609
- preSelected: ["engine"],
4610
- });
4611
-
4612
- if (backupChoices && backupChoices.length > 0 && !(backupChoices.length === 1 && backupChoices.includes("skip"))) {
4613
- const now = new Date();
4614
- const ts = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}_${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
4615
- const archivesDir = path.join(vaultPath, "04_Archives");
4616
-
4617
- // Engine files backup
4618
- if (backupChoices.includes("engine")) {
4619
- const backupDir = path.join(archivesDir, `Engine_Backup_${ts}`);
4620
- const engineDir = path.join(vaultPath, "02_Areas", "Engine");
4621
- try {
4622
- fs.mkdirSync(backupDir, { recursive: true });
4623
- let backed = 0;
4624
- for (const file of fs.readdirSync(engineDir)) {
4625
- const src = path.join(engineDir, file);
4626
- if (fs.statSync(src).isFile()) {
4627
- fs.copyFileSync(src, path.join(backupDir, file));
4628
- backed++;
4629
- }
4630
- }
4631
- barLn(green(` Backed up ${backed} Engine files to 04_Archives/Engine_Backup_${ts}/`));
4632
- } catch (err) {
4633
- barLn(yellow(` Engine backup failed: ${err.message}. Continuing anyway.`));
4634
- }
4635
- }
4636
-
4637
- // Full Areas folder backup
4638
- if (backupChoices.includes("areas")) {
4639
- const backupDir = path.join(archivesDir, `Areas_Backup_${ts}`);
4640
- try {
4641
- copyDirRecursive(path.join(vaultPath, "02_Areas"), backupDir);
4642
- barLn(green(` Backed up full Areas folder to 04_Archives/Areas_Backup_${ts}/`));
4643
- } catch (err) {
4644
- barLn(yellow(` Areas backup failed: ${err.message}. Continuing anyway.`));
4645
- }
4646
- }
4647
-
4648
- // Agent configs backup
4649
- if (backupChoices.includes("agents")) {
4650
- const home = os.homedir();
4651
- const agentBackupDir = path.join(archivesDir, `Agent_Backup_${ts}`);
4652
- const AGENT_CONFIG_PATHS = {
4653
- "claude-code": [
4654
- { src: path.join(home, ".claude", "CLAUDE.md"), label: "CLAUDE.md" },
4655
- { src: path.join(home, ".claude", "commands"), label: "commands" },
4656
- { src: path.join(home, ".claude", "skills"), label: "skills" },
4657
- { src: path.join(home, ".claude", "hooks"), label: "hooks" },
4658
- ],
4659
- cursor: [
4660
- { src: path.join(vaultPath, ".cursor", "rules"), label: "rules" },
4661
- { src: path.join(home, ".cursor", "commands"), label: "commands" },
4662
- { src: path.join(home, ".cursor", "skills"), label: "skills" },
4663
- ],
4664
- cline: [
4665
- { src: path.join(vaultPath, ".clinerules"), label: ".clinerules" },
4666
- { src: path.join(vaultPath, ".cline", "skills"), label: "skills" },
4667
- ],
4668
- windsurf: [
4669
- { src: path.join(vaultPath, ".windsurfrules"), label: ".windsurfrules" },
4670
- { src: path.join(vaultPath, ".windsurf", "rules"), label: "rules" },
4671
- { src: path.join(vaultPath, ".windsurf", "workflows"), label: "workflows" },
4672
- { src: path.join(home, ".windsurf", "skills"), label: "skills" },
4673
- ],
4674
- "gemini-cli": [
4675
- { src: path.join(home, ".gemini", "GEMINI.md"), label: "GEMINI.md" },
4676
- { src: path.join(home, ".gemini", "commands"), label: "commands" },
4677
- { src: path.join(home, ".gemini", "skills"), label: "skills" },
4678
- ],
4679
- antigravity: [
4680
- { src: path.join(home, ".gemini", "GEMINI.md"), label: "GEMINI.md" },
4681
- { src: path.join(home, ".gemini", "antigravity", "global_workflows"), label: "workflows" },
4682
- { src: path.join(home, ".gemini", "antigravity", "skills"), label: "skills" },
4683
- ],
4684
- copilot: [
4685
- { src: path.join(vaultPath, ".github", "copilot-instructions.md"), label: "copilot-instructions.md" },
4686
- { src: path.join(vaultPath, ".github", "prompts"), label: "prompts" },
4687
- { src: path.join(vaultPath, ".github", "skills"), label: "skills" },
4688
- ],
4689
- codex: [
4690
- { src: path.join(home, ".codex", "AGENTS.md"), label: "AGENTS.md" },
4691
- { src: path.join(home, ".codex", "skills"), label: "skills" },
4692
- ],
4693
- "roo-code": [
4694
- { src: path.join(vaultPath, ".roo", "rules"), label: "rules" },
4695
- { src: path.join(vaultPath, ".roo", "commands"), label: "commands" },
4696
- { src: path.join(vaultPath, ".roo", "skills"), label: "skills" },
4697
- ],
4698
- "amazon-q": [
4699
- { src: path.join(vaultPath, ".amazonq", "rules"), label: "rules" },
4700
- { src: path.join(home, ".aws", "amazonq", "cli-agents"), label: "cli-agents" },
4701
- ],
4702
- "kilo-code": [
4703
- { src: path.join(vaultPath, ".kilocode", "rules"), label: "rules" },
4704
- { src: path.join(vaultPath, ".kilocode", "skills"), label: "skills" },
4705
- { src: path.join(vaultPath, ".kilocode", "commands"), label: "commands" },
4706
- ],
4707
- amp: [
4708
- { src: path.join(vaultPath, "AGENTS.md"), label: "AGENTS.md" },
4709
- { src: path.join(vaultPath, ".agents", "skills"), label: "skills" },
4710
- ],
4711
- "continue": [
4712
- { src: path.join(vaultPath, ".continue", "rules"), label: "rules" },
4713
- { src: path.join(vaultPath, ".continue", "prompts"), label: "prompts" },
4714
- ],
4715
- opencode: [
4716
- { src: path.join(vaultPath, "AGENTS.md"), label: "AGENTS.md" },
4717
- { src: path.join(vaultPath, ".opencode", "agents"), label: "agents" },
4718
- ],
4719
- aider: [
4720
- { src: path.join(vaultPath, "CONVENTIONS.md"), label: "CONVENTIONS.md" },
4721
- ],
4722
- };
4723
-
4724
- let agentsBacked = 0;
4725
- for (const agentId of detectedForBackup) {
4726
- const paths = AGENT_CONFIG_PATHS[agentId];
4727
- if (!paths) continue;
4728
- const agentDir = path.join(agentBackupDir, agentId);
4729
- let hasContent = false;
4730
- for (const { src, label } of paths) {
4731
- try {
4732
- if (!fs.existsSync(src)) continue;
4733
- const stat = fs.statSync(src);
4734
- if (stat.isDirectory()) {
4735
- copyDirRecursive(src, path.join(agentDir, label));
4736
- hasContent = true;
4737
- } else {
4738
- fs.mkdirSync(agentDir, { recursive: true });
4739
- fs.copyFileSync(src, path.join(agentDir, label));
4740
- hasContent = true;
4741
- }
4742
- } catch { /* skip inaccessible paths */ }
4743
- }
4744
- if (hasContent) agentsBacked++;
4745
- }
4746
- if (agentsBacked > 0) {
4747
- barLn(green(` Backed up configs from ${agentsBacked} agent(s) to 04_Archives/Agent_Backup_${ts}/`));
4748
- }
4749
- }
4750
- }
4751
- }
5108
+ const updateMode = false;
4752
5109
 
4753
5110
  if (!fs.existsSync(vaultPath)) fs.mkdirSync(vaultPath, { recursive: true });
4754
5111
 
@@ -4781,102 +5138,9 @@ async function main() {
4781
5138
  return;
4782
5139
  }
4783
5140
 
4784
- // ── Change detection + selection (update mode only) ──
4785
- let selectedWorkflows = null; // null = install all
4786
- let skipHooks = false;
4787
- let skipRules = false;
4788
- let skipTemplates = false;
4789
-
4790
- if (updateMode) {
4791
- const changes = detectChanges(bundleDir, vaultPath, selectedIds);
4792
- const totalChanged = countChanges(changes);
4793
-
4794
- // Read versions for display
4795
- const versionFilePath = path.join(vaultPath, ".mover-version");
4796
- const installedVersion = fs.existsSync(versionFilePath)
4797
- ? fs.readFileSync(versionFilePath, "utf8").trim()
4798
- : null;
4799
- let newVersion = `V${VERSION}`;
4800
- try {
4801
- const pkgPath = path.join(bundleDir, "package.json");
4802
- if (fs.existsSync(pkgPath)) {
4803
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
4804
- newVersion = pkg.version || newVersion;
4805
- }
4806
- } catch {}
4807
-
4808
- barLn();
4809
- question("Change Summary");
4810
- barLn();
4811
- displayChangeSummary(changes, installedVersion, newVersion);
4812
-
4813
- if (totalChanged === 0) {
4814
- barLn(green(" Already up to date."));
4815
- barLn();
4816
- } else {
4817
- const applyChoice = await interactiveSelect(
4818
- [
4819
- { id: "all", name: "Yes, update all changed files", tier: "" },
4820
- { id: "select", name: "Select individually", tier: "" },
4821
- { id: "cancel", name: "Cancel", tier: "" },
4822
- ],
4823
- { multi: false, defaultIndex: 0 }
4824
- );
4825
-
4826
- if (!applyChoice || applyChoice === "cancel") {
4827
- outro("Cancelled.");
4828
- return;
4829
- }
4830
-
4831
- if (applyChoice === "select") {
4832
- // Build list of only changed/new files for individual selection
4833
- const changedItems = [];
4834
- const changedPreSelected = [];
4835
- for (const f of changes.workflows.filter((x) => x.status !== "unchanged")) {
4836
- const id = `wf:${f.file}`;
4837
- changedItems.push({ id, name: `/${f.file.replace(".md", "")}`, tier: dim(f.status === "new" ? "new workflow" : "workflow") });
4838
- changedPreSelected.push(id);
4839
- }
4840
- for (const f of changes.hooks.filter((x) => x.status !== "unchanged")) {
4841
- const id = `hook:${f.file}`;
4842
- changedItems.push({ id, name: f.file, tier: dim(f.status === "new" ? "new hook" : "hook") });
4843
- changedPreSelected.push(id);
4844
- }
4845
- if (changes.rules === "changed") {
4846
- changedItems.push({ id: "rules", name: "Global Rules", tier: dim("rules") });
4847
- changedPreSelected.push("rules");
4848
- }
4849
- for (const f of changes.templates.filter((x) => x.status !== "unchanged")) {
4850
- const id = `tmpl:${f.file}`;
4851
- changedItems.push({ id, name: f.file.replace(/\\/g, "/"), tier: dim(f.status === "new" ? "new template" : "template") });
4852
- changedPreSelected.push(id);
4853
- }
4854
-
4855
- if (changedItems.length > 0) {
4856
- question("Select files to update");
4857
- barLn();
4858
- const selectedFileIds = await interactiveSelect(changedItems, {
4859
- multi: true,
4860
- preSelected: changedPreSelected,
4861
- });
4862
- if (!selectedFileIds) return;
4863
-
4864
- // Build workflow filter Set
4865
- const selectedWfFiles = selectedFileIds
4866
- .filter((id) => id.startsWith("wf:"))
4867
- .map((id) => id.slice(3));
4868
- if (selectedWfFiles.length < changes.workflows.filter((x) => x.status !== "unchanged").length) {
4869
- selectedWorkflows = new Set(selectedWfFiles);
4870
- }
4871
- // Check if hooks/rules/templates were deselected
4872
- skipHooks = !selectedFileIds.some((id) => id.startsWith("hook:"));
4873
- skipRules = !selectedFileIds.includes("rules");
4874
- skipTemplates = !selectedFileIds.some((id) => id.startsWith("tmpl:"));
4875
- }
4876
- }
4877
- // "all" = selectedWorkflows stays null, skip flags stay false
4878
- }
4879
- }
5141
+ // Fresh install no change detection needed
5142
+ const selectedWorkflows = null;
5143
+ const skipHooks = false, skipRules = false, skipTemplates = false;
4880
5144
 
4881
5145
  // ── Skills ──
4882
5146
  const allSkills = findSkills(bundleDir);
@@ -5020,9 +5284,9 @@ async function main() {
5020
5284
  } else if (method === "fetch") {
5021
5285
  barLn();
5022
5286
  const city = await textInput({ label: "City (e.g. London, Watford, Istanbul)", placeholder: "London" });
5023
- if (city === null) break;
5287
+ if (city === null) return;
5024
5288
  const country = await textInput({ label: "Country", placeholder: "United Kingdom" });
5025
- if (country === null) break;
5289
+ if (country === null) return;
5026
5290
  barLn();
5027
5291
 
5028
5292
  if (city && country) {
@@ -5043,9 +5307,44 @@ async function main() {
5043
5307
  }
5044
5308
  }
5045
5309
 
5310
+ // ── Settings step — let user configure before install ──
5311
+ {
5312
+ barLn();
5313
+ question("Configure settings " + dim("(esc to use defaults)"));
5314
+ barLn();
5315
+ const settingsItems = [
5316
+ { id: "review_day", name: "review_day Sunday Weekly review day" },
5317
+ { id: "track_food", name: "track_food on Track food in daily notes" },
5318
+ { id: "track_sleep", name: "track_sleep on Track sleep in daily notes" },
5319
+ { id: "friction_level", name: "friction_level 3 Max friction level (1-4)" },
5320
+ ];
5321
+ const settingsPick = await interactiveSelect(settingsItems);
5322
+ if (settingsPick) {
5323
+ const meta = KNOWN_SETTINGS[settingsPick];
5324
+ if (meta) {
5325
+ const cfgPath = path.join(os.homedir(), ".mover", "config.json");
5326
+ let cfg = {};
5327
+ if (fs.existsSync(cfgPath)) { try { cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8")); } catch {} }
5328
+ if (!cfg.settings) cfg.settings = {};
5329
+ if (meta.type === "boolean") {
5330
+ cfg.settings[settingsPick] = !(cfg.settings[settingsPick] !== undefined ? cfg.settings[settingsPick] : meta.defaults);
5331
+ statusLine("ok", settingsPick, cfg.settings[settingsPick] ? "on" : "off");
5332
+ } else {
5333
+ const answer = await textInput({ label: settingsPick, initial: String(meta.defaults) });
5334
+ if (answer !== null && answer.trim() !== "") {
5335
+ cfg.settings[settingsPick] = meta.type === "number" ? parseInt(answer.trim(), 10) : answer.trim();
5336
+ statusLine("ok", settingsPick, JSON.stringify(cfg.settings[settingsPick]));
5337
+ }
5338
+ }
5339
+ fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
5340
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
5341
+ }
5342
+ }
5343
+ }
5344
+
5046
5345
  // ── Install with animated spinners ──
5047
5346
  barLn();
5048
- question(updateMode ? bold("Updating...") : bold("Installing..."));
5347
+ question(bold("Installing..."));
5049
5348
  barLn();
5050
5349
 
5051
5350
  let totalSteps = 0;
@@ -5077,17 +5376,15 @@ async function main() {
5077
5376
 
5078
5377
  // 2. Template files (runs in both modes — only creates missing files, never overwrites)
5079
5378
  if (!skipTemplates) {
5080
- sp = spinner(updateMode ? "New template files" : "Engine templates");
5379
+ sp = spinner("Engine templates");
5081
5380
  const templatesInstalled = installTemplateFiles(bundleDir, vaultPath);
5082
5381
  await sleep(200);
5083
- sp.stop(updateMode
5084
- ? `New template files${templatesInstalled > 0 ? dim(` ${templatesInstalled} added`) : dim(" all present")}`
5085
- : `Engine templates${templatesInstalled > 0 ? dim(` ${templatesInstalled} files`) : dim(" up to date")}`);
5382
+ sp.stop(`Engine templates${templatesInstalled > 0 ? dim(` ${templatesInstalled} files`) : dim(" up to date")}`);
5086
5383
  totalSteps++;
5087
5384
  }
5088
5385
 
5089
- // 3. CLAUDE.md (skip on update)
5090
- if (!updateMode) {
5386
+ // 3. CLAUDE.md
5387
+ {
5091
5388
  const vaultClaudeMd = path.join(vaultPath, "CLAUDE.md");
5092
5389
  const bundleClaudeMd = path.join(bundleDir, "CLAUDE.md");
5093
5390
  if (!fs.existsSync(vaultClaudeMd) && fs.existsSync(bundleClaudeMd)) {
@@ -5150,8 +5447,8 @@ async function main() {
5150
5447
  }
5151
5448
  }
5152
5449
 
5153
- // 7. Git init Engine folder only (fresh install only)
5154
- if (!updateMode) {
5450
+ // 7. Git init Engine folder
5451
+ {
5155
5452
  const hasGit = cmdExists("git");
5156
5453
  if (hasGit) {
5157
5454
  const engineDir = path.join(vaultPath, "02_Areas", "Engine");
@@ -5177,9 +5474,9 @@ async function main() {
5177
5474
  }
5178
5475
  }
5179
5476
 
5180
- // 8. Version stamp (fresh install only — update mode lets /update workflow stamp after migrations)
5181
- if (!updateMode) {
5182
- fs.writeFileSync(path.join(vaultPath, ".mover-version"), `V${VERSION}\n`, "utf8");
5477
+ // 8. Version stamp
5478
+ {
5479
+ fs.writeFileSync(path.join(vaultPath, ".mover-version"), `${require("./package.json").version}\n`, "utf8");
5183
5480
  }
5184
5481
 
5185
5482
  // 9. Write ~/.mover/config.json (both fresh + update)
@@ -5189,8 +5486,7 @@ async function main() {
5189
5486
 
5190
5487
  // ── Done ──
5191
5488
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
5192
- const verb = updateMode ? "updated" : "installed";
5193
- outro(`${green("Done.")} Mover OS v${VERSION} ${verb}. ${dim(`${totalSteps} steps in ${elapsed}s`)}`);
5489
+ outro(`${green("Done.")} Mover OS v${VERSION} installed. ${dim(`${totalSteps} steps in ${elapsed}s`)}`);
5194
5490
 
5195
5491
  // Size check on installed rules files
5196
5492
  const rulesFile = path.join(bundleDir, "src", "system", "Mover_Global_Rules.md");
@@ -5225,30 +5521,22 @@ async function main() {
5225
5521
  ln();
5226
5522
  ln(` ${bold("Next steps")}`);
5227
5523
  ln();
5228
- if (updateMode) {
5229
- ln(` ${cyan("1")} Open the vault folder in your AI agent`);
5230
- ln(` ${dim("Updated: " + agentNames.join(", "))}`);
5231
- ln();
5232
- ln(` ${cyan("2")} Run ${bold("/update")}`);
5233
- ln(` ${dim("Syncs your Engine with the latest version")}`);
5234
- } else {
5235
- ln(` ${cyan("1")} Open your vault in ${bold("Obsidian")}`);
5236
- ln(` ${dim("This is where you view and browse your files")}`);
5237
- ln();
5238
- ln(` ${cyan("2")} Open the vault folder in your AI agent`);
5239
- ln(` ${dim("Installed: " + agentNames.join(", "))}`);
5240
- ln();
5241
- ln(` ${cyan("3")} Enable the Obsidian theme`);
5242
- ln(` ${dim("Settings Appearance CSS snippets → minimal-theme")}`);
5243
- ln();
5244
- ln(` ${cyan("4")} Run ${bold("/setup")}`);
5245
- ln(` ${dim("Builds your Identity, Strategy, and Goals")}`);
5246
- ln();
5247
- ln(gray(" ─────────────────────────────────────────────"));
5248
- ln();
5249
- ln(` ${dim("Obsidian = view your files")}`);
5250
- ln(` ${dim("Your AI agent = where you work")}`);
5251
- }
5524
+ ln(` ${cyan("1")} Open your vault in ${bold("Obsidian")}`);
5525
+ ln(` ${dim("This is where you view and browse your files")}`);
5526
+ ln();
5527
+ ln(` ${cyan("2")} Open the vault folder in your AI agent`);
5528
+ ln(` ${dim("Installed: " + agentNames.join(", "))}`);
5529
+ ln();
5530
+ ln(` ${cyan("3")} Enable the Obsidian theme`);
5531
+ ln(` ${dim("Settings Appearance CSS snippets → minimal-theme")}`);
5532
+ ln();
5533
+ ln(` ${cyan("4")} Run ${bold("/setup")}`);
5534
+ ln(` ${dim("Builds your Identity, Strategy, and Goals")}`);
5535
+ ln();
5536
+ ln(gray(" ─────────────────────────────────────────────"));
5537
+ ln();
5538
+ ln(` ${dim("Obsidian = view your files")}`);
5539
+ ln(` ${dim("Your AI agent = where you work")}`);
5252
5540
  ln();
5253
5541
  ln(` ${dim("/morning → [work] → /log → /analyse-day → /plan-tomorrow")}`);
5254
5542
  ln();
@@ -5259,8 +5547,7 @@ async function main() {
5259
5547
  ln(` ${green("▸")} ${bold("moveros pulse")} ${dim("Dashboard — energy, tasks, streaks")}`);
5260
5548
  ln(` ${green("▸")} ${bold("moveros doctor")} ${dim("Health check across all agents")}`);
5261
5549
  ln(` ${green("▸")} ${bold("moveros capture")} ${dim("Quick inbox — tasks, links, ideas")}`);
5262
- ln(` ${green("▸")} ${bold("moveros warm")} ${dim("Pre-warm your next AI session")}`);
5263
- ln(` ${green("▸")} ${bold("moveros sync")} ${dim("Update all agents to latest")}`);
5550
+ ln(` ${green("▸")} ${bold("moveros update")} ${dim("Update agents, rules, and skills")}`);
5264
5551
  ln(` ${green("▸")} ${bold("moveros")} ${dim("Full menu with all commands")}`);
5265
5552
  ln();
5266
5553
  }