mover-os 4.3.2 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/install.js +1000 -80
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  Mover OS turns Obsidian into an AI-powered execution engine. It audits your behavior against your stated strategy, extracts reusable knowledge from daily work, and evolves based on your corrections.
8
8
 
9
- **Works best with Claude Code.** Supports 13 AI coding agents.
9
+ **Works best with Claude Code.** Supports 14 AI coding agents.
10
10
 
11
11
  **Version:** 4.3 | **Status:** Production
12
12
 
@@ -22,7 +22,7 @@ Mover OS turns Obsidian into an AI-powered execution engine. It audits your beha
22
22
  - **Escalating Friction** — 4 levels of pushback when you drift from your plan. Speed bumps, not walls.
23
23
  - **Correction Lifecycle** — Correct the AI and it proposes a fix, waits for approval, and updates its own rules.
24
24
  - **Entity Memory** — Remembers people, organizations, and decisions across sessions.
25
- - **22 Connected Workflows** — Each one hands off to the next. You never wonder what to do next.
25
+ - **23 Connected Workflows** — Each one hands off to the next. You never wonder what to do next.
26
26
 
27
27
  ---
28
28
 
@@ -91,7 +91,7 @@ bash src/install/link.sh
91
91
 
92
92
  ## Supported Agents
93
93
 
94
- Works with **13 AI coding agents.** The installer auto-detects and configures each one:
94
+ Works with **14 AI coding agents.** The installer auto-detects and configures each one:
95
95
 
96
96
  | Agent | What Gets Installed |
97
97
  |-------|---------------------|
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
 
@@ -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);
@@ -552,6 +639,8 @@ const CLI_COMMANDS = {
552
639
  settings: { desc: "View/edit config", alias: [] },
553
640
  backup: { desc: "Manual backup wizard", alias: [] },
554
641
  restore: { desc: "Restore from backup", alias: [] },
642
+ prayer: { desc: "Manage prayer times", alias: [] },
643
+ help: { desc: "Interactive guide to Mover OS", alias: ["-h"] },
555
644
  test: { desc: "Run integration tests (dev)", alias: [], hidden: true },
556
645
  };
557
646
 
@@ -1512,14 +1601,155 @@ function generateClaudeSettings() {
1512
1601
  );
1513
1602
  }
1514
1603
 
1604
+ // ─── Prayer timetable helpers ───────────────────────────────────────────────
1605
+
1606
+ function parsePrayerTimetable(lines) {
1607
+ // Parse user-pasted mosque timetable into date-keyed JSON
1608
+ // Supports various formats:
1609
+ // 2026-03-08 05:20 13:00 16:15 18:01 19:45
1610
+ // March 8: Fajr 05:20, Dhuhr 13:00, ...
1611
+ // Tab/comma separated tables from spreadsheets
1612
+ const times = {};
1613
+ const prayerNames = ["fajr", "dhuhr", "asr", "maghrib", "isha"];
1614
+ 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,
1615
+ january: 1, february: 2, march: 3, april: 4, june: 6, july: 7, august: 8, september: 9, october: 10, november: 11, december: 12 };
1616
+ const currentYear = new Date().getFullYear();
1617
+
1618
+ for (const line of lines) {
1619
+ const trimmed = line.trim();
1620
+ if (!trimmed || trimmed.toLowerCase() === "done") continue;
1621
+
1622
+ // Try ISO date format: 2026-03-08 ...
1623
+ const isoMatch = trimmed.match(/^(\d{4}-\d{2}-\d{2})\s+(.+)/);
1624
+ if (isoMatch) {
1625
+ const dateKey = isoMatch[1];
1626
+ const timeValues = isoMatch[2].match(/\d{1,2}:\d{2}/g);
1627
+ if (timeValues && timeValues.length >= 5) {
1628
+ times[dateKey] = {};
1629
+ for (let i = 0; i < Math.min(5, timeValues.length); i++) {
1630
+ times[dateKey][prayerNames[i]] = timeValues[i];
1631
+ }
1632
+ }
1633
+ continue;
1634
+ }
1635
+
1636
+ // Try month name format: March 8: ... or 8 March: ...
1637
+ const monthMatch = trimmed.match(/(?:(\d{1,2})\s+)?(\w+)\s*(\d{1,2})?\s*[:\s]\s*(.+)/i);
1638
+ if (monthMatch) {
1639
+ const mName = monthMatch[2].toLowerCase();
1640
+ const monthNum = monthNames[mName];
1641
+ if (monthNum) {
1642
+ const day = parseInt(monthMatch[1] || monthMatch[3]);
1643
+ if (day >= 1 && day <= 31) {
1644
+ const dateKey = `${currentYear}-${String(monthNum).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
1645
+ const timeValues = monthMatch[4].match(/\d{1,2}:\d{2}/g);
1646
+ if (timeValues && timeValues.length >= 5) {
1647
+ times[dateKey] = {};
1648
+ for (let i = 0; i < Math.min(5, timeValues.length); i++) {
1649
+ times[dateKey][prayerNames[i]] = timeValues[i];
1650
+ }
1651
+ }
1652
+ }
1653
+ }
1654
+ continue;
1655
+ }
1656
+
1657
+ // Try pure time extraction from tab/comma separated
1658
+ const timeValues = trimmed.match(/\d{1,2}:\d{2}/g);
1659
+ if (timeValues && timeValues.length >= 5) {
1660
+ // Look for a date number at the start
1661
+ const dayMatch = trimmed.match(/^(\d{1,2})\D/);
1662
+ if (dayMatch) {
1663
+ const day = parseInt(dayMatch[1]);
1664
+ if (day >= 1 && day <= 31) {
1665
+ // Best guess: current month
1666
+ const now = new Date();
1667
+ const dateKey = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
1668
+ times[dateKey] = {};
1669
+ for (let i = 0; i < Math.min(5, timeValues.length); i++) {
1670
+ times[dateKey][prayerNames[i]] = timeValues[i];
1671
+ }
1672
+ }
1673
+ }
1674
+ }
1675
+ }
1676
+
1677
+ return {
1678
+ mosque: "Custom",
1679
+ type: "jamaah",
1680
+ note: "Paste your mosque's yearly timetable into Claude and ask it to convert to this format.",
1681
+ times,
1682
+ };
1683
+ }
1684
+
1685
+ async function fetchPrayerTimes(city, country) {
1686
+ // Fetch 12 months of prayer times from aladhan.com API
1687
+ const https = require("https");
1688
+ const year = new Date().getFullYear();
1689
+ const allTimes = {};
1690
+
1691
+ for (let month = 1; month <= 12; month++) {
1692
+ try {
1693
+ const url = `https://api.aladhan.com/v1/calendarByCity/${year}/${month}?city=${encodeURIComponent(city)}&country=${encodeURIComponent(country)}&method=15`;
1694
+ const body = await new Promise((resolve, reject) => {
1695
+ const req = https.request(url, { method: "GET", timeout: 10000 }, (res) => {
1696
+ let data = "";
1697
+ res.on("data", (c) => (data += c));
1698
+ res.on("end", () => resolve(data));
1699
+ });
1700
+ req.on("error", reject);
1701
+ req.on("timeout", () => { req.destroy(); reject(new Error("Timeout")); });
1702
+ req.end();
1703
+ });
1704
+
1705
+ const json = JSON.parse(body);
1706
+ if (json.code === 200 && json.data) {
1707
+ for (const day of json.data) {
1708
+ const t = day.timings;
1709
+ const d = day.date.gregorian;
1710
+ const dateKey = `${d.year}-${d.month.number.toString().padStart(2, "0")}-${d.day.padStart(2, "0")}`;
1711
+ allTimes[dateKey] = {
1712
+ fajr: t.Fajr.replace(/\s*\(.*\)/, ""),
1713
+ dhuhr: t.Dhuhr.replace(/\s*\(.*\)/, ""),
1714
+ asr: t.Asr.replace(/\s*\(.*\)/, ""),
1715
+ maghrib: t.Maghrib.replace(/\s*\(.*\)/, ""),
1716
+ isha: t.Isha.replace(/\s*\(.*\)/, ""),
1717
+ };
1718
+ }
1719
+ }
1720
+ } catch {
1721
+ // Skip failed months — partial data is still useful
1722
+ }
1723
+ }
1724
+
1725
+ return {
1726
+ mosque: `Calculated (${city}, ${country})`,
1727
+ type: "adhan",
1728
+ note: "Calculated adhan times from aladhan.com. For mosque jama'ah times, run: moveros prayer",
1729
+ city,
1730
+ country,
1731
+ times: allTimes,
1732
+ };
1733
+ }
1734
+
1515
1735
  // ─── .gitignore ─────────────────────────────────────────────────────────────
1516
1736
  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/
1737
+ return `# Mover OS Engine track core files only
1738
+ # Ignore everything by default
1739
+ *
1740
+
1741
+ # Track core Engine files
1742
+ !.gitignore
1743
+ !Identity_Prime.md
1744
+ !Strategy.md
1745
+ !Active_Context.md
1746
+ !Goals.md
1747
+ !Auto_Learnings.md
1748
+ !Mover_Dossier.md
1749
+ !Metrics_Log.md
1750
+ !Voice_DNA.md
1751
+ !Daily_Template.md
1752
+ !Someday_Maybe.md
1523
1753
  `;
1524
1754
  }
1525
1755
 
@@ -1553,7 +1783,7 @@ function createVaultStructure(vaultPath) {
1553
1783
  return created;
1554
1784
  }
1555
1785
 
1556
- function writeMoverConfig(vaultPath, agentIds, licenseKey) {
1786
+ function writeMoverConfig(vaultPath, agentIds, licenseKey, opts = {}) {
1557
1787
  const configDir = path.join(os.homedir(), ".mover");
1558
1788
  if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
1559
1789
  const configPath = path.join(configDir, "config.json");
@@ -1566,7 +1796,7 @@ function writeMoverConfig(vaultPath, agentIds, licenseKey) {
1566
1796
  installedAt: new Date().toISOString(),
1567
1797
  };
1568
1798
  if (licenseKey) config.licenseKey = licenseKey;
1569
- // If config exists, preserve installedAt and licenseKey from original install
1799
+ // If config exists, preserve existing values
1570
1800
  if (fs.existsSync(configPath)) {
1571
1801
  try {
1572
1802
  const existing = JSON.parse(fs.readFileSync(configPath, "utf8"));
@@ -1575,9 +1805,18 @@ function writeMoverConfig(vaultPath, agentIds, licenseKey) {
1575
1805
  if (existing.feedbackWebhook) config.feedbackWebhook = existing.feedbackWebhook;
1576
1806
  if (existing.track_food !== undefined) config.track_food = existing.track_food;
1577
1807
  if (existing.track_sleep !== undefined) config.track_sleep = existing.track_sleep;
1808
+ // Preserve settings block (prayer times, review_day, etc.)
1809
+ if (existing.settings) config.settings = { ...existing.settings };
1810
+ // Preserve prayer_times fallback
1811
+ if (existing.prayer_times) config.prayer_times = existing.prayer_times;
1578
1812
  config.updatedAt = new Date().toISOString();
1579
1813
  } catch {}
1580
1814
  }
1815
+ // Apply prayer setup from installer
1816
+ if (opts.prayerSetup) {
1817
+ if (!config.settings) config.settings = {};
1818
+ config.settings.show_prayer_times = true;
1819
+ }
1581
1820
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
1582
1821
  return configPath;
1583
1822
  }
@@ -2442,6 +2681,8 @@ const CLI_HANDLERS = {
2442
2681
  backup: async (opts) => { await cmdBackup(opts); },
2443
2682
  restore: async (opts) => { await cmdRestore(opts); },
2444
2683
  doctor: async (opts) => { await cmdDoctor(opts); },
2684
+ prayer: async (opts) => { await cmdPrayer(opts); },
2685
+ help: async (opts) => { await cmdHelp(opts); },
2445
2686
  test: async (opts) => { await cmdTest(opts); },
2446
2687
  };
2447
2688
 
@@ -2515,10 +2756,20 @@ async function cmdDoctor(opts) {
2515
2756
  statusLine(allOk ? "ok" : "warn", ` ${reg.name}`, checks.join(", "));
2516
2757
  }
2517
2758
 
2518
- // Git
2759
+ // Engine Git
2519
2760
  barLn();
2520
- const hasGit = fs.existsSync(path.join(vault, ".git"));
2521
- statusLine(hasGit ? "ok" : "info", "Git", hasGit ? "initialized" : "not a git repo");
2761
+ const engineGit = fs.existsSync(path.join(engineDir, ".git"));
2762
+ statusLine(engineGit ? "ok" : "warn", "Engine Git", engineGit ? "initialized" : "not version controlled — run moveros install to fix");
2763
+
2764
+ // Engine gitignore
2765
+ const engineGitignore = path.join(engineDir, ".gitignore");
2766
+ if (engineGit && fs.existsSync(engineGitignore)) {
2767
+ const gi = fs.readFileSync(engineGitignore, "utf8");
2768
+ const isWhitelist = gi.includes("*") && gi.includes("!Identity_Prime.md");
2769
+ statusLine(isWhitelist ? "ok" : "warn", "Engine .gitignore", isWhitelist ? "whitelist (core files only)" : "legacy format — re-run moveros install");
2770
+ } else if (engineGit) {
2771
+ statusLine("warn", "Engine .gitignore", "missing");
2772
+ }
2522
2773
 
2523
2774
  barLn();
2524
2775
  barLn(dim(" Run moveros install or moveros update to fix any issues."));
@@ -2846,13 +3097,23 @@ async function cmdDiff(opts) {
2846
3097
  return;
2847
3098
  }
2848
3099
 
3100
+ // Determine git root — Engine files use Engine's own repo
3101
+ const engineDir = path.join(vault, "02_Areas", "Engine");
3102
+ const isEngineFile = relPath.startsWith("02_Areas/Engine/") || relPath.startsWith("02_Areas\\Engine\\");
3103
+ const gitCwd = isEngineFile && fs.existsSync(path.join(engineDir, ".git"))
3104
+ ? engineDir
3105
+ : vault;
3106
+ const gitRelPath = isEngineFile && gitCwd === engineDir
3107
+ ? path.basename(relPath)
3108
+ : relPath;
3109
+
2849
3110
  barLn(bold(` ${path.basename(relPath)} — last ${days} days`));
2850
3111
  barLn();
2851
3112
 
2852
3113
  try {
2853
3114
  const log = execSync(
2854
- `git log --since="${days} days ago" --oneline --follow -- "${relPath}"`,
2855
- { cwd: vault, encoding: "utf8", timeout: 10000 }
3115
+ `git log --since="${days} days ago" --oneline --follow -- "${gitRelPath}"`,
3116
+ { cwd: gitCwd, encoding: "utf8", timeout: 10000 }
2856
3117
  ).trim();
2857
3118
 
2858
3119
  if (!log) {
@@ -3011,40 +3272,67 @@ async function cmdContext(opts) {
3011
3272
 
3012
3273
  const target = opts.rest[0];
3013
3274
  const home = os.homedir();
3275
+ const cfgPath = path.join(home, ".mover", "config.json");
3276
+ const agents = fs.existsSync(cfgPath)
3277
+ ? (JSON.parse(fs.readFileSync(cfgPath, "utf8")).agents || [])
3278
+ : [];
3014
3279
 
3015
3280
  if (!target) {
3016
- // Show all agents
3017
- barLn(bold(" Agent Context Overview"));
3281
+ // Overview: show what each agent actually loads
3282
+ barLn(bold(" What Your Agents See"));
3018
3283
  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
3284
 
3024
3285
  for (const agentId of agents) {
3025
3286
  const reg = AGENT_REGISTRY[agentId];
3026
3287
  if (!reg) continue;
3027
- let totalBytes = 0, fileCount = 0;
3028
3288
 
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));
3289
+ const parts = [];
3290
+ let totalBytes = 0;
3291
+
3292
+ // Rules
3293
+ if (reg.rules) {
3294
+ const rp = reg.rules.dest(vault);
3295
+ if (fs.existsSync(rp)) {
3296
+ const sz = fs.statSync(rp).size;
3297
+ totalBytes += sz;
3298
+ parts.push(`rules ${dim(`${(sz / 1024).toFixed(0)}KB`)}`);
3299
+ } else { parts.push(red("rules missing")); }
3300
+ }
3301
+
3302
+ // Skills
3303
+ if (reg.skills) {
3304
+ const sp = reg.skills.dest(vault);
3305
+ if (fs.existsSync(sp)) {
3306
+ const skills = fs.readdirSync(sp, { withFileTypes: true }).filter((d) => d.isDirectory());
3307
+ let skillBytes = 0;
3308
+ for (const sk of skills) {
3309
+ const sm = path.join(sp, sk.name, "SKILL.md");
3310
+ if (fs.existsSync(sm)) skillBytes += fs.statSync(sm).size;
3035
3311
  }
3036
- } catch {}
3037
- };
3312
+ totalBytes += skillBytes;
3313
+ parts.push(`${skills.length} skills ${dim(`${(skillBytes / 1024).toFixed(0)}KB`)}`);
3314
+ }
3315
+ }
3316
+
3317
+ // Commands
3318
+ if (reg.commands) {
3319
+ const cp = reg.commands.dest(vault);
3320
+ if (fs.existsSync(cp)) {
3321
+ const cmds = fs.readdirSync(cp).filter((f) => f.endsWith(".md") || f.endsWith(".toml") || f.endsWith(".json"));
3322
+ if (cmds.length > 0) parts.push(`${cmds.length} commands`);
3323
+ }
3324
+ }
3038
3325
 
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));
3326
+ // Hooks
3327
+ if (reg.hooks) parts.push(`${reg.hooks.events?.length || "?"} hook events`);
3042
3328
 
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`);
3329
+ const tokens = Math.round(totalBytes / 4);
3330
+ const tokenWarn = tokens > 40000 ? red("heavy") : tokens > 20000 ? yellow("moderate") : green("lean");
3331
+ barLn(` ${bold(reg.name.padEnd(18))} ${parts.join(dim(" · "))} ${dim("~")}${tokens.toLocaleString()} tok ${tokenWarn}`);
3046
3332
  }
3047
3333
  barLn();
3334
+ barLn(dim(" Detail: moveros context <agent>"));
3335
+ barLn();
3048
3336
  return;
3049
3337
  }
3050
3338
 
@@ -3052,43 +3340,249 @@ async function cmdContext(opts) {
3052
3340
  const reg = AGENT_REGISTRY[target] || Object.values(AGENT_REGISTRY).find((r) => r.name.toLowerCase().includes(target.toLowerCase()));
3053
3341
  if (!reg) { barLn(yellow(` Unknown agent: ${target}`)); return; }
3054
3342
 
3055
- barLn(bold(` ${reg.name} Context`));
3343
+ barLn(bold(` ${reg.name}`));
3344
+ barLn(dim(` Tier: ${reg.tier || "unknown"}`));
3056
3345
  barLn();
3057
3346
 
3347
+ // Rules — show actual file path and key sections
3058
3348
  if (reg.rules) {
3059
3349
  const rp = reg.rules.dest(vault);
3060
3350
  if (fs.existsSync(rp)) {
3061
- const size = fs.statSync(rp).size;
3062
- statusLine("ok", "Rules", `${(size / 1024).toFixed(1)} KB ${rp}`);
3351
+ const content = fs.readFileSync(rp, "utf8");
3352
+ const sz = Buffer.byteLength(content);
3353
+ statusLine("ok", "Rules", `${(sz / 1024).toFixed(1)} KB`);
3354
+ barLn(dim(` ${rp}`));
3355
+ // Show top-level headings as table of contents
3356
+ const headings = content.match(/^## .+$/gm) || [];
3357
+ if (headings.length > 0) {
3358
+ barLn(dim(" Sections:"));
3359
+ for (const h of headings.slice(0, 12)) barLn(dim(` ${h.replace(/^## /, "")}`));
3360
+ if (headings.length > 12) barLn(dim(` ...+${headings.length - 12} more`));
3361
+ }
3063
3362
  } else {
3064
- statusLine("fail", "Rules", "not found");
3363
+ statusLine("fail", "Rules", "not installed");
3065
3364
  }
3066
3365
  }
3067
3366
 
3367
+ // Skills — list actual skill names with trigger descriptions
3068
3368
  if (reg.skills) {
3069
3369
  const sp = reg.skills.dest(vault);
3370
+ barLn();
3070
3371
  if (fs.existsSync(sp)) {
3071
- const skills = fs.readdirSync(sp, { withFileTypes: true }).filter((d) => d.isDirectory());
3372
+ const skillDirs = fs.readdirSync(sp, { withFileTypes: true }).filter((d) => d.isDirectory());
3072
3373
  let totalChars = 0;
3073
- for (const sk of skills) {
3374
+ const skillList = [];
3375
+ for (const sk of skillDirs) {
3074
3376
  const sm = path.join(sp, sk.name, "SKILL.md");
3075
- if (fs.existsSync(sm)) totalChars += fs.statSync(sm).size;
3377
+ if (fs.existsSync(sm)) {
3378
+ const sc = fs.readFileSync(sm, "utf8");
3379
+ totalChars += sc.length;
3380
+ // Extract first line of description
3381
+ const descMatch = sc.match(/^description:\s*["']?(.+?)["']?\s*$/m);
3382
+ const desc = descMatch ? descMatch[1].slice(0, 55) : "";
3383
+ skillList.push({ name: sk.name, desc });
3384
+ }
3385
+ }
3386
+ statusLine("ok", "Skills", `${skillList.length} packs (${(totalChars / 1024).toFixed(0)} KB descriptions)`);
3387
+ for (const s of skillList) {
3388
+ barLn(` ${s.name.padEnd(25)} ${dim(s.desc)}`);
3076
3389
  }
3077
- statusLine("ok", "Skills", `${skills.length} packs ${(totalChars / 1024).toFixed(1)} KB descriptions`);
3078
3390
  } else {
3079
3391
  statusLine("info", "Skills", "none installed");
3080
3392
  }
3081
3393
  }
3082
3394
 
3395
+ // Commands — list actual command names
3083
3396
  if (reg.commands) {
3084
3397
  const cp = reg.commands.dest(vault);
3398
+ barLn();
3085
3399
  if (fs.existsSync(cp)) {
3086
3400
  const files = fs.readdirSync(cp).filter((f) => !f.startsWith("."));
3087
- statusLine("ok", "Commands", `${files.length} files`);
3401
+ statusLine("ok", "Commands", `${files.length} loaded`);
3402
+ const cols = 4;
3403
+ for (let i = 0; i < files.length; i += cols) {
3404
+ const row = files.slice(i, i + cols).map((f) => f.replace(/\.(md|toml|json)$/, "").padEnd(18)).join(" ");
3405
+ barLn(` ${dim(row)}`);
3406
+ }
3088
3407
  } else {
3089
3408
  statusLine("info", "Commands", "none installed");
3090
3409
  }
3091
3410
  }
3411
+
3412
+ // Hooks
3413
+ if (reg.hooks) {
3414
+ barLn();
3415
+ statusLine("ok", "Hooks", `${reg.hooks.events?.length || 0} events`);
3416
+ if (reg.hooks.events) barLn(dim(` ${reg.hooks.events.join(", ")}`));
3417
+ }
3418
+
3419
+ // Token budget summary
3420
+ barLn();
3421
+ let total = 0;
3422
+ if (reg.rules) { const rp = reg.rules.dest(vault); if (fs.existsSync(rp)) total += fs.statSync(rp).size; }
3423
+ if (reg.skills) {
3424
+ const sp = reg.skills.dest(vault);
3425
+ if (fs.existsSync(sp)) {
3426
+ for (const sk of fs.readdirSync(sp, { withFileTypes: true }).filter((d) => d.isDirectory())) {
3427
+ const sm = path.join(sp, sk.name, "SKILL.md");
3428
+ if (fs.existsSync(sm)) total += fs.statSync(sm).size;
3429
+ }
3430
+ }
3431
+ }
3432
+ const tokens = Math.round(total / 4);
3433
+ const pct = Math.round((tokens / 200000) * 100);
3434
+ barLn(` ${dim("Total context load:")} ~${tokens.toLocaleString()} tokens ${dim(`(${pct}% of 200K window)`)}`);
3435
+ if (pct > 20) barLn(yellow(` Warning: heavy context load may slow agent startup`));
3436
+ barLn();
3437
+ }
3438
+
3439
+ // ─── moveros prayer ─────────────────────────────────────────────────────────
3440
+ async function cmdPrayer(opts) {
3441
+ const home = os.homedir();
3442
+ const moverDir = path.join(home, ".mover");
3443
+ const cfgPath = path.join(moverDir, "config.json");
3444
+ const ttPath = path.join(moverDir, "prayer-timetable.json");
3445
+
3446
+ if (!fs.existsSync(moverDir)) fs.mkdirSync(moverDir, { recursive: true, mode: 0o700 });
3447
+
3448
+ // Show current state
3449
+ barLn(bold(" Prayer Times"));
3450
+ barLn();
3451
+
3452
+ let cfg = {};
3453
+ if (fs.existsSync(cfgPath)) {
3454
+ try { cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8")); } catch {}
3455
+ }
3456
+
3457
+ const enabled = cfg.settings?.show_prayer_times;
3458
+ let tt = null;
3459
+ if (fs.existsSync(ttPath)) {
3460
+ try { tt = JSON.parse(fs.readFileSync(ttPath, "utf8")); } catch {}
3461
+ }
3462
+
3463
+ if (tt && Object.keys(tt.times || {}).length > 0) {
3464
+ statusLine("ok", "Source", `${tt.mosque || "Custom"} (${tt.type || "unknown"})`);
3465
+ statusLine("ok", "Days", `${Object.keys(tt.times).length} entries`);
3466
+
3467
+ // Show today's times
3468
+ const now = new Date();
3469
+ const todayKey = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,"0")}-${String(now.getDate()).padStart(2,"0")}`;
3470
+ const today = tt.times[todayKey];
3471
+ if (today) {
3472
+ const nowMins = now.getHours() * 60 + now.getMinutes();
3473
+ barLn();
3474
+ barLn(dim(" Today:"));
3475
+ for (const [name, time] of Object.entries(today)) {
3476
+ const [h, m] = time.split(":").map(Number);
3477
+ const pMins = h * 60 + m;
3478
+ const isPast = pMins < nowMins;
3479
+ const isNext = !isPast && pMins - nowMins < 60;
3480
+ const icon = isPast ? dim("\u2713") : isNext ? yellow("\u25B8") : dim("\u25CB");
3481
+ const fmt = isPast ? dim : isNext ? yellow : (s) => s;
3482
+ barLn(` ${icon} ${fmt(name.padEnd(10) + " " + time)}`);
3483
+ }
3484
+ } else {
3485
+ barLn(yellow(` No times for ${todayKey}. Timetable may need updating.`));
3486
+ }
3487
+ } else {
3488
+ statusLine("warn", "Timetable", "not set up");
3489
+ }
3490
+
3491
+ statusLine(enabled ? "ok" : "warn", "Status", enabled ? "enabled" : "disabled");
3492
+ barLn();
3493
+
3494
+ // Menu
3495
+ const items = [
3496
+ { id: "fetch", name: "Fetch calculated times by city", tier: "Adhan times from aladhan.com" },
3497
+ { id: "paste", name: "Paste mosque timetable", tier: "Jama'ah times — most accurate" },
3498
+ { id: "toggle", name: enabled ? "Disable prayer times" : "Enable prayer times", tier: "Toggle visibility in status line" },
3499
+ { id: "back", name: "Back", tier: "" },
3500
+ ];
3501
+
3502
+ question("What would you like to do?");
3503
+ barLn();
3504
+ const choice = await interactiveSelect(items, { multi: false });
3505
+
3506
+ if (choice === "fetch") {
3507
+ barLn();
3508
+ const city = await textInput({ label: "City", placeholder: "London" });
3509
+ const country = await textInput({ label: "Country", placeholder: "United Kingdom" });
3510
+ barLn();
3511
+
3512
+ if (city && country) {
3513
+ const sp = spinner("Fetching prayer times (12 months)");
3514
+ const result = await fetchPrayerTimes(city.trim(), country.trim());
3515
+ if (result && Object.keys(result.times).length > 0) {
3516
+ fs.writeFileSync(ttPath, JSON.stringify(result, null, 2), "utf8");
3517
+ sp.stop(`Saved ${Object.keys(result.times).length} days`);
3518
+ barLn(dim(" These are calculated adhan times, not mosque jama'ah times."));
3519
+ barLn(dim(" For your mosque's specific times, choose 'Paste mosque timetable'."));
3520
+ } else {
3521
+ sp.stop(yellow("Could not fetch. Check city/country spelling."));
3522
+ }
3523
+ }
3524
+
3525
+ // Auto-enable
3526
+ if (!cfg.settings) cfg.settings = {};
3527
+ cfg.settings.show_prayer_times = true;
3528
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
3529
+ } else if (choice === "paste") {
3530
+ barLn();
3531
+ barLn(dim(" Paste your mosque's timetable below."));
3532
+ barLn(dim(" Format examples:"));
3533
+ barLn(dim(" 2026-03-08 05:20 13:00 16:15 18:01 19:45"));
3534
+ barLn(dim(" March 8: Fajr 05:20, Dhuhr 13:00, Asr 16:15, Maghrib 18:01, Isha 19:45"));
3535
+ barLn(dim(" Type 'done' on a new line when finished."));
3536
+ barLn();
3537
+
3538
+ const lines = [];
3539
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3540
+ await new Promise((resolve) => {
3541
+ const ask = () => {
3542
+ rl.question(`${BAR_COLOR}\u2502${S.reset} `, (line) => {
3543
+ if (line.trim().toLowerCase() === "done" || line.trim() === "") {
3544
+ rl.close();
3545
+ resolve();
3546
+ return;
3547
+ }
3548
+ lines.push(line);
3549
+ ask();
3550
+ });
3551
+ };
3552
+ ask();
3553
+ });
3554
+
3555
+ if (lines.length > 0) {
3556
+ // Ask for mosque name
3557
+ const mosqueName = await textInput({ label: "Mosque name (optional)", placeholder: "My Local Mosque" });
3558
+
3559
+ const result = parsePrayerTimetable(lines);
3560
+ if (mosqueName) result.mosque = mosqueName;
3561
+ if (result && Object.keys(result.times).length > 0) {
3562
+ // Merge with existing timetable if present
3563
+ if (tt && tt.times) {
3564
+ result.times = { ...tt.times, ...result.times };
3565
+ }
3566
+ fs.writeFileSync(ttPath, JSON.stringify(result, null, 2), "utf8");
3567
+ barLn(`${green("\u2713")} Saved ${Object.keys(result.times).length} days`);
3568
+ } else {
3569
+ barLn(yellow(" Could not parse the timetable."));
3570
+ barLn(dim(" Tip: paste it into Claude and ask to convert to this JSON format:"));
3571
+ barLn(dim(` { "times": { "2026-03-08": { "fajr": "05:20", "dhuhr": "13:00", ... } } }`));
3572
+ }
3573
+ }
3574
+
3575
+ // Auto-enable
3576
+ if (!cfg.settings) cfg.settings = {};
3577
+ cfg.settings.show_prayer_times = true;
3578
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
3579
+ } else if (choice === "toggle") {
3580
+ if (!cfg.settings) cfg.settings = {};
3581
+ cfg.settings.show_prayer_times = !enabled;
3582
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
3583
+ barLn(`${green("\u2713")} Prayer times ${cfg.settings.show_prayer_times ? "enabled" : "disabled"}`);
3584
+ }
3585
+
3092
3586
  barLn();
3093
3587
  }
3094
3588
 
@@ -3302,6 +3796,310 @@ async function cmdRestore(opts) {
3302
3796
  barLn();
3303
3797
  }
3304
3798
 
3799
+ // ─── moveros help ──────────────────────────────────────────────────────────
3800
+ async function cmdHelp(opts) {
3801
+ // Interactive animated guide — paginated walkthrough of Mover OS
3802
+ const pages = [
3803
+ {
3804
+ title: "Welcome to Mover OS",
3805
+ body: [
3806
+ `${bold("The agentic operating system for Obsidian.")}`,
3807
+ "",
3808
+ "Mover OS turns your Obsidian vault into an AI-powered execution engine.",
3809
+ "It works across 16 AI coding agents — Claude Code, Cursor, Gemini,",
3810
+ "Copilot, Codex, and more. Same brain, every editor.",
3811
+ "",
3812
+ `${dim("How it works:")}`,
3813
+ ` ${cyan("1.")} Your ${bold("Engine")} stores who you are — identity, strategy, goals`,
3814
+ ` ${cyan("2.")} ${bold("Workflows")} run your day — plan, execute, log, analyse, repeat`,
3815
+ ` ${cyan("3.")} ${bold("Skills")} give your AI agents deep domain knowledge`,
3816
+ ` ${cyan("4.")} The system ${bold("learns")} from your behavior and adapts`,
3817
+ "",
3818
+ `${dim("This guide walks through everything. Use ← → to navigate.")}`,
3819
+ ],
3820
+ },
3821
+ {
3822
+ title: "The Engine — Your Brain",
3823
+ body: [
3824
+ `${dim("Location:")} 02_Areas/Engine/`,
3825
+ "",
3826
+ `${bold("Core files that define you:")}`,
3827
+ "",
3828
+ ` ${cyan("Identity_Prime.md")} Who you are — values, psychology, anti-identity`,
3829
+ ` ${cyan("Strategy.md")} What you're testing — current hypothesis`,
3830
+ ` ${cyan("Active_Context.md")} What's happening NOW — blockers, focus, state`,
3831
+ ` ${cyan("Goals.md")} Where you're going — 90d, 1yr, 10yr targets`,
3832
+ ` ${cyan("Mover_Dossier.md")} What you have — skills, capital, network`,
3833
+ ` ${cyan("Auto_Learnings.md")} What the AI notices — behavioral patterns`,
3834
+ "",
3835
+ `These files are ${bold("irreplaceable")}. The system never overwrites them.`,
3836
+ "Every AI session reads them. Every workflow updates them.",
3837
+ `Your Engine ${bold("evolves")} as you do.`,
3838
+ ],
3839
+ },
3840
+ {
3841
+ title: "Daily Rhythm — The Loop",
3842
+ body: [
3843
+ `${bold("The daily execution cycle:")}`,
3844
+ "",
3845
+ ` ${green("→")} ${bold("/morning")} Start your day — energy check, set focus`,
3846
+ ` ${green("→")} ${bold("[WORK]")} Build, ship, create`,
3847
+ ` ${green("→")} ${bold("/log")} Capture what happened — syncs plan + state`,
3848
+ ` ${green("→")} ${bold("/analyse-day")} Brutal daily audit — patterns + verdict`,
3849
+ ` ${green("→")} ${bold("/plan-tomorrow")} Generate tomorrow's battle plan`,
3850
+ "",
3851
+ `${dim("Weekly:")}`,
3852
+ ` ${green("→")} ${bold("/review-week")} Sunday deep review + strategy validation`,
3853
+ "",
3854
+ `${dim("The rhythm is the system.")} Miss a day, patterns detect it.`,
3855
+ "Miss three, /reboot triggers recovery protocol.",
3856
+ `Every workflow hands off to the next — ${bold("no dead ends")}.`,
3857
+ ],
3858
+ },
3859
+ {
3860
+ title: "Workflows — 23 Commands",
3861
+ body: [
3862
+ `${bold("Daily:")} /morning /log /analyse-day /plan-tomorrow /review-week`,
3863
+ `${bold("Build:")} /ignite /overview /refactor-plan /capture /debrief`,
3864
+ `${bold("Think:")} /debug-resistance /pivot-strategy /mover-ideas /screenshot`,
3865
+ `${bold("Grow:")} /harvest /history /reboot`,
3866
+ `${bold("Meta:")} /setup /update /walkthrough /migrate /mover-check /mover-report`,
3867
+ "",
3868
+ `${dim("Key concepts:")}`,
3869
+ ` ${cyan("•")} ${bold("/ignite")} creates projects — brief + plan in one workflow`,
3870
+ ` ${cyan("•")} ${bold("/debug-resistance")} diagnoses WHY you're avoiding something`,
3871
+ ` ${cyan("•")} ${bold("/pivot-strategy")} formally changes direction with logging`,
3872
+ ` ${cyan("•")} ${bold("/harvest")} extracts permanent knowledge to your Library`,
3873
+ ` ${cyan("•")} ${bold("/screenshot")} does meta-analysis of your AI session patterns`,
3874
+ "",
3875
+ "Every command reads your Engine context and adapts to your state.",
3876
+ ],
3877
+ },
3878
+ {
3879
+ title: "Skills — Domain Intelligence",
3880
+ body: [
3881
+ `${bold("Skills give your AI agents specialized knowledge.")}`,
3882
+ "",
3883
+ `${dim("Categories:")}`,
3884
+ ` ${cyan("dev")} Debugging, TDD, refactoring, error handling, React`,
3885
+ ` ${cyan("marketing")} Copywriting, SEO, CRO, social content, email`,
3886
+ ` ${cyan("cro")} Page optimization, forms, popups, paywalls, onboarding`,
3887
+ ` ${cyan("strategy")} Pricing, launch, competitors, referrals, analytics`,
3888
+ ` ${cyan("seo")} Audits, schema markup, programmatic SEO, content`,
3889
+ ` ${cyan("design")} UI/UX, frontend design, Obsidian markdown/canvas`,
3890
+ ` ${cyan("obsidian")} JSON Canvas, Bases, Obsidian CLI, markdown`,
3891
+ "",
3892
+ `${dim("System skills (always active):")}`,
3893
+ ` friction-enforcer pattern-detector plan-md-guardian`,
3894
+ ` mover-os-context workflow-router daily-note-writer`,
3895
+ "",
3896
+ "Skills trigger automatically based on what you're doing.",
3897
+ ],
3898
+ },
3899
+ {
3900
+ title: "The Friction System",
3901
+ body: [
3902
+ `${bold("Your AI pushes back when you drift from your plan.")}`,
3903
+ "",
3904
+ ` ${dim("Level 1")} ${cyan("Surface")} \"Your plan says X. You're working on Y.\"`,
3905
+ ` ${dim("Level 2")} ${yellow("Justify")} \"Why is this more important than your Single Test?\"`,
3906
+ ` ${dim("Level 3")} ${red("Earn It")} AI stops helping with off-plan work.`,
3907
+ ` ${dim("Level 4")} ${red("Hard Block")} Destructive actions require explicit reason.`,
3908
+ "",
3909
+ `You can always push through Levels 1-3. Friction creates`,
3910
+ `${bold("awareness")}, not walls. But the AI won't silently comply`,
3911
+ "when you're avoiding the hard thing.",
3912
+ "",
3913
+ `${dim("Pre-Escalation Gate:")} If the work is exploration or compound`,
3914
+ `value (not avoidance), it logs as ${dim("[COMPOUND]")} and doesn't escalate.`,
3915
+ ],
3916
+ },
3917
+ {
3918
+ title: "Pattern Detection",
3919
+ body: [
3920
+ `${bold("The system watches your behavior across sessions.")}`,
3921
+ "",
3922
+ `${dim("How it works:")}`,
3923
+ ` ${cyan("1.")} AI observes recurring behavior (3+ data points)`,
3924
+ ` ${cyan("2.")} Logs to Auto_Learnings.md with confidence score (1-5)`,
3925
+ ` ${cyan("3.")} Patterns surface proactively in workflows`,
3926
+ ` ${cyan("4.")} You confirm or dismiss — the system adapts`,
3927
+ "",
3928
+ `${dim("Pattern types:")}`,
3929
+ ` ${yellow("Avoidance")} Dodging specific tasks repeatedly`,
3930
+ ` ${yellow("Time Drift")} Working past shutdown, skipping rest`,
3931
+ ` ${yellow("System Spree")} Building tools instead of shipping`,
3932
+ ` ${yellow("Scope Creep")} Tasks growing beyond plan boundaries`,
3933
+ ` ${yellow("Energy Cycles")} Performance tied to sleep/food/time`,
3934
+ "",
3935
+ `Confidence 3+ patterns route to the workflow that fixes them.`,
3936
+ ],
3937
+ },
3938
+ {
3939
+ title: "16 AI Agents — One Brain",
3940
+ body: [
3941
+ `${bold("Mover OS installs to every major AI coding agent.")}`,
3942
+ "",
3943
+ `${dim("Full tier (rules + skills + commands + hooks):")}`,
3944
+ ` Claude Code Cursor Cline Windsurf Gemini CLI`,
3945
+ ` Copilot Amazon Q OpenCode Kilo Code`,
3946
+ "",
3947
+ `${dim("Enhanced tier (rules + skills):")}`,
3948
+ ` Codex Amp Roo Code Antigravity`,
3949
+ "",
3950
+ `${dim("Basic tier (rules only):")}`,
3951
+ ` Continue Aider`,
3952
+ "",
3953
+ "Switch editors freely. Your identity, strategy, and patterns",
3954
+ `follow you everywhere. The ${bold("Engine is the constant")}.`,
3955
+ `Run ${cyan("moveros sync")} to update all agents at once.`,
3956
+ ],
3957
+ },
3958
+ {
3959
+ title: "CLI Commands",
3960
+ body: [
3961
+ `${bold("Terminal utilities — no AI session needed.")}`,
3962
+ "",
3963
+ ` ${cyan("moveros pulse")} Dashboard — energy, tasks, streaks, blockers`,
3964
+ ` ${cyan("moveros doctor")} Health check across all installed agents`,
3965
+ ` ${cyan("moveros capture")} Quick capture — tasks, links, ideas from terminal`,
3966
+ ` ${cyan("moveros who")} Entity memory lookup — people, orgs, places`,
3967
+ ` ${cyan("moveros sync")} Update all agents to latest rules/skills`,
3968
+ ` ${cyan("moveros context")} See what each agent loads — rules, skills, tokens`,
3969
+ ` ${cyan("moveros diff")} Engine file evolution via git history`,
3970
+ ` ${cyan("moveros replay")} Session replay from Daily Notes`,
3971
+ ` ${cyan("moveros settings")} View/edit config — ${dim("settings set <key> <val>")}`,
3972
+ ` ${cyan("moveros backup")} Manual backup wizard (engine, areas, agents)`,
3973
+ ` ${cyan("moveros restore")} Restore from backup`,
3974
+ ` ${cyan("moveros warm")} Pre-warm an AI session with context`,
3975
+ ],
3976
+ },
3977
+ {
3978
+ title: "The Status Line",
3979
+ body: [
3980
+ `${bold("Your life, glanceable. Always visible in Claude Code.")}`,
3981
+ "",
3982
+ ` ${dim("Line 1:")} Model · Context% · Project(branch) · Lines · Time · Cost`,
3983
+ ` ${dim("Line 2:")} Next task · Progress · Vitality · Prayer · Last log`,
3984
+ ` ${dim("Line 3:")} Rate limits (5hr + 7day + extra usage)`,
3985
+ "",
3986
+ `${dim("Features:")}`,
3987
+ ` ${cyan("•")} Next unchecked task from today's Daily Note`,
3988
+ ` ${cyan("•")} Strategic task count (vitality excluded)`,
3989
+ ` ${cyan("•")} Vitality slot machine — rotates reminders every minute`,
3990
+ ` ${cyan("•")} Next prayer time (if ${dim("show_prayer_times: true")} in settings)`,
3991
+ ` ${cyan("•")} Time since last /log — so you never forget`,
3992
+ ` ${cyan("•")} Rate limit bars with reset times`,
3993
+ "",
3994
+ `All data from your vault. ${bold("No API calls")} except rate limits.`,
3995
+ ],
3996
+ },
3997
+ {
3998
+ title: "Getting Started",
3999
+ body: [
4000
+ `${bold("You're ready.")}`,
4001
+ "",
4002
+ ` ${cyan("1.")} Run ${bold("/setup")} in any AI agent to build your Engine`,
4003
+ ` ${cyan("2.")} Run ${bold("/morning")} to start your first session`,
4004
+ ` ${cyan("3.")} Work. Build. Ship.`,
4005
+ ` ${cyan("4.")} Run ${bold("/log")} to capture what happened`,
4006
+ ` ${cyan("5.")} Run ${bold("/plan-tomorrow")} before bed`,
4007
+ "",
4008
+ `${dim("Quick tips:")}`,
4009
+ ` ${cyan("•")} ${bold("Single Test")} — one thing that makes the day a win`,
4010
+ ` ${cyan("•")} ${bold("Sacrifice")} — what you won't do today (as important as what you will)`,
4011
+ ` ${cyan("•")} The system gets smarter the more you use it`,
4012
+ ` ${cyan("•")} Trust the Engine files — they're your memory between sessions`,
4013
+ "",
4014
+ `${dim("moveros.dev")} ${dim("·")} ${dim("$49 one-time")} ${dim("·")} ${dim("updates included")}`,
4015
+ ],
4016
+ },
4017
+ ];
4018
+
4019
+ // Paginated display with keyboard navigation
4020
+ let page = 0;
4021
+
4022
+ function renderPage() {
4023
+ const p = pages[page];
4024
+ // Clear screen area
4025
+ w("\x1b[2J\x1b[H"); // clear screen, cursor to top
4026
+ ln();
4027
+
4028
+ // Page indicator
4029
+ const dots = pages.map((_, i) => i === page ? cyan("●") : dim("○")).join(" ");
4030
+ ln(` ${dots}`);
4031
+ ln();
4032
+
4033
+ // Title with box
4034
+ ln(` ${S.cyan}┌${"─".repeat(56)}┐${S.reset}`);
4035
+ const titlePad = Math.max(0, 54 - strip(p.title).length);
4036
+ ln(` ${S.cyan}│${S.reset} ${bold(p.title)}${" ".repeat(titlePad)} ${S.cyan}│${S.reset}`);
4037
+ ln(` ${S.cyan}└${"─".repeat(56)}┘${S.reset}`);
4038
+ ln();
4039
+
4040
+ // Body
4041
+ for (const line of p.body) {
4042
+ ln(` ${line}`);
4043
+ }
4044
+ ln();
4045
+ ln();
4046
+
4047
+ // Navigation
4048
+ const nav = [];
4049
+ if (page > 0) nav.push(`${dim("←")} prev`);
4050
+ if (page < pages.length - 1) nav.push(`${dim("→")} next`);
4051
+ nav.push(`${dim("q")} quit`);
4052
+ ln(` ${dim(`${page + 1}/${pages.length}`)} ${nav.join(dim(" · "))}`);
4053
+ }
4054
+
4055
+ return new Promise((resolve) => {
4056
+ if (!IS_TTY) {
4057
+ // Non-interactive: dump all pages
4058
+ for (const p of pages) {
4059
+ ln(bold(`\n## ${p.title}\n`));
4060
+ for (const line of p.body) ln(` ${line}`);
4061
+ }
4062
+ resolve();
4063
+ return;
4064
+ }
4065
+
4066
+ const { stdin } = process;
4067
+ stdin.setRawMode(true);
4068
+ stdin.resume();
4069
+ stdin.setEncoding("utf8");
4070
+ w(S.hide);
4071
+
4072
+ renderPage();
4073
+
4074
+ const handler = (data) => {
4075
+ if (data === "\x1b[C" || data === "l" || data === " ") {
4076
+ if (page < pages.length - 1) { page++; renderPage(); }
4077
+ } else if (data === "\x1b[D" || data === "h") {
4078
+ if (page > 0) { page--; renderPage(); }
4079
+ } else if (data === "q" || data === "\x1b" || data === "\x03") {
4080
+ stdin.removeListener("data", handler);
4081
+ stdin.setRawMode(false);
4082
+ stdin.pause();
4083
+ w(S.show);
4084
+ w("\x1b[2J\x1b[H"); // clear
4085
+ resolve();
4086
+ } else if (data === "\r" || data === "\n") {
4087
+ if (page < pages.length - 1) { page++; renderPage(); }
4088
+ else {
4089
+ stdin.removeListener("data", handler);
4090
+ stdin.setRawMode(false);
4091
+ stdin.pause();
4092
+ w(S.show);
4093
+ w("\x1b[2J\x1b[H");
4094
+ resolve();
4095
+ }
4096
+ }
4097
+ };
4098
+
4099
+ stdin.on("data", handler);
4100
+ });
4101
+ }
4102
+
3305
4103
  // ─── moveros test (dev only) ────────────────────────────────────────────────
3306
4104
  async function cmdTest(opts) { barLn(yellow("moveros test — not yet implemented.")); }
3307
4105
 
@@ -3312,11 +4110,12 @@ async function cmdMainMenu() {
3312
4110
  { header: "Dashboard", cmds: ["pulse", "replay", "diff"] },
3313
4111
  { header: "Agents", cmds: ["doctor", "sync", "context", "warm"] },
3314
4112
  { header: "Capture", cmds: ["capture", "who"] },
3315
- { header: "System", cmds: ["settings", "backup", "restore"] },
4113
+ { header: "System", cmds: ["settings", "prayer", "backup", "restore", "help"] },
3316
4114
  ];
3317
4115
 
3318
4116
  const menuItems = [];
3319
4117
  for (const cat of categories) {
4118
+ menuItems.push({ id: `_sep_${cat.header}`, name: `── ${cat.header}`, _separator: true });
3320
4119
  for (const cmd of cat.cmds) {
3321
4120
  const meta = CLI_COMMANDS[cmd];
3322
4121
  if (!meta || meta.hidden) continue;
@@ -3367,20 +4166,36 @@ async function main() {
3367
4166
  // ── Intro ──
3368
4167
  await printHeader();
3369
4168
 
3370
- // ── Route: no command → interactive menu ──
4169
+ // ── Route: no command → interactive menu (persistent loop) ──
4170
+ const lightCommands = ["pulse", "warm", "capture", "who", "diff", "sync", "replay", "context", "settings", "backup", "restore", "doctor", "prayer", "help", "test"];
4171
+
3371
4172
  if (!opts.command) {
3372
- opts.command = await cmdMainMenu();
4173
+ // Interactive loop — stay open like a real app
4174
+ while (true) {
4175
+ opts.command = await cmdMainMenu();
4176
+ if (!opts.command) break; // user cancelled
4177
+
4178
+ if (lightCommands.includes(opts.command)) {
4179
+ const handler = CLI_HANDLERS[opts.command];
4180
+ if (handler) await handler(opts);
4181
+ else barLn(yellow(`Command '${opts.command}' is not yet implemented.`));
4182
+ opts.command = ""; // reset for next loop
4183
+ barLn(dim(" Press enter to return to menu..."));
4184
+ await new Promise((r) => { process.stdin.resume(); process.stdin.once("data", () => { process.stdin.pause(); r(); }); });
4185
+ continue;
4186
+ }
4187
+ break; // install/update break out of loop into pre-flight
4188
+ }
4189
+ if (!opts.command) return;
3373
4190
  }
3374
4191
 
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"];
4192
+ // ── Route: direct CLI command (non-interactive) ──
3377
4193
  if (lightCommands.includes(opts.command)) {
3378
4194
  const handler = CLI_HANDLERS[opts.command];
3379
4195
  if (handler) {
3380
4196
  await handler(opts);
3381
4197
  } else {
3382
4198
  barLn(yellow(`Command '${opts.command}' is not yet implemented.`));
3383
- barLn(dim("Coming soon in a future update."));
3384
4199
  }
3385
4200
  return;
3386
4201
  }
@@ -4031,6 +4846,101 @@ async function main() {
4031
4846
  installStatusLine = slChoice === "yes";
4032
4847
  }
4033
4848
 
4849
+ // ── Prayer times (optional) ──
4850
+ let prayerSetup = false;
4851
+ {
4852
+ barLn();
4853
+ question("Would you like prayer time reminders?");
4854
+ barLn(dim(" Shows next prayer time in the status line and Daily Notes."));
4855
+ barLn(dim(" Designed for Muslims — skip if not relevant to you."));
4856
+ barLn();
4857
+
4858
+ const ptChoice = await interactiveSelect(
4859
+ [
4860
+ { id: "yes", name: "Yes, set up prayer times", tier: "You can paste your mosque's timetable or fetch by city" },
4861
+ { id: "no", name: "No, skip", tier: "You can enable later with: moveros prayer" },
4862
+ ],
4863
+ { multi: false, defaultIndex: 1 }
4864
+ );
4865
+ if (ptChoice === "yes") {
4866
+ prayerSetup = true;
4867
+ barLn();
4868
+ question("How would you like to set up prayer times?");
4869
+ barLn();
4870
+
4871
+ const method = await interactiveSelect(
4872
+ [
4873
+ { id: "paste", name: "Paste my mosque's timetable", tier: "Jama'ah times — most accurate for your local mosque" },
4874
+ { id: "fetch", name: "Fetch calculated times by city", tier: "Adhan times from aladhan.com — good starting point" },
4875
+ { id: "later", name: "I'll set it up later", tier: "Run: moveros prayer" },
4876
+ ],
4877
+ { multi: false, defaultIndex: 0 }
4878
+ );
4879
+
4880
+ const moverDir = path.join(os.homedir(), ".mover");
4881
+ if (!fs.existsSync(moverDir)) fs.mkdirSync(moverDir, { recursive: true, mode: 0o700 });
4882
+
4883
+ if (method === "paste") {
4884
+ barLn();
4885
+ barLn(dim(" Paste your mosque's timetable below."));
4886
+ barLn(dim(" Format: one line per entry, any of these work:"));
4887
+ barLn(dim(" 2026-03-08 05:20 13:00 16:15 18:01 19:45"));
4888
+ barLn(dim(" March 8: Fajr 05:20, Dhuhr 13:00, Asr 16:15, Maghrib 18:01, Isha 19:45"));
4889
+ barLn(dim(" Or paste a whole table — the system will parse it."));
4890
+ barLn(dim(" When done, type 'done' on a new line and press Enter."));
4891
+ barLn();
4892
+
4893
+ const lines = [];
4894
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
4895
+ await new Promise((resolve) => {
4896
+ const ask = () => {
4897
+ rl.question(`${BAR_COLOR}\u2502${S.reset} `, (line) => {
4898
+ if (line.trim().toLowerCase() === "done" || line.trim() === "") {
4899
+ rl.close();
4900
+ resolve();
4901
+ return;
4902
+ }
4903
+ lines.push(line);
4904
+ ask();
4905
+ });
4906
+ };
4907
+ ask();
4908
+ });
4909
+
4910
+ if (lines.length > 0) {
4911
+ const tt = parsePrayerTimetable(lines);
4912
+ if (tt && Object.keys(tt.times).length > 0) {
4913
+ const ttPath = path.join(moverDir, "prayer-timetable.json");
4914
+ fs.writeFileSync(ttPath, JSON.stringify(tt, null, 2), "utf8");
4915
+ barLn(`${green("\u2713")} ${dim(`Saved ${Object.keys(tt.times).length} days to prayer-timetable.json`)}`);
4916
+ } else {
4917
+ barLn(yellow(" Could not parse timetable. Run moveros prayer later to try again."));
4918
+ }
4919
+ }
4920
+ } else if (method === "fetch") {
4921
+ barLn();
4922
+ const city = await textInput({ label: "City (e.g. London, Watford, Istanbul)", placeholder: "London" });
4923
+ const country = await textInput({ label: "Country", placeholder: "United Kingdom" });
4924
+ barLn();
4925
+
4926
+ if (city && country) {
4927
+ const sp = spinner("Fetching prayer times");
4928
+ const tt = await fetchPrayerTimes(city.trim(), country.trim());
4929
+ if (tt && Object.keys(tt.times).length > 0) {
4930
+ const ttPath = path.join(moverDir, "prayer-timetable.json");
4931
+ fs.writeFileSync(ttPath, JSON.stringify(tt, null, 2), "utf8");
4932
+ sp.stop(`Prayer times ${dim(`${Object.keys(tt.times).length} days from aladhan.com`)}`);
4933
+ barLn(dim(" Note: these are calculated adhan times, not mosque jama'ah times."));
4934
+ barLn(dim(" For your mosque's specific times, run: moveros prayer"));
4935
+ } else {
4936
+ sp.stop(yellow("Could not fetch. Run moveros prayer later."));
4937
+ }
4938
+ }
4939
+ }
4940
+ // method === "later" → just enable the setting, no timetable yet
4941
+ }
4942
+ }
4943
+
4034
4944
  // ── Install with animated spinners ──
4035
4945
  barLn();
4036
4946
  question(updateMode ? bold("Updating...") : bold("Installing..."));
@@ -4138,29 +5048,28 @@ async function main() {
4138
5048
  }
4139
5049
  }
4140
5050
 
4141
- // 7. .gitignore + git init (fresh install only)
5051
+ // 7. Git init Engine folder only (fresh install only)
4142
5052
  if (!updateMode) {
4143
5053
  const hasGit = cmdExists("git");
4144
5054
  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");
5055
+ const engineDir = path.join(vaultPath, "02_Areas", "Engine");
5056
+ const engineGit = path.join(engineDir, ".git");
5057
+ if (!fs.existsSync(engineGit) && fs.existsSync(engineDir)) {
5058
+ sp = spinner("Engine git repository");
4155
5059
  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" });
5060
+ // .gitignore inside Engine (excludes Dailies + Weekly Reviews)
5061
+ const gitignorePath = path.join(engineDir, ".gitignore");
5062
+ if (!fs.existsSync(gitignorePath)) {
5063
+ fs.writeFileSync(gitignorePath, generateGitignore(), "utf8");
5064
+ }
5065
+ execSync("git init", { cwd: engineDir, stdio: "ignore" });
5066
+ execSync("git add -A", { cwd: engineDir, stdio: "ignore" });
5067
+ execSync('git commit -m "Initial commit — Mover OS Engine v' + VERSION + '"', { cwd: engineDir, stdio: "ignore" });
4159
5068
  await sleep(300);
4160
- sp.stop("Git initialized");
5069
+ sp.stop(`Engine git initialized ${dim("02_Areas/Engine/")}`);
4161
5070
  totalSteps++;
4162
5071
  } catch {
4163
- sp.stop(dim("Git skipped"));
5072
+ sp.stop(dim("Engine git skipped"));
4164
5073
  }
4165
5074
  }
4166
5075
  }
@@ -4172,7 +5081,7 @@ async function main() {
4172
5081
  }
4173
5082
 
4174
5083
  // 9. Write ~/.mover/config.json (both fresh + update)
4175
- writeMoverConfig(vaultPath, selectedIds, key);
5084
+ writeMoverConfig(vaultPath, selectedIds, key, { prayerSetup });
4176
5085
 
4177
5086
  barLn();
4178
5087
 
@@ -4200,8 +5109,8 @@ async function main() {
4200
5109
  ln();
4201
5110
  ln(` ${bold("What was installed")}`);
4202
5111
  ln();
4203
- ln(` ${green("▸")} ${bold("22")} workflows ${dim("slash commands for daily rhythm, projects, strategy")}`);
4204
- ln(` ${green("▸")} ${bold("61")} skills ${dim("curated packs for dev, marketing, CRO, design")}`);
5112
+ ln(` ${green("▸")} ${bold("23")} workflows ${dim("slash commands for daily rhythm, projects, strategy")}`);
5113
+ ln(` ${green("▸")} ${bold("63")} skills ${dim("curated packs for dev, marketing, CRO, design")}`);
4205
5114
  if (selectedIds.includes("claude-code")) {
4206
5115
  ln(` ${green("▸")} ${bold("6")} hooks ${dim("lifecycle guards (engine protection, git safety)")}`);
4207
5116
  }
@@ -4241,6 +5150,17 @@ async function main() {
4241
5150
  ln();
4242
5151
  ln(` ${dim("/morning → [work] → /log → /analyse-day → /plan-tomorrow")}`);
4243
5152
  ln();
5153
+ ln(gray(" ─────────────────────────────────────────────"));
5154
+ ln();
5155
+ ln(` ${bold("CLI commands")} ${dim("(run from any terminal)")}`);
5156
+ ln();
5157
+ ln(` ${green("▸")} ${bold("moveros pulse")} ${dim("Dashboard — energy, tasks, streaks")}`);
5158
+ ln(` ${green("▸")} ${bold("moveros doctor")} ${dim("Health check across all agents")}`);
5159
+ ln(` ${green("▸")} ${bold("moveros capture")} ${dim("Quick inbox — tasks, links, ideas")}`);
5160
+ ln(` ${green("▸")} ${bold("moveros warm")} ${dim("Pre-warm your next AI session")}`);
5161
+ ln(` ${green("▸")} ${bold("moveros sync")} ${dim("Update all agents to latest")}`);
5162
+ ln(` ${green("▸")} ${bold("moveros")} ${dim("Full menu with all commands")}`);
5163
+ ln();
4244
5164
  }
4245
5165
 
4246
5166
  main().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mover-os",
3
- "version": "4.3.2",
3
+ "version": "4.4.0",
4
4
  "description": "The self-improving OS for AI agents. Turns Obsidian into an execution engine.",
5
5
  "bin": {
6
6
  "moveros": "install.js"