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.
- package/README.md +3 -3
- package/install.js +1000 -80
- 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
|
|
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
|
-
- **
|
|
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 **
|
|
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
|
|
62
|
-
const
|
|
63
|
-
|
|
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)) * (
|
|
69
|
-
return `${S.fg(
|
|
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
|
-
//
|
|
104
|
-
for (
|
|
105
|
-
ln(
|
|
106
|
-
|
|
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
|
-
|
|
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") {
|
|
348
|
-
else if (data === "\x1b[B") {
|
|
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 —
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
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
|
|
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
|
|
2521
|
-
statusLine(
|
|
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 -- "${
|
|
2855
|
-
{ cwd:
|
|
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
|
-
//
|
|
3017
|
-
barLn(bold("
|
|
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
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3040
|
-
if (reg.
|
|
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);
|
|
3044
|
-
const
|
|
3045
|
-
barLn(` ${reg.name.padEnd(
|
|
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}
|
|
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
|
|
3062
|
-
|
|
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
|
|
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
|
|
3372
|
+
const skillDirs = fs.readdirSync(sp, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
3072
3373
|
let totalChars = 0;
|
|
3073
|
-
|
|
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))
|
|
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}
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
sp = spinner("
|
|
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
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
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(
|
|
5069
|
+
sp.stop(`Engine git initialized ${dim("02_Areas/Engine/")}`);
|
|
4161
5070
|
totalSteps++;
|
|
4162
5071
|
} catch {
|
|
4163
|
-
sp.stop(dim("
|
|
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("
|
|
4204
|
-
ln(` ${green("▸")} ${bold("
|
|
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) => {
|