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.
Files changed (2) hide show
  1. package/install.js +1642 -167
  2. 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
- function printHeader() {
98
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
99
+
100
+ async function printHeader(animate = IS_TTY) {
98
101
  ln();
99
- for (const line of LOGO) {
100
- ln(gradient(line));
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: "", update: false };
560
+ const opts = { command: "", vault: "", key: "", rest: [] };
561
+
479
562
  for (let i = 0; i < args.length; i++) {
480
- if (args[i] === "--vault" && args[i + 1]) opts.vault = args[++i];
481
- else if (args[i] === "--key" && args[i + 1]) opts.key = args[++i];
482
- else if (args[i] === "--update" || args[i] === "-u") opts.update = true;
483
- else if (args[i] === "--help" || args[i] === "-h") {
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(` ${bold("mover os")} ${dim("installer")}`);
573
+ ln(` ${dim("Usage")} moveros [command] [options]`);
486
574
  ln();
487
- ln(` ${dim("Usage")} npx moveros`);
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 (auto-detect vault + agents, no prompts)`);
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
- const AGENTS = [
918
- {
919
- id: "claude-code",
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: "Full integration — rules, 22 commands, skills, 6 hooks",
922
- detect: () => cmdExists("claude") || fs.existsSync(path.join(os.homedir(), ".claude")),
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: "Rules, commands, skills, hooks",
928
- detect: () => fs.existsSync(path.join(os.homedir(), ".cursor")) || cmdExists("cursor"),
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: "Rules, skills, hooks",
934
- detect: () => globDirExists(path.join(os.homedir(), ".vscode", "extensions"), "saoudrizwan.claude-dev-*"),
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: "Rules, skills, workflows",
940
- detect: () => fs.existsSync(path.join(os.homedir(), ".windsurf")) || cmdExists("windsurf"),
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: "Rules, skills, commands",
946
- detect: () => cmdExists("gemini") || fs.existsSync(path.join(os.homedir(), ".gemini", "settings.json")),
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: "Rules, skills",
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
- id: "codex",
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: "Rules, skills",
958
- detect: () => cmdExists("codex") || fs.existsSync(path.join(os.homedir(), ".codex")),
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
- // Hidden agents: install functions preserved but not shown in selection UI.
961
- // Vault-root AGENTS.md provides basic rules for these agents.
962
- {
963
- id: "antigravity",
1150
+ "antigravity": {
964
1151
  name: "Antigravity",
965
- tier: "Rules, skills, workflows",
966
- detect: () => fs.existsSync(path.join(os.homedir(), ".gemini", "antigravity")),
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
- // Hidden agents: install functions preserved but not shown in selection UI.
969
- {
970
- id: "openclaw",
971
- name: "OpenClaw",
972
- tier: "Rules, skills",
973
- hidden: true,
974
- detect: () => fs.existsSync(path.join(os.homedir(), ".openclaw")) || cmdExists("openclaw"),
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: "Rules, skills",
980
- hidden: true,
981
- detect: () => globDirExists(path.join(os.homedir(), ".vscode", "extensions"), "rooveterinaryinc.roo-cline-*"),
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
- > You are not a chatbot. You are a co-founder who happens to live inside a terminal.
1440
+ ## CLI Utility
1195
1441
 
1196
- ## Core Truths
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
- if (vaultPath) {
1678
- if (installRules(bundleDir, path.join(vaultPath, ".cursorrules"), "cursor")) steps.push("rules");
1679
- }
1680
-
1681
- const cursorRulesDir = path.join(home, ".cursor", "rules");
1682
- fs.mkdirSync(cursorRulesDir, { recursive: true });
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
- linkOrCopy(src, path.join(cursorRulesDir, "mover-os.mdc"));
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 content, not raw Global Rules (too large, wrong format)
1731
- fs.writeFileSync(path.join(codexDir, "instructions.md"), generateAgentsMd(), "utf8");
1732
- steps.push("rules");
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
- if (installRules(bundleDir, path.join(vaultPath, ".windsurfrules"), "windsurf")) steps.push("rules");
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
- function installOpenClaw(bundleDir, vaultPath, skillOpts) {
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
- // ── Pre-flight ──
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 (--update flag) ──
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 usesWrittenFiles = agent.id === "antigravity" || agent.id === "gemini-cli";
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 (!await validateKey(key)) {
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(home, ".cursor", "rules"), label: "rules" },
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", "instructions.md"), label: "instructions.md" },
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
- const fn = AGENT_INSTALLERS[agent.id];
2625
- if (!fn) continue;
2626
- sp = spinner(agent.name);
2627
- const usesWrittenFiles = agent.id === "antigravity" || agent.id === "gemini-cli";
2628
- const steps = usesWrittenFiles
2629
- ? fn(bundleDir, vaultPath, skillOpts, writtenFiles)
2630
- : fn(bundleDir, vaultPath, skillOpts);
2631
- await sleep(250);
2632
- if (steps.length > 0) {
2633
- sp.stop(`${agent.name} ${dim(steps.join(", "))}`);
2634
- totalSteps++;
2635
- } else {
2636
- sp.stop(`${agent.name} ${dim("configured")}`);
2637
- totalSteps++;
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 — only for agents that need it (consolidated write)
2642
- const needsAgentsMd = selectedAgents.some((a) => ["codex"].includes(a.id));
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");