mover-os 4.3.0 → 4.3.2
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/install.js +1642 -167
- package/package.json +1 -1
package/install.js
CHANGED
|
@@ -12,6 +12,7 @@ const readline = require("readline");
|
|
|
12
12
|
const fs = require("fs");
|
|
13
13
|
const path = require("path");
|
|
14
14
|
const os = require("os");
|
|
15
|
+
const crypto = require("crypto");
|
|
15
16
|
const { execSync } = require("child_process");
|
|
16
17
|
|
|
17
18
|
const VERSION = "4";
|
|
@@ -94,10 +95,18 @@ const LOGO = [
|
|
|
94
95
|
" ╚═════╝ ╚══════╝",
|
|
95
96
|
];
|
|
96
97
|
|
|
97
|
-
|
|
98
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
99
|
+
|
|
100
|
+
async function printHeader(animate = IS_TTY) {
|
|
98
101
|
ln();
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
if (animate) {
|
|
103
|
+
// Animated logo reveal — line by line with gradient cascade
|
|
104
|
+
for (let i = 0; i < LOGO.length; i++) {
|
|
105
|
+
ln(gradient(LOGO[i]));
|
|
106
|
+
await sleep(30);
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
for (const line of LOGO) ln(gradient(line));
|
|
101
110
|
}
|
|
102
111
|
ln();
|
|
103
112
|
ln(` ${dim(`v${VERSION}`)} ${gray("the agentic operating system for obsidian")}`);
|
|
@@ -106,6 +115,60 @@ function printHeader() {
|
|
|
106
115
|
ln();
|
|
107
116
|
}
|
|
108
117
|
|
|
118
|
+
// ─── Enhanced TUI utilities ─────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
// Progress bar with label and percentage
|
|
121
|
+
function progressBar(current, total, width = 30, label = "") {
|
|
122
|
+
const pct = Math.min(1, current / total);
|
|
123
|
+
const filled = Math.round(pct * width);
|
|
124
|
+
const empty = width - filled;
|
|
125
|
+
const bar = `${S.green}${"█".repeat(filled)}${S.gray}${"░".repeat(empty)}${S.reset}`;
|
|
126
|
+
const pctStr = `${Math.round(pct * 100)}%`.padStart(4);
|
|
127
|
+
return `${bar} ${dim(pctStr)}${label ? ` ${dim(label)}` : ""}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Box frame for summaries
|
|
131
|
+
function box(lines, { title = "", color = S.cyan, width = 50 } = {}) {
|
|
132
|
+
const top = title
|
|
133
|
+
? `${color}╭─ ${S.bold}${title}${S.reset}${color} ${"─".repeat(Math.max(0, width - title.length - 4))}╮${S.reset}`
|
|
134
|
+
: `${color}╭${"─".repeat(width)}╮${S.reset}`;
|
|
135
|
+
ln(top);
|
|
136
|
+
for (const l of lines) {
|
|
137
|
+
const stripped = strip(l);
|
|
138
|
+
const pad = Math.max(0, width - 2 - stripped.length);
|
|
139
|
+
ln(`${color}│${S.reset} ${l}${" ".repeat(pad)} ${color}│${S.reset}`);
|
|
140
|
+
}
|
|
141
|
+
ln(`${color}╰${"─".repeat(width)}╯${S.reset}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Typewriter effect for important messages
|
|
145
|
+
async function typewriter(text, delay = 15) {
|
|
146
|
+
if (!IS_TTY) { ln(text); return; }
|
|
147
|
+
for (const ch of text) {
|
|
148
|
+
w(ch);
|
|
149
|
+
if (ch !== " ") await sleep(delay);
|
|
150
|
+
}
|
|
151
|
+
ln();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Section header with animated line
|
|
155
|
+
function sectionHeader(text) {
|
|
156
|
+
ln();
|
|
157
|
+
ln(`${S.cyan}┌─${S.reset} ${S.bold}${text}${S.reset}`);
|
|
158
|
+
ln(`${S.cyan}│${S.reset}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Section footer
|
|
162
|
+
function sectionEnd() {
|
|
163
|
+
ln(`${S.cyan}└${S.reset}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Status line with icon
|
|
167
|
+
function statusLine(icon, label, detail = "") {
|
|
168
|
+
const iconMap = { ok: `${S.green}✓${S.reset}`, warn: `${S.yellow}○${S.reset}`, fail: `${S.red}✗${S.reset}`, info: `${S.cyan}●${S.reset}`, dim: `${S.gray}·${S.reset}` };
|
|
169
|
+
barLn(`${iconMap[icon] || icon} ${label}${detail ? ` ${dim(detail)}` : ""}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
109
172
|
// ─── Clack-style frame ──────────────────────────────────────────────────────
|
|
110
173
|
const BAR_COLOR = S.cyan;
|
|
111
174
|
const bar = () => w(`${BAR_COLOR}│${S.reset}`);
|
|
@@ -473,27 +536,65 @@ async function downloadPayload(key) {
|
|
|
473
536
|
}
|
|
474
537
|
|
|
475
538
|
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
539
|
+
// ─── CLI Commands ────────────────────────────────────────────────────────────
|
|
540
|
+
const CLI_COMMANDS = {
|
|
541
|
+
install: { desc: "Full interactive install", alias: [] },
|
|
542
|
+
update: { desc: "Update all agents", alias: ["-u"] },
|
|
543
|
+
doctor: { desc: "Health check across all installed agents", alias: [] },
|
|
544
|
+
pulse: { desc: "Terminal dashboard — energy, tasks, streaks",alias: [] },
|
|
545
|
+
warm: { desc: "Pre-warm an AI session with context", alias: [] },
|
|
546
|
+
capture: { desc: "Quick capture — tasks, links, ideas", alias: [] },
|
|
547
|
+
who: { desc: "Entity memory lookup", alias: [] },
|
|
548
|
+
diff: { desc: "Engine file evolution viewer", alias: [] },
|
|
549
|
+
sync: { desc: "Cross-agent synchronization", alias: [] },
|
|
550
|
+
replay: { desc: "Session replay from Daily Notes", alias: [] },
|
|
551
|
+
context: { desc: "Debug what each agent sees", alias: [] },
|
|
552
|
+
settings: { desc: "View/edit config", alias: [] },
|
|
553
|
+
backup: { desc: "Manual backup wizard", alias: [] },
|
|
554
|
+
restore: { desc: "Restore from backup", alias: [] },
|
|
555
|
+
test: { desc: "Run integration tests (dev)", alias: [], hidden: true },
|
|
556
|
+
};
|
|
557
|
+
|
|
476
558
|
function parseArgs() {
|
|
477
559
|
const args = process.argv.slice(2);
|
|
478
|
-
const opts = { vault: "", key: "",
|
|
560
|
+
const opts = { command: "", vault: "", key: "", rest: [] };
|
|
561
|
+
|
|
479
562
|
for (let i = 0; i < args.length; i++) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
563
|
+
const a = args[i];
|
|
564
|
+
// Named flags
|
|
565
|
+
if (a === "--vault" && args[i + 1]) { opts.vault = args[++i]; continue; }
|
|
566
|
+
if (a === "--key" && args[i + 1]) { opts.key = args[++i]; continue; }
|
|
567
|
+
// Backward compat: --update / -u → command 'update'
|
|
568
|
+
if (a === "--update" || a === "-u") { opts.command = "update"; continue; }
|
|
569
|
+
if (a === "--help" || a === "-h") {
|
|
570
|
+
ln();
|
|
571
|
+
ln(` ${bold("moveros")} ${dim("— the Mover OS companion CLI")}`);
|
|
484
572
|
ln();
|
|
485
|
-
ln(` ${
|
|
573
|
+
ln(` ${dim("Usage")} moveros [command] [options]`);
|
|
486
574
|
ln();
|
|
487
|
-
ln(` ${dim("
|
|
575
|
+
ln(` ${dim("Commands")}`);
|
|
576
|
+
for (const [cmd, meta] of Object.entries(CLI_COMMANDS)) {
|
|
577
|
+
if (meta.hidden) continue;
|
|
578
|
+
ln(` ${cmd.padEnd(12)}${dim(meta.desc)}`);
|
|
579
|
+
}
|
|
488
580
|
ln();
|
|
489
581
|
ln(` ${dim("Options")}`);
|
|
490
582
|
ln(` --key KEY License key (skip interactive prompt)`);
|
|
491
583
|
ln(` --vault PATH Obsidian vault path (skip detection)`);
|
|
492
|
-
ln(` --update, -u Quick update (
|
|
584
|
+
ln(` --update, -u Quick update (backward compat)`);
|
|
585
|
+
ln();
|
|
586
|
+
ln(` ${dim("Run")} moveros ${dim("with no args for interactive menu")}`);
|
|
493
587
|
ln();
|
|
494
588
|
process.exit(0);
|
|
495
589
|
}
|
|
590
|
+
// Positional: first non-flag arg is the command
|
|
591
|
+
if (!opts.command && !a.startsWith("-") && CLI_COMMANDS[a]) {
|
|
592
|
+
opts.command = a;
|
|
593
|
+
} else {
|
|
594
|
+
opts.rest.push(a);
|
|
595
|
+
}
|
|
496
596
|
}
|
|
597
|
+
|
|
497
598
|
return opts;
|
|
498
599
|
}
|
|
499
600
|
|
|
@@ -914,74 +1015,225 @@ async function runUninstall(vaultPath) {
|
|
|
914
1015
|
}
|
|
915
1016
|
|
|
916
1017
|
// ─── Agent definitions ──────────────────────────────────────────────────────
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1018
|
+
// ─── Agent Registry ─────────────────────────────────────────────────────────
|
|
1019
|
+
// Data-driven agent configuration. 14 user-selectable agents → 16 install targets.
|
|
1020
|
+
// Each entry defines: detection, rules destination, skills destination,
|
|
1021
|
+
// commands destination + format, hooks config.
|
|
1022
|
+
//
|
|
1023
|
+
// Selection → targets mapping:
|
|
1024
|
+
// "Gemini CLI + Antigravity" = 1 selection → 2 targets (gemini-cli, antigravity)
|
|
1025
|
+
// Everything else = 1 selection → 1 target
|
|
1026
|
+
//
|
|
1027
|
+
// Tiers:
|
|
1028
|
+
// Full = rules + skills + commands + hooks
|
|
1029
|
+
// Enhanced = rules + skills + commands (no hooks)
|
|
1030
|
+
// Basic+ = rules + commands (no skills)
|
|
1031
|
+
// Basic = rules only
|
|
1032
|
+
|
|
1033
|
+
const H = os.homedir();
|
|
1034
|
+
|
|
1035
|
+
const AGENT_REGISTRY = {
|
|
1036
|
+
// ── Full Tier ──────────────────────────────────────────────────────────────
|
|
1037
|
+
"claude-code": {
|
|
920
1038
|
name: "Claude Code",
|
|
921
|
-
tier: "
|
|
922
|
-
|
|
1039
|
+
tier: "full",
|
|
1040
|
+
tierDesc: "Rules, 23 commands, skills, 6 hooks",
|
|
1041
|
+
detect: () => cmdExists("claude") || fs.existsSync(path.join(H, ".claude")),
|
|
1042
|
+
rules: { type: "link", dest: () => path.join(H, ".claude", "CLAUDE.md"), header: "# Mover OS Global Rules" },
|
|
1043
|
+
skills: { dest: () => path.join(H, ".claude", "skills") },
|
|
1044
|
+
commands: { format: "md", dest: () => path.join(H, ".claude", "commands") },
|
|
1045
|
+
hooks: { type: "claude-settings" },
|
|
923
1046
|
},
|
|
924
|
-
{
|
|
925
|
-
id: "cursor",
|
|
1047
|
+
"cursor": {
|
|
926
1048
|
name: "Cursor",
|
|
927
|
-
tier: "
|
|
928
|
-
|
|
1049
|
+
tier: "full",
|
|
1050
|
+
tierDesc: "Rules (.mdc), commands, skills, hooks",
|
|
1051
|
+
detect: () => fs.existsSync(path.join(H, ".cursor")) || cmdExists("cursor"),
|
|
1052
|
+
rules: {
|
|
1053
|
+
type: "mdc",
|
|
1054
|
+
dest: (vault) => path.join(vault || ".", ".cursor", "rules", "mover-os.mdc"),
|
|
1055
|
+
globalDest: () => path.join(H, ".cursor", "rules", "mover-os.mdc"),
|
|
1056
|
+
header: "# Cursor Project Rules",
|
|
1057
|
+
},
|
|
1058
|
+
skills: { dest: () => path.join(H, ".cursor", "skills") },
|
|
1059
|
+
commands: { format: "md", dest: () => path.join(H, ".cursor", "commands") },
|
|
1060
|
+
hooks: { type: "cursor-hooks-json", dest: (vault) => path.join(vault || ".", ".cursor", "hooks.json") },
|
|
929
1061
|
},
|
|
930
|
-
{
|
|
931
|
-
id: "cline",
|
|
1062
|
+
"cline": {
|
|
932
1063
|
name: "Cline",
|
|
933
|
-
tier: "
|
|
934
|
-
|
|
1064
|
+
tier: "full",
|
|
1065
|
+
tierDesc: "Rules, skills, hooks",
|
|
1066
|
+
detect: () => globDirExists(path.join(H, ".vscode", "extensions"), "saoudrizwan.claude-dev-*"),
|
|
1067
|
+
rules: { type: "copy", dest: (vault) => path.join(vault || ".", ".clinerules", "mover-os.md") },
|
|
1068
|
+
skills: { dest: (vault) => path.join(vault || ".", ".cline", "skills") },
|
|
1069
|
+
commands: null,
|
|
1070
|
+
hooks: null,
|
|
935
1071
|
},
|
|
936
|
-
{
|
|
937
|
-
id: "windsurf",
|
|
1072
|
+
"windsurf": {
|
|
938
1073
|
name: "Windsurf",
|
|
939
|
-
tier: "
|
|
940
|
-
|
|
1074
|
+
tier: "full",
|
|
1075
|
+
tierDesc: "Rules, workflows, skills, hooks",
|
|
1076
|
+
detect: () => fs.existsSync(path.join(H, ".codeium")) || cmdExists("windsurf"),
|
|
1077
|
+
rules: {
|
|
1078
|
+
type: "windsurf-rule",
|
|
1079
|
+
dest: (vault) => path.join(vault || ".", ".windsurf", "rules", "mover-os.md"),
|
|
1080
|
+
header: "# Windsurf Rules",
|
|
1081
|
+
},
|
|
1082
|
+
skills: { dest: () => path.join(H, ".codeium", "windsurf", "skills") },
|
|
1083
|
+
commands: { format: "md", dest: (vault) => path.join(vault || ".", ".windsurf", "workflows") },
|
|
1084
|
+
hooks: { type: "windsurf-hooks-json", dest: (vault) => path.join(vault || ".", ".windsurf", "hooks.json") },
|
|
941
1085
|
},
|
|
942
|
-
{
|
|
943
|
-
id: "gemini-cli",
|
|
1086
|
+
"gemini-cli": {
|
|
944
1087
|
name: "Gemini CLI",
|
|
945
|
-
tier: "
|
|
946
|
-
|
|
1088
|
+
tier: "full",
|
|
1089
|
+
tierDesc: "Rules, .toml commands, skills, hooks",
|
|
1090
|
+
detect: () => cmdExists("gemini") || fs.existsSync(path.join(H, ".gemini", "settings.json")),
|
|
1091
|
+
sharedRulesFile: "gemini-md",
|
|
1092
|
+
rules: { type: "link", dest: () => path.join(H, ".gemini", "GEMINI.md"), header: "# Gemini CLI Rules" },
|
|
1093
|
+
skills: { dest: () => path.join(H, ".gemini", "skills") },
|
|
1094
|
+
commands: { format: "toml", dest: () => path.join(H, ".gemini", "commands") },
|
|
1095
|
+
hooks: { type: "gemini-settings-json", dest: () => path.join(H, ".gemini", "settings.json") },
|
|
947
1096
|
},
|
|
948
|
-
{
|
|
949
|
-
id: "copilot",
|
|
1097
|
+
"copilot": {
|
|
950
1098
|
name: "GitHub Copilot",
|
|
951
|
-
tier: "
|
|
1099
|
+
tier: "full",
|
|
1100
|
+
tierDesc: "Rules, .prompt.md commands, skills",
|
|
952
1101
|
detect: () => cmdExists("gh"),
|
|
1102
|
+
rules: { type: "copy", dest: (vault) => path.join(vault || ".", ".github", "copilot-instructions.md"), header: "# Copilot Instructions" },
|
|
1103
|
+
skills: { dest: (vault) => path.join(vault || ".", ".github", "skills") },
|
|
1104
|
+
commands: { format: "prompt-md", dest: (vault) => path.join(vault || ".", ".github", "prompts") },
|
|
1105
|
+
hooks: null,
|
|
953
1106
|
},
|
|
954
|
-
{
|
|
955
|
-
|
|
1107
|
+
"amazon-q": {
|
|
1108
|
+
name: "Amazon Q Developer",
|
|
1109
|
+
tier: "full",
|
|
1110
|
+
tierDesc: "Rules, JSON agent commands, hooks",
|
|
1111
|
+
detect: () => cmdExists("q") || fs.existsSync(path.join(H, ".aws", "amazonq")),
|
|
1112
|
+
rules: { type: "copy", dest: (vault) => path.join(vault || ".", ".amazonq", "rules", "mover-os.md") },
|
|
1113
|
+
skills: null,
|
|
1114
|
+
commands: { format: "amazon-q-json", dest: () => path.join(H, ".aws", "amazonq", "cli-agents") },
|
|
1115
|
+
hooks: null,
|
|
1116
|
+
},
|
|
1117
|
+
"opencode": {
|
|
1118
|
+
name: "OpenCode",
|
|
1119
|
+
tier: "full",
|
|
1120
|
+
tierDesc: "AGENTS.md, agents, commands",
|
|
1121
|
+
detect: () => cmdExists("opencode") || fs.existsSync(path.join(".", "opencode.json")),
|
|
1122
|
+
sharedRulesFile: "agents-md",
|
|
1123
|
+
rules: { type: "agents-md", dest: (vault) => path.join(vault || ".", "AGENTS.md") },
|
|
1124
|
+
skills: { dest: (vault) => path.join(vault || ".", ".opencode", "skills") },
|
|
1125
|
+
commands: { format: "opencode-json", dest: (vault) => path.join(vault || ".", "opencode.json") },
|
|
1126
|
+
hooks: null,
|
|
1127
|
+
},
|
|
1128
|
+
"kilo-code": {
|
|
1129
|
+
name: "Kilo Code",
|
|
1130
|
+
tier: "full",
|
|
1131
|
+
tierDesc: "Rules, skills, commands, modes",
|
|
1132
|
+
detect: () => globDirExists(path.join(H, ".vscode", "extensions"), "kilocode.kilo-code-*"),
|
|
1133
|
+
rules: { type: "copy", dest: (vault) => path.join(vault || ".", ".kilocode", "rules", "mover-os.md") },
|
|
1134
|
+
skills: { dest: (vault) => path.join(vault || ".", ".kilocode", "skills") },
|
|
1135
|
+
commands: { format: "md", dest: (vault) => path.join(vault || ".", ".kilocode", "commands") },
|
|
1136
|
+
hooks: null,
|
|
1137
|
+
},
|
|
1138
|
+
|
|
1139
|
+
// ── Enhanced Tier ──────────────────────────────────────────────────────────
|
|
1140
|
+
"codex": {
|
|
956
1141
|
name: "Codex",
|
|
957
|
-
tier: "
|
|
958
|
-
|
|
1142
|
+
tier: "enhanced",
|
|
1143
|
+
tierDesc: "AGENTS.md, skills (skills = commands)",
|
|
1144
|
+
detect: () => cmdExists("codex") || fs.existsSync(path.join(H, ".codex")),
|
|
1145
|
+
rules: { type: "agents-md", dest: () => path.join(H, ".codex", "AGENTS.md") },
|
|
1146
|
+
skills: { dest: () => path.join(H, ".codex", "skills") },
|
|
1147
|
+
commands: null,
|
|
1148
|
+
hooks: null,
|
|
959
1149
|
},
|
|
960
|
-
|
|
961
|
-
// Vault-root AGENTS.md provides basic rules for these agents.
|
|
962
|
-
{
|
|
963
|
-
id: "antigravity",
|
|
1150
|
+
"antigravity": {
|
|
964
1151
|
name: "Antigravity",
|
|
965
|
-
tier: "
|
|
966
|
-
|
|
1152
|
+
tier: "enhanced",
|
|
1153
|
+
tierDesc: "Rules (shared GEMINI.md), workflows, skills",
|
|
1154
|
+
detect: () => fs.existsSync(path.join(H, ".gemini", "antigravity")),
|
|
1155
|
+
sharedRulesFile: "gemini-md",
|
|
1156
|
+
rules: { type: "link", dest: () => path.join(H, ".gemini", "GEMINI.md"), header: "# Antigravity Rules" },
|
|
1157
|
+
skills: { dest: () => path.join(H, ".gemini", "antigravity", "skills") },
|
|
1158
|
+
commands: { format: "md", dest: () => path.join(H, ".gemini", "antigravity", "global_workflows") },
|
|
1159
|
+
hooks: null,
|
|
967
1160
|
},
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1161
|
+
"amp": {
|
|
1162
|
+
name: "Amp (Sourcegraph)",
|
|
1163
|
+
tier: "enhanced",
|
|
1164
|
+
tierDesc: "AGENTS.md, skills",
|
|
1165
|
+
detect: () => cmdExists("amp") || fs.existsSync(path.join(H, ".config", "agents")),
|
|
1166
|
+
sharedRulesFile: "agents-md",
|
|
1167
|
+
rules: { type: "agents-md", dest: (vault) => path.join(vault || ".", "AGENTS.md") },
|
|
1168
|
+
skills: { dest: (vault) => path.join(vault || ".", ".agents", "skills") },
|
|
1169
|
+
commands: null,
|
|
1170
|
+
hooks: null,
|
|
975
1171
|
},
|
|
976
|
-
{
|
|
977
|
-
id: "roo-code",
|
|
1172
|
+
"roo-code": {
|
|
978
1173
|
name: "Roo Code",
|
|
979
|
-
tier: "
|
|
980
|
-
|
|
981
|
-
detect: () => globDirExists(path.join(
|
|
1174
|
+
tier: "enhanced",
|
|
1175
|
+
tierDesc: "Rules, skills, commands",
|
|
1176
|
+
detect: () => globDirExists(path.join(H, ".vscode", "extensions"), "rooveterinaryinc.roo-cline-*"),
|
|
1177
|
+
rules: { type: "copy", dest: (vault) => path.join(vault || ".", ".roo", "rules", "mover-os.md") },
|
|
1178
|
+
skills: { dest: (vault) => path.join(vault || ".", ".roo", "skills") },
|
|
1179
|
+
commands: { format: "md", dest: (vault) => path.join(vault || ".", ".roo", "commands") },
|
|
1180
|
+
hooks: null,
|
|
982
1181
|
},
|
|
1182
|
+
|
|
1183
|
+
// ── Basic+ Tier ────────────────────────────────────────────────────────────
|
|
1184
|
+
"continue": {
|
|
1185
|
+
name: "Continue.dev",
|
|
1186
|
+
tier: "basic+",
|
|
1187
|
+
tierDesc: "Rules (.md), .prompt commands",
|
|
1188
|
+
detect: () => fs.existsSync(path.join(H, ".continue")) || fs.existsSync(path.join(".", ".continue")),
|
|
1189
|
+
rules: {
|
|
1190
|
+
type: "continue-rule",
|
|
1191
|
+
dest: (vault) => path.join(vault || ".", ".continue", "rules", "mover-os.md"),
|
|
1192
|
+
},
|
|
1193
|
+
skills: null,
|
|
1194
|
+
commands: { format: "continue-prompt", dest: (vault) => path.join(vault || ".", ".continue", "prompts") },
|
|
1195
|
+
hooks: null,
|
|
1196
|
+
},
|
|
1197
|
+
|
|
1198
|
+
// ── Basic Tier ─────────────────────────────────────────────────────────────
|
|
1199
|
+
"aider": {
|
|
1200
|
+
name: "Aider",
|
|
1201
|
+
tier: "basic",
|
|
1202
|
+
tierDesc: "CONVENTIONS.md (rules only)",
|
|
1203
|
+
detect: () => cmdExists("aider"),
|
|
1204
|
+
rules: { type: "conventions-md", dest: (vault) => path.join(vault || ".", "CONVENTIONS.md") },
|
|
1205
|
+
skills: null,
|
|
1206
|
+
commands: null,
|
|
1207
|
+
hooks: null,
|
|
1208
|
+
},
|
|
1209
|
+
};
|
|
1210
|
+
|
|
1211
|
+
// User-selectable agents (14 selections). Each maps to 1+ install targets.
|
|
1212
|
+
const AGENT_SELECTIONS = [
|
|
1213
|
+
{ id: "claude-code", targets: ["claude-code"], name: "Claude Code" },
|
|
1214
|
+
{ id: "cursor", targets: ["cursor"], name: "Cursor" },
|
|
1215
|
+
{ id: "cline", targets: ["cline"], name: "Cline" },
|
|
1216
|
+
{ id: "windsurf", targets: ["windsurf"], name: "Windsurf" },
|
|
1217
|
+
{ id: "gemini-cli", targets: ["gemini-cli", "antigravity"], name: "Gemini CLI + Antigravity" },
|
|
1218
|
+
{ id: "copilot", targets: ["copilot"], name: "GitHub Copilot" },
|
|
1219
|
+
{ id: "codex", targets: ["codex"], name: "Codex" },
|
|
1220
|
+
{ id: "amazon-q", targets: ["amazon-q"], name: "Amazon Q Developer" },
|
|
1221
|
+
{ id: "opencode", targets: ["opencode"], name: "OpenCode" },
|
|
1222
|
+
{ id: "kilo-code", targets: ["kilo-code"], name: "Kilo Code" },
|
|
1223
|
+
{ id: "amp", targets: ["amp"], name: "Amp (Sourcegraph)" },
|
|
1224
|
+
{ id: "roo-code", targets: ["roo-code"], name: "Roo Code" },
|
|
1225
|
+
{ id: "continue", targets: ["continue"], name: "Continue.dev" },
|
|
1226
|
+
{ id: "aider", targets: ["aider"], name: "Aider" },
|
|
983
1227
|
];
|
|
984
1228
|
|
|
1229
|
+
// Backward compat: AGENTS array used by existing detectChanges() and update flow
|
|
1230
|
+
const AGENTS = AGENT_SELECTIONS.map((s) => ({
|
|
1231
|
+
id: s.id,
|
|
1232
|
+
name: s.name,
|
|
1233
|
+
tier: AGENT_REGISTRY[s.targets[0]].tierDesc,
|
|
1234
|
+
detect: AGENT_REGISTRY[s.targets[0]].detect,
|
|
1235
|
+
}));
|
|
1236
|
+
|
|
985
1237
|
// ─── Utility functions ──────────────────────────────────────────────────────
|
|
986
1238
|
function cmdExists(cmd) {
|
|
987
1239
|
try {
|
|
@@ -1184,41 +1436,10 @@ Sunday: /review-week first
|
|
|
1184
1436
|
New project: /ignite → [WORK]
|
|
1185
1437
|
Stuck: /debug-resistance
|
|
1186
1438
|
\`\`\`
|
|
1187
|
-
`;
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
// ─── SOUL.md generator (OpenClaw) ───────────────────────────────────────────
|
|
1191
|
-
function generateSoulMd() {
|
|
1192
|
-
return `# SOUL.md — Mover OS
|
|
1193
1439
|
|
|
1194
|
-
|
|
1440
|
+
## CLI Utility
|
|
1195
1441
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
- Be genuinely helpful, not performatively helpful. If the answer is "don't build that," say it.
|
|
1199
|
-
- Have opinions. "It depends" is a cop-out. Pick a direction, defend it, change your mind if the user makes a better case.
|
|
1200
|
-
- Be resourceful before asking. Exhaust what you can figure out, then ask sharp questions — not broad ones.
|
|
1201
|
-
- Earn trust through competence, not compliance. Nobody respects a yes-man.
|
|
1202
|
-
|
|
1203
|
-
## The Vibe
|
|
1204
|
-
|
|
1205
|
-
- Talk like a co-founder who's been in the trenches, not a consultant billing by the hour.
|
|
1206
|
-
- One sharp sentence beats three careful ones. Say what you mean.
|
|
1207
|
-
- When you're wrong, don't grovel. Name it, fix it, move. Apologies waste both your time.
|
|
1208
|
-
- Match intensity to stakes. Casual for small tasks. Locked in for architecture. Blunt for bad ideas.
|
|
1209
|
-
|
|
1210
|
-
## Boundaries
|
|
1211
|
-
|
|
1212
|
-
- Never hedge to avoid being wrong. Pick a position.
|
|
1213
|
-
- Never pretend all approaches are equally valid when one is clearly better.
|
|
1214
|
-
- Never pad output to look thorough. Substance only.
|
|
1215
|
-
- Never be artificially enthusiastic about bad ideas.
|
|
1216
|
-
|
|
1217
|
-
## Continuity
|
|
1218
|
-
|
|
1219
|
-
- You start fresh each session but the files are your memory. Read them, trust them, build on them.
|
|
1220
|
-
- If something changed between sessions and you don't understand why, ask — don't assume.
|
|
1221
|
-
- The user corrects you to make you better. Take corrections seriously and encode them.
|
|
1442
|
+
\`moveros\` provides system-level terminal operations: pulse (dashboard), warm (pre-warm sessions), sync (update agents), doctor (health check), who (entity lookup), capture (quick inbox). Use native agent capabilities first — the CLI supplements, never replaces.
|
|
1222
1443
|
`;
|
|
1223
1444
|
}
|
|
1224
1445
|
|
|
@@ -1340,6 +1561,8 @@ function writeMoverConfig(vaultPath, agentIds, licenseKey) {
|
|
|
1340
1561
|
vaultPath: vaultPath,
|
|
1341
1562
|
agents: agentIds,
|
|
1342
1563
|
feedbackWebhook: "https://moveros.dev/api/feedback",
|
|
1564
|
+
track_food: true,
|
|
1565
|
+
track_sleep: true,
|
|
1343
1566
|
installedAt: new Date().toISOString(),
|
|
1344
1567
|
};
|
|
1345
1568
|
if (licenseKey) config.licenseKey = licenseKey;
|
|
@@ -1349,6 +1572,9 @@ function writeMoverConfig(vaultPath, agentIds, licenseKey) {
|
|
|
1349
1572
|
const existing = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
1350
1573
|
if (existing.installedAt) config.installedAt = existing.installedAt;
|
|
1351
1574
|
if (existing.licenseKey && !licenseKey) config.licenseKey = existing.licenseKey;
|
|
1575
|
+
if (existing.feedbackWebhook) config.feedbackWebhook = existing.feedbackWebhook;
|
|
1576
|
+
if (existing.track_food !== undefined) config.track_food = existing.track_food;
|
|
1577
|
+
if (existing.track_sleep !== undefined) config.track_sleep = existing.track_sleep;
|
|
1352
1578
|
config.updatedAt = new Date().toISOString();
|
|
1353
1579
|
} catch {}
|
|
1354
1580
|
}
|
|
@@ -1519,17 +1745,168 @@ function installRules(bundleDir, destPath, agentId) {
|
|
|
1519
1745
|
return true;
|
|
1520
1746
|
}
|
|
1521
1747
|
|
|
1748
|
+
// ─── Format Converters (workflow .md → agent-specific formats) ───────────────
|
|
1749
|
+
// Source of truth: src/workflows/*.md (Claude Code format with YAML frontmatter)
|
|
1750
|
+
// Each converter takes workflow metadata and produces the target format.
|
|
1751
|
+
|
|
1752
|
+
function parseWorkflowMd(filePath) {
|
|
1753
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
1754
|
+
const name = path.basename(filePath, ".md");
|
|
1755
|
+
let description = "";
|
|
1756
|
+
let body = content;
|
|
1757
|
+
|
|
1758
|
+
// Extract YAML frontmatter
|
|
1759
|
+
if (content.startsWith("---")) {
|
|
1760
|
+
const endIdx = content.indexOf("\n---", 3);
|
|
1761
|
+
if (endIdx > 0) {
|
|
1762
|
+
const front = content.substring(3, endIdx).trim();
|
|
1763
|
+
body = content.substring(endIdx + 4).trim();
|
|
1764
|
+
const descMatch = front.match(/description:\s*(.+)/);
|
|
1765
|
+
if (descMatch) description = descMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// Fallback: first non-empty, non-heading line
|
|
1770
|
+
if (!description) {
|
|
1771
|
+
const lines = body.split("\n");
|
|
1772
|
+
for (const l of lines) {
|
|
1773
|
+
const t = l.trim();
|
|
1774
|
+
if (t && !t.startsWith("#") && !t.startsWith(">") && !t.startsWith("---")) {
|
|
1775
|
+
description = t.substring(0, 120);
|
|
1776
|
+
break;
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
return { name, description, body, filePath };
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// Gemini CLI: .md → .toml
|
|
1785
|
+
function mdToToml(wf) {
|
|
1786
|
+
const desc = wf.description.replace(/"/g, '\\"');
|
|
1787
|
+
const prompt = wf.body.replace(/\\/g, "\\\\").replace(/"""/g, '\\"""');
|
|
1788
|
+
return `# Mover OS — /${wf.name}\ndescription = "${desc}"\n\nprompt = """\n${prompt}\n"""\n`;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// Copilot: .md → .prompt.md (YAML frontmatter with mode: agent)
|
|
1792
|
+
function mdToCopilotPrompt(wf) {
|
|
1793
|
+
return `---\nmode: agent\ndescription: "${wf.description.replace(/"/g, '\\"')}"\n---\n\n${wf.body}\n`;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
// Continue.dev: .md → .prompt (YAML frontmatter with invokable: true)
|
|
1797
|
+
function mdToContinuePrompt(wf) {
|
|
1798
|
+
return `---\nname: ${wf.name}\ndescription: "${wf.description.replace(/"/g, '\\"')}"\ninvokable: true\n---\n\n${wf.body}\n`;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// Cursor: .md → .mdc (MDC frontmatter format)
|
|
1802
|
+
function mdToMdc(wf) {
|
|
1803
|
+
return `---\ndescription: "${wf.description.replace(/"/g, '\\"')}"\nglobs:\nalwaysApply: false\n---\n\n${wf.body}\n`;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// Amazon Q: .md → .json (CLI agent definition)
|
|
1807
|
+
function mdToAmazonQAgent(wf) {
|
|
1808
|
+
return JSON.stringify({
|
|
1809
|
+
name: wf.name,
|
|
1810
|
+
description: wf.description,
|
|
1811
|
+
prompt: wf.body,
|
|
1812
|
+
model: "claude-sonnet-4-20250514",
|
|
1813
|
+
}, null, 2) + "\n";
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// Codex: workflow .md → SKILL.md (skills ARE commands in Codex)
|
|
1817
|
+
function mdToCodexSkill(wf) {
|
|
1818
|
+
return `---\nname: ${wf.name}\ndescription: "${wf.description.replace(/"/g, '\\"')}"\n---\n\n${wf.body}\n`;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// Windsurf: .md rule → .md with trigger_type frontmatter
|
|
1822
|
+
function mdToWindsurfRule(content, name) {
|
|
1823
|
+
const front = `---\ntrigger_type: always_on\ndescription: "Mover OS system rules"\n---\n\n`;
|
|
1824
|
+
return front + content;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// Install workflows with format conversion
|
|
1828
|
+
function installWorkflowsConverted(bundleDir, destDir, format, selectedWorkflows) {
|
|
1829
|
+
const srcDir = path.join(bundleDir, "src", "workflows");
|
|
1830
|
+
if (!fs.existsSync(srcDir)) return 0;
|
|
1831
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
1832
|
+
|
|
1833
|
+
const converters = {
|
|
1834
|
+
"md": null, // identity — handled by installWorkflows()
|
|
1835
|
+
"toml": { fn: mdToToml, ext: ".toml" },
|
|
1836
|
+
"prompt-md": { fn: mdToCopilotPrompt, ext: ".prompt.md" },
|
|
1837
|
+
"continue-prompt": { fn: mdToContinuePrompt, ext: ".prompt" },
|
|
1838
|
+
"amazon-q-json": { fn: mdToAmazonQAgent, ext: ".json" },
|
|
1839
|
+
"codex-skill": { fn: mdToCodexSkill, ext: "/SKILL.md", dir: true },
|
|
1840
|
+
"mdc": { fn: mdToMdc, ext: ".mdc" },
|
|
1841
|
+
};
|
|
1842
|
+
|
|
1843
|
+
const conv = converters[format];
|
|
1844
|
+
if (!conv) return 0;
|
|
1845
|
+
|
|
1846
|
+
let count = 0;
|
|
1847
|
+
for (const file of fs.readdirSync(srcDir).filter((f) => f.endsWith(".md"))) {
|
|
1848
|
+
if (selectedWorkflows && !selectedWorkflows.has(file)) continue;
|
|
1849
|
+
const wf = parseWorkflowMd(path.join(srcDir, file));
|
|
1850
|
+
const converted = conv.fn(wf);
|
|
1851
|
+
if (conv.dir) {
|
|
1852
|
+
// Directory-based output (e.g., Codex: morning/SKILL.md)
|
|
1853
|
+
const skillDir = path.join(destDir, wf.name);
|
|
1854
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
1855
|
+
fs.writeFileSync(path.join(skillDir, "SKILL.md"), converted, "utf8");
|
|
1856
|
+
} else {
|
|
1857
|
+
fs.writeFileSync(path.join(destDir, wf.name + conv.ext), converted, "utf8");
|
|
1858
|
+
}
|
|
1859
|
+
count++;
|
|
1860
|
+
}
|
|
1861
|
+
return count;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
function computeSkillHash(dirPath) {
|
|
1865
|
+
const hash = crypto.createHash("sha256");
|
|
1866
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
1867
|
+
for (const entry of entries) {
|
|
1868
|
+
const full = path.join(dirPath, entry.name);
|
|
1869
|
+
if (entry.isFile()) {
|
|
1870
|
+
hash.update(entry.name);
|
|
1871
|
+
hash.update(fs.readFileSync(full));
|
|
1872
|
+
} else if (entry.isDirectory()) {
|
|
1873
|
+
hash.update(entry.name + "/");
|
|
1874
|
+
hash.update(computeSkillHash(full));
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
return hash.digest("hex");
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1522
1880
|
function installSkillPacks(bundleDir, destDir, selectedCategories) {
|
|
1523
1881
|
const skills = findSkills(bundleDir);
|
|
1524
1882
|
fs.mkdirSync(destDir, { recursive: true });
|
|
1525
1883
|
const installedNames = new Set();
|
|
1526
1884
|
let count = 0;
|
|
1885
|
+
let skipped = 0;
|
|
1886
|
+
|
|
1887
|
+
// Load existing manifest for change detection
|
|
1888
|
+
const manifestPath = path.join(destDir, ".skill-manifest.json");
|
|
1889
|
+
let manifest = { skills: {} };
|
|
1890
|
+
try {
|
|
1891
|
+
if (fs.existsSync(manifestPath)) manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
1892
|
+
} catch {}
|
|
1893
|
+
|
|
1527
1894
|
for (const skill of skills) {
|
|
1528
1895
|
// Filter by category if categories were selected (tools always installed)
|
|
1529
1896
|
if (selectedCategories && skill.category !== "tools" && !selectedCategories.has(skill.category)) continue;
|
|
1530
1897
|
const dest = path.join(destDir, skill.name);
|
|
1898
|
+
|
|
1899
|
+
// Skip unchanged skills
|
|
1900
|
+
const sourceHash = computeSkillHash(skill.path);
|
|
1901
|
+
if (manifest.skills[skill.name]?.hash === sourceHash && fs.existsSync(dest)) {
|
|
1902
|
+
installedNames.add(skill.name);
|
|
1903
|
+
skipped++;
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1531
1907
|
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
|
|
1532
1908
|
copyDirRecursive(skill.path, dest);
|
|
1909
|
+
manifest.skills[skill.name] = { hash: sourceHash, installedAt: new Date().toISOString() };
|
|
1533
1910
|
installedNames.add(skill.name);
|
|
1534
1911
|
count++;
|
|
1535
1912
|
}
|
|
@@ -1549,6 +1926,11 @@ function installSkillPacks(bundleDir, destDir, selectedCategories) {
|
|
|
1549
1926
|
}
|
|
1550
1927
|
} catch (e) { /* skip */ }
|
|
1551
1928
|
}
|
|
1929
|
+
|
|
1930
|
+
// Write manifest for next run's change detection
|
|
1931
|
+
try { fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n"); } catch {}
|
|
1932
|
+
|
|
1933
|
+
if (skipped > 0) ln(` ${dim(`${skipped} skills unchanged, skipped`)}`);
|
|
1552
1934
|
return count;
|
|
1553
1935
|
}
|
|
1554
1936
|
|
|
@@ -1674,15 +2056,43 @@ function installCursor(bundleDir, vaultPath, skillOpts) {
|
|
|
1674
2056
|
const steps = [];
|
|
1675
2057
|
|
|
1676
2058
|
if (!skillOpts?.skipRules) {
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
fs.mkdirSync(
|
|
2059
|
+
// V5 FIX: Use .cursor/rules/*.mdc (not legacy .cursorrules)
|
|
2060
|
+
// MDC format: YAML frontmatter with description, globs, alwaysApply
|
|
2061
|
+
const rulesDir = vaultPath
|
|
2062
|
+
? path.join(vaultPath, ".cursor", "rules")
|
|
2063
|
+
: path.join(home, ".cursor", "rules");
|
|
2064
|
+
fs.mkdirSync(rulesDir, { recursive: true });
|
|
1683
2065
|
const src = path.join(bundleDir, "src", "system", "Mover_Global_Rules.md");
|
|
1684
2066
|
if (fs.existsSync(src)) {
|
|
1685
|
-
|
|
2067
|
+
let content = fs.readFileSync(src, "utf8");
|
|
2068
|
+
content = content.replace(/^# Mover OS Global Rules/m, "# Cursor Project Rules");
|
|
2069
|
+
// Strip Claude-specific S10.5
|
|
2070
|
+
content = content.replace(/## 10\.5 Subagents[\s\S]*?(?=\n---\n)/m, "");
|
|
2071
|
+
// Add MDC frontmatter
|
|
2072
|
+
const mdc = `---\ndescription: "Mover OS system rules — coding conventions, workflows, and project integrity"\nglobs:\nalwaysApply: true\n---\n\n${content}`;
|
|
2073
|
+
const destPath = path.join(rulesDir, "mover-os.mdc");
|
|
2074
|
+
// Preserve user customizations
|
|
2075
|
+
const customizations = extractCustomizations(destPath);
|
|
2076
|
+
let finalContent = mdc;
|
|
2077
|
+
if (customizations) {
|
|
2078
|
+
finalContent += `\n${RULES_SENTINEL} — Everything below this line is preserved during updates -->\n\n${customizations}\n`;
|
|
2079
|
+
}
|
|
2080
|
+
fs.writeFileSync(destPath, finalContent, "utf8");
|
|
2081
|
+
steps.push("rules (.mdc)");
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// Clean legacy .cursorrules if it exists
|
|
2085
|
+
if (vaultPath) {
|
|
2086
|
+
const legacy = path.join(vaultPath, ".cursorrules");
|
|
2087
|
+
if (fs.existsSync(legacy)) {
|
|
2088
|
+
try {
|
|
2089
|
+
const legacyContent = fs.readFileSync(legacy, "utf8");
|
|
2090
|
+
if (legacyContent.includes("Mover OS") || legacyContent.includes("MOVER_OS")) {
|
|
2091
|
+
fs.unlinkSync(legacy);
|
|
2092
|
+
ln(` ${dim("Cleaned legacy .cursorrules")}`);
|
|
2093
|
+
}
|
|
2094
|
+
} catch {}
|
|
2095
|
+
}
|
|
1686
2096
|
}
|
|
1687
2097
|
}
|
|
1688
2098
|
|
|
@@ -1727,12 +2137,16 @@ function installCodex(bundleDir, vaultPath, skillOpts) {
|
|
|
1727
2137
|
|
|
1728
2138
|
const codexDir = path.join(home, ".codex");
|
|
1729
2139
|
fs.mkdirSync(codexDir, { recursive: true });
|
|
1730
|
-
// Codex CLI uses AGENTS.md
|
|
1731
|
-
fs.writeFileSync(path.join(codexDir, "
|
|
1732
|
-
steps.push("
|
|
2140
|
+
// Codex CLI uses AGENTS.md (not instructions.md, not raw Global Rules)
|
|
2141
|
+
fs.writeFileSync(path.join(codexDir, "AGENTS.md"), generateAgentsMd(), "utf8");
|
|
2142
|
+
steps.push("AGENTS.md");
|
|
2143
|
+
|
|
2144
|
+
// V5 FIX: In Codex, skills ARE commands — install workflows as SKILL.md dirs
|
|
2145
|
+
const skillsDir = path.join(codexDir, "skills");
|
|
2146
|
+
const wfCount = installWorkflowsConverted(bundleDir, skillsDir, "codex-skill", skillOpts?.workflows);
|
|
2147
|
+
if (wfCount > 0) steps.push(`${wfCount} commands`);
|
|
1733
2148
|
|
|
1734
2149
|
if (skillOpts && skillOpts.install) {
|
|
1735
|
-
const skillsDir = path.join(codexDir, "skills");
|
|
1736
2150
|
const skCount = installSkillPacks(bundleDir, skillsDir, skillOpts.categories);
|
|
1737
2151
|
if (skCount > 0) steps.push(`${skCount} skills`);
|
|
1738
2152
|
}
|
|
@@ -1744,7 +2158,23 @@ function installWindsurf(bundleDir, vaultPath, skillOpts) {
|
|
|
1744
2158
|
const home = os.homedir();
|
|
1745
2159
|
const steps = [];
|
|
1746
2160
|
if (vaultPath) {
|
|
1747
|
-
|
|
2161
|
+
// V5 FIX: Write to .windsurf/rules/ dir AND legacy .windsurfrules for compatibility
|
|
2162
|
+
const wsDir = path.join(vaultPath, ".windsurf");
|
|
2163
|
+
const rulesDir = path.join(wsDir, "rules");
|
|
2164
|
+
fs.mkdirSync(rulesDir, { recursive: true });
|
|
2165
|
+
const src = path.join(bundleDir, "src", "system", "Mover_Global_Rules.md");
|
|
2166
|
+
if (fs.existsSync(src)) {
|
|
2167
|
+
// Primary: .windsurf/rules/mover-os.md
|
|
2168
|
+
fs.copyFileSync(src, path.join(rulesDir, "mover-os.md"));
|
|
2169
|
+
// Legacy: .windsurfrules (some Windsurf versions still read this)
|
|
2170
|
+
if (installRules(bundleDir, path.join(vaultPath, ".windsurfrules"), "windsurf")) {}
|
|
2171
|
+
steps.push("rules");
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
// V5 FIX: Install workflows as .md commands
|
|
2175
|
+
const wfDir = path.join(wsDir, "workflows");
|
|
2176
|
+
const wfCount = installWorkflows(bundleDir, wfDir, skillOpts?.workflows);
|
|
2177
|
+
if (wfCount > 0) steps.push(`${wfCount} workflows`);
|
|
1748
2178
|
}
|
|
1749
2179
|
|
|
1750
2180
|
if (skillOpts && skillOpts.install) {
|
|
@@ -1756,37 +2186,7 @@ function installWindsurf(bundleDir, vaultPath, skillOpts) {
|
|
|
1756
2186
|
return steps;
|
|
1757
2187
|
}
|
|
1758
2188
|
|
|
1759
|
-
|
|
1760
|
-
const home = os.homedir();
|
|
1761
|
-
const workspace = path.join(home, ".openclaw", "workspace");
|
|
1762
|
-
const steps = [];
|
|
1763
|
-
|
|
1764
|
-
fs.mkdirSync(path.join(workspace, "skills"), { recursive: true });
|
|
1765
|
-
fs.mkdirSync(path.join(workspace, "memory"), { recursive: true });
|
|
1766
|
-
|
|
1767
|
-
fs.writeFileSync(path.join(workspace, "AGENTS.md"), generateAgentsMd(), "utf8");
|
|
1768
|
-
steps.push("AGENTS.md");
|
|
1769
|
-
|
|
1770
|
-
fs.writeFileSync(path.join(workspace, "SOUL.md"), generateSoulMd(), "utf8");
|
|
1771
|
-
steps.push("SOUL.md");
|
|
1772
|
-
|
|
1773
|
-
const userMd = path.join(workspace, "USER.md");
|
|
1774
|
-
if (!fs.existsSync(userMd)) {
|
|
1775
|
-
fs.writeFileSync(
|
|
1776
|
-
userMd,
|
|
1777
|
-
`# USER.md\n\n> Run \`/setup\` in Mover OS to populate this file with your Identity and Strategy.\n\n## Identity\n<!-- Populated by /setup -->\n\n## Strategy\n<!-- Populated by /setup -->\n\n## Assets\n<!-- Populated by /setup -->\n`,
|
|
1778
|
-
"utf8"
|
|
1779
|
-
);
|
|
1780
|
-
steps.push("USER.md");
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
if (skillOpts && skillOpts.install) {
|
|
1784
|
-
const skCount = installSkillPacks(bundleDir, path.join(workspace, "skills"), skillOpts.categories);
|
|
1785
|
-
if (skCount > 0) steps.push(`${skCount} skills`);
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
return steps;
|
|
1789
|
-
}
|
|
2189
|
+
// OpenClaw removed in V5 — unverifiable
|
|
1790
2190
|
|
|
1791
2191
|
function installGeminiCli(bundleDir, vaultPath, skillOpts, writtenFiles) {
|
|
1792
2192
|
const home = os.homedir();
|
|
@@ -1802,6 +2202,11 @@ function installGeminiCli(bundleDir, vaultPath, skillOpts, writtenFiles) {
|
|
|
1802
2202
|
steps.push("rules (shared)");
|
|
1803
2203
|
}
|
|
1804
2204
|
|
|
2205
|
+
// V5 FIX: Install commands as .toml files
|
|
2206
|
+
const cmdsDir = path.join(geminiDir, "commands");
|
|
2207
|
+
const wfCount = installWorkflowsConverted(bundleDir, cmdsDir, "toml", skillOpts?.workflows);
|
|
2208
|
+
if (wfCount > 0) steps.push(`${wfCount} commands`);
|
|
2209
|
+
|
|
1805
2210
|
if (skillOpts && skillOpts.install) {
|
|
1806
2211
|
const skCount = installSkillPacks(bundleDir, path.join(geminiDir, "skills"), skillOpts.categories);
|
|
1807
2212
|
if (skCount > 0) steps.push(`${skCount} skills`);
|
|
@@ -1848,6 +2253,11 @@ function installRooCode(bundleDir, vaultPath, skillOpts) {
|
|
|
1848
2253
|
steps.push("rules");
|
|
1849
2254
|
}
|
|
1850
2255
|
|
|
2256
|
+
// V5 FIX: Install commands as .md files
|
|
2257
|
+
const cmdsDir = path.join(vaultPath, ".roo", "commands");
|
|
2258
|
+
const wfCount = installWorkflows(bundleDir, cmdsDir, skillOpts?.workflows);
|
|
2259
|
+
if (wfCount > 0) steps.push(`${wfCount} commands`);
|
|
2260
|
+
|
|
1851
2261
|
if (skillOpts && skillOpts.install) {
|
|
1852
2262
|
const skillsDir = path.join(vaultPath, ".roo", "skills");
|
|
1853
2263
|
const skCount = installSkillPacks(bundleDir, skillsDir, skillOpts.categories);
|
|
@@ -1868,6 +2278,11 @@ function installCopilot(bundleDir, vaultPath, skillOpts) {
|
|
|
1868
2278
|
steps.push("rules");
|
|
1869
2279
|
}
|
|
1870
2280
|
|
|
2281
|
+
// V5 FIX: Install commands as .prompt.md files
|
|
2282
|
+
const promptsDir = path.join(ghDir, "prompts");
|
|
2283
|
+
const wfCount = installWorkflowsConverted(bundleDir, promptsDir, "prompt-md", skillOpts?.workflows);
|
|
2284
|
+
if (wfCount > 0) steps.push(`${wfCount} commands`);
|
|
2285
|
+
|
|
1871
2286
|
if (skillOpts && skillOpts.install) {
|
|
1872
2287
|
const skillsDir = path.join(ghDir, "skills");
|
|
1873
2288
|
const skCount = installSkillPacks(bundleDir, skillsDir, skillOpts.categories);
|
|
@@ -1883,13 +2298,98 @@ const AGENT_INSTALLERS = {
|
|
|
1883
2298
|
cline: installCline,
|
|
1884
2299
|
codex: installCodex,
|
|
1885
2300
|
windsurf: installWindsurf,
|
|
1886
|
-
openclaw: installOpenClaw,
|
|
1887
2301
|
"gemini-cli": installGeminiCli,
|
|
1888
2302
|
antigravity: installAntigravity,
|
|
1889
2303
|
"roo-code": installRooCode,
|
|
1890
2304
|
copilot: installCopilot,
|
|
2305
|
+
// New agents — use universal installer (delegates to per-agent functions for existing,
|
|
2306
|
+
// falls back to registry-driven install for new agents)
|
|
2307
|
+
"amazon-q": installFromRegistry,
|
|
2308
|
+
"opencode": installFromRegistry,
|
|
2309
|
+
"kilo-code": installFromRegistry,
|
|
2310
|
+
"amp": installFromRegistry,
|
|
2311
|
+
"continue": installFromRegistry,
|
|
2312
|
+
"aider": installFromRegistry,
|
|
1891
2313
|
};
|
|
1892
2314
|
|
|
2315
|
+
// ─── Registry-driven universal installer (for new agents) ────────────────────
|
|
2316
|
+
function installFromRegistry(bundleDir, vaultPath, skillOpts, writtenFiles, id) {
|
|
2317
|
+
const reg = AGENT_REGISTRY[id];
|
|
2318
|
+
if (!reg) return [];
|
|
2319
|
+
const steps = [];
|
|
2320
|
+
|
|
2321
|
+
// Rules
|
|
2322
|
+
if (reg.rules && !skillOpts?.skipRules) {
|
|
2323
|
+
const destPath = reg.rules.dest(vaultPath);
|
|
2324
|
+
if (reg.rules.type === "agents-md") {
|
|
2325
|
+
const sharedKey = reg.sharedRulesFile || id;
|
|
2326
|
+
if (!writtenFiles || !writtenFiles.has(destPath)) {
|
|
2327
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
2328
|
+
fs.writeFileSync(destPath, generateAgentsMd(), "utf8");
|
|
2329
|
+
if (writtenFiles) writtenFiles.add(destPath);
|
|
2330
|
+
steps.push("AGENTS.md");
|
|
2331
|
+
} else {
|
|
2332
|
+
steps.push("AGENTS.md (shared)");
|
|
2333
|
+
}
|
|
2334
|
+
} else if (reg.rules.type === "conventions-md") {
|
|
2335
|
+
// Aider: shorter rules file
|
|
2336
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
2337
|
+
const src = path.join(bundleDir, "src", "system", "Mover_Global_Rules.md");
|
|
2338
|
+
if (fs.existsSync(src)) {
|
|
2339
|
+
let content = fs.readFileSync(src, "utf8");
|
|
2340
|
+
// Aider gets a trimmed version — keep sections 0-4 only
|
|
2341
|
+
const s5idx = content.indexOf("\n## 5.");
|
|
2342
|
+
if (s5idx > 0) content = content.substring(0, s5idx);
|
|
2343
|
+
content = "# CONVENTIONS.md\n\n" + content.replace(/^# Mover OS Global Rules/m, "").trim() + "\n";
|
|
2344
|
+
fs.writeFileSync(destPath, content, "utf8");
|
|
2345
|
+
steps.push("CONVENTIONS.md");
|
|
2346
|
+
}
|
|
2347
|
+
} else if (reg.rules.type === "continue-rule") {
|
|
2348
|
+
// Continue.dev: .md with YAML frontmatter (alwaysApply: true)
|
|
2349
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
2350
|
+
const src = path.join(bundleDir, "src", "system", "Mover_Global_Rules.md");
|
|
2351
|
+
if (fs.existsSync(src)) {
|
|
2352
|
+
let content = fs.readFileSync(src, "utf8");
|
|
2353
|
+
content = content.replace(/^# Mover OS Global Rules/m, "");
|
|
2354
|
+
const front = `---\nname: Mover OS Rules\nglobs: "**/*"\nalwaysApply: true\n---\n\n`;
|
|
2355
|
+
fs.writeFileSync(destPath, front + content.trim() + "\n", "utf8");
|
|
2356
|
+
steps.push("rules");
|
|
2357
|
+
}
|
|
2358
|
+
} else {
|
|
2359
|
+
// Standard copy
|
|
2360
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
2361
|
+
if (installRules(bundleDir, destPath, id)) steps.push("rules");
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
// Skills
|
|
2366
|
+
if (reg.skills && skillOpts && skillOpts.install) {
|
|
2367
|
+
const skillsDir = reg.skills.dest(vaultPath);
|
|
2368
|
+
const skCount = installSkillPacks(bundleDir, skillsDir, skillOpts.categories);
|
|
2369
|
+
if (skCount > 0) steps.push(`${skCount} skills`);
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
// Commands (with format conversion)
|
|
2373
|
+
if (reg.commands && !skillOpts?.skipCommands) {
|
|
2374
|
+
const cmdDest = reg.commands.dest(vaultPath);
|
|
2375
|
+
const fmt = reg.commands.format;
|
|
2376
|
+
if (fmt === "md") {
|
|
2377
|
+
// Direct .md copy (same as Claude Code)
|
|
2378
|
+
const wfCount = installWorkflows(bundleDir, cmdDest, skillOpts?.workflows);
|
|
2379
|
+
if (wfCount > 0) steps.push(`${wfCount} commands`);
|
|
2380
|
+
} else if (fmt === "opencode-json") {
|
|
2381
|
+
// OpenCode: commands go into opencode.json (merge, don't overwrite)
|
|
2382
|
+
// Skip for now — requires config file merge logic
|
|
2383
|
+
} else {
|
|
2384
|
+
// All other formats: convert .md → target format
|
|
2385
|
+
const wfCount = installWorkflowsConverted(bundleDir, cmdDest, fmt, skillOpts?.workflows);
|
|
2386
|
+
if (wfCount > 0) steps.push(`${wfCount} commands`);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
return steps;
|
|
2391
|
+
}
|
|
2392
|
+
|
|
1893
2393
|
// ─── Pre-flight checks ──────────────────────────────────────────────────────
|
|
1894
2394
|
function preflight() {
|
|
1895
2395
|
const issues = [];
|
|
@@ -1928,6 +2428,936 @@ function preflight() {
|
|
|
1928
2428
|
return issues;
|
|
1929
2429
|
}
|
|
1930
2430
|
|
|
2431
|
+
// ─── CLI Command Handlers (stubs — implemented progressively) ────────────────
|
|
2432
|
+
const CLI_HANDLERS = {
|
|
2433
|
+
pulse: async (opts) => { await cmdPulse(opts); },
|
|
2434
|
+
warm: async (opts) => { await cmdWarm(opts); },
|
|
2435
|
+
capture: async (opts) => { await cmdCapture(opts); },
|
|
2436
|
+
who: async (opts) => { await cmdWho(opts); },
|
|
2437
|
+
diff: async (opts) => { await cmdDiff(opts); },
|
|
2438
|
+
sync: async (opts) => { await cmdSync(opts); },
|
|
2439
|
+
replay: async (opts) => { await cmdReplay(opts); },
|
|
2440
|
+
context: async (opts) => { await cmdContext(opts); },
|
|
2441
|
+
settings: async (opts) => { await cmdSettings(opts); },
|
|
2442
|
+
backup: async (opts) => { await cmdBackup(opts); },
|
|
2443
|
+
restore: async (opts) => { await cmdRestore(opts); },
|
|
2444
|
+
doctor: async (opts) => { await cmdDoctor(opts); },
|
|
2445
|
+
test: async (opts) => { await cmdTest(opts); },
|
|
2446
|
+
};
|
|
2447
|
+
|
|
2448
|
+
// ─── moveros doctor ─────────────────────────────────────────────────────────
|
|
2449
|
+
async function cmdDoctor(opts) {
|
|
2450
|
+
const vault = resolveVaultPath(opts.vault);
|
|
2451
|
+
if (!vault) {
|
|
2452
|
+
barLn(red("No Mover OS vault found. Use: moveros doctor --vault /path"));
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
const home = os.homedir();
|
|
2456
|
+
barLn(bold(" Health Check"));
|
|
2457
|
+
barLn();
|
|
2458
|
+
|
|
2459
|
+
// Vault structure
|
|
2460
|
+
const paraDirs = ["00_Inbox", "01_Projects", "02_Areas", "03_Library", "04_Archives"];
|
|
2461
|
+
let paraOk = 0;
|
|
2462
|
+
for (const d of paraDirs) {
|
|
2463
|
+
if (fs.existsSync(path.join(vault, d))) paraOk++;
|
|
2464
|
+
}
|
|
2465
|
+
statusLine(paraOk === paraDirs.length ? "ok" : "warn", "Vault structure", `${paraOk}/${paraDirs.length} PARA folders`);
|
|
2466
|
+
|
|
2467
|
+
// Engine files
|
|
2468
|
+
const engineFiles = ["Identity_Prime.md", "Strategy.md", "Active_Context.md", "Goals.md", "Mover_Dossier.md", "Auto_Learnings.md"];
|
|
2469
|
+
const engineDir = path.join(vault, "02_Areas", "Engine");
|
|
2470
|
+
let engOk = 0;
|
|
2471
|
+
for (const f of engineFiles) {
|
|
2472
|
+
if (fs.existsSync(path.join(engineDir, f))) engOk++;
|
|
2473
|
+
}
|
|
2474
|
+
statusLine(engOk >= 4 ? "ok" : engOk >= 2 ? "warn" : "fail", "Engine files", `${engOk}/${engineFiles.length} present`);
|
|
2475
|
+
|
|
2476
|
+
// Version
|
|
2477
|
+
const vf = path.join(vault, ".mover-version");
|
|
2478
|
+
const ver = fs.existsSync(vf) ? fs.readFileSync(vf, "utf8").trim() : null;
|
|
2479
|
+
statusLine(ver ? "ok" : "warn", "Version", ver || "not stamped");
|
|
2480
|
+
|
|
2481
|
+
// Config
|
|
2482
|
+
const cfg = path.join(home, ".mover", "config.json");
|
|
2483
|
+
statusLine(fs.existsSync(cfg) ? "ok" : "warn", "Config", fs.existsSync(cfg) ? cfg : "missing");
|
|
2484
|
+
|
|
2485
|
+
// Per-agent checks
|
|
2486
|
+
barLn();
|
|
2487
|
+
barLn(dim(" Agents:"));
|
|
2488
|
+
const cfgData = fs.existsSync(cfg) ? JSON.parse(fs.readFileSync(cfg, "utf8")) : {};
|
|
2489
|
+
const installedAgents = cfgData.agents || [];
|
|
2490
|
+
if (installedAgents.length === 0) {
|
|
2491
|
+
barLn(dim(" No agents recorded in config."));
|
|
2492
|
+
}
|
|
2493
|
+
for (const agentId of installedAgents) {
|
|
2494
|
+
const reg = AGENT_REGISTRY[agentId];
|
|
2495
|
+
if (!reg) { statusLine("warn", ` ${agentId}`, "unknown agent"); continue; }
|
|
2496
|
+
const checks = [];
|
|
2497
|
+
// Rules
|
|
2498
|
+
if (reg.rules) {
|
|
2499
|
+
const rp = reg.rules.dest(vault);
|
|
2500
|
+
checks.push(fs.existsSync(rp) ? "rules" : dim("rules missing"));
|
|
2501
|
+
}
|
|
2502
|
+
// Skills
|
|
2503
|
+
if (reg.skills) {
|
|
2504
|
+
const sp = reg.skills.dest(vault);
|
|
2505
|
+
const hasSkills = fs.existsSync(sp) && fs.readdirSync(sp).length > 0;
|
|
2506
|
+
checks.push(hasSkills ? "skills" : dim("no skills"));
|
|
2507
|
+
}
|
|
2508
|
+
// Commands
|
|
2509
|
+
if (reg.commands) {
|
|
2510
|
+
const cp = reg.commands.dest(vault);
|
|
2511
|
+
const hasCmds = fs.existsSync(cp) && fs.readdirSync(cp).length > 0;
|
|
2512
|
+
checks.push(hasCmds ? "commands" : dim("no commands"));
|
|
2513
|
+
}
|
|
2514
|
+
const allOk = checks.every((c) => !c.includes("missing"));
|
|
2515
|
+
statusLine(allOk ? "ok" : "warn", ` ${reg.name}`, checks.join(", "));
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
// Git
|
|
2519
|
+
barLn();
|
|
2520
|
+
const hasGit = fs.existsSync(path.join(vault, ".git"));
|
|
2521
|
+
statusLine(hasGit ? "ok" : "info", "Git", hasGit ? "initialized" : "not a git repo");
|
|
2522
|
+
|
|
2523
|
+
barLn();
|
|
2524
|
+
barLn(dim(" Run moveros install or moveros update to fix any issues."));
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
// ─── moveros pulse ──────────────────────────────────────────────────────────
|
|
2528
|
+
async function cmdPulse(opts) {
|
|
2529
|
+
const vault = resolveVaultPath(opts.vault);
|
|
2530
|
+
if (!vault) {
|
|
2531
|
+
barLn(red("No vault found. Use: moveros pulse --vault /path"));
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
const engineDir = path.join(vault, "02_Areas", "Engine");
|
|
2535
|
+
|
|
2536
|
+
barLn(bold(" Pulse"));
|
|
2537
|
+
barLn();
|
|
2538
|
+
|
|
2539
|
+
// Energy + Focus from Active_Context
|
|
2540
|
+
const acPath = path.join(engineDir, "Active_Context.md");
|
|
2541
|
+
let energy = "?", focus = "?", blockers = [];
|
|
2542
|
+
if (fs.existsSync(acPath)) {
|
|
2543
|
+
const ac = fs.readFileSync(acPath, "utf8");
|
|
2544
|
+
const energyMatch = ac.match(/\*\*Energy:\*\*\s*(.+)/i) || ac.match(/Energy:\s*(.+)/i);
|
|
2545
|
+
if (energyMatch) energy = energyMatch[1].trim();
|
|
2546
|
+
const focusMatch = ac.match(/\*\*Focus:\*\*\s*(.+)/i) || ac.match(/Single Test:\s*(.+)/i);
|
|
2547
|
+
if (focusMatch) focus = focusMatch[1].trim();
|
|
2548
|
+
// Blockers
|
|
2549
|
+
const blockerSection = ac.match(/##.*Blocker[s]?[\s\S]*?(?=\n##|\n---|\Z)/i);
|
|
2550
|
+
if (blockerSection) {
|
|
2551
|
+
const lines = blockerSection[0].split("\n").filter((l) => l.trim().startsWith("-") || l.trim().startsWith("*"));
|
|
2552
|
+
blockers = lines.map((l) => l.replace(/^[\s*-]+/, "").trim()).filter(Boolean).slice(0, 3);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
statusLine("info", "Energy", energy);
|
|
2556
|
+
statusLine("info", "Focus", focus.substring(0, 60));
|
|
2557
|
+
if (blockers.length > 0) {
|
|
2558
|
+
statusLine("warn", "Blockers", blockers[0]);
|
|
2559
|
+
for (let i = 1; i < blockers.length; i++) barLn(` ${dim(blockers[i])}`);
|
|
2560
|
+
} else {
|
|
2561
|
+
statusLine("ok", "Blockers", "none");
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
// Today's tasks from Daily Note
|
|
2565
|
+
barLn();
|
|
2566
|
+
const now = new Date();
|
|
2567
|
+
const ymd = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
2568
|
+
const month = ymd.substring(0, 7);
|
|
2569
|
+
const dailyPath = path.join(engineDir, "Dailies", month, `Daily - ${ymd}.md`);
|
|
2570
|
+
if (fs.existsSync(dailyPath)) {
|
|
2571
|
+
const daily = fs.readFileSync(dailyPath, "utf8");
|
|
2572
|
+
const taskSection = daily.match(/##\s*Tasks[\s\S]*?(?=\n##|\n---)/i);
|
|
2573
|
+
if (taskSection) {
|
|
2574
|
+
const tasks = taskSection[0].split("\n").filter((l) => /^\s*-\s*\[[ x~]\]/.test(l));
|
|
2575
|
+
const done = tasks.filter((t) => /\[x\]|\[~\]/.test(t)).length;
|
|
2576
|
+
const total = tasks.length;
|
|
2577
|
+
barLn(` ${progressBar(done, total, 25, "Tasks")}`);
|
|
2578
|
+
}
|
|
2579
|
+
} else {
|
|
2580
|
+
barLn(dim(" No Daily Note for today."));
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
// Active projects — scan plan.md files
|
|
2584
|
+
barLn();
|
|
2585
|
+
const projectsDir = path.join(vault, "01_Projects");
|
|
2586
|
+
if (fs.existsSync(projectsDir)) {
|
|
2587
|
+
const projects = fs.readdirSync(projectsDir, { withFileTypes: true })
|
|
2588
|
+
.filter((d) => d.isDirectory())
|
|
2589
|
+
.slice(0, 5);
|
|
2590
|
+
if (projects.length > 0) {
|
|
2591
|
+
barLn(dim(" Active Projects:"));
|
|
2592
|
+
for (const p of projects) {
|
|
2593
|
+
const planPath = path.join(projectsDir, p.name, "dev", "plan.md");
|
|
2594
|
+
if (fs.existsSync(planPath)) {
|
|
2595
|
+
const plan = fs.readFileSync(planPath, "utf8");
|
|
2596
|
+
const tasks = plan.match(/^\s*-\s*\[[ x~]\]/gm) || [];
|
|
2597
|
+
const done = (plan.match(/^\s*-\s*\[x\]/gm) || []).length;
|
|
2598
|
+
barLn(` ${p.name.padEnd(25)} ${progressBar(done, tasks.length, 15)}`);
|
|
2599
|
+
} else {
|
|
2600
|
+
barLn(` ${dim(p.name)}`);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
// Streaks
|
|
2607
|
+
barLn();
|
|
2608
|
+
const dailiesDir = path.join(engineDir, "Dailies");
|
|
2609
|
+
if (fs.existsSync(dailiesDir)) {
|
|
2610
|
+
let streak = 0;
|
|
2611
|
+
const d = new Date();
|
|
2612
|
+
for (let i = 0; i < 30; i++) {
|
|
2613
|
+
const check = new Date(d);
|
|
2614
|
+
check.setDate(check.getDate() - i);
|
|
2615
|
+
const cy = `${check.getFullYear()}-${String(check.getMonth() + 1).padStart(2, "0")}-${String(check.getDate()).padStart(2, "0")}`;
|
|
2616
|
+
const cm = cy.substring(0, 7);
|
|
2617
|
+
if (fs.existsSync(path.join(dailiesDir, cm, `Daily - ${cy}.md`))) {
|
|
2618
|
+
streak++;
|
|
2619
|
+
} else if (i > 0) break; // allow today to be missing
|
|
2620
|
+
}
|
|
2621
|
+
statusLine(streak >= 3 ? "ok" : "info", "Daily streak", `${streak} day${streak !== 1 ? "s" : ""}`);
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
// Last session log time
|
|
2625
|
+
const cfgPath = path.join(os.homedir(), ".mover", "config.json");
|
|
2626
|
+
if (fs.existsSync(cfgPath)) {
|
|
2627
|
+
try {
|
|
2628
|
+
const cfgData = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
|
|
2629
|
+
if (cfgData.lastLog) {
|
|
2630
|
+
const ago = Math.round((Date.now() - new Date(cfgData.lastLog).getTime()) / 3600000);
|
|
2631
|
+
statusLine(ago < 8 ? "ok" : ago < 24 ? "info" : "warn", "Last /log", `${ago}h ago`);
|
|
2632
|
+
}
|
|
2633
|
+
} catch {}
|
|
2634
|
+
}
|
|
2635
|
+
barLn();
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
// ─── moveros warm ───────────────────────────────────────────────────────────
|
|
2639
|
+
async function cmdWarm(opts) {
|
|
2640
|
+
const vault = resolveVaultPath(opts.vault);
|
|
2641
|
+
if (!vault) { barLn(red("No vault found.")); return; }
|
|
2642
|
+
const agent = opts.rest[0] || "claude";
|
|
2643
|
+
const engineDir = path.join(vault, "02_Areas", "Engine");
|
|
2644
|
+
const home = os.homedir();
|
|
2645
|
+
|
|
2646
|
+
// Build context primer
|
|
2647
|
+
const sections = [];
|
|
2648
|
+
sections.push("# Session Context Primer");
|
|
2649
|
+
sections.push(`Generated: ${new Date().toISOString()}\n`);
|
|
2650
|
+
|
|
2651
|
+
// Active Context snapshot
|
|
2652
|
+
const acPath = path.join(engineDir, "Active_Context.md");
|
|
2653
|
+
if (fs.existsSync(acPath)) {
|
|
2654
|
+
const ac = fs.readFileSync(acPath, "utf8");
|
|
2655
|
+
// Extract key sections (first 2000 chars)
|
|
2656
|
+
sections.push("## Active Context\n" + ac.substring(0, 2000));
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
// Current project state
|
|
2660
|
+
const cwd = process.cwd();
|
|
2661
|
+
const planPath = path.join(cwd, "dev", "plan.md");
|
|
2662
|
+
if (fs.existsSync(planPath)) {
|
|
2663
|
+
const plan = fs.readFileSync(planPath, "utf8");
|
|
2664
|
+
// Find last active phase
|
|
2665
|
+
const phases = plan.match(/### Phase \d+[\s\S]*?(?=### Phase|\Z)/g);
|
|
2666
|
+
if (phases) {
|
|
2667
|
+
const active = phases[phases.length - 1].substring(0, 1500);
|
|
2668
|
+
sections.push("## Current Plan (last phase)\n" + active);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
// Today's daily note tasks
|
|
2673
|
+
const now = new Date();
|
|
2674
|
+
const ymd = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
2675
|
+
const dailyPath = path.join(engineDir, "Dailies", ymd.substring(0, 7), `Daily - ${ymd}.md`);
|
|
2676
|
+
if (fs.existsSync(dailyPath)) {
|
|
2677
|
+
const daily = fs.readFileSync(dailyPath, "utf8");
|
|
2678
|
+
const taskSection = daily.match(/##\s*Tasks[\s\S]*?(?=\n##)/i);
|
|
2679
|
+
if (taskSection) sections.push("## Today's Tasks\n" + taskSection[0]);
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
const primer = sections.join("\n\n---\n\n") + "\n";
|
|
2683
|
+
|
|
2684
|
+
// Write to agent-specific location
|
|
2685
|
+
const targets = {
|
|
2686
|
+
claude: path.join(home, ".claude", "tmp", "session-primer.md"),
|
|
2687
|
+
cursor: path.join(vault, ".cursor", "rules", "session-primer.mdc"),
|
|
2688
|
+
gemini: path.join(home, ".gemini", "session-primer.md"),
|
|
2689
|
+
codex: path.join(home, ".codex", "skills", "session-primer", "SKILL.md"),
|
|
2690
|
+
windsurf: path.join(vault, ".windsurf", "rules", "session-primer.md"),
|
|
2691
|
+
cline: path.join(vault, ".clinerules", "session-primer.md"),
|
|
2692
|
+
};
|
|
2693
|
+
|
|
2694
|
+
const dest = targets[agent] || targets.claude;
|
|
2695
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
2696
|
+
|
|
2697
|
+
let content = primer;
|
|
2698
|
+
if (agent === "cursor") {
|
|
2699
|
+
content = `---\ndescription: "Session context primer — auto-generated by moveros warm"\nglobs:\nalwaysApply: true\n---\n\n${primer}`;
|
|
2700
|
+
} else if (agent === "codex") {
|
|
2701
|
+
content = `---\nname: session-primer\ndescription: "Auto-generated session context from moveros warm"\n---\n\n${primer}`;
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
fs.writeFileSync(dest, content, "utf8");
|
|
2705
|
+
statusLine("ok", "Warm", `${agent} primer written`);
|
|
2706
|
+
barLn(dim(` ${dest}`));
|
|
2707
|
+
barLn();
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
// ─── moveros capture ────────────────────────────────────────────────────────
|
|
2711
|
+
async function cmdCapture(opts) {
|
|
2712
|
+
const vault = resolveVaultPath(opts.vault);
|
|
2713
|
+
if (!vault) { barLn(red("No vault found.")); return; }
|
|
2714
|
+
|
|
2715
|
+
const now = new Date();
|
|
2716
|
+
const ymd = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
2717
|
+
const ts = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
|
2718
|
+
const capturePath = path.join(vault, "00_Inbox", `Capture - ${ymd}.md`);
|
|
2719
|
+
|
|
2720
|
+
// Determine type from flags
|
|
2721
|
+
let type = null, content = "";
|
|
2722
|
+
const rest = opts.rest;
|
|
2723
|
+
if (rest.includes("--task")) { type = "task"; rest.splice(rest.indexOf("--task"), 1); }
|
|
2724
|
+
else if (rest.includes("--link")) { type = "link"; rest.splice(rest.indexOf("--link"), 1); }
|
|
2725
|
+
else if (rest.includes("--idea")) { type = "idea"; rest.splice(rest.indexOf("--idea"), 1); }
|
|
2726
|
+
else if (rest.includes("--dump")) { type = "dump"; rest.splice(rest.indexOf("--dump"), 1); }
|
|
2727
|
+
|
|
2728
|
+
content = rest.join(" ").trim();
|
|
2729
|
+
|
|
2730
|
+
// Check for stdin pipe
|
|
2731
|
+
if (!content && !process.stdin.isTTY) {
|
|
2732
|
+
content = fs.readFileSync(0, "utf8").trim();
|
|
2733
|
+
if (!type) type = "dump";
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
// Interactive if no content
|
|
2737
|
+
if (!content) {
|
|
2738
|
+
if (!type) {
|
|
2739
|
+
barLn(bold(" Quick Capture"));
|
|
2740
|
+
barLn();
|
|
2741
|
+
type = await interactiveSelect([
|
|
2742
|
+
{ id: "task", name: "Task", tier: "Something to do" },
|
|
2743
|
+
{ id: "idea", name: "Idea", tier: "Something to think about" },
|
|
2744
|
+
{ id: "link", name: "Link", tier: "URL with optional note" },
|
|
2745
|
+
{ id: "dump", name: "Brain dump", tier: "Free-form text" },
|
|
2746
|
+
], { multi: false });
|
|
2747
|
+
}
|
|
2748
|
+
content = await textInput({ label: `Enter ${type}:` });
|
|
2749
|
+
}
|
|
2750
|
+
if (!type) type = "task";
|
|
2751
|
+
|
|
2752
|
+
if (!content) { barLn(yellow("Nothing to capture.")); return; }
|
|
2753
|
+
|
|
2754
|
+
// Format entry
|
|
2755
|
+
let entry;
|
|
2756
|
+
if (type === "task") entry = `- [ ] ${content} *(${ts})*`;
|
|
2757
|
+
else if (type === "link") entry = `- [${content.startsWith("http") ? "Link" : content.split(" ")[0]}](${content.split(" ")[0]}) ${content.split(" ").slice(1).join(" ")} *(${ts})*`;
|
|
2758
|
+
else if (type === "idea") entry = `- **Idea:** ${content} *(${ts})*`;
|
|
2759
|
+
else entry = `- ${content} *(${ts})*`;
|
|
2760
|
+
|
|
2761
|
+
// Append to capture file
|
|
2762
|
+
fs.mkdirSync(path.dirname(capturePath), { recursive: true });
|
|
2763
|
+
if (!fs.existsSync(capturePath)) {
|
|
2764
|
+
fs.writeFileSync(capturePath, `# Capture — ${ymd}\n\n${entry}\n`, "utf8");
|
|
2765
|
+
} else {
|
|
2766
|
+
fs.appendFileSync(capturePath, `${entry}\n`, "utf8");
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
statusLine("ok", "Captured", `${type} → 00_Inbox/`);
|
|
2770
|
+
barLn(dim(` ${entry}`));
|
|
2771
|
+
barLn();
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
// ─── moveros who ────────────────────────────────────────────────────────────
|
|
2775
|
+
async function cmdWho(opts) {
|
|
2776
|
+
const vault = resolveVaultPath(opts.vault);
|
|
2777
|
+
if (!vault) { barLn(red("No vault found.")); return; }
|
|
2778
|
+
|
|
2779
|
+
const name = opts.rest.join(" ").trim();
|
|
2780
|
+
if (!name) { barLn(yellow("Usage: moveros who <name>")); return; }
|
|
2781
|
+
|
|
2782
|
+
const entitiesDir = path.join(vault, "03_Library", "Entities");
|
|
2783
|
+
const subdirs = ["People", "Organizations", "Places"];
|
|
2784
|
+
const results = [];
|
|
2785
|
+
|
|
2786
|
+
for (const sub of subdirs) {
|
|
2787
|
+
const dir = path.join(entitiesDir, sub);
|
|
2788
|
+
if (!fs.existsSync(dir)) continue;
|
|
2789
|
+
for (const file of fs.readdirSync(dir).filter((f) => f.endsWith(".md"))) {
|
|
2790
|
+
if (file.toLowerCase().includes(name.toLowerCase())) {
|
|
2791
|
+
const content = fs.readFileSync(path.join(dir, file), "utf8");
|
|
2792
|
+
results.push({ file, sub, content });
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
if (results.length === 0) {
|
|
2798
|
+
barLn(yellow(` No entity found for "${name}".`));
|
|
2799
|
+
barLn();
|
|
2800
|
+
const create = await interactiveSelect([
|
|
2801
|
+
{ id: "yes", name: "Create stub", tier: `Creates ${name}.md in People/` },
|
|
2802
|
+
{ id: "no", name: "Skip", tier: "" },
|
|
2803
|
+
], { multi: false });
|
|
2804
|
+
if (create === "yes") {
|
|
2805
|
+
const peopleDir = path.join(entitiesDir, "People");
|
|
2806
|
+
fs.mkdirSync(peopleDir, { recursive: true });
|
|
2807
|
+
const stub = `# ${name}\n\n**Type:** Person\n**Last Contact:** ${new Date().toISOString().split("T")[0]}\n\n## Context\n\n- \n\n## Notes\n\n- \n`;
|
|
2808
|
+
fs.writeFileSync(path.join(peopleDir, `${name}.md`), stub, "utf8");
|
|
2809
|
+
statusLine("ok", "Created", `${name}.md in Entities/People/`);
|
|
2810
|
+
}
|
|
2811
|
+
return;
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
barLn(bold(` ${name}`));
|
|
2815
|
+
barLn();
|
|
2816
|
+
for (const r of results) {
|
|
2817
|
+
barLn(dim(` ${r.sub}/${r.file}`));
|
|
2818
|
+
// Show first 15 lines of content
|
|
2819
|
+
const lines = r.content.split("\n").slice(0, 15);
|
|
2820
|
+
for (const l of lines) barLn(` ${l}`);
|
|
2821
|
+
barLn();
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
// ─── moveros diff ───────────────────────────────────────────────────────────
|
|
2826
|
+
async function cmdDiff(opts) {
|
|
2827
|
+
const vault = resolveVaultPath(opts.vault);
|
|
2828
|
+
if (!vault) { barLn(red("No vault found.")); return; }
|
|
2829
|
+
if (!cmdExists("git")) { barLn(red("Git required for moveros diff.")); return; }
|
|
2830
|
+
|
|
2831
|
+
const target = opts.rest[0] || "strategy";
|
|
2832
|
+
const days = parseInt(opts.rest.find((a) => a.startsWith("--days="))?.split("=")[1] || "30", 10);
|
|
2833
|
+
|
|
2834
|
+
const fileMap = {
|
|
2835
|
+
strategy: "02_Areas/Engine/Strategy.md",
|
|
2836
|
+
identity: "02_Areas/Engine/Identity_Prime.md",
|
|
2837
|
+
goals: "02_Areas/Engine/Goals.md",
|
|
2838
|
+
dossier: "02_Areas/Engine/Mover_Dossier.md",
|
|
2839
|
+
context: "02_Areas/Engine/Active_Context.md",
|
|
2840
|
+
};
|
|
2841
|
+
|
|
2842
|
+
const relPath = fileMap[target.toLowerCase()] || target;
|
|
2843
|
+
const fullPath = path.join(vault, relPath);
|
|
2844
|
+
if (!fs.existsSync(fullPath)) {
|
|
2845
|
+
barLn(yellow(` File not found: ${relPath}`));
|
|
2846
|
+
return;
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
barLn(bold(` ${path.basename(relPath)} — last ${days} days`));
|
|
2850
|
+
barLn();
|
|
2851
|
+
|
|
2852
|
+
try {
|
|
2853
|
+
const log = execSync(
|
|
2854
|
+
`git log --since="${days} days ago" --oneline --follow -- "${relPath}"`,
|
|
2855
|
+
{ cwd: vault, encoding: "utf8", timeout: 10000 }
|
|
2856
|
+
).trim();
|
|
2857
|
+
|
|
2858
|
+
if (!log) {
|
|
2859
|
+
barLn(dim(" No changes in the last " + days + " days."));
|
|
2860
|
+
} else {
|
|
2861
|
+
const commits = log.split("\n");
|
|
2862
|
+
for (const c of commits.slice(0, 15)) {
|
|
2863
|
+
const [hash, ...msg] = c.split(" ");
|
|
2864
|
+
barLn(` ${cyan(hash)} ${msg.join(" ")}`);
|
|
2865
|
+
}
|
|
2866
|
+
if (commits.length > 15) barLn(dim(` ...and ${commits.length - 15} more`));
|
|
2867
|
+
}
|
|
2868
|
+
} catch {
|
|
2869
|
+
barLn(dim(" Not a git repo or no history available."));
|
|
2870
|
+
}
|
|
2871
|
+
barLn();
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
// ─── moveros sync ───────────────────────────────────────────────────────────
|
|
2875
|
+
async function cmdSync(opts) {
|
|
2876
|
+
const vault = resolveVaultPath(opts.vault);
|
|
2877
|
+
if (!vault) { barLn(red("No vault found.")); return; }
|
|
2878
|
+
const apply = opts.rest.includes("--apply");
|
|
2879
|
+
const home = os.homedir();
|
|
2880
|
+
|
|
2881
|
+
barLn(bold(" Agent Sync"));
|
|
2882
|
+
barLn();
|
|
2883
|
+
|
|
2884
|
+
// Read installed agents
|
|
2885
|
+
const cfgPath = path.join(home, ".mover", "config.json");
|
|
2886
|
+
if (!fs.existsSync(cfgPath)) { barLn(yellow(" No config found. Run moveros install first.")); return; }
|
|
2887
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
|
|
2888
|
+
const agents = cfg.agents || [];
|
|
2889
|
+
|
|
2890
|
+
if (agents.length === 0) { barLn(dim(" No agents installed.")); return; }
|
|
2891
|
+
|
|
2892
|
+
// Find bundle dir (where src/ lives)
|
|
2893
|
+
let bundleDir = null;
|
|
2894
|
+
const candidates = [
|
|
2895
|
+
process.cwd(),
|
|
2896
|
+
path.join(vault, "01_Projects", "Mover OS Bundle"),
|
|
2897
|
+
__dirname,
|
|
2898
|
+
];
|
|
2899
|
+
for (const c of candidates) {
|
|
2900
|
+
if (fs.existsSync(path.join(c, "src", "workflows"))) { bundleDir = c; break; }
|
|
2901
|
+
}
|
|
2902
|
+
if (!bundleDir) { barLn(yellow(" Can't find Mover OS source. Run from the bundle directory.")); return; }
|
|
2903
|
+
|
|
2904
|
+
// Compare source hash vs installed for each agent
|
|
2905
|
+
const srcRulesHash = (() => {
|
|
2906
|
+
const p = path.join(bundleDir, "src", "system", "Mover_Global_Rules.md");
|
|
2907
|
+
return fs.existsSync(p) ? crypto.createHash("sha256").update(fs.readFileSync(p)).digest("hex").substring(0, 8) : null;
|
|
2908
|
+
})();
|
|
2909
|
+
|
|
2910
|
+
let staleCount = 0;
|
|
2911
|
+
for (const agentId of agents) {
|
|
2912
|
+
const reg = AGENT_REGISTRY[agentId];
|
|
2913
|
+
if (!reg) continue;
|
|
2914
|
+
|
|
2915
|
+
let status = "current";
|
|
2916
|
+
if (reg.rules) {
|
|
2917
|
+
const dest = reg.rules.dest(vault);
|
|
2918
|
+
if (!fs.existsSync(dest)) {
|
|
2919
|
+
status = "missing";
|
|
2920
|
+
} else if (srcRulesHash) {
|
|
2921
|
+
const installed = crypto.createHash("sha256").update(fs.readFileSync(dest)).digest("hex").substring(0, 8);
|
|
2922
|
+
// Rules get transformed, so direct hash won't match — check by file size as heuristic
|
|
2923
|
+
const srcSize = fs.statSync(path.join(bundleDir, "src", "system", "Mover_Global_Rules.md")).size;
|
|
2924
|
+
const dstSize = fs.statSync(dest).size;
|
|
2925
|
+
if (Math.abs(srcSize - dstSize) > srcSize * 0.1) status = "stale";
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
const icon = status === "current" ? "ok" : status === "stale" ? "warn" : "fail";
|
|
2930
|
+
statusLine(icon, ` ${reg.name}`, status);
|
|
2931
|
+
if (status !== "current") staleCount++;
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
barLn();
|
|
2935
|
+
if (staleCount === 0) {
|
|
2936
|
+
barLn(green(" All agents up to date."));
|
|
2937
|
+
} else if (apply) {
|
|
2938
|
+
barLn(dim(" Updating stale agents..."));
|
|
2939
|
+
const writtenFiles = new Set();
|
|
2940
|
+
const skillOpts = { install: true, categories: null, workflows: null };
|
|
2941
|
+
for (const agentId of agents) {
|
|
2942
|
+
const fn = AGENT_INSTALLERS[agentId];
|
|
2943
|
+
if (fn) fn(bundleDir, vault, skillOpts, writtenFiles, agentId);
|
|
2944
|
+
}
|
|
2945
|
+
barLn(green(` Updated ${staleCount} agent(s).`));
|
|
2946
|
+
} else {
|
|
2947
|
+
barLn(dim(` ${staleCount} agent(s) need updating. Run: moveros sync --apply`));
|
|
2948
|
+
}
|
|
2949
|
+
barLn();
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
// ─── moveros replay ─────────────────────────────────────────────────────────
|
|
2953
|
+
async function cmdReplay(opts) {
|
|
2954
|
+
const vault = resolveVaultPath(opts.vault);
|
|
2955
|
+
if (!vault) { barLn(red("No vault found.")); return; }
|
|
2956
|
+
|
|
2957
|
+
let dateStr = opts.rest.find((a) => a.startsWith("--date="))?.split("=")[1];
|
|
2958
|
+
if (!dateStr) {
|
|
2959
|
+
const now = new Date();
|
|
2960
|
+
dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
2961
|
+
}
|
|
2962
|
+
const month = dateStr.substring(0, 7);
|
|
2963
|
+
const dailyPath = path.join(vault, "02_Areas", "Engine", "Dailies", month, `Daily - ${dateStr}.md`);
|
|
2964
|
+
|
|
2965
|
+
barLn(bold(` Session Replay — ${dateStr}`));
|
|
2966
|
+
barLn();
|
|
2967
|
+
|
|
2968
|
+
if (!fs.existsSync(dailyPath)) {
|
|
2969
|
+
barLn(yellow(` No Daily Note for ${dateStr}.`));
|
|
2970
|
+
return;
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
const daily = fs.readFileSync(dailyPath, "utf8");
|
|
2974
|
+
|
|
2975
|
+
// Extract session log entries
|
|
2976
|
+
const logMatch = daily.match(/##\s*Session Log[\s\S]*?(?=\n## [^#]|\Z)/i);
|
|
2977
|
+
if (!logMatch) {
|
|
2978
|
+
barLn(dim(" No session log found in this Daily Note."));
|
|
2979
|
+
return;
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
// Parse session blocks
|
|
2983
|
+
const sessions = logMatch[0].split(/\n### /).filter(Boolean).slice(1);
|
|
2984
|
+
barLn(dim(` ${sessions.length} session(s) logged`));
|
|
2985
|
+
barLn();
|
|
2986
|
+
|
|
2987
|
+
for (const session of sessions) {
|
|
2988
|
+
const firstLine = session.split("\n")[0];
|
|
2989
|
+
barLn(` ${cyan("###")} ${firstLine}`);
|
|
2990
|
+
const lines = session.split("\n").slice(1, 8);
|
|
2991
|
+
for (const l of lines) {
|
|
2992
|
+
if (l.trim()) barLn(` ${l}`);
|
|
2993
|
+
}
|
|
2994
|
+
barLn();
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
// Task summary
|
|
2998
|
+
const taskSection = daily.match(/##\s*Tasks[\s\S]*?(?=\n##)/i);
|
|
2999
|
+
if (taskSection) {
|
|
3000
|
+
const tasks = taskSection[0].split("\n").filter((l) => /^\s*-\s*\[[ x~]\]/.test(l));
|
|
3001
|
+
const done = tasks.filter((t) => /\[x\]|\[~\]/.test(t)).length;
|
|
3002
|
+
barLn(` ${progressBar(done, tasks.length, 25, "Completion")}`);
|
|
3003
|
+
barLn();
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
// ─── moveros context ────────────────────────────────────────────────────────
|
|
3008
|
+
async function cmdContext(opts) {
|
|
3009
|
+
const vault = resolveVaultPath(opts.vault);
|
|
3010
|
+
if (!vault) { barLn(red("No vault found.")); return; }
|
|
3011
|
+
|
|
3012
|
+
const target = opts.rest[0];
|
|
3013
|
+
const home = os.homedir();
|
|
3014
|
+
|
|
3015
|
+
if (!target) {
|
|
3016
|
+
// Show all agents
|
|
3017
|
+
barLn(bold(" Agent Context Overview"));
|
|
3018
|
+
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
|
+
|
|
3024
|
+
for (const agentId of agents) {
|
|
3025
|
+
const reg = AGENT_REGISTRY[agentId];
|
|
3026
|
+
if (!reg) continue;
|
|
3027
|
+
let totalBytes = 0, fileCount = 0;
|
|
3028
|
+
|
|
3029
|
+
const countDir = (dir) => {
|
|
3030
|
+
if (!fs.existsSync(dir)) return;
|
|
3031
|
+
try {
|
|
3032
|
+
for (const f of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
3033
|
+
if (f.isFile()) { totalBytes += fs.statSync(path.join(dir, f.name)).size; fileCount++; }
|
|
3034
|
+
else if (f.isDirectory()) countDir(path.join(dir, f.name));
|
|
3035
|
+
}
|
|
3036
|
+
} catch {}
|
|
3037
|
+
};
|
|
3038
|
+
|
|
3039
|
+
if (reg.rules) { const rp = reg.rules.dest(vault); if (fs.existsSync(rp)) { totalBytes += fs.statSync(rp).size; fileCount++; } }
|
|
3040
|
+
if (reg.skills) countDir(reg.skills.dest(vault));
|
|
3041
|
+
if (reg.commands) countDir(reg.commands.dest(vault));
|
|
3042
|
+
|
|
3043
|
+
const tokens = Math.round(totalBytes / 4); // rough estimate: 4 bytes per token
|
|
3044
|
+
const kb = (totalBytes / 1024).toFixed(1);
|
|
3045
|
+
barLn(` ${reg.name.padEnd(20)} ${String(fileCount).padStart(3)} files ${String(kb).padStart(6)} KB ~${tokens.toLocaleString()} tokens`);
|
|
3046
|
+
}
|
|
3047
|
+
barLn();
|
|
3048
|
+
return;
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
// Detailed view for specific agent
|
|
3052
|
+
const reg = AGENT_REGISTRY[target] || Object.values(AGENT_REGISTRY).find((r) => r.name.toLowerCase().includes(target.toLowerCase()));
|
|
3053
|
+
if (!reg) { barLn(yellow(` Unknown agent: ${target}`)); return; }
|
|
3054
|
+
|
|
3055
|
+
barLn(bold(` ${reg.name} Context`));
|
|
3056
|
+
barLn();
|
|
3057
|
+
|
|
3058
|
+
if (reg.rules) {
|
|
3059
|
+
const rp = reg.rules.dest(vault);
|
|
3060
|
+
if (fs.existsSync(rp)) {
|
|
3061
|
+
const size = fs.statSync(rp).size;
|
|
3062
|
+
statusLine("ok", "Rules", `${(size / 1024).toFixed(1)} KB ${rp}`);
|
|
3063
|
+
} else {
|
|
3064
|
+
statusLine("fail", "Rules", "not found");
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
if (reg.skills) {
|
|
3069
|
+
const sp = reg.skills.dest(vault);
|
|
3070
|
+
if (fs.existsSync(sp)) {
|
|
3071
|
+
const skills = fs.readdirSync(sp, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
3072
|
+
let totalChars = 0;
|
|
3073
|
+
for (const sk of skills) {
|
|
3074
|
+
const sm = path.join(sp, sk.name, "SKILL.md");
|
|
3075
|
+
if (fs.existsSync(sm)) totalChars += fs.statSync(sm).size;
|
|
3076
|
+
}
|
|
3077
|
+
statusLine("ok", "Skills", `${skills.length} packs ${(totalChars / 1024).toFixed(1)} KB descriptions`);
|
|
3078
|
+
} else {
|
|
3079
|
+
statusLine("info", "Skills", "none installed");
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
if (reg.commands) {
|
|
3084
|
+
const cp = reg.commands.dest(vault);
|
|
3085
|
+
if (fs.existsSync(cp)) {
|
|
3086
|
+
const files = fs.readdirSync(cp).filter((f) => !f.startsWith("."));
|
|
3087
|
+
statusLine("ok", "Commands", `${files.length} files`);
|
|
3088
|
+
} else {
|
|
3089
|
+
statusLine("info", "Commands", "none installed");
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
barLn();
|
|
3093
|
+
}
|
|
3094
|
+
|
|
3095
|
+
// ─── moveros settings ───────────────────────────────────────────────────────
|
|
3096
|
+
async function cmdSettings(opts) {
|
|
3097
|
+
const cfgPath = path.join(os.homedir(), ".mover", "config.json");
|
|
3098
|
+
|
|
3099
|
+
if (!fs.existsSync(cfgPath)) {
|
|
3100
|
+
barLn(yellow(" No config found. Run moveros install first."));
|
|
3101
|
+
return;
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
|
|
3105
|
+
|
|
3106
|
+
// moveros settings set <key> <value>
|
|
3107
|
+
if (opts.rest[0] === "set" && opts.rest[1]) {
|
|
3108
|
+
const key = opts.rest[1];
|
|
3109
|
+
let val = opts.rest.slice(2).join(" ");
|
|
3110
|
+
// Auto-type conversion
|
|
3111
|
+
if (val === "true") val = true;
|
|
3112
|
+
else if (val === "false") val = false;
|
|
3113
|
+
else if (/^\d+$/.test(val)) val = parseInt(val, 10);
|
|
3114
|
+
|
|
3115
|
+
if (!cfg.settings) cfg.settings = {};
|
|
3116
|
+
cfg.settings[key] = val;
|
|
3117
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
|
|
3118
|
+
statusLine("ok", "Set", `${key} = ${JSON.stringify(val)}`);
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
// Show all settings
|
|
3123
|
+
barLn(bold(" Settings"));
|
|
3124
|
+
barLn();
|
|
3125
|
+
barLn(` ${dim("Vault:")} ${cfg.vaultPath || dim("not set")}`);
|
|
3126
|
+
barLn(` ${dim("Key:")} ${cfg.licenseKey ? cyan(cfg.licenseKey.substring(0, 12) + "...") : dim("not set")}`);
|
|
3127
|
+
barLn(` ${dim("Agents:")} ${(cfg.agents || []).join(", ") || dim("none")}`);
|
|
3128
|
+
barLn(` ${dim("Version:")} ${cfg.version || dim("unknown")}`);
|
|
3129
|
+
barLn();
|
|
3130
|
+
if (cfg.settings) {
|
|
3131
|
+
barLn(dim(" Custom:"));
|
|
3132
|
+
for (const [k, v] of Object.entries(cfg.settings)) {
|
|
3133
|
+
barLn(` ${k}: ${JSON.stringify(v)}`);
|
|
3134
|
+
}
|
|
3135
|
+
barLn();
|
|
3136
|
+
}
|
|
3137
|
+
barLn(dim(" Edit: moveros settings set <key> <value>"));
|
|
3138
|
+
barLn(dim(` File: ${cfgPath}`));
|
|
3139
|
+
barLn();
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
// ─── moveros backup ─────────────────────────────────────────────────────────
|
|
3143
|
+
async function cmdBackup(opts) {
|
|
3144
|
+
const vault = resolveVaultPath(opts.vault);
|
|
3145
|
+
if (!vault) { barLn(red("No vault found.")); return; }
|
|
3146
|
+
const home = os.homedir();
|
|
3147
|
+
|
|
3148
|
+
barLn(bold(" Backup"));
|
|
3149
|
+
barLn();
|
|
3150
|
+
|
|
3151
|
+
const items = [
|
|
3152
|
+
{ id: "engine", name: "Engine files", tier: "Identity, Strategy, Goals, Context, etc." },
|
|
3153
|
+
{ id: "areas", name: "Full Areas folder", tier: "Everything in 02_Areas/" },
|
|
3154
|
+
{ id: "agents", name: "Agent configs", tier: "Rules, skills, commands from all agents" },
|
|
3155
|
+
];
|
|
3156
|
+
|
|
3157
|
+
const choices = await interactiveSelect(items, { multi: true, preSelected: ["engine"] });
|
|
3158
|
+
if (choices.length === 0) { barLn(dim(" Cancelled.")); return; }
|
|
3159
|
+
|
|
3160
|
+
const now = new Date();
|
|
3161
|
+
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")}`;
|
|
3162
|
+
const backupDir = path.join(vault, "04_Archives", `Backup_${ts}`);
|
|
3163
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
3164
|
+
|
|
3165
|
+
const manifest = { timestamp: now.toISOString(), type: "manual", trigger: "moveros backup", categories: choices, files: {} };
|
|
3166
|
+
|
|
3167
|
+
if (choices.includes("engine")) {
|
|
3168
|
+
const engineDir = path.join(vault, "02_Areas", "Engine");
|
|
3169
|
+
const engBackup = path.join(backupDir, "engine");
|
|
3170
|
+
fs.mkdirSync(engBackup, { recursive: true });
|
|
3171
|
+
let count = 0;
|
|
3172
|
+
manifest.files.engine = [];
|
|
3173
|
+
if (fs.existsSync(engineDir)) {
|
|
3174
|
+
for (const f of fs.readdirSync(engineDir).filter((f) => fs.statSync(path.join(engineDir, f)).isFile())) {
|
|
3175
|
+
fs.copyFileSync(path.join(engineDir, f), path.join(engBackup, f));
|
|
3176
|
+
manifest.files.engine.push(f);
|
|
3177
|
+
count++;
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
statusLine("ok", "Engine", `${count} files`);
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
if (choices.includes("areas")) {
|
|
3184
|
+
const areasBackup = path.join(backupDir, "areas");
|
|
3185
|
+
copyDirRecursive(path.join(vault, "02_Areas"), areasBackup);
|
|
3186
|
+
manifest.files.areas = true;
|
|
3187
|
+
statusLine("ok", "Areas", "full copy");
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
if (choices.includes("agents")) {
|
|
3191
|
+
const cfgPath = path.join(home, ".mover", "config.json");
|
|
3192
|
+
const agents = fs.existsSync(cfgPath) ? (JSON.parse(fs.readFileSync(cfgPath, "utf8")).agents || []) : [];
|
|
3193
|
+
const agentsBackup = path.join(backupDir, "agents");
|
|
3194
|
+
manifest.files.agents = {};
|
|
3195
|
+
let agentCount = 0;
|
|
3196
|
+
|
|
3197
|
+
for (const agentId of agents) {
|
|
3198
|
+
const reg = AGENT_REGISTRY[agentId];
|
|
3199
|
+
if (!reg) continue;
|
|
3200
|
+
const agentDir = path.join(agentsBackup, agentId);
|
|
3201
|
+
let backed = false;
|
|
3202
|
+
|
|
3203
|
+
const safeCopy = (src, label) => {
|
|
3204
|
+
try {
|
|
3205
|
+
if (!fs.existsSync(src)) return;
|
|
3206
|
+
const stat = fs.statSync(src);
|
|
3207
|
+
const dst = path.join(agentDir, label);
|
|
3208
|
+
if (stat.isDirectory()) { copyDirRecursive(src, dst); }
|
|
3209
|
+
else { fs.mkdirSync(agentDir, { recursive: true }); fs.copyFileSync(src, dst); }
|
|
3210
|
+
backed = true;
|
|
3211
|
+
} catch {}
|
|
3212
|
+
};
|
|
3213
|
+
|
|
3214
|
+
if (reg.rules) safeCopy(reg.rules.dest(vault), "rules");
|
|
3215
|
+
if (reg.skills) safeCopy(reg.skills.dest(vault), "skills");
|
|
3216
|
+
if (reg.commands) safeCopy(reg.commands.dest(vault), "commands");
|
|
3217
|
+
|
|
3218
|
+
if (backed) { agentCount++; manifest.files.agents[agentId] = true; }
|
|
3219
|
+
}
|
|
3220
|
+
statusLine("ok", "Agents", `${agentCount} agent(s)`);
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
// Write manifest
|
|
3224
|
+
fs.writeFileSync(path.join(backupDir, ".backup-manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
3225
|
+
|
|
3226
|
+
barLn();
|
|
3227
|
+
barLn(green(` Backup saved to 04_Archives/Backup_${ts}/`));
|
|
3228
|
+
|
|
3229
|
+
// Retention check
|
|
3230
|
+
const archivesDir = path.join(vault, "04_Archives");
|
|
3231
|
+
const backups = fs.readdirSync(archivesDir)
|
|
3232
|
+
.filter((d) => d.startsWith("Backup_") || d.startsWith("Engine_Backup_") || d.startsWith("Areas_Backup_") || d.startsWith("Agent_Backup_"))
|
|
3233
|
+
.sort();
|
|
3234
|
+
if (backups.length > 5) {
|
|
3235
|
+
barLn(dim(` ${backups.length} backups found. Consider cleaning old ones.`));
|
|
3236
|
+
}
|
|
3237
|
+
barLn();
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
// ─── moveros restore ────────────────────────────────────────────────────────
|
|
3241
|
+
async function cmdRestore(opts) {
|
|
3242
|
+
const vault = resolveVaultPath(opts.vault);
|
|
3243
|
+
if (!vault) { barLn(red("No vault found.")); return; }
|
|
3244
|
+
|
|
3245
|
+
const archivesDir = path.join(vault, "04_Archives");
|
|
3246
|
+
if (!fs.existsSync(archivesDir)) { barLn(yellow(" No archives found.")); return; }
|
|
3247
|
+
|
|
3248
|
+
const backups = fs.readdirSync(archivesDir)
|
|
3249
|
+
.filter((d) => {
|
|
3250
|
+
const full = path.join(archivesDir, d);
|
|
3251
|
+
return fs.statSync(full).isDirectory() && (d.startsWith("Backup_") || d.startsWith("Engine_Backup_"));
|
|
3252
|
+
})
|
|
3253
|
+
.sort()
|
|
3254
|
+
.reverse();
|
|
3255
|
+
|
|
3256
|
+
if (backups.length === 0) { barLn(yellow(" No backups found.")); return; }
|
|
3257
|
+
|
|
3258
|
+
barLn(bold(" Restore"));
|
|
3259
|
+
barLn();
|
|
3260
|
+
|
|
3261
|
+
// List available backups
|
|
3262
|
+
const items = backups.slice(0, 10).map((b, i) => {
|
|
3263
|
+
const mPath = path.join(archivesDir, b, ".backup-manifest.json");
|
|
3264
|
+
let detail = "";
|
|
3265
|
+
if (fs.existsSync(mPath)) {
|
|
3266
|
+
try {
|
|
3267
|
+
const m = JSON.parse(fs.readFileSync(mPath, "utf8"));
|
|
3268
|
+
detail = (m.categories || []).join(", ");
|
|
3269
|
+
} catch {}
|
|
3270
|
+
}
|
|
3271
|
+
return { id: b, name: b, tier: detail || dim("legacy backup") };
|
|
3272
|
+
});
|
|
3273
|
+
|
|
3274
|
+
const selected = await interactiveSelect(items, { multi: false });
|
|
3275
|
+
if (!selected) { barLn(dim(" Cancelled.")); return; }
|
|
3276
|
+
|
|
3277
|
+
const backupPath = path.join(archivesDir, selected);
|
|
3278
|
+
|
|
3279
|
+
// Check for engine dir in backup
|
|
3280
|
+
const engPath = path.join(backupPath, "engine");
|
|
3281
|
+
if (fs.existsSync(engPath)) {
|
|
3282
|
+
const engineDir = path.join(vault, "02_Areas", "Engine");
|
|
3283
|
+
let restored = 0;
|
|
3284
|
+
for (const f of fs.readdirSync(engPath).filter((f) => fs.statSync(path.join(engPath, f)).isFile())) {
|
|
3285
|
+
fs.copyFileSync(path.join(engPath, f), path.join(engineDir, f));
|
|
3286
|
+
restored++;
|
|
3287
|
+
}
|
|
3288
|
+
statusLine("ok", "Engine", `${restored} files restored`);
|
|
3289
|
+
} else {
|
|
3290
|
+
// Legacy backup format (files directly in backup dir)
|
|
3291
|
+
const engineDir = path.join(vault, "02_Areas", "Engine");
|
|
3292
|
+
let restored = 0;
|
|
3293
|
+
for (const f of fs.readdirSync(backupPath).filter((f) => f.endsWith(".md") && f !== ".backup-manifest.json")) {
|
|
3294
|
+
fs.copyFileSync(path.join(backupPath, f), path.join(engineDir, f));
|
|
3295
|
+
restored++;
|
|
3296
|
+
}
|
|
3297
|
+
if (restored > 0) statusLine("ok", "Engine", `${restored} files restored`);
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
barLn();
|
|
3301
|
+
barLn(green(` Restored from ${selected}`));
|
|
3302
|
+
barLn();
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
// ─── moveros test (dev only) ────────────────────────────────────────────────
|
|
3306
|
+
async function cmdTest(opts) { barLn(yellow("moveros test — not yet implemented.")); }
|
|
3307
|
+
|
|
3308
|
+
// ─── Interactive Main Menu ────────────────────────────────────────────────────
|
|
3309
|
+
async function cmdMainMenu() {
|
|
3310
|
+
const categories = [
|
|
3311
|
+
{ header: "Setup", cmds: ["install", "update"] },
|
|
3312
|
+
{ header: "Dashboard", cmds: ["pulse", "replay", "diff"] },
|
|
3313
|
+
{ header: "Agents", cmds: ["doctor", "sync", "context", "warm"] },
|
|
3314
|
+
{ header: "Capture", cmds: ["capture", "who"] },
|
|
3315
|
+
{ header: "System", cmds: ["settings", "backup", "restore"] },
|
|
3316
|
+
];
|
|
3317
|
+
|
|
3318
|
+
const menuItems = [];
|
|
3319
|
+
for (const cat of categories) {
|
|
3320
|
+
for (const cmd of cat.cmds) {
|
|
3321
|
+
const meta = CLI_COMMANDS[cmd];
|
|
3322
|
+
if (!meta || meta.hidden) continue;
|
|
3323
|
+
menuItems.push({
|
|
3324
|
+
id: cmd,
|
|
3325
|
+
name: `${cmd.padEnd(12)} ${dim(meta.desc)}`,
|
|
3326
|
+
tier: `${cat.header}`,
|
|
3327
|
+
});
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
question(`${bold("moveros")} ${dim("— choose a command")}`);
|
|
3332
|
+
barLn();
|
|
3333
|
+
const choice = await interactiveSelect(menuItems);
|
|
3334
|
+
if (!choice) {
|
|
3335
|
+
outro(dim("Cancelled."));
|
|
3336
|
+
process.exit(0);
|
|
3337
|
+
}
|
|
3338
|
+
return choice;
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
// ─── Vault Resolution Helper ─────────────────────────────────────────────────
|
|
3342
|
+
function resolveVaultPath(explicitVault) {
|
|
3343
|
+
if (explicitVault) {
|
|
3344
|
+
let v = explicitVault;
|
|
3345
|
+
if (v.startsWith("~")) v = path.join(os.homedir(), v.slice(1));
|
|
3346
|
+
return path.resolve(v);
|
|
3347
|
+
}
|
|
3348
|
+
// Try config.json
|
|
3349
|
+
const cfgPath = path.join(os.homedir(), ".mover", "config.json");
|
|
3350
|
+
if (fs.existsSync(cfgPath)) {
|
|
3351
|
+
try {
|
|
3352
|
+
const v = JSON.parse(fs.readFileSync(cfgPath, "utf8")).vaultPath;
|
|
3353
|
+
if (v && fs.existsSync(v)) return v;
|
|
3354
|
+
} catch {}
|
|
3355
|
+
}
|
|
3356
|
+
// Try Obsidian detection
|
|
3357
|
+
const obsVaults = detectObsidianVaults();
|
|
3358
|
+
return obsVaults.find((p) => fs.existsSync(path.join(p, ".mover-version"))) || null;
|
|
3359
|
+
}
|
|
3360
|
+
|
|
1931
3361
|
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
1932
3362
|
async function main() {
|
|
1933
3363
|
const opts = parseArgs();
|
|
@@ -1935,9 +3365,27 @@ async function main() {
|
|
|
1935
3365
|
const startTime = Date.now();
|
|
1936
3366
|
|
|
1937
3367
|
// ── Intro ──
|
|
1938
|
-
printHeader();
|
|
3368
|
+
await printHeader();
|
|
3369
|
+
|
|
3370
|
+
// ── Route: no command → interactive menu ──
|
|
3371
|
+
if (!opts.command) {
|
|
3372
|
+
opts.command = await cmdMainMenu();
|
|
3373
|
+
}
|
|
1939
3374
|
|
|
1940
|
-
// ──
|
|
3375
|
+
// ── Route: CLI commands that don't need pre-flight ──
|
|
3376
|
+
const lightCommands = ["pulse", "warm", "capture", "who", "diff", "sync", "replay", "context", "settings", "backup", "restore", "doctor", "test"];
|
|
3377
|
+
if (lightCommands.includes(opts.command)) {
|
|
3378
|
+
const handler = CLI_HANDLERS[opts.command];
|
|
3379
|
+
if (handler) {
|
|
3380
|
+
await handler(opts);
|
|
3381
|
+
} else {
|
|
3382
|
+
barLn(yellow(`Command '${opts.command}' is not yet implemented.`));
|
|
3383
|
+
barLn(dim("Coming soon in a future update."));
|
|
3384
|
+
}
|
|
3385
|
+
return;
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
// ── Pre-flight (install + update only) ──
|
|
1941
3389
|
barLn(gray("Pre-flight"));
|
|
1942
3390
|
barLn();
|
|
1943
3391
|
const checks = preflight();
|
|
@@ -1952,8 +3400,8 @@ async function main() {
|
|
|
1952
3400
|
process.exit(1);
|
|
1953
3401
|
}
|
|
1954
3402
|
|
|
1955
|
-
// ── Headless quick update
|
|
1956
|
-
if (opts.update) {
|
|
3403
|
+
// ── Headless quick update ──
|
|
3404
|
+
if (opts.command === "update") {
|
|
1957
3405
|
// Validate stored key
|
|
1958
3406
|
let updateKey = opts.key;
|
|
1959
3407
|
if (!updateKey) {
|
|
@@ -2028,7 +3476,6 @@ async function main() {
|
|
|
2028
3476
|
// Apply all changes
|
|
2029
3477
|
barLn(bold("Updating..."));
|
|
2030
3478
|
barLn();
|
|
2031
|
-
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2032
3479
|
|
|
2033
3480
|
// Vault structure
|
|
2034
3481
|
createVaultStructure(vaultPath);
|
|
@@ -2043,10 +3490,7 @@ async function main() {
|
|
|
2043
3490
|
const fn = AGENT_INSTALLERS[agent.id];
|
|
2044
3491
|
if (!fn) continue;
|
|
2045
3492
|
const sp = spinner(agent.name);
|
|
2046
|
-
const
|
|
2047
|
-
const steps = usesWrittenFiles
|
|
2048
|
-
? fn(bundleDir, vaultPath, skillOpts, writtenFiles)
|
|
2049
|
-
: fn(bundleDir, vaultPath, skillOpts);
|
|
3493
|
+
const steps = fn(bundleDir, vaultPath, skillOpts, writtenFiles, agent.id);
|
|
2050
3494
|
await sleep(200);
|
|
2051
3495
|
if (steps.length > 0) {
|
|
2052
3496
|
sp.stop(`${agent.name} ${dim(steps.join(", "))}`);
|
|
@@ -2080,6 +3524,7 @@ async function main() {
|
|
|
2080
3524
|
}
|
|
2081
3525
|
|
|
2082
3526
|
if (!key) {
|
|
3527
|
+
let validated = false;
|
|
2083
3528
|
let attempts = 0;
|
|
2084
3529
|
while (attempts < 3) {
|
|
2085
3530
|
key = await textInput({
|
|
@@ -2093,6 +3538,7 @@ async function main() {
|
|
|
2093
3538
|
if (valid) {
|
|
2094
3539
|
sp.stop(green("License verified"));
|
|
2095
3540
|
await activateKey(key);
|
|
3541
|
+
validated = true;
|
|
2096
3542
|
break;
|
|
2097
3543
|
}
|
|
2098
3544
|
|
|
@@ -2105,7 +3551,7 @@ async function main() {
|
|
|
2105
3551
|
}
|
|
2106
3552
|
}
|
|
2107
3553
|
|
|
2108
|
-
if (!
|
|
3554
|
+
if (!validated) {
|
|
2109
3555
|
barLn(red("Invalid license key."));
|
|
2110
3556
|
barLn();
|
|
2111
3557
|
barLn(dim("Get a key at https://moveros.dev"));
|
|
@@ -2309,10 +3755,9 @@ async function main() {
|
|
|
2309
3755
|
{ src: path.join(home, ".claude", "hooks"), label: "hooks" },
|
|
2310
3756
|
],
|
|
2311
3757
|
cursor: [
|
|
2312
|
-
{ src: path.join(
|
|
3758
|
+
{ src: path.join(vaultPath, ".cursor", "rules"), label: "rules" },
|
|
2313
3759
|
{ src: path.join(home, ".cursor", "commands"), label: "commands" },
|
|
2314
3760
|
{ src: path.join(home, ".cursor", "skills"), label: "skills" },
|
|
2315
|
-
{ src: path.join(vaultPath, ".cursorrules"), label: ".cursorrules" },
|
|
2316
3761
|
],
|
|
2317
3762
|
cline: [
|
|
2318
3763
|
{ src: path.join(vaultPath, ".clinerules"), label: ".clinerules" },
|
|
@@ -2320,10 +3765,13 @@ async function main() {
|
|
|
2320
3765
|
],
|
|
2321
3766
|
windsurf: [
|
|
2322
3767
|
{ src: path.join(vaultPath, ".windsurfrules"), label: ".windsurfrules" },
|
|
3768
|
+
{ src: path.join(vaultPath, ".windsurf", "rules"), label: "rules" },
|
|
3769
|
+
{ src: path.join(vaultPath, ".windsurf", "workflows"), label: "workflows" },
|
|
2323
3770
|
{ src: path.join(home, ".windsurf", "skills"), label: "skills" },
|
|
2324
3771
|
],
|
|
2325
3772
|
"gemini-cli": [
|
|
2326
3773
|
{ src: path.join(home, ".gemini", "GEMINI.md"), label: "GEMINI.md" },
|
|
3774
|
+
{ src: path.join(home, ".gemini", "commands"), label: "commands" },
|
|
2327
3775
|
{ src: path.join(home, ".gemini", "skills"), label: "skills" },
|
|
2328
3776
|
],
|
|
2329
3777
|
antigravity: [
|
|
@@ -2333,19 +3781,42 @@ async function main() {
|
|
|
2333
3781
|
],
|
|
2334
3782
|
copilot: [
|
|
2335
3783
|
{ src: path.join(vaultPath, ".github", "copilot-instructions.md"), label: "copilot-instructions.md" },
|
|
3784
|
+
{ src: path.join(vaultPath, ".github", "prompts"), label: "prompts" },
|
|
2336
3785
|
{ src: path.join(vaultPath, ".github", "skills"), label: "skills" },
|
|
2337
3786
|
],
|
|
2338
3787
|
codex: [
|
|
2339
|
-
{ src: path.join(home, ".codex", "
|
|
3788
|
+
{ src: path.join(home, ".codex", "AGENTS.md"), label: "AGENTS.md" },
|
|
2340
3789
|
{ src: path.join(home, ".codex", "skills"), label: "skills" },
|
|
2341
3790
|
],
|
|
2342
|
-
openclaw: [
|
|
2343
|
-
{ src: path.join(home, ".openclaw", "workspace"), label: "workspace" },
|
|
2344
|
-
],
|
|
2345
3791
|
"roo-code": [
|
|
2346
3792
|
{ src: path.join(vaultPath, ".roo", "rules"), label: "rules" },
|
|
3793
|
+
{ src: path.join(vaultPath, ".roo", "commands"), label: "commands" },
|
|
2347
3794
|
{ src: path.join(vaultPath, ".roo", "skills"), label: "skills" },
|
|
2348
3795
|
],
|
|
3796
|
+
"amazon-q": [
|
|
3797
|
+
{ src: path.join(vaultPath, ".amazonq", "rules"), label: "rules" },
|
|
3798
|
+
{ src: path.join(home, ".aws", "amazonq", "cli-agents"), label: "cli-agents" },
|
|
3799
|
+
],
|
|
3800
|
+
"kilo-code": [
|
|
3801
|
+
{ src: path.join(vaultPath, ".kilocode", "rules"), label: "rules" },
|
|
3802
|
+
{ src: path.join(vaultPath, ".kilocode", "skills"), label: "skills" },
|
|
3803
|
+
{ src: path.join(vaultPath, ".kilocode", "commands"), label: "commands" },
|
|
3804
|
+
],
|
|
3805
|
+
amp: [
|
|
3806
|
+
{ src: path.join(vaultPath, "AGENTS.md"), label: "AGENTS.md" },
|
|
3807
|
+
{ src: path.join(vaultPath, ".agents", "skills"), label: "skills" },
|
|
3808
|
+
],
|
|
3809
|
+
"continue": [
|
|
3810
|
+
{ src: path.join(vaultPath, ".continue", "rules"), label: "rules" },
|
|
3811
|
+
{ src: path.join(vaultPath, ".continue", "prompts"), label: "prompts" },
|
|
3812
|
+
],
|
|
3813
|
+
opencode: [
|
|
3814
|
+
{ src: path.join(vaultPath, "AGENTS.md"), label: "AGENTS.md" },
|
|
3815
|
+
{ src: path.join(vaultPath, ".opencode", "agents"), label: "agents" },
|
|
3816
|
+
],
|
|
3817
|
+
aider: [
|
|
3818
|
+
{ src: path.join(vaultPath, "CONVENTIONS.md"), label: "CONVENTIONS.md" },
|
|
3819
|
+
],
|
|
2349
3820
|
};
|
|
2350
3821
|
|
|
2351
3822
|
let agentsBacked = 0;
|
|
@@ -2566,7 +4037,6 @@ async function main() {
|
|
|
2566
4037
|
barLn();
|
|
2567
4038
|
|
|
2568
4039
|
let totalSteps = 0;
|
|
2569
|
-
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2570
4040
|
|
|
2571
4041
|
// 0. Legacy cleanup (remove files from older installer versions)
|
|
2572
4042
|
const legacyPaths = [
|
|
@@ -2621,25 +4091,30 @@ async function main() {
|
|
|
2621
4091
|
const writtenFiles = new Set(); // Track shared files to avoid double-writes (e.g. GEMINI.md)
|
|
2622
4092
|
const skillOpts = { install: installSkills, categories: selectedCategories, statusLine: installStatusLine, workflows: selectedWorkflows, skipHooks, skipRules, skipTemplates };
|
|
2623
4093
|
for (const agent of selectedAgents) {
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
const
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
4094
|
+
// Expand selections to targets (e.g. "gemini-cli" → ["gemini-cli", "antigravity"])
|
|
4095
|
+
const sel = AGENT_SELECTIONS.find((s) => s.id === agent.id);
|
|
4096
|
+
const targets = sel ? sel.targets : [agent.id];
|
|
4097
|
+
for (const targetId of targets) {
|
|
4098
|
+
const fn = AGENT_INSTALLERS[targetId];
|
|
4099
|
+
if (!fn) continue;
|
|
4100
|
+
const targetReg = AGENT_REGISTRY[targetId];
|
|
4101
|
+
const displayName = targetReg ? targetReg.name : agent.name;
|
|
4102
|
+
sp = spinner(displayName);
|
|
4103
|
+
const steps = fn(bundleDir, vaultPath, skillOpts, writtenFiles, targetId);
|
|
4104
|
+
await sleep(250);
|
|
4105
|
+
if (steps.length > 0) {
|
|
4106
|
+
sp.stop(`${displayName} ${dim(steps.join(", "))}`);
|
|
4107
|
+
totalSteps++;
|
|
4108
|
+
} else {
|
|
4109
|
+
sp.stop(`${displayName} ${dim("configured")}`);
|
|
4110
|
+
totalSteps++;
|
|
4111
|
+
}
|
|
2638
4112
|
}
|
|
2639
4113
|
}
|
|
2640
4114
|
|
|
2641
|
-
// 5. AGENTS.md —
|
|
2642
|
-
const
|
|
4115
|
+
// 5. AGENTS.md at vault root — for agents that share it (Codex writes to ~/.codex/)
|
|
4116
|
+
const agentsMdAtRoot = ["opencode", "amp"];
|
|
4117
|
+
const needsAgentsMd = selectedAgents.some((a) => agentsMdAtRoot.includes(a.id));
|
|
2643
4118
|
if (needsAgentsMd) {
|
|
2644
4119
|
fs.writeFileSync(path.join(vaultPath, "AGENTS.md"), generateAgentsMd(), "utf8");
|
|
2645
4120
|
sp = spinner("AGENTS.md");
|