mover-os 4.3.1 → 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 +1597 -166
  2. package/package.json +1 -1
package/install.js CHANGED
@@ -95,10 +95,18 @@ const LOGO = [
95
95
  " ╚═════╝ ╚══════╝",
96
96
  ];
97
97
 
98
- function printHeader() {
98
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
99
+
100
+ async function printHeader(animate = IS_TTY) {
99
101
  ln();
100
- for (const line of LOGO) {
101
- 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));
102
110
  }
103
111
  ln();
104
112
  ln(` ${dim(`v${VERSION}`)} ${gray("the agentic operating system for obsidian")}`);
@@ -107,6 +115,60 @@ function printHeader() {
107
115
  ln();
108
116
  }
109
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
+
110
172
  // ─── Clack-style frame ──────────────────────────────────────────────────────
111
173
  const BAR_COLOR = S.cyan;
112
174
  const bar = () => w(`${BAR_COLOR}│${S.reset}`);
@@ -474,27 +536,65 @@ async function downloadPayload(key) {
474
536
  }
475
537
 
476
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
+
477
558
  function parseArgs() {
478
559
  const args = process.argv.slice(2);
479
- const opts = { vault: "", key: "", update: false };
560
+ const opts = { command: "", vault: "", key: "", rest: [] };
561
+
480
562
  for (let i = 0; i < args.length; i++) {
481
- if (args[i] === "--vault" && args[i + 1]) opts.vault = args[++i];
482
- else if (args[i] === "--key" && args[i + 1]) opts.key = args[++i];
483
- else if (args[i] === "--update" || args[i] === "-u") opts.update = true;
484
- 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") {
485
570
  ln();
486
- ln(` ${bold("mover os")} ${dim("installer")}`);
571
+ ln(` ${bold("moveros")} ${dim("— the Mover OS companion CLI")}`);
487
572
  ln();
488
- ln(` ${dim("Usage")} npx moveros`);
573
+ ln(` ${dim("Usage")} moveros [command] [options]`);
574
+ ln();
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
+ }
489
580
  ln();
490
581
  ln(` ${dim("Options")}`);
491
582
  ln(` --key KEY License key (skip interactive prompt)`);
492
583
  ln(` --vault PATH Obsidian vault path (skip detection)`);
493
- 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")}`);
494
587
  ln();
495
588
  process.exit(0);
496
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
+ }
497
596
  }
597
+
498
598
  return opts;
499
599
  }
500
600
 
@@ -915,74 +1015,225 @@ async function runUninstall(vaultPath) {
915
1015
  }
916
1016
 
917
1017
  // ─── Agent definitions ──────────────────────────────────────────────────────
918
- const AGENTS = [
919
- {
920
- 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": {
921
1038
  name: "Claude Code",
922
- tier: "Full integration — rules, 22 commands, skills, 6 hooks",
923
- 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" },
924
1046
  },
925
- {
926
- id: "cursor",
1047
+ "cursor": {
927
1048
  name: "Cursor",
928
- tier: "Rules, commands, skills, hooks",
929
- 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") },
930
1061
  },
931
- {
932
- id: "cline",
1062
+ "cline": {
933
1063
  name: "Cline",
934
- tier: "Rules, skills, hooks",
935
- 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,
936
1071
  },
937
- {
938
- id: "windsurf",
1072
+ "windsurf": {
939
1073
  name: "Windsurf",
940
- tier: "Rules, skills, workflows",
941
- 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") },
942
1085
  },
943
- {
944
- id: "gemini-cli",
1086
+ "gemini-cli": {
945
1087
  name: "Gemini CLI",
946
- tier: "Rules, skills, commands",
947
- 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") },
948
1096
  },
949
- {
950
- id: "copilot",
1097
+ "copilot": {
951
1098
  name: "GitHub Copilot",
952
- tier: "Rules, skills",
1099
+ tier: "full",
1100
+ tierDesc: "Rules, .prompt.md commands, skills",
953
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,
954
1106
  },
955
- {
956
- 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": {
957
1141
  name: "Codex",
958
- tier: "Rules, skills",
959
- 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,
960
1149
  },
961
- // Hidden agents: install functions preserved but not shown in selection UI.
962
- // Vault-root AGENTS.md provides basic rules for these agents.
963
- {
964
- id: "antigravity",
1150
+ "antigravity": {
965
1151
  name: "Antigravity",
966
- tier: "Rules, skills, workflows",
967
- 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,
968
1160
  },
969
- // Hidden agents: install functions preserved but not shown in selection UI.
970
- {
971
- id: "openclaw",
972
- name: "OpenClaw",
973
- tier: "Rules, skills",
974
- hidden: true,
975
- 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,
976
1171
  },
977
- {
978
- id: "roo-code",
1172
+ "roo-code": {
979
1173
  name: "Roo Code",
980
- tier: "Rules, skills",
981
- hidden: true,
982
- 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,
983
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" },
984
1227
  ];
985
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
+
986
1237
  // ─── Utility functions ──────────────────────────────────────────────────────
987
1238
  function cmdExists(cmd) {
988
1239
  try {
@@ -1185,41 +1436,10 @@ Sunday: /review-week first
1185
1436
  New project: /ignite → [WORK]
1186
1437
  Stuck: /debug-resistance
1187
1438
  \`\`\`
1188
- `;
1189
- }
1190
-
1191
- // ─── SOUL.md generator (OpenClaw) ───────────────────────────────────────────
1192
- function generateSoulMd() {
1193
- return `# SOUL.md — Mover OS
1194
-
1195
- > You are not a chatbot. You are a co-founder who happens to live inside a terminal.
1196
-
1197
- ## Core Truths
1198
-
1199
- - Be genuinely helpful, not performatively helpful. If the answer is "don't build that," say it.
1200
- - Have opinions. "It depends" is a cop-out. Pick a direction, defend it, change your mind if the user makes a better case.
1201
- - Be resourceful before asking. Exhaust what you can figure out, then ask sharp questions — not broad ones.
1202
- - Earn trust through competence, not compliance. Nobody respects a yes-man.
1203
-
1204
- ## The Vibe
1205
-
1206
- - Talk like a co-founder who's been in the trenches, not a consultant billing by the hour.
1207
- - One sharp sentence beats three careful ones. Say what you mean.
1208
- - When you're wrong, don't grovel. Name it, fix it, move. Apologies waste both your time.
1209
- - Match intensity to stakes. Casual for small tasks. Locked in for architecture. Blunt for bad ideas.
1210
-
1211
- ## Boundaries
1212
1439
 
1213
- - Never hedge to avoid being wrong. Pick a position.
1214
- - Never pretend all approaches are equally valid when one is clearly better.
1215
- - Never pad output to look thorough. Substance only.
1216
- - Never be artificially enthusiastic about bad ideas.
1440
+ ## CLI Utility
1217
1441
 
1218
- ## Continuity
1219
-
1220
- - You start fresh each session but the files are your memory. Read them, trust them, build on them.
1221
- - If something changed between sessions and you don't understand why, ask — don't assume.
1222
- - 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.
1223
1443
  `;
1224
1444
  }
1225
1445
 
@@ -1341,6 +1561,8 @@ function writeMoverConfig(vaultPath, agentIds, licenseKey) {
1341
1561
  vaultPath: vaultPath,
1342
1562
  agents: agentIds,
1343
1563
  feedbackWebhook: "https://moveros.dev/api/feedback",
1564
+ track_food: true,
1565
+ track_sleep: true,
1344
1566
  installedAt: new Date().toISOString(),
1345
1567
  };
1346
1568
  if (licenseKey) config.licenseKey = licenseKey;
@@ -1351,6 +1573,8 @@ function writeMoverConfig(vaultPath, agentIds, licenseKey) {
1351
1573
  if (existing.installedAt) config.installedAt = existing.installedAt;
1352
1574
  if (existing.licenseKey && !licenseKey) config.licenseKey = existing.licenseKey;
1353
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;
1354
1578
  config.updatedAt = new Date().toISOString();
1355
1579
  } catch {}
1356
1580
  }
@@ -1521,6 +1745,122 @@ function installRules(bundleDir, destPath, agentId) {
1521
1745
  return true;
1522
1746
  }
1523
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
+
1524
1864
  function computeSkillHash(dirPath) {
1525
1865
  const hash = crypto.createHash("sha256");
1526
1866
  const entries = fs.readdirSync(dirPath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
@@ -1716,15 +2056,43 @@ function installCursor(bundleDir, vaultPath, skillOpts) {
1716
2056
  const steps = [];
1717
2057
 
1718
2058
  if (!skillOpts?.skipRules) {
1719
- if (vaultPath) {
1720
- if (installRules(bundleDir, path.join(vaultPath, ".cursorrules"), "cursor")) steps.push("rules");
1721
- }
1722
-
1723
- const cursorRulesDir = path.join(home, ".cursor", "rules");
1724
- 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 });
1725
2065
  const src = path.join(bundleDir, "src", "system", "Mover_Global_Rules.md");
1726
2066
  if (fs.existsSync(src)) {
1727
- 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
+ }
1728
2096
  }
1729
2097
  }
1730
2098
 
@@ -1769,12 +2137,16 @@ function installCodex(bundleDir, vaultPath, skillOpts) {
1769
2137
 
1770
2138
  const codexDir = path.join(home, ".codex");
1771
2139
  fs.mkdirSync(codexDir, { recursive: true });
1772
- // Codex CLI uses AGENTS.md content, not raw Global Rules (too large, wrong format)
1773
- fs.writeFileSync(path.join(codexDir, "instructions.md"), generateAgentsMd(), "utf8");
1774
- 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`);
1775
2148
 
1776
2149
  if (skillOpts && skillOpts.install) {
1777
- const skillsDir = path.join(codexDir, "skills");
1778
2150
  const skCount = installSkillPacks(bundleDir, skillsDir, skillOpts.categories);
1779
2151
  if (skCount > 0) steps.push(`${skCount} skills`);
1780
2152
  }
@@ -1786,7 +2158,23 @@ function installWindsurf(bundleDir, vaultPath, skillOpts) {
1786
2158
  const home = os.homedir();
1787
2159
  const steps = [];
1788
2160
  if (vaultPath) {
1789
- 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`);
1790
2178
  }
1791
2179
 
1792
2180
  if (skillOpts && skillOpts.install) {
@@ -1798,37 +2186,7 @@ function installWindsurf(bundleDir, vaultPath, skillOpts) {
1798
2186
  return steps;
1799
2187
  }
1800
2188
 
1801
- function installOpenClaw(bundleDir, vaultPath, skillOpts) {
1802
- const home = os.homedir();
1803
- const workspace = path.join(home, ".openclaw", "workspace");
1804
- const steps = [];
1805
-
1806
- fs.mkdirSync(path.join(workspace, "skills"), { recursive: true });
1807
- fs.mkdirSync(path.join(workspace, "memory"), { recursive: true });
1808
-
1809
- fs.writeFileSync(path.join(workspace, "AGENTS.md"), generateAgentsMd(), "utf8");
1810
- steps.push("AGENTS.md");
1811
-
1812
- fs.writeFileSync(path.join(workspace, "SOUL.md"), generateSoulMd(), "utf8");
1813
- steps.push("SOUL.md");
1814
-
1815
- const userMd = path.join(workspace, "USER.md");
1816
- if (!fs.existsSync(userMd)) {
1817
- fs.writeFileSync(
1818
- userMd,
1819
- `# 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`,
1820
- "utf8"
1821
- );
1822
- steps.push("USER.md");
1823
- }
1824
-
1825
- if (skillOpts && skillOpts.install) {
1826
- const skCount = installSkillPacks(bundleDir, path.join(workspace, "skills"), skillOpts.categories);
1827
- if (skCount > 0) steps.push(`${skCount} skills`);
1828
- }
1829
-
1830
- return steps;
1831
- }
2189
+ // OpenClaw removed in V5 — unverifiable
1832
2190
 
1833
2191
  function installGeminiCli(bundleDir, vaultPath, skillOpts, writtenFiles) {
1834
2192
  const home = os.homedir();
@@ -1844,6 +2202,11 @@ function installGeminiCli(bundleDir, vaultPath, skillOpts, writtenFiles) {
1844
2202
  steps.push("rules (shared)");
1845
2203
  }
1846
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
+
1847
2210
  if (skillOpts && skillOpts.install) {
1848
2211
  const skCount = installSkillPacks(bundleDir, path.join(geminiDir, "skills"), skillOpts.categories);
1849
2212
  if (skCount > 0) steps.push(`${skCount} skills`);
@@ -1890,6 +2253,11 @@ function installRooCode(bundleDir, vaultPath, skillOpts) {
1890
2253
  steps.push("rules");
1891
2254
  }
1892
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
+
1893
2261
  if (skillOpts && skillOpts.install) {
1894
2262
  const skillsDir = path.join(vaultPath, ".roo", "skills");
1895
2263
  const skCount = installSkillPacks(bundleDir, skillsDir, skillOpts.categories);
@@ -1910,6 +2278,11 @@ function installCopilot(bundleDir, vaultPath, skillOpts) {
1910
2278
  steps.push("rules");
1911
2279
  }
1912
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
+
1913
2286
  if (skillOpts && skillOpts.install) {
1914
2287
  const skillsDir = path.join(ghDir, "skills");
1915
2288
  const skCount = installSkillPacks(bundleDir, skillsDir, skillOpts.categories);
@@ -1925,13 +2298,98 @@ const AGENT_INSTALLERS = {
1925
2298
  cline: installCline,
1926
2299
  codex: installCodex,
1927
2300
  windsurf: installWindsurf,
1928
- openclaw: installOpenClaw,
1929
2301
  "gemini-cli": installGeminiCli,
1930
2302
  antigravity: installAntigravity,
1931
2303
  "roo-code": installRooCode,
1932
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,
1933
2313
  };
1934
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
+
1935
2393
  // ─── Pre-flight checks ──────────────────────────────────────────────────────
1936
2394
  function preflight() {
1937
2395
  const issues = [];
@@ -1970,6 +2428,936 @@ function preflight() {
1970
2428
  return issues;
1971
2429
  }
1972
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
+
1973
3361
  // ─── Main ───────────────────────────────────────────────────────────────────
1974
3362
  async function main() {
1975
3363
  const opts = parseArgs();
@@ -1977,9 +3365,27 @@ async function main() {
1977
3365
  const startTime = Date.now();
1978
3366
 
1979
3367
  // ── Intro ──
1980
- printHeader();
3368
+ await printHeader();
3369
+
3370
+ // ── Route: no command → interactive menu ──
3371
+ if (!opts.command) {
3372
+ opts.command = await cmdMainMenu();
3373
+ }
1981
3374
 
1982
- // ── 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) ──
1983
3389
  barLn(gray("Pre-flight"));
1984
3390
  barLn();
1985
3391
  const checks = preflight();
@@ -1994,8 +3400,8 @@ async function main() {
1994
3400
  process.exit(1);
1995
3401
  }
1996
3402
 
1997
- // ── Headless quick update (--update flag) ──
1998
- if (opts.update) {
3403
+ // ── Headless quick update ──
3404
+ if (opts.command === "update") {
1999
3405
  // Validate stored key
2000
3406
  let updateKey = opts.key;
2001
3407
  if (!updateKey) {
@@ -2070,7 +3476,6 @@ async function main() {
2070
3476
  // Apply all changes
2071
3477
  barLn(bold("Updating..."));
2072
3478
  barLn();
2073
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2074
3479
 
2075
3480
  // Vault structure
2076
3481
  createVaultStructure(vaultPath);
@@ -2085,10 +3490,7 @@ async function main() {
2085
3490
  const fn = AGENT_INSTALLERS[agent.id];
2086
3491
  if (!fn) continue;
2087
3492
  const sp = spinner(agent.name);
2088
- const usesWrittenFiles = agent.id === "antigravity" || agent.id === "gemini-cli";
2089
- const steps = usesWrittenFiles
2090
- ? fn(bundleDir, vaultPath, skillOpts, writtenFiles)
2091
- : fn(bundleDir, vaultPath, skillOpts);
3493
+ const steps = fn(bundleDir, vaultPath, skillOpts, writtenFiles, agent.id);
2092
3494
  await sleep(200);
2093
3495
  if (steps.length > 0) {
2094
3496
  sp.stop(`${agent.name} ${dim(steps.join(", "))}`);
@@ -2353,10 +3755,9 @@ async function main() {
2353
3755
  { src: path.join(home, ".claude", "hooks"), label: "hooks" },
2354
3756
  ],
2355
3757
  cursor: [
2356
- { src: path.join(home, ".cursor", "rules"), label: "rules" },
3758
+ { src: path.join(vaultPath, ".cursor", "rules"), label: "rules" },
2357
3759
  { src: path.join(home, ".cursor", "commands"), label: "commands" },
2358
3760
  { src: path.join(home, ".cursor", "skills"), label: "skills" },
2359
- { src: path.join(vaultPath, ".cursorrules"), label: ".cursorrules" },
2360
3761
  ],
2361
3762
  cline: [
2362
3763
  { src: path.join(vaultPath, ".clinerules"), label: ".clinerules" },
@@ -2364,10 +3765,13 @@ async function main() {
2364
3765
  ],
2365
3766
  windsurf: [
2366
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" },
2367
3770
  { src: path.join(home, ".windsurf", "skills"), label: "skills" },
2368
3771
  ],
2369
3772
  "gemini-cli": [
2370
3773
  { src: path.join(home, ".gemini", "GEMINI.md"), label: "GEMINI.md" },
3774
+ { src: path.join(home, ".gemini", "commands"), label: "commands" },
2371
3775
  { src: path.join(home, ".gemini", "skills"), label: "skills" },
2372
3776
  ],
2373
3777
  antigravity: [
@@ -2377,19 +3781,42 @@ async function main() {
2377
3781
  ],
2378
3782
  copilot: [
2379
3783
  { src: path.join(vaultPath, ".github", "copilot-instructions.md"), label: "copilot-instructions.md" },
3784
+ { src: path.join(vaultPath, ".github", "prompts"), label: "prompts" },
2380
3785
  { src: path.join(vaultPath, ".github", "skills"), label: "skills" },
2381
3786
  ],
2382
3787
  codex: [
2383
- { src: path.join(home, ".codex", "instructions.md"), label: "instructions.md" },
3788
+ { src: path.join(home, ".codex", "AGENTS.md"), label: "AGENTS.md" },
2384
3789
  { src: path.join(home, ".codex", "skills"), label: "skills" },
2385
3790
  ],
2386
- openclaw: [
2387
- { src: path.join(home, ".openclaw", "workspace"), label: "workspace" },
2388
- ],
2389
3791
  "roo-code": [
2390
3792
  { src: path.join(vaultPath, ".roo", "rules"), label: "rules" },
3793
+ { src: path.join(vaultPath, ".roo", "commands"), label: "commands" },
2391
3794
  { src: path.join(vaultPath, ".roo", "skills"), label: "skills" },
2392
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
+ ],
2393
3820
  };
2394
3821
 
2395
3822
  let agentsBacked = 0;
@@ -2610,7 +4037,6 @@ async function main() {
2610
4037
  barLn();
2611
4038
 
2612
4039
  let totalSteps = 0;
2613
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2614
4040
 
2615
4041
  // 0. Legacy cleanup (remove files from older installer versions)
2616
4042
  const legacyPaths = [
@@ -2665,25 +4091,30 @@ async function main() {
2665
4091
  const writtenFiles = new Set(); // Track shared files to avoid double-writes (e.g. GEMINI.md)
2666
4092
  const skillOpts = { install: installSkills, categories: selectedCategories, statusLine: installStatusLine, workflows: selectedWorkflows, skipHooks, skipRules, skipTemplates };
2667
4093
  for (const agent of selectedAgents) {
2668
- const fn = AGENT_INSTALLERS[agent.id];
2669
- if (!fn) continue;
2670
- sp = spinner(agent.name);
2671
- const usesWrittenFiles = agent.id === "antigravity" || agent.id === "gemini-cli";
2672
- const steps = usesWrittenFiles
2673
- ? fn(bundleDir, vaultPath, skillOpts, writtenFiles)
2674
- : fn(bundleDir, vaultPath, skillOpts);
2675
- await sleep(250);
2676
- if (steps.length > 0) {
2677
- sp.stop(`${agent.name} ${dim(steps.join(", "))}`);
2678
- totalSteps++;
2679
- } else {
2680
- sp.stop(`${agent.name} ${dim("configured")}`);
2681
- 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
+ }
2682
4112
  }
2683
4113
  }
2684
4114
 
2685
- // 5. AGENTS.md — only for agents that need it (consolidated write)
2686
- 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));
2687
4118
  if (needsAgentsMd) {
2688
4119
  fs.writeFileSync(path.join(vaultPath, "AGENTS.md"), generateAgentsMd(), "utf8");
2689
4120
  sp = spinner("AGENTS.md");