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