mover-os 4.3.3 → 4.4.1

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 +3 -3
  2. package/install.js +1065 -85
  3. package/package.json +1 -1
package/install.js CHANGED
@@ -58,15 +58,34 @@ const red = (s) => `${S.red}${s}${S.reset}`;
58
58
  const gray = (s) => `${S.gray}${s}${S.reset}`;
59
59
  const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
60
60
 
61
- // Gradient text using 256-color palette (white → gray, matching website monochrome theme)
62
- const GRADIENT = [255, 255, 254, 253, 252, 251, 250, 249, 248, 247];
63
- function gradient(text) {
61
+ // Gradient text using 256-color palette
62
+ const GRADIENT_COOL = [39, 38, 44, 43, 49, 48, 84, 83, 119, 118]; // cyan → green sweep
63
+ const GRADIENT_WARM = [255, 254, 253, 252, 251, 250, 249, 248, 247, 246]; // white → gray
64
+ function gradient(text, palette = GRADIENT_WARM) {
64
65
  if (!IS_TTY) return text;
65
66
  const chars = [...text];
66
67
  return chars.map((ch, i) => {
67
68
  if (ch === " ") return ch;
68
- const idx = Math.floor((i / Math.max(chars.length - 1, 1)) * (GRADIENT.length - 1));
69
- return `${S.fg(GRADIENT[idx])}${ch}`;
69
+ const idx = Math.floor((i / Math.max(chars.length - 1, 1)) * (palette.length - 1));
70
+ return `${S.fg(palette[idx])}${ch}`;
71
+ }).join("") + S.reset;
72
+ }
73
+
74
+ // Animated gradient that sweeps a color wave across text
75
+ function waveGradient(text, frame, totalFrames) {
76
+ if (!IS_TTY) return text;
77
+ const wave = [240, 244, 248, 252, 255, 255, 252, 248, 244, 240]; // dim → bright → dim
78
+ const chars = [...text];
79
+ const waveWidth = wave.length;
80
+ const progress = frame / totalFrames;
81
+ const waveCenter = Math.floor(progress * (chars.length + waveWidth)) - waveWidth / 2;
82
+ return chars.map((ch, i) => {
83
+ if (ch === " ") return ch;
84
+ const dist = i - waveCenter;
85
+ if (dist >= 0 && dist < waveWidth) {
86
+ return `${S.fg(wave[dist])}${ch}`;
87
+ }
88
+ return `${S.fg(240)}${ch}`; // dim base color
70
89
  }).join("") + S.reset;
71
90
  }
72
91
 
@@ -100,16 +119,70 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
100
119
  async function printHeader(animate = IS_TTY) {
101
120
  ln();
102
121
  if (animate) {
103
- // Animated logo reveal line by line with gradient cascade
104
- for (let i = 0; i < LOGO.length; i++) {
105
- ln(gradient(LOGO[i]));
106
- await sleep(30);
122
+ // Phase 1: Print logo dim (instant base)
123
+ for (const line of LOGO) {
124
+ ln(`${S.fg(236)}${line}${S.reset}`);
125
+ }
126
+
127
+ // Phase 2: Shine sweep — a bright band sweeps left to right across all lines
128
+ const maxLen = Math.max(...LOGO.map((l) => l.length));
129
+ const totalFrames = 24;
130
+ const shineWidth = 8; // width of the bright band
131
+ const trailLen = maxLen; // trail fills behind the shine
132
+
133
+ for (let frame = 0; frame <= totalFrames; frame++) {
134
+ w(`\x1b[${LOGO.length}A`);
135
+ const progress = frame / totalFrames;
136
+ // Shine position sweeps from left edge to past right edge
137
+ const shinePos = Math.floor(progress * (maxLen + shineWidth + 4)) - shineWidth;
138
+
139
+ for (const line of LOGO) {
140
+ const chars = [...line];
141
+ const rendered = chars.map((ch, ci) => {
142
+ if (ch === " ") return ch;
143
+ const relPos = ci - shinePos;
144
+ // Bright core of shine
145
+ if (relPos >= 0 && relPos < 2) return `${S.bold}${S.fg(255)}${ch}${S.reset}`;
146
+ if (relPos >= 2 && relPos < 4) return `${S.fg(255)}${ch}`;
147
+ if (relPos >= 4 && relPos < shineWidth) return `${S.fg(250)}${ch}`;
148
+ // Glow ahead of shine
149
+ if (relPos >= shineWidth && relPos < shineWidth + 3) return `${S.fg(244)}${ch}`;
150
+ // Trail behind shine — settled bright
151
+ if (relPos < 0 && ci < shinePos) return `${S.fg(251)}${ch}`;
152
+ // Not yet reached
153
+ return `${S.fg(236)}${ch}`;
154
+ }).join("") + S.reset;
155
+ w(`\x1b[2K${rendered}\n`);
156
+ }
157
+ await sleep(20);
158
+ }
159
+
160
+ // Phase 3: Final settle — clean gradient
161
+ w(`\x1b[${LOGO.length}A`);
162
+ for (const line of LOGO) {
163
+ w(`\x1b[2K${gradient(line)}\n`);
107
164
  }
108
165
  } else {
109
166
  for (const line of LOGO) ln(gradient(line));
110
167
  }
111
168
  ln();
112
- ln(` ${dim(`v${VERSION}`)} ${gray("the agentic operating system for obsidian")}`);
169
+
170
+ // Compact info line
171
+ let infoRight = "";
172
+ try {
173
+ const cfgPath = path.join(os.homedir(), ".mover", "config.json");
174
+ if (fs.existsSync(cfgPath)) {
175
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
176
+ const vaultName = cfg.vaultPath ? path.basename(cfg.vaultPath) : null;
177
+ const agentCount = (cfg.agents || []).length;
178
+ const parts = [];
179
+ if (vaultName) parts.push(vaultName);
180
+ if (agentCount > 0) parts.push(`${agentCount} agent${agentCount > 1 ? "s" : ""}`);
181
+ if (parts.length > 0) infoRight = gray(parts.join(" · "));
182
+ }
183
+ } catch {}
184
+
185
+ ln(` ${dim(`v${VERSION}`)} ${gray("the agentic operating system for obsidian")}${infoRight ? ` ${infoRight}` : ""}`);
113
186
  ln();
114
187
  ln(gray(" ─────────────────────────────────────────────"));
115
188
  ln();
@@ -304,12 +377,26 @@ function interactiveSelect(items, { multi = false, preSelected = [], defaultInde
304
377
  const selected = new Set(preSelected);
305
378
  let prevLines = 0;
306
379
 
380
+ // Build list of selectable indices (skip separators)
381
+ const selectableIndices = items.map((item, i) => item._separator ? -1 : i).filter((i) => i >= 0);
382
+ let selectPos = selectableIndices.indexOf(defaultIndex);
383
+ if (selectPos < 0) selectPos = 0;
384
+ cursor = selectableIndices[selectPos] || 0;
385
+
307
386
  const render = () => {
308
387
  if (prevLines > 0) w(`\x1b[${prevLines}A`);
309
388
 
310
389
  let lines = 0;
311
390
  for (let i = 0; i < items.length; i++) {
312
391
  const item = items[i];
392
+
393
+ // Separator: render as category header
394
+ if (item._separator) {
395
+ w(`\x1b[2K${BAR_COLOR}│${S.reset} ${dim(item.name)}\n`);
396
+ lines++;
397
+ continue;
398
+ }
399
+
313
400
  const active = i === cursor;
314
401
  const checked = selected.has(item.id);
315
402
 
@@ -333,8 +420,8 @@ function interactiveSelect(items, { multi = false, preSelected = [], defaultInde
333
420
  lines++;
334
421
 
335
422
  const hint = multi
336
- ? dim(" ↑↓ navigate space select a all enter confirm")
337
- : dim(" ↑↓ navigate enter select");
423
+ ? dim(" ↑↓ navigate space select a all enter confirm esc back")
424
+ : dim(" ↑↓ navigate enter select esc back");
338
425
  w(`\x1b[2K${BAR_COLOR}│${S.reset}${hint}\n`);
339
426
  lines++;
340
427
 
@@ -344,8 +431,8 @@ function interactiveSelect(items, { multi = false, preSelected = [], defaultInde
344
431
  render();
345
432
 
346
433
  const handler = (data) => {
347
- if (data === "\x1b[A") { cursor = (cursor - 1 + items.length) % items.length; }
348
- else if (data === "\x1b[B") { cursor = (cursor + 1) % items.length; }
434
+ if (data === "\x1b[A") { selectPos = (selectPos - 1 + selectableIndices.length) % selectableIndices.length; cursor = selectableIndices[selectPos]; }
435
+ else if (data === "\x1b[B") { selectPos = (selectPos + 1) % selectableIndices.length; cursor = selectableIndices[selectPos]; }
349
436
  else if (data === " " && multi) {
350
437
  const id = items[cursor].id;
351
438
  if (selected.has(id)) selected.delete(id);
@@ -380,6 +467,20 @@ function interactiveSelect(items, { multi = false, preSelected = [], defaultInde
380
467
  }
381
468
  return;
382
469
  }
470
+ else if (data === "\x1b" || data === "\x1b\x1b" || (!multi && data === "q")) {
471
+ // Escape — go back / cancel
472
+ stdin.removeListener("data", handler);
473
+ stdin.setRawMode(false);
474
+ stdin.pause();
475
+ if (prevLines > 0) {
476
+ w(`\x1b[${prevLines}A`);
477
+ for (let i = 0; i < prevLines; i++) w("\x1b[2K\n");
478
+ w(`\x1b[${prevLines}A`);
479
+ }
480
+ w(S.show);
481
+ resolve(null);
482
+ return;
483
+ }
383
484
  else if (data === "\x03") { cleanup(); ln(); process.exit(0); }
384
485
 
385
486
  render();
@@ -552,6 +653,8 @@ const CLI_COMMANDS = {
552
653
  settings: { desc: "View/edit config", alias: [] },
553
654
  backup: { desc: "Manual backup wizard", alias: [] },
554
655
  restore: { desc: "Restore from backup", alias: [] },
656
+ prayer: { desc: "Manage prayer times", alias: [] },
657
+ help: { desc: "Interactive guide to Mover OS", alias: ["-h"] },
555
658
  test: { desc: "Run integration tests (dev)", alias: [], hidden: true },
556
659
  };
557
660
 
@@ -566,6 +669,7 @@ function parseArgs() {
566
669
  if (a === "--key" && args[i + 1]) { opts.key = args[++i]; continue; }
567
670
  // Backward compat: --update / -u → command 'update'
568
671
  if (a === "--update" || a === "-u") { opts.command = "update"; continue; }
672
+ if (a === "--_self-updated") { opts._selfUpdated = true; continue; }
569
673
  if (a === "--help" || a === "-h") {
570
674
  ln();
571
675
  ln(` ${bold("moveros")} ${dim("— the Mover OS companion CLI")}`);
@@ -619,6 +723,17 @@ function detectObsidianVaults() {
619
723
  }
620
724
  }
621
725
 
726
+ function compareVersions(a, b) {
727
+ const pa = a.split(".").map(Number);
728
+ const pb = b.split(".").map(Number);
729
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
730
+ const va = pa[i] || 0, vb = pb[i] || 0;
731
+ if (va > vb) return 1;
732
+ if (va < vb) return -1;
733
+ }
734
+ return 0;
735
+ }
736
+
622
737
  // ─── Change detection (update mode) ─────────────────────────────────────────
623
738
  function detectChanges(bundleDir, vaultPath, selectedAgentIds) {
624
739
  const home = os.homedir();
@@ -1512,14 +1627,155 @@ function generateClaudeSettings() {
1512
1627
  );
1513
1628
  }
1514
1629
 
1630
+ // ─── Prayer timetable helpers ───────────────────────────────────────────────
1631
+
1632
+ function parsePrayerTimetable(lines) {
1633
+ // Parse user-pasted mosque timetable into date-keyed JSON
1634
+ // Supports various formats:
1635
+ // 2026-03-08 05:20 13:00 16:15 18:01 19:45
1636
+ // March 8: Fajr 05:20, Dhuhr 13:00, ...
1637
+ // Tab/comma separated tables from spreadsheets
1638
+ const times = {};
1639
+ const prayerNames = ["fajr", "dhuhr", "asr", "maghrib", "isha"];
1640
+ const monthNames = { jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6, jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12,
1641
+ january: 1, february: 2, march: 3, april: 4, june: 6, july: 7, august: 8, september: 9, october: 10, november: 11, december: 12 };
1642
+ const currentYear = new Date().getFullYear();
1643
+
1644
+ for (const line of lines) {
1645
+ const trimmed = line.trim();
1646
+ if (!trimmed || trimmed.toLowerCase() === "done") continue;
1647
+
1648
+ // Try ISO date format: 2026-03-08 ...
1649
+ const isoMatch = trimmed.match(/^(\d{4}-\d{2}-\d{2})\s+(.+)/);
1650
+ if (isoMatch) {
1651
+ const dateKey = isoMatch[1];
1652
+ const timeValues = isoMatch[2].match(/\d{1,2}:\d{2}/g);
1653
+ if (timeValues && timeValues.length >= 5) {
1654
+ times[dateKey] = {};
1655
+ for (let i = 0; i < Math.min(5, timeValues.length); i++) {
1656
+ times[dateKey][prayerNames[i]] = timeValues[i];
1657
+ }
1658
+ }
1659
+ continue;
1660
+ }
1661
+
1662
+ // Try month name format: March 8: ... or 8 March: ...
1663
+ const monthMatch = trimmed.match(/(?:(\d{1,2})\s+)?(\w+)\s*(\d{1,2})?\s*[:\s]\s*(.+)/i);
1664
+ if (monthMatch) {
1665
+ const mName = monthMatch[2].toLowerCase();
1666
+ const monthNum = monthNames[mName];
1667
+ if (monthNum) {
1668
+ const day = parseInt(monthMatch[1] || monthMatch[3]);
1669
+ if (day >= 1 && day <= 31) {
1670
+ const dateKey = `${currentYear}-${String(monthNum).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
1671
+ const timeValues = monthMatch[4].match(/\d{1,2}:\d{2}/g);
1672
+ if (timeValues && timeValues.length >= 5) {
1673
+ times[dateKey] = {};
1674
+ for (let i = 0; i < Math.min(5, timeValues.length); i++) {
1675
+ times[dateKey][prayerNames[i]] = timeValues[i];
1676
+ }
1677
+ }
1678
+ }
1679
+ }
1680
+ continue;
1681
+ }
1682
+
1683
+ // Try pure time extraction from tab/comma separated
1684
+ const timeValues = trimmed.match(/\d{1,2}:\d{2}/g);
1685
+ if (timeValues && timeValues.length >= 5) {
1686
+ // Look for a date number at the start
1687
+ const dayMatch = trimmed.match(/^(\d{1,2})\D/);
1688
+ if (dayMatch) {
1689
+ const day = parseInt(dayMatch[1]);
1690
+ if (day >= 1 && day <= 31) {
1691
+ // Best guess: current month
1692
+ const now = new Date();
1693
+ const dateKey = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
1694
+ times[dateKey] = {};
1695
+ for (let i = 0; i < Math.min(5, timeValues.length); i++) {
1696
+ times[dateKey][prayerNames[i]] = timeValues[i];
1697
+ }
1698
+ }
1699
+ }
1700
+ }
1701
+ }
1702
+
1703
+ return {
1704
+ mosque: "Custom",
1705
+ type: "jamaah",
1706
+ note: "Paste your mosque's yearly timetable into Claude and ask it to convert to this format.",
1707
+ times,
1708
+ };
1709
+ }
1710
+
1711
+ async function fetchPrayerTimes(city, country) {
1712
+ // Fetch 12 months of prayer times from aladhan.com API
1713
+ const https = require("https");
1714
+ const year = new Date().getFullYear();
1715
+ const allTimes = {};
1716
+
1717
+ for (let month = 1; month <= 12; month++) {
1718
+ try {
1719
+ const url = `https://api.aladhan.com/v1/calendarByCity/${year}/${month}?city=${encodeURIComponent(city)}&country=${encodeURIComponent(country)}&method=15`;
1720
+ const body = await new Promise((resolve, reject) => {
1721
+ const req = https.request(url, { method: "GET", timeout: 10000 }, (res) => {
1722
+ let data = "";
1723
+ res.on("data", (c) => (data += c));
1724
+ res.on("end", () => resolve(data));
1725
+ });
1726
+ req.on("error", reject);
1727
+ req.on("timeout", () => { req.destroy(); reject(new Error("Timeout")); });
1728
+ req.end();
1729
+ });
1730
+
1731
+ const json = JSON.parse(body);
1732
+ if (json.code === 200 && json.data) {
1733
+ for (const day of json.data) {
1734
+ const t = day.timings;
1735
+ const d = day.date.gregorian;
1736
+ const dateKey = `${d.year}-${d.month.number.toString().padStart(2, "0")}-${d.day.padStart(2, "0")}`;
1737
+ allTimes[dateKey] = {
1738
+ fajr: t.Fajr.replace(/\s*\(.*\)/, ""),
1739
+ dhuhr: t.Dhuhr.replace(/\s*\(.*\)/, ""),
1740
+ asr: t.Asr.replace(/\s*\(.*\)/, ""),
1741
+ maghrib: t.Maghrib.replace(/\s*\(.*\)/, ""),
1742
+ isha: t.Isha.replace(/\s*\(.*\)/, ""),
1743
+ };
1744
+ }
1745
+ }
1746
+ } catch {
1747
+ // Skip failed months — partial data is still useful
1748
+ }
1749
+ }
1750
+
1751
+ return {
1752
+ mosque: `Calculated (${city}, ${country})`,
1753
+ type: "adhan",
1754
+ note: "Calculated adhan times from aladhan.com. For mosque jama'ah times, run: moveros prayer",
1755
+ city,
1756
+ country,
1757
+ times: allTimes,
1758
+ };
1759
+ }
1760
+
1515
1761
  // ─── .gitignore ─────────────────────────────────────────────────────────────
1516
1762
  function generateGitignore() {
1517
- return `# Mover OS — protected from git
1518
- 02_Areas/Engine/Dailies/
1519
- 02_Areas/Engine/Weekly Reviews/
1520
- .obsidian/
1521
- .trash/
1522
- dev/
1763
+ return `# Mover OS Engine track core files only
1764
+ # Ignore everything by default
1765
+ *
1766
+
1767
+ # Track core Engine files
1768
+ !.gitignore
1769
+ !Identity_Prime.md
1770
+ !Strategy.md
1771
+ !Active_Context.md
1772
+ !Goals.md
1773
+ !Auto_Learnings.md
1774
+ !Mover_Dossier.md
1775
+ !Metrics_Log.md
1776
+ !Voice_DNA.md
1777
+ !Daily_Template.md
1778
+ !Someday_Maybe.md
1523
1779
  `;
1524
1780
  }
1525
1781
 
@@ -1553,7 +1809,7 @@ function createVaultStructure(vaultPath) {
1553
1809
  return created;
1554
1810
  }
1555
1811
 
1556
- function writeMoverConfig(vaultPath, agentIds, licenseKey) {
1812
+ function writeMoverConfig(vaultPath, agentIds, licenseKey, opts = {}) {
1557
1813
  const configDir = path.join(os.homedir(), ".mover");
1558
1814
  if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
1559
1815
  const configPath = path.join(configDir, "config.json");
@@ -1566,7 +1822,7 @@ function writeMoverConfig(vaultPath, agentIds, licenseKey) {
1566
1822
  installedAt: new Date().toISOString(),
1567
1823
  };
1568
1824
  if (licenseKey) config.licenseKey = licenseKey;
1569
- // If config exists, preserve installedAt and licenseKey from original install
1825
+ // If config exists, preserve existing values
1570
1826
  if (fs.existsSync(configPath)) {
1571
1827
  try {
1572
1828
  const existing = JSON.parse(fs.readFileSync(configPath, "utf8"));
@@ -1575,9 +1831,18 @@ function writeMoverConfig(vaultPath, agentIds, licenseKey) {
1575
1831
  if (existing.feedbackWebhook) config.feedbackWebhook = existing.feedbackWebhook;
1576
1832
  if (existing.track_food !== undefined) config.track_food = existing.track_food;
1577
1833
  if (existing.track_sleep !== undefined) config.track_sleep = existing.track_sleep;
1834
+ // Preserve settings block (prayer times, review_day, etc.)
1835
+ if (existing.settings) config.settings = { ...existing.settings };
1836
+ // Preserve prayer_times fallback
1837
+ if (existing.prayer_times) config.prayer_times = existing.prayer_times;
1578
1838
  config.updatedAt = new Date().toISOString();
1579
1839
  } catch {}
1580
1840
  }
1841
+ // Apply prayer setup from installer
1842
+ if (opts.prayerSetup) {
1843
+ if (!config.settings) config.settings = {};
1844
+ config.settings.show_prayer_times = true;
1845
+ }
1581
1846
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
1582
1847
  return configPath;
1583
1848
  }
@@ -2442,6 +2707,8 @@ const CLI_HANDLERS = {
2442
2707
  backup: async (opts) => { await cmdBackup(opts); },
2443
2708
  restore: async (opts) => { await cmdRestore(opts); },
2444
2709
  doctor: async (opts) => { await cmdDoctor(opts); },
2710
+ prayer: async (opts) => { await cmdPrayer(opts); },
2711
+ help: async (opts) => { await cmdHelp(opts); },
2445
2712
  test: async (opts) => { await cmdTest(opts); },
2446
2713
  };
2447
2714
 
@@ -2515,10 +2782,20 @@ async function cmdDoctor(opts) {
2515
2782
  statusLine(allOk ? "ok" : "warn", ` ${reg.name}`, checks.join(", "));
2516
2783
  }
2517
2784
 
2518
- // Git
2785
+ // Engine Git
2519
2786
  barLn();
2520
- const hasGit = fs.existsSync(path.join(vault, ".git"));
2521
- statusLine(hasGit ? "ok" : "info", "Git", hasGit ? "initialized" : "not a git repo");
2787
+ const engineGit = fs.existsSync(path.join(engineDir, ".git"));
2788
+ statusLine(engineGit ? "ok" : "warn", "Engine Git", engineGit ? "initialized" : "not version controlled — run moveros install to fix");
2789
+
2790
+ // Engine gitignore
2791
+ const engineGitignore = path.join(engineDir, ".gitignore");
2792
+ if (engineGit && fs.existsSync(engineGitignore)) {
2793
+ const gi = fs.readFileSync(engineGitignore, "utf8");
2794
+ const isWhitelist = gi.includes("*") && gi.includes("!Identity_Prime.md");
2795
+ statusLine(isWhitelist ? "ok" : "warn", "Engine .gitignore", isWhitelist ? "whitelist (core files only)" : "legacy format — re-run moveros install");
2796
+ } else if (engineGit) {
2797
+ statusLine("warn", "Engine .gitignore", "missing");
2798
+ }
2522
2799
 
2523
2800
  barLn();
2524
2801
  barLn(dim(" Run moveros install or moveros update to fix any issues."));
@@ -2744,6 +3021,7 @@ async function cmdCapture(opts) {
2744
3021
  { id: "link", name: "Link", tier: "URL with optional note" },
2745
3022
  { id: "dump", name: "Brain dump", tier: "Free-form text" },
2746
3023
  ], { multi: false });
3024
+ if (!type) return;
2747
3025
  }
2748
3026
  content = await textInput({ label: `Enter ${type}:` });
2749
3027
  }
@@ -2801,6 +3079,7 @@ async function cmdWho(opts) {
2801
3079
  { id: "yes", name: "Create stub", tier: `Creates ${name}.md in People/` },
2802
3080
  { id: "no", name: "Skip", tier: "" },
2803
3081
  ], { multi: false });
3082
+ if (!create) return;
2804
3083
  if (create === "yes") {
2805
3084
  const peopleDir = path.join(entitiesDir, "People");
2806
3085
  fs.mkdirSync(peopleDir, { recursive: true });
@@ -2846,13 +3125,23 @@ async function cmdDiff(opts) {
2846
3125
  return;
2847
3126
  }
2848
3127
 
3128
+ // Determine git root — Engine files use Engine's own repo
3129
+ const engineDir = path.join(vault, "02_Areas", "Engine");
3130
+ const isEngineFile = relPath.startsWith("02_Areas/Engine/") || relPath.startsWith("02_Areas\\Engine\\");
3131
+ const gitCwd = isEngineFile && fs.existsSync(path.join(engineDir, ".git"))
3132
+ ? engineDir
3133
+ : vault;
3134
+ const gitRelPath = isEngineFile && gitCwd === engineDir
3135
+ ? path.basename(relPath)
3136
+ : relPath;
3137
+
2849
3138
  barLn(bold(` ${path.basename(relPath)} — last ${days} days`));
2850
3139
  barLn();
2851
3140
 
2852
3141
  try {
2853
3142
  const log = execSync(
2854
- `git log --since="${days} days ago" --oneline --follow -- "${relPath}"`,
2855
- { cwd: vault, encoding: "utf8", timeout: 10000 }
3143
+ `git log --since="${days} days ago" --oneline --follow -- "${gitRelPath}"`,
3144
+ { cwd: gitCwd, encoding: "utf8", timeout: 10000 }
2856
3145
  ).trim();
2857
3146
 
2858
3147
  if (!log) {
@@ -3011,40 +3300,67 @@ async function cmdContext(opts) {
3011
3300
 
3012
3301
  const target = opts.rest[0];
3013
3302
  const home = os.homedir();
3303
+ const cfgPath = path.join(home, ".mover", "config.json");
3304
+ const agents = fs.existsSync(cfgPath)
3305
+ ? (JSON.parse(fs.readFileSync(cfgPath, "utf8")).agents || [])
3306
+ : [];
3014
3307
 
3015
3308
  if (!target) {
3016
- // Show all agents
3017
- barLn(bold(" Agent Context Overview"));
3309
+ // Overview: show what each agent actually loads
3310
+ barLn(bold(" What Your Agents See"));
3018
3311
  barLn();
3019
- const cfgPath = path.join(home, ".mover", "config.json");
3020
- const agents = fs.existsSync(cfgPath)
3021
- ? (JSON.parse(fs.readFileSync(cfgPath, "utf8")).agents || [])
3022
- : [];
3023
3312
 
3024
3313
  for (const agentId of agents) {
3025
3314
  const reg = AGENT_REGISTRY[agentId];
3026
3315
  if (!reg) continue;
3027
- let totalBytes = 0, fileCount = 0;
3028
3316
 
3029
- const countDir = (dir) => {
3030
- if (!fs.existsSync(dir)) return;
3031
- try {
3032
- for (const f of fs.readdirSync(dir, { withFileTypes: true })) {
3033
- if (f.isFile()) { totalBytes += fs.statSync(path.join(dir, f.name)).size; fileCount++; }
3034
- else if (f.isDirectory()) countDir(path.join(dir, f.name));
3317
+ const parts = [];
3318
+ let totalBytes = 0;
3319
+
3320
+ // Rules
3321
+ if (reg.rules) {
3322
+ const rp = reg.rules.dest(vault);
3323
+ if (fs.existsSync(rp)) {
3324
+ const sz = fs.statSync(rp).size;
3325
+ totalBytes += sz;
3326
+ parts.push(`rules ${dim(`${(sz / 1024).toFixed(0)}KB`)}`);
3327
+ } else { parts.push(red("rules missing")); }
3328
+ }
3329
+
3330
+ // Skills
3331
+ if (reg.skills) {
3332
+ const sp = reg.skills.dest(vault);
3333
+ if (fs.existsSync(sp)) {
3334
+ const skills = fs.readdirSync(sp, { withFileTypes: true }).filter((d) => d.isDirectory());
3335
+ let skillBytes = 0;
3336
+ for (const sk of skills) {
3337
+ const sm = path.join(sp, sk.name, "SKILL.md");
3338
+ if (fs.existsSync(sm)) skillBytes += fs.statSync(sm).size;
3035
3339
  }
3036
- } catch {}
3037
- };
3340
+ totalBytes += skillBytes;
3341
+ parts.push(`${skills.length} skills ${dim(`${(skillBytes / 1024).toFixed(0)}KB`)}`);
3342
+ }
3343
+ }
3038
3344
 
3039
- if (reg.rules) { const rp = reg.rules.dest(vault); if (fs.existsSync(rp)) { totalBytes += fs.statSync(rp).size; fileCount++; } }
3040
- if (reg.skills) countDir(reg.skills.dest(vault));
3041
- if (reg.commands) countDir(reg.commands.dest(vault));
3345
+ // Commands
3346
+ if (reg.commands) {
3347
+ const cp = reg.commands.dest(vault);
3348
+ if (fs.existsSync(cp)) {
3349
+ const cmds = fs.readdirSync(cp).filter((f) => f.endsWith(".md") || f.endsWith(".toml") || f.endsWith(".json"));
3350
+ if (cmds.length > 0) parts.push(`${cmds.length} commands`);
3351
+ }
3352
+ }
3042
3353
 
3043
- const tokens = Math.round(totalBytes / 4); // rough estimate: 4 bytes per token
3044
- const kb = (totalBytes / 1024).toFixed(1);
3045
- barLn(` ${reg.name.padEnd(20)} ${String(fileCount).padStart(3)} files ${String(kb).padStart(6)} KB ~${tokens.toLocaleString()} tokens`);
3354
+ // Hooks
3355
+ if (reg.hooks) parts.push(`${reg.hooks.events?.length || "?"} hook events`);
3356
+
3357
+ const tokens = Math.round(totalBytes / 4);
3358
+ const tokenWarn = tokens > 40000 ? red("heavy") : tokens > 20000 ? yellow("moderate") : green("lean");
3359
+ barLn(` ${bold(reg.name.padEnd(18))} ${parts.join(dim(" · "))} ${dim("~")}${tokens.toLocaleString()} tok ${tokenWarn}`);
3046
3360
  }
3047
3361
  barLn();
3362
+ barLn(dim(" Detail: moveros context <agent>"));
3363
+ barLn();
3048
3364
  return;
3049
3365
  }
3050
3366
 
@@ -3052,43 +3368,251 @@ async function cmdContext(opts) {
3052
3368
  const reg = AGENT_REGISTRY[target] || Object.values(AGENT_REGISTRY).find((r) => r.name.toLowerCase().includes(target.toLowerCase()));
3053
3369
  if (!reg) { barLn(yellow(` Unknown agent: ${target}`)); return; }
3054
3370
 
3055
- barLn(bold(` ${reg.name} Context`));
3371
+ barLn(bold(` ${reg.name}`));
3372
+ barLn(dim(` Tier: ${reg.tier || "unknown"}`));
3056
3373
  barLn();
3057
3374
 
3375
+ // Rules — show actual file path and key sections
3058
3376
  if (reg.rules) {
3059
3377
  const rp = reg.rules.dest(vault);
3060
3378
  if (fs.existsSync(rp)) {
3061
- const size = fs.statSync(rp).size;
3062
- statusLine("ok", "Rules", `${(size / 1024).toFixed(1)} KB ${rp}`);
3379
+ const content = fs.readFileSync(rp, "utf8");
3380
+ const sz = Buffer.byteLength(content);
3381
+ statusLine("ok", "Rules", `${(sz / 1024).toFixed(1)} KB`);
3382
+ barLn(dim(` ${rp}`));
3383
+ // Show top-level headings as table of contents
3384
+ const headings = content.match(/^## .+$/gm) || [];
3385
+ if (headings.length > 0) {
3386
+ barLn(dim(" Sections:"));
3387
+ for (const h of headings.slice(0, 12)) barLn(dim(` ${h.replace(/^## /, "")}`));
3388
+ if (headings.length > 12) barLn(dim(` ...+${headings.length - 12} more`));
3389
+ }
3063
3390
  } else {
3064
- statusLine("fail", "Rules", "not found");
3391
+ statusLine("fail", "Rules", "not installed");
3065
3392
  }
3066
3393
  }
3067
3394
 
3395
+ // Skills — list actual skill names with trigger descriptions
3068
3396
  if (reg.skills) {
3069
3397
  const sp = reg.skills.dest(vault);
3398
+ barLn();
3070
3399
  if (fs.existsSync(sp)) {
3071
- const skills = fs.readdirSync(sp, { withFileTypes: true }).filter((d) => d.isDirectory());
3400
+ const skillDirs = fs.readdirSync(sp, { withFileTypes: true }).filter((d) => d.isDirectory());
3072
3401
  let totalChars = 0;
3073
- for (const sk of skills) {
3402
+ const skillList = [];
3403
+ for (const sk of skillDirs) {
3074
3404
  const sm = path.join(sp, sk.name, "SKILL.md");
3075
- if (fs.existsSync(sm)) totalChars += fs.statSync(sm).size;
3405
+ if (fs.existsSync(sm)) {
3406
+ const sc = fs.readFileSync(sm, "utf8");
3407
+ totalChars += sc.length;
3408
+ // Extract first line of description
3409
+ const descMatch = sc.match(/^description:\s*["']?(.+?)["']?\s*$/m);
3410
+ const desc = descMatch ? descMatch[1].slice(0, 55) : "";
3411
+ skillList.push({ name: sk.name, desc });
3412
+ }
3413
+ }
3414
+ statusLine("ok", "Skills", `${skillList.length} packs (${(totalChars / 1024).toFixed(0)} KB descriptions)`);
3415
+ for (const s of skillList) {
3416
+ barLn(` ${s.name.padEnd(25)} ${dim(s.desc)}`);
3076
3417
  }
3077
- statusLine("ok", "Skills", `${skills.length} packs ${(totalChars / 1024).toFixed(1)} KB descriptions`);
3078
3418
  } else {
3079
3419
  statusLine("info", "Skills", "none installed");
3080
3420
  }
3081
3421
  }
3082
3422
 
3423
+ // Commands — list actual command names
3083
3424
  if (reg.commands) {
3084
3425
  const cp = reg.commands.dest(vault);
3426
+ barLn();
3085
3427
  if (fs.existsSync(cp)) {
3086
3428
  const files = fs.readdirSync(cp).filter((f) => !f.startsWith("."));
3087
- statusLine("ok", "Commands", `${files.length} files`);
3429
+ statusLine("ok", "Commands", `${files.length} loaded`);
3430
+ const cols = 4;
3431
+ for (let i = 0; i < files.length; i += cols) {
3432
+ const row = files.slice(i, i + cols).map((f) => f.replace(/\.(md|toml|json)$/, "").padEnd(18)).join(" ");
3433
+ barLn(` ${dim(row)}`);
3434
+ }
3088
3435
  } else {
3089
3436
  statusLine("info", "Commands", "none installed");
3090
3437
  }
3091
3438
  }
3439
+
3440
+ // Hooks
3441
+ if (reg.hooks) {
3442
+ barLn();
3443
+ statusLine("ok", "Hooks", `${reg.hooks.events?.length || 0} events`);
3444
+ if (reg.hooks.events) barLn(dim(` ${reg.hooks.events.join(", ")}`));
3445
+ }
3446
+
3447
+ // Token budget summary
3448
+ barLn();
3449
+ let total = 0;
3450
+ if (reg.rules) { const rp = reg.rules.dest(vault); if (fs.existsSync(rp)) total += fs.statSync(rp).size; }
3451
+ if (reg.skills) {
3452
+ const sp = reg.skills.dest(vault);
3453
+ if (fs.existsSync(sp)) {
3454
+ for (const sk of fs.readdirSync(sp, { withFileTypes: true }).filter((d) => d.isDirectory())) {
3455
+ const sm = path.join(sp, sk.name, "SKILL.md");
3456
+ if (fs.existsSync(sm)) total += fs.statSync(sm).size;
3457
+ }
3458
+ }
3459
+ }
3460
+ const tokens = Math.round(total / 4);
3461
+ const pct = Math.round((tokens / 200000) * 100);
3462
+ barLn(` ${dim("Total context load:")} ~${tokens.toLocaleString()} tokens ${dim(`(${pct}% of 200K window)`)}`);
3463
+ if (pct > 20) barLn(yellow(` Warning: heavy context load may slow agent startup`));
3464
+ barLn();
3465
+ }
3466
+
3467
+ // ─── moveros prayer ─────────────────────────────────────────────────────────
3468
+ async function cmdPrayer(opts) {
3469
+ const home = os.homedir();
3470
+ const moverDir = path.join(home, ".mover");
3471
+ const cfgPath = path.join(moverDir, "config.json");
3472
+ const ttPath = path.join(moverDir, "prayer-timetable.json");
3473
+
3474
+ if (!fs.existsSync(moverDir)) fs.mkdirSync(moverDir, { recursive: true, mode: 0o700 });
3475
+
3476
+ // Show current state
3477
+ barLn(bold(" Prayer Times"));
3478
+ barLn();
3479
+
3480
+ let cfg = {};
3481
+ if (fs.existsSync(cfgPath)) {
3482
+ try { cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8")); } catch {}
3483
+ }
3484
+
3485
+ const enabled = cfg.settings?.show_prayer_times;
3486
+ let tt = null;
3487
+ if (fs.existsSync(ttPath)) {
3488
+ try { tt = JSON.parse(fs.readFileSync(ttPath, "utf8")); } catch {}
3489
+ }
3490
+
3491
+ if (tt && Object.keys(tt.times || {}).length > 0) {
3492
+ statusLine("ok", "Source", `${tt.mosque || "Custom"} (${tt.type || "unknown"})`);
3493
+ statusLine("ok", "Days", `${Object.keys(tt.times).length} entries`);
3494
+
3495
+ // Show today's times
3496
+ const now = new Date();
3497
+ const todayKey = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,"0")}-${String(now.getDate()).padStart(2,"0")}`;
3498
+ const today = tt.times[todayKey];
3499
+ if (today) {
3500
+ const nowMins = now.getHours() * 60 + now.getMinutes();
3501
+ barLn();
3502
+ barLn(dim(" Today:"));
3503
+ for (const [name, time] of Object.entries(today)) {
3504
+ const [h, m] = time.split(":").map(Number);
3505
+ const pMins = h * 60 + m;
3506
+ const isPast = pMins < nowMins;
3507
+ const isNext = !isPast && pMins - nowMins < 60;
3508
+ const icon = isPast ? dim("\u2713") : isNext ? yellow("\u25B8") : dim("\u25CB");
3509
+ const fmt = isPast ? dim : isNext ? yellow : (s) => s;
3510
+ barLn(` ${icon} ${fmt(name.padEnd(10) + " " + time)}`);
3511
+ }
3512
+ } else {
3513
+ barLn(yellow(` No times for ${todayKey}. Timetable may need updating.`));
3514
+ }
3515
+ } else {
3516
+ statusLine("warn", "Timetable", "not set up");
3517
+ }
3518
+
3519
+ statusLine(enabled ? "ok" : "warn", "Status", enabled ? "enabled" : "disabled");
3520
+ barLn();
3521
+
3522
+ // Menu
3523
+ const items = [
3524
+ { id: "fetch", name: "Fetch calculated times by city", tier: "Adhan times from aladhan.com" },
3525
+ { id: "paste", name: "Paste mosque timetable", tier: "Jama'ah times — most accurate" },
3526
+ { id: "toggle", name: enabled ? "Disable prayer times" : "Enable prayer times", tier: "Toggle visibility in status line" },
3527
+ { id: "back", name: "Back", tier: "" },
3528
+ ];
3529
+
3530
+ question("What would you like to do?");
3531
+ barLn();
3532
+ const choice = await interactiveSelect(items, { multi: false });
3533
+
3534
+ if (!choice || choice === "back") return;
3535
+
3536
+ if (choice === "fetch") {
3537
+ barLn();
3538
+ const city = await textInput({ label: "City", placeholder: "London" });
3539
+ const country = await textInput({ label: "Country", placeholder: "United Kingdom" });
3540
+ barLn();
3541
+
3542
+ if (city && country) {
3543
+ const sp = spinner("Fetching prayer times (12 months)");
3544
+ const result = await fetchPrayerTimes(city.trim(), country.trim());
3545
+ if (result && Object.keys(result.times).length > 0) {
3546
+ fs.writeFileSync(ttPath, JSON.stringify(result, null, 2), "utf8");
3547
+ sp.stop(`Saved ${Object.keys(result.times).length} days`);
3548
+ barLn(dim(" These are calculated adhan times, not mosque jama'ah times."));
3549
+ barLn(dim(" For your mosque's specific times, choose 'Paste mosque timetable'."));
3550
+ } else {
3551
+ sp.stop(yellow("Could not fetch. Check city/country spelling."));
3552
+ }
3553
+ }
3554
+
3555
+ // Auto-enable
3556
+ if (!cfg.settings) cfg.settings = {};
3557
+ cfg.settings.show_prayer_times = true;
3558
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
3559
+ } else if (choice === "paste") {
3560
+ barLn();
3561
+ barLn(dim(" Paste your mosque's timetable below."));
3562
+ barLn(dim(" Format examples:"));
3563
+ barLn(dim(" 2026-03-08 05:20 13:00 16:15 18:01 19:45"));
3564
+ barLn(dim(" March 8: Fajr 05:20, Dhuhr 13:00, Asr 16:15, Maghrib 18:01, Isha 19:45"));
3565
+ barLn(dim(" Type 'done' on a new line when finished."));
3566
+ barLn();
3567
+
3568
+ const lines = [];
3569
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3570
+ await new Promise((resolve) => {
3571
+ const ask = () => {
3572
+ rl.question(`${BAR_COLOR}\u2502${S.reset} `, (line) => {
3573
+ if (line.trim().toLowerCase() === "done" || line.trim() === "") {
3574
+ rl.close();
3575
+ resolve();
3576
+ return;
3577
+ }
3578
+ lines.push(line);
3579
+ ask();
3580
+ });
3581
+ };
3582
+ ask();
3583
+ });
3584
+
3585
+ if (lines.length > 0) {
3586
+ // Ask for mosque name
3587
+ const mosqueName = await textInput({ label: "Mosque name (optional)", placeholder: "My Local Mosque" });
3588
+
3589
+ const result = parsePrayerTimetable(lines);
3590
+ if (mosqueName) result.mosque = mosqueName;
3591
+ if (result && Object.keys(result.times).length > 0) {
3592
+ // Merge with existing timetable if present
3593
+ if (tt && tt.times) {
3594
+ result.times = { ...tt.times, ...result.times };
3595
+ }
3596
+ fs.writeFileSync(ttPath, JSON.stringify(result, null, 2), "utf8");
3597
+ barLn(`${green("\u2713")} Saved ${Object.keys(result.times).length} days`);
3598
+ } else {
3599
+ barLn(yellow(" Could not parse the timetable."));
3600
+ barLn(dim(" Tip: paste it into Claude and ask to convert to this JSON format:"));
3601
+ barLn(dim(` { "times": { "2026-03-08": { "fajr": "05:20", "dhuhr": "13:00", ... } } }`));
3602
+ }
3603
+ }
3604
+
3605
+ // Auto-enable
3606
+ if (!cfg.settings) cfg.settings = {};
3607
+ cfg.settings.show_prayer_times = true;
3608
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
3609
+ } else if (choice === "toggle") {
3610
+ if (!cfg.settings) cfg.settings = {};
3611
+ cfg.settings.show_prayer_times = !enabled;
3612
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
3613
+ barLn(`${green("\u2713")} Prayer times ${cfg.settings.show_prayer_times ? "enabled" : "disabled"}`);
3614
+ }
3615
+
3092
3616
  barLn();
3093
3617
  }
3094
3618
 
@@ -3155,7 +3679,7 @@ async function cmdBackup(opts) {
3155
3679
  ];
3156
3680
 
3157
3681
  const choices = await interactiveSelect(items, { multi: true, preSelected: ["engine"] });
3158
- if (choices.length === 0) { barLn(dim(" Cancelled.")); return; }
3682
+ if (!choices || choices.length === 0) { barLn(dim(" Cancelled.")); return; }
3159
3683
 
3160
3684
  const now = new Date();
3161
3685
  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")}`;
@@ -3302,6 +3826,310 @@ async function cmdRestore(opts) {
3302
3826
  barLn();
3303
3827
  }
3304
3828
 
3829
+ // ─── moveros help ──────────────────────────────────────────────────────────
3830
+ async function cmdHelp(opts) {
3831
+ // Interactive animated guide — paginated walkthrough of Mover OS
3832
+ const pages = [
3833
+ {
3834
+ title: "Welcome to Mover OS",
3835
+ body: [
3836
+ `${bold("The agentic operating system for Obsidian.")}`,
3837
+ "",
3838
+ "Mover OS turns your Obsidian vault into an AI-powered execution engine.",
3839
+ "It works across 16 AI coding agents — Claude Code, Cursor, Gemini,",
3840
+ "Copilot, Codex, and more. Same brain, every editor.",
3841
+ "",
3842
+ `${dim("How it works:")}`,
3843
+ ` ${cyan("1.")} Your ${bold("Engine")} stores who you are — identity, strategy, goals`,
3844
+ ` ${cyan("2.")} ${bold("Workflows")} run your day — plan, execute, log, analyse, repeat`,
3845
+ ` ${cyan("3.")} ${bold("Skills")} give your AI agents deep domain knowledge`,
3846
+ ` ${cyan("4.")} The system ${bold("learns")} from your behavior and adapts`,
3847
+ "",
3848
+ `${dim("This guide walks through everything. Use ← → to navigate.")}`,
3849
+ ],
3850
+ },
3851
+ {
3852
+ title: "The Engine — Your Brain",
3853
+ body: [
3854
+ `${dim("Location:")} 02_Areas/Engine/`,
3855
+ "",
3856
+ `${bold("Core files that define you:")}`,
3857
+ "",
3858
+ ` ${cyan("Identity_Prime.md")} Who you are — values, psychology, anti-identity`,
3859
+ ` ${cyan("Strategy.md")} What you're testing — current hypothesis`,
3860
+ ` ${cyan("Active_Context.md")} What's happening NOW — blockers, focus, state`,
3861
+ ` ${cyan("Goals.md")} Where you're going — 90d, 1yr, 10yr targets`,
3862
+ ` ${cyan("Mover_Dossier.md")} What you have — skills, capital, network`,
3863
+ ` ${cyan("Auto_Learnings.md")} What the AI notices — behavioral patterns`,
3864
+ "",
3865
+ `These files are ${bold("irreplaceable")}. The system never overwrites them.`,
3866
+ "Every AI session reads them. Every workflow updates them.",
3867
+ `Your Engine ${bold("evolves")} as you do.`,
3868
+ ],
3869
+ },
3870
+ {
3871
+ title: "Daily Rhythm — The Loop",
3872
+ body: [
3873
+ `${bold("The daily execution cycle:")}`,
3874
+ "",
3875
+ ` ${green("→")} ${bold("/morning")} Start your day — energy check, set focus`,
3876
+ ` ${green("→")} ${bold("[WORK]")} Build, ship, create`,
3877
+ ` ${green("→")} ${bold("/log")} Capture what happened — syncs plan + state`,
3878
+ ` ${green("→")} ${bold("/analyse-day")} Brutal daily audit — patterns + verdict`,
3879
+ ` ${green("→")} ${bold("/plan-tomorrow")} Generate tomorrow's battle plan`,
3880
+ "",
3881
+ `${dim("Weekly:")}`,
3882
+ ` ${green("→")} ${bold("/review-week")} Sunday deep review + strategy validation`,
3883
+ "",
3884
+ `${dim("The rhythm is the system.")} Miss a day, patterns detect it.`,
3885
+ "Miss three, /reboot triggers recovery protocol.",
3886
+ `Every workflow hands off to the next — ${bold("no dead ends")}.`,
3887
+ ],
3888
+ },
3889
+ {
3890
+ title: "Workflows — 23 Commands",
3891
+ body: [
3892
+ `${bold("Daily:")} /morning /log /analyse-day /plan-tomorrow /review-week`,
3893
+ `${bold("Build:")} /ignite /overview /refactor-plan /capture /debrief`,
3894
+ `${bold("Think:")} /debug-resistance /pivot-strategy /mover-ideas /screenshot`,
3895
+ `${bold("Grow:")} /harvest /history /reboot`,
3896
+ `${bold("Meta:")} /setup /update /walkthrough /migrate /mover-check /mover-report`,
3897
+ "",
3898
+ `${dim("Key concepts:")}`,
3899
+ ` ${cyan("•")} ${bold("/ignite")} creates projects — brief + plan in one workflow`,
3900
+ ` ${cyan("•")} ${bold("/debug-resistance")} diagnoses WHY you're avoiding something`,
3901
+ ` ${cyan("•")} ${bold("/pivot-strategy")} formally changes direction with logging`,
3902
+ ` ${cyan("•")} ${bold("/harvest")} extracts permanent knowledge to your Library`,
3903
+ ` ${cyan("•")} ${bold("/screenshot")} does meta-analysis of your AI session patterns`,
3904
+ "",
3905
+ "Every command reads your Engine context and adapts to your state.",
3906
+ ],
3907
+ },
3908
+ {
3909
+ title: "Skills — Domain Intelligence",
3910
+ body: [
3911
+ `${bold("Skills give your AI agents specialized knowledge.")}`,
3912
+ "",
3913
+ `${dim("Categories:")}`,
3914
+ ` ${cyan("dev")} Debugging, TDD, refactoring, error handling, React`,
3915
+ ` ${cyan("marketing")} Copywriting, SEO, CRO, social content, email`,
3916
+ ` ${cyan("cro")} Page optimization, forms, popups, paywalls, onboarding`,
3917
+ ` ${cyan("strategy")} Pricing, launch, competitors, referrals, analytics`,
3918
+ ` ${cyan("seo")} Audits, schema markup, programmatic SEO, content`,
3919
+ ` ${cyan("design")} UI/UX, frontend design, Obsidian markdown/canvas`,
3920
+ ` ${cyan("obsidian")} JSON Canvas, Bases, Obsidian CLI, markdown`,
3921
+ "",
3922
+ `${dim("System skills (always active):")}`,
3923
+ ` friction-enforcer pattern-detector plan-md-guardian`,
3924
+ ` mover-os-context workflow-router daily-note-writer`,
3925
+ "",
3926
+ "Skills trigger automatically based on what you're doing.",
3927
+ ],
3928
+ },
3929
+ {
3930
+ title: "The Friction System",
3931
+ body: [
3932
+ `${bold("Your AI pushes back when you drift from your plan.")}`,
3933
+ "",
3934
+ ` ${dim("Level 1")} ${cyan("Surface")} \"Your plan says X. You're working on Y.\"`,
3935
+ ` ${dim("Level 2")} ${yellow("Justify")} \"Why is this more important than your Single Test?\"`,
3936
+ ` ${dim("Level 3")} ${red("Earn It")} AI stops helping with off-plan work.`,
3937
+ ` ${dim("Level 4")} ${red("Hard Block")} Destructive actions require explicit reason.`,
3938
+ "",
3939
+ `You can always push through Levels 1-3. Friction creates`,
3940
+ `${bold("awareness")}, not walls. But the AI won't silently comply`,
3941
+ "when you're avoiding the hard thing.",
3942
+ "",
3943
+ `${dim("Pre-Escalation Gate:")} If the work is exploration or compound`,
3944
+ `value (not avoidance), it logs as ${dim("[COMPOUND]")} and doesn't escalate.`,
3945
+ ],
3946
+ },
3947
+ {
3948
+ title: "Pattern Detection",
3949
+ body: [
3950
+ `${bold("The system watches your behavior across sessions.")}`,
3951
+ "",
3952
+ `${dim("How it works:")}`,
3953
+ ` ${cyan("1.")} AI observes recurring behavior (3+ data points)`,
3954
+ ` ${cyan("2.")} Logs to Auto_Learnings.md with confidence score (1-5)`,
3955
+ ` ${cyan("3.")} Patterns surface proactively in workflows`,
3956
+ ` ${cyan("4.")} You confirm or dismiss — the system adapts`,
3957
+ "",
3958
+ `${dim("Pattern types:")}`,
3959
+ ` ${yellow("Avoidance")} Dodging specific tasks repeatedly`,
3960
+ ` ${yellow("Time Drift")} Working past shutdown, skipping rest`,
3961
+ ` ${yellow("System Spree")} Building tools instead of shipping`,
3962
+ ` ${yellow("Scope Creep")} Tasks growing beyond plan boundaries`,
3963
+ ` ${yellow("Energy Cycles")} Performance tied to sleep/food/time`,
3964
+ "",
3965
+ `Confidence 3+ patterns route to the workflow that fixes them.`,
3966
+ ],
3967
+ },
3968
+ {
3969
+ title: "16 AI Agents — One Brain",
3970
+ body: [
3971
+ `${bold("Mover OS installs to every major AI coding agent.")}`,
3972
+ "",
3973
+ `${dim("Full tier (rules + skills + commands + hooks):")}`,
3974
+ ` Claude Code Cursor Cline Windsurf Gemini CLI`,
3975
+ ` Copilot Amazon Q OpenCode Kilo Code`,
3976
+ "",
3977
+ `${dim("Enhanced tier (rules + skills):")}`,
3978
+ ` Codex Amp Roo Code Antigravity`,
3979
+ "",
3980
+ `${dim("Basic tier (rules only):")}`,
3981
+ ` Continue Aider`,
3982
+ "",
3983
+ "Switch editors freely. Your identity, strategy, and patterns",
3984
+ `follow you everywhere. The ${bold("Engine is the constant")}.`,
3985
+ `Run ${cyan("moveros sync")} to update all agents at once.`,
3986
+ ],
3987
+ },
3988
+ {
3989
+ title: "CLI Commands",
3990
+ body: [
3991
+ `${bold("Terminal utilities — no AI session needed.")}`,
3992
+ "",
3993
+ ` ${cyan("moveros pulse")} Dashboard — energy, tasks, streaks, blockers`,
3994
+ ` ${cyan("moveros doctor")} Health check across all installed agents`,
3995
+ ` ${cyan("moveros capture")} Quick capture — tasks, links, ideas from terminal`,
3996
+ ` ${cyan("moveros who")} Entity memory lookup — people, orgs, places`,
3997
+ ` ${cyan("moveros sync")} Update all agents to latest rules/skills`,
3998
+ ` ${cyan("moveros context")} See what each agent loads — rules, skills, tokens`,
3999
+ ` ${cyan("moveros diff")} Engine file evolution via git history`,
4000
+ ` ${cyan("moveros replay")} Session replay from Daily Notes`,
4001
+ ` ${cyan("moveros settings")} View/edit config — ${dim("settings set <key> <val>")}`,
4002
+ ` ${cyan("moveros backup")} Manual backup wizard (engine, areas, agents)`,
4003
+ ` ${cyan("moveros restore")} Restore from backup`,
4004
+ ` ${cyan("moveros warm")} Pre-warm an AI session with context`,
4005
+ ],
4006
+ },
4007
+ {
4008
+ title: "The Status Line",
4009
+ body: [
4010
+ `${bold("Your life, glanceable. Always visible in Claude Code.")}`,
4011
+ "",
4012
+ ` ${dim("Line 1:")} Model · Context% · Project(branch) · Lines · Time · Cost`,
4013
+ ` ${dim("Line 2:")} Next task · Progress · Vitality · Prayer · Last log`,
4014
+ ` ${dim("Line 3:")} Rate limits (5hr + 7day + extra usage)`,
4015
+ "",
4016
+ `${dim("Features:")}`,
4017
+ ` ${cyan("•")} Next unchecked task from today's Daily Note`,
4018
+ ` ${cyan("•")} Strategic task count (vitality excluded)`,
4019
+ ` ${cyan("•")} Vitality slot machine — rotates reminders every minute`,
4020
+ ` ${cyan("•")} Next prayer time (if ${dim("show_prayer_times: true")} in settings)`,
4021
+ ` ${cyan("•")} Time since last /log — so you never forget`,
4022
+ ` ${cyan("•")} Rate limit bars with reset times`,
4023
+ "",
4024
+ `All data from your vault. ${bold("No API calls")} except rate limits.`,
4025
+ ],
4026
+ },
4027
+ {
4028
+ title: "Getting Started",
4029
+ body: [
4030
+ `${bold("You're ready.")}`,
4031
+ "",
4032
+ ` ${cyan("1.")} Run ${bold("/setup")} in any AI agent to build your Engine`,
4033
+ ` ${cyan("2.")} Run ${bold("/morning")} to start your first session`,
4034
+ ` ${cyan("3.")} Work. Build. Ship.`,
4035
+ ` ${cyan("4.")} Run ${bold("/log")} to capture what happened`,
4036
+ ` ${cyan("5.")} Run ${bold("/plan-tomorrow")} before bed`,
4037
+ "",
4038
+ `${dim("Quick tips:")}`,
4039
+ ` ${cyan("•")} ${bold("Single Test")} — one thing that makes the day a win`,
4040
+ ` ${cyan("•")} ${bold("Sacrifice")} — what you won't do today (as important as what you will)`,
4041
+ ` ${cyan("•")} The system gets smarter the more you use it`,
4042
+ ` ${cyan("•")} Trust the Engine files — they're your memory between sessions`,
4043
+ "",
4044
+ `${dim("moveros.dev")} ${dim("·")} ${dim("$49 one-time")} ${dim("·")} ${dim("updates included")}`,
4045
+ ],
4046
+ },
4047
+ ];
4048
+
4049
+ // Paginated display with keyboard navigation
4050
+ let page = 0;
4051
+
4052
+ function renderPage() {
4053
+ const p = pages[page];
4054
+ // Clear screen area
4055
+ w("\x1b[2J\x1b[H"); // clear screen, cursor to top
4056
+ ln();
4057
+
4058
+ // Page indicator
4059
+ const dots = pages.map((_, i) => i === page ? cyan("●") : dim("○")).join(" ");
4060
+ ln(` ${dots}`);
4061
+ ln();
4062
+
4063
+ // Title with box
4064
+ ln(` ${S.cyan}┌${"─".repeat(56)}┐${S.reset}`);
4065
+ const titlePad = Math.max(0, 54 - strip(p.title).length);
4066
+ ln(` ${S.cyan}│${S.reset} ${bold(p.title)}${" ".repeat(titlePad)} ${S.cyan}│${S.reset}`);
4067
+ ln(` ${S.cyan}└${"─".repeat(56)}┘${S.reset}`);
4068
+ ln();
4069
+
4070
+ // Body
4071
+ for (const line of p.body) {
4072
+ ln(` ${line}`);
4073
+ }
4074
+ ln();
4075
+ ln();
4076
+
4077
+ // Navigation
4078
+ const nav = [];
4079
+ if (page > 0) nav.push(`${dim("←")} prev`);
4080
+ if (page < pages.length - 1) nav.push(`${dim("→")} next`);
4081
+ nav.push(`${dim("q")} quit`);
4082
+ ln(` ${dim(`${page + 1}/${pages.length}`)} ${nav.join(dim(" · "))}`);
4083
+ }
4084
+
4085
+ return new Promise((resolve) => {
4086
+ if (!IS_TTY) {
4087
+ // Non-interactive: dump all pages
4088
+ for (const p of pages) {
4089
+ ln(bold(`\n## ${p.title}\n`));
4090
+ for (const line of p.body) ln(` ${line}`);
4091
+ }
4092
+ resolve();
4093
+ return;
4094
+ }
4095
+
4096
+ const { stdin } = process;
4097
+ stdin.setRawMode(true);
4098
+ stdin.resume();
4099
+ stdin.setEncoding("utf8");
4100
+ w(S.hide);
4101
+
4102
+ renderPage();
4103
+
4104
+ const handler = (data) => {
4105
+ if (data === "\x1b[C" || data === "l" || data === " ") {
4106
+ if (page < pages.length - 1) { page++; renderPage(); }
4107
+ } else if (data === "\x1b[D" || data === "h") {
4108
+ if (page > 0) { page--; renderPage(); }
4109
+ } else if (data === "q" || data === "\x1b" || data === "\x03") {
4110
+ stdin.removeListener("data", handler);
4111
+ stdin.setRawMode(false);
4112
+ stdin.pause();
4113
+ w(S.show);
4114
+ w("\x1b[2J\x1b[H"); // clear
4115
+ resolve();
4116
+ } else if (data === "\r" || data === "\n") {
4117
+ if (page < pages.length - 1) { page++; renderPage(); }
4118
+ else {
4119
+ stdin.removeListener("data", handler);
4120
+ stdin.setRawMode(false);
4121
+ stdin.pause();
4122
+ w(S.show);
4123
+ w("\x1b[2J\x1b[H");
4124
+ resolve();
4125
+ }
4126
+ }
4127
+ };
4128
+
4129
+ stdin.on("data", handler);
4130
+ });
4131
+ }
4132
+
3305
4133
  // ─── moveros test (dev only) ────────────────────────────────────────────────
3306
4134
  async function cmdTest(opts) { barLn(yellow("moveros test — not yet implemented.")); }
3307
4135
 
@@ -3312,11 +4140,12 @@ async function cmdMainMenu() {
3312
4140
  { header: "Dashboard", cmds: ["pulse", "replay", "diff"] },
3313
4141
  { header: "Agents", cmds: ["doctor", "sync", "context", "warm"] },
3314
4142
  { header: "Capture", cmds: ["capture", "who"] },
3315
- { header: "System", cmds: ["settings", "backup", "restore"] },
4143
+ { header: "System", cmds: ["settings", "prayer", "backup", "restore", "help"] },
3316
4144
  ];
3317
4145
 
3318
4146
  const menuItems = [];
3319
4147
  for (const cat of categories) {
4148
+ menuItems.push({ id: `_sep_${cat.header}`, name: `── ${cat.header}`, _separator: true });
3320
4149
  for (const cmd of cat.cmds) {
3321
4150
  const meta = CLI_COMMANDS[cmd];
3322
4151
  if (!meta || meta.hidden) continue;
@@ -3367,20 +4196,36 @@ async function main() {
3367
4196
  // ── Intro ──
3368
4197
  await printHeader();
3369
4198
 
3370
- // ── Route: no command → interactive menu ──
4199
+ // ── Route: no command → interactive menu (persistent loop) ──
4200
+ const lightCommands = ["pulse", "warm", "capture", "who", "diff", "sync", "replay", "context", "settings", "backup", "restore", "doctor", "prayer", "help", "test"];
4201
+
3371
4202
  if (!opts.command) {
3372
- opts.command = await cmdMainMenu();
4203
+ // Interactive loop — stay open like a real app
4204
+ while (true) {
4205
+ opts.command = await cmdMainMenu();
4206
+ if (!opts.command) break; // user cancelled
4207
+
4208
+ if (lightCommands.includes(opts.command)) {
4209
+ const handler = CLI_HANDLERS[opts.command];
4210
+ if (handler) await handler(opts);
4211
+ else barLn(yellow(`Command '${opts.command}' is not yet implemented.`));
4212
+ opts.command = ""; // reset for next loop
4213
+ barLn(dim(" Press enter to return to menu..."));
4214
+ await new Promise((r) => { process.stdin.resume(); process.stdin.once("data", () => { process.stdin.pause(); r(); }); });
4215
+ continue;
4216
+ }
4217
+ break; // install/update break out of loop into pre-flight
4218
+ }
4219
+ if (!opts.command) return;
3373
4220
  }
3374
4221
 
3375
- // ── Route: CLI commands that don't need pre-flight ──
3376
- const lightCommands = ["pulse", "warm", "capture", "who", "diff", "sync", "replay", "context", "settings", "backup", "restore", "doctor", "test"];
4222
+ // ── Route: direct CLI command (non-interactive) ──
3377
4223
  if (lightCommands.includes(opts.command)) {
3378
4224
  const handler = CLI_HANDLERS[opts.command];
3379
4225
  if (handler) {
3380
4226
  await handler(opts);
3381
4227
  } else {
3382
4228
  barLn(yellow(`Command '${opts.command}' is not yet implemented.`));
3383
- barLn(dim("Coming soon in a future update."));
3384
4229
  }
3385
4230
  return;
3386
4231
  }
@@ -3400,6 +4245,39 @@ async function main() {
3400
4245
  process.exit(1);
3401
4246
  }
3402
4247
 
4248
+ // ── CLI self-update check ──
4249
+ if (opts.command === "update" && !opts._selfUpdated) {
4250
+ try {
4251
+ const localVer = require("./package.json").version;
4252
+ const npmVer = execSync("npm view mover-os version", { encoding: "utf8", timeout: 10000 }).trim();
4253
+ if (npmVer && npmVer !== localVer && compareVersions(npmVer, localVer) > 0) {
4254
+ barLn(`${yellow("CLI update available:")} ${dim(localVer)} ${dim("\u2192")} ${green(npmVer)}`);
4255
+ const sp = spinner("Updating CLI");
4256
+ try {
4257
+ execSync("npm i -g mover-os", { stdio: "ignore", timeout: 60000 });
4258
+ sp.stop(`CLI updated to ${npmVer}`);
4259
+ barLn(dim(" Re-running with updated CLI..."));
4260
+ barLn();
4261
+ // Re-exec with new code — pass args through, add flag to prevent loop
4262
+ const args = process.argv.slice(2).concat("--_self-updated");
4263
+ const { spawnSync } = require("child_process");
4264
+ const result = spawnSync(process.argv[0], [process.argv[1], ...args], {
4265
+ stdio: "inherit", cwd: process.cwd(),
4266
+ });
4267
+ process.exit(result.status || 0);
4268
+ } catch (e) {
4269
+ sp.stop(yellow(`CLI self-update failed: ${e.message}`));
4270
+ barLn(dim(" Continuing with current version..."));
4271
+ }
4272
+ } else {
4273
+ barLn(`${green("\u2713")} ${dim("CLI is up to date")} ${dim(`(${localVer})`)}`);
4274
+ }
4275
+ } catch {
4276
+ barLn(dim(" Could not check for CLI updates (offline?)"));
4277
+ }
4278
+ barLn();
4279
+ }
4280
+
3403
4281
  // ── Headless quick update ──
3404
4282
  if (opts.command === "update") {
3405
4283
  // Validate stored key
@@ -3618,6 +4496,7 @@ async function main() {
3618
4496
  });
3619
4497
 
3620
4498
  const selected = await interactiveSelect(vaultItems, { multi: false });
4499
+ if (!selected) return;
3621
4500
 
3622
4501
  if (selected === "__manual__") {
3623
4502
  vaultPath = await textInput({
@@ -3668,6 +4547,7 @@ async function main() {
3668
4547
  ],
3669
4548
  { multi: false, defaultIndex: 0 }
3670
4549
  );
4550
+ if (!installMode) return;
3671
4551
  }
3672
4552
 
3673
4553
  // ── Uninstall flow ──
@@ -3707,7 +4587,7 @@ async function main() {
3707
4587
  preSelected: ["engine"],
3708
4588
  });
3709
4589
 
3710
- if (backupChoices.length > 0 && !(backupChoices.length === 1 && backupChoices.includes("skip"))) {
4590
+ if (backupChoices && backupChoices.length > 0 && !(backupChoices.length === 1 && backupChoices.includes("skip"))) {
3711
4591
  const now = new Date();
3712
4592
  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")}`;
3713
4593
  const archivesDir = path.join(vaultPath, "04_Archives");
@@ -3870,12 +4750,13 @@ async function main() {
3870
4750
  multi: true,
3871
4751
  preSelected: detectedIds,
3872
4752
  });
4753
+ if (!selectedIds) return;
3873
4754
  const selectedAgents = AGENTS.filter((a) => selectedIds.includes(a.id));
3874
4755
 
3875
4756
  if (selectedAgents.length === 0) {
3876
4757
  barLn(yellow("No agents selected."));
3877
4758
  outro("Cancelled.");
3878
- process.exit(0);
4759
+ return;
3879
4760
  }
3880
4761
 
3881
4762
  // ── Change detection + selection (update mode only) ──
@@ -3920,9 +4801,9 @@ async function main() {
3920
4801
  { multi: false, defaultIndex: 0 }
3921
4802
  );
3922
4803
 
3923
- if (applyChoice === "cancel") {
4804
+ if (!applyChoice || applyChoice === "cancel") {
3924
4805
  outro("Cancelled.");
3925
- process.exit(0);
4806
+ return;
3926
4807
  }
3927
4808
 
3928
4809
  if (applyChoice === "select") {
@@ -3956,6 +4837,7 @@ async function main() {
3956
4837
  multi: true,
3957
4838
  preSelected: changedPreSelected,
3958
4839
  });
4840
+ if (!selectedFileIds) return;
3959
4841
 
3960
4842
  // Build workflow filter Set
3961
4843
  const selectedWfFiles = selectedFileIds
@@ -4002,6 +4884,7 @@ async function main() {
4002
4884
  multi: true,
4003
4885
  preSelected,
4004
4886
  });
4887
+ if (!selectedCatIds) return;
4005
4888
 
4006
4889
  installSkills = selectedCatIds.length > 0;
4007
4890
  if (installSkills) {
@@ -4028,9 +4911,107 @@ async function main() {
4028
4911
  ],
4029
4912
  { multi: false, defaultIndex: 0 }
4030
4913
  );
4914
+ if (!slChoice) return;
4031
4915
  installStatusLine = slChoice === "yes";
4032
4916
  }
4033
4917
 
4918
+ // ── Prayer times (optional) ──
4919
+ let prayerSetup = false;
4920
+ {
4921
+ barLn();
4922
+ question("Would you like prayer time reminders?");
4923
+ barLn(dim(" Shows next prayer time in the status line and Daily Notes."));
4924
+ barLn(dim(" Designed for Muslims — skip if not relevant to you."));
4925
+ barLn();
4926
+
4927
+ const ptChoice = await interactiveSelect(
4928
+ [
4929
+ { id: "yes", name: "Yes, set up prayer times", tier: "You can paste your mosque's timetable or fetch by city" },
4930
+ { id: "no", name: "No, skip", tier: "You can enable later with: moveros prayer" },
4931
+ ],
4932
+ { multi: false, defaultIndex: 1 }
4933
+ );
4934
+ if (!ptChoice) return;
4935
+ if (ptChoice === "yes") {
4936
+ prayerSetup = true;
4937
+ barLn();
4938
+ question("How would you like to set up prayer times?");
4939
+ barLn();
4940
+
4941
+ const method = await interactiveSelect(
4942
+ [
4943
+ { id: "paste", name: "Paste my mosque's timetable", tier: "Jama'ah times — most accurate for your local mosque" },
4944
+ { id: "fetch", name: "Fetch calculated times by city", tier: "Adhan times from aladhan.com — good starting point" },
4945
+ { id: "later", name: "I'll set it up later", tier: "Run: moveros prayer" },
4946
+ ],
4947
+ { multi: false, defaultIndex: 0 }
4948
+ );
4949
+ if (!method || method === "later") { /* skip */ }
4950
+
4951
+ const moverDir = path.join(os.homedir(), ".mover");
4952
+ if (!fs.existsSync(moverDir)) fs.mkdirSync(moverDir, { recursive: true, mode: 0o700 });
4953
+
4954
+ if (method === "paste") {
4955
+ barLn();
4956
+ barLn(dim(" Paste your mosque's timetable below."));
4957
+ barLn(dim(" Format: one line per entry, any of these work:"));
4958
+ barLn(dim(" 2026-03-08 05:20 13:00 16:15 18:01 19:45"));
4959
+ barLn(dim(" March 8: Fajr 05:20, Dhuhr 13:00, Asr 16:15, Maghrib 18:01, Isha 19:45"));
4960
+ barLn(dim(" Or paste a whole table — the system will parse it."));
4961
+ barLn(dim(" When done, type 'done' on a new line and press Enter."));
4962
+ barLn();
4963
+
4964
+ const lines = [];
4965
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
4966
+ await new Promise((resolve) => {
4967
+ const ask = () => {
4968
+ rl.question(`${BAR_COLOR}\u2502${S.reset} `, (line) => {
4969
+ if (line.trim().toLowerCase() === "done" || line.trim() === "") {
4970
+ rl.close();
4971
+ resolve();
4972
+ return;
4973
+ }
4974
+ lines.push(line);
4975
+ ask();
4976
+ });
4977
+ };
4978
+ ask();
4979
+ });
4980
+
4981
+ if (lines.length > 0) {
4982
+ const tt = parsePrayerTimetable(lines);
4983
+ if (tt && Object.keys(tt.times).length > 0) {
4984
+ const ttPath = path.join(moverDir, "prayer-timetable.json");
4985
+ fs.writeFileSync(ttPath, JSON.stringify(tt, null, 2), "utf8");
4986
+ barLn(`${green("\u2713")} ${dim(`Saved ${Object.keys(tt.times).length} days to prayer-timetable.json`)}`);
4987
+ } else {
4988
+ barLn(yellow(" Could not parse timetable. Run moveros prayer later to try again."));
4989
+ }
4990
+ }
4991
+ } else if (method === "fetch") {
4992
+ barLn();
4993
+ const city = await textInput({ label: "City (e.g. London, Watford, Istanbul)", placeholder: "London" });
4994
+ const country = await textInput({ label: "Country", placeholder: "United Kingdom" });
4995
+ barLn();
4996
+
4997
+ if (city && country) {
4998
+ const sp = spinner("Fetching prayer times");
4999
+ const tt = await fetchPrayerTimes(city.trim(), country.trim());
5000
+ if (tt && Object.keys(tt.times).length > 0) {
5001
+ const ttPath = path.join(moverDir, "prayer-timetable.json");
5002
+ fs.writeFileSync(ttPath, JSON.stringify(tt, null, 2), "utf8");
5003
+ sp.stop(`Prayer times ${dim(`${Object.keys(tt.times).length} days from aladhan.com`)}`);
5004
+ barLn(dim(" Note: these are calculated adhan times, not mosque jama'ah times."));
5005
+ barLn(dim(" For your mosque's specific times, run: moveros prayer"));
5006
+ } else {
5007
+ sp.stop(yellow("Could not fetch. Run moveros prayer later."));
5008
+ }
5009
+ }
5010
+ }
5011
+ // method === "later" → just enable the setting, no timetable yet
5012
+ }
5013
+ }
5014
+
4034
5015
  // ── Install with animated spinners ──
4035
5016
  barLn();
4036
5017
  question(updateMode ? bold("Updating...") : bold("Installing..."));
@@ -4138,29 +5119,28 @@ async function main() {
4138
5119
  }
4139
5120
  }
4140
5121
 
4141
- // 7. .gitignore + git init (fresh install only)
5122
+ // 7. Git init Engine folder only (fresh install only)
4142
5123
  if (!updateMode) {
4143
5124
  const hasGit = cmdExists("git");
4144
5125
  if (hasGit) {
4145
- const gitignorePath = path.join(vaultPath, ".gitignore");
4146
- if (!fs.existsSync(gitignorePath)) {
4147
- fs.writeFileSync(gitignorePath, generateGitignore(), "utf8");
4148
- sp = spinner(".gitignore");
4149
- await sleep(100);
4150
- sp.stop(".gitignore");
4151
- totalSteps++;
4152
- }
4153
- if (!fs.existsSync(path.join(vaultPath, ".git"))) {
4154
- sp = spinner("Git repository");
5126
+ const engineDir = path.join(vaultPath, "02_Areas", "Engine");
5127
+ const engineGit = path.join(engineDir, ".git");
5128
+ if (!fs.existsSync(engineGit) && fs.existsSync(engineDir)) {
5129
+ sp = spinner("Engine git repository");
4155
5130
  try {
4156
- execSync("git init", { cwd: vaultPath, stdio: "ignore" });
4157
- execSync("git add 01_Projects 02_Areas/Engine 03_Library 04_Archives .gitignore .mover-version CLAUDE.md", { cwd: vaultPath, stdio: "ignore" });
4158
- execSync('git commit -m "Initial commit — Mover OS v' + VERSION + '"', { cwd: vaultPath, stdio: "ignore" });
5131
+ // .gitignore inside Engine (excludes Dailies + Weekly Reviews)
5132
+ const gitignorePath = path.join(engineDir, ".gitignore");
5133
+ if (!fs.existsSync(gitignorePath)) {
5134
+ fs.writeFileSync(gitignorePath, generateGitignore(), "utf8");
5135
+ }
5136
+ execSync("git init", { cwd: engineDir, stdio: "ignore" });
5137
+ execSync("git add -A", { cwd: engineDir, stdio: "ignore" });
5138
+ execSync('git commit -m "Initial commit — Mover OS Engine v' + VERSION + '"', { cwd: engineDir, stdio: "ignore" });
4159
5139
  await sleep(300);
4160
- sp.stop("Git initialized");
5140
+ sp.stop(`Engine git initialized ${dim("02_Areas/Engine/")}`);
4161
5141
  totalSteps++;
4162
5142
  } catch {
4163
- sp.stop(dim("Git skipped"));
5143
+ sp.stop(dim("Engine git skipped"));
4164
5144
  }
4165
5145
  }
4166
5146
  }
@@ -4172,7 +5152,7 @@ async function main() {
4172
5152
  }
4173
5153
 
4174
5154
  // 9. Write ~/.mover/config.json (both fresh + update)
4175
- writeMoverConfig(vaultPath, selectedIds, key);
5155
+ writeMoverConfig(vaultPath, selectedIds, key, { prayerSetup });
4176
5156
 
4177
5157
  barLn();
4178
5158