mover-os 4.7.3 → 4.7.5

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 +439 -14
  2. package/package.json +1 -1
package/install.js CHANGED
@@ -17,6 +17,18 @@ const { execSync } = require("child_process");
17
17
 
18
18
  const VERSION = "4";
19
19
 
20
+ // ─── Windows path normalization ─────────────────────────────────────────────
21
+ // Git Bash / MSYS / WSL surface paths like "/c/Users/foo" inside terminals
22
+ // that Node treats as drive-less when run from PowerShell or cmd. path.resolve
23
+ // then prepends the *current* drive, producing "C:\c\Users\foo". Convert
24
+ // "/c/Users/foo" → "C:\Users\foo" before any path.join(vaultPath, ...) call.
25
+ function normalizeWinPath(p) {
26
+ if (process.platform !== "win32" || !p) return p;
27
+ const m = p.match(/^\/([a-zA-Z])\/(.*)$/);
28
+ if (m) return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
29
+ return p;
30
+ }
31
+
20
32
  // ─── JSON output helper ──────────────────────────────────────────────────────
21
33
  function jsonOut(command, data, ok = true) {
22
34
  const envelope = {
@@ -1053,12 +1065,14 @@ function detectChanges(bundleDir, vaultPath, selectedAgentIds) {
1053
1065
 
1054
1066
  if (fs.existsSync(wfSrc)) {
1055
1067
  for (const file of fs.readdirSync(wfSrc).filter((f) => f.endsWith(".md"))) {
1056
- const srcContent = fs.readFileSync(path.join(wfSrc, file), "utf8");
1068
+ const srcContent = fs.readFileSync(path.join(wfSrc, file), "utf8")
1069
+ .replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1057
1070
  const destFile = wfDest && path.join(wfDest, file);
1058
1071
  if (!destFile || !fs.existsSync(destFile)) {
1059
1072
  result.workflows.push({ file, status: "new" });
1060
1073
  } else {
1061
- const destContent = fs.readFileSync(destFile, "utf8");
1074
+ const destContent = fs.readFileSync(destFile, "utf8")
1075
+ .replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1062
1076
  result.workflows.push({
1063
1077
  file,
1064
1078
  status: srcContent === destContent ? "unchanged" : "changed",
@@ -1097,8 +1111,10 @@ function detectChanges(bundleDir, vaultPath, selectedAgentIds) {
1097
1111
  ].filter(Boolean);
1098
1112
  const rulesDest = rulesDests.find((d) => fs.existsSync(d));
1099
1113
  if (fs.existsSync(rulesSrc) && rulesDest) {
1100
- const srcContent = fs.readFileSync(rulesSrc, "utf8");
1101
- const destContent = fs.readFileSync(rulesDest, "utf8");
1114
+ const srcContent = fs.readFileSync(rulesSrc, "utf8")
1115
+ .replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1116
+ const destContent = fs.readFileSync(rulesDest, "utf8")
1117
+ .replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1102
1118
  result.rules = srcContent === destContent ? "unchanged" : "changed";
1103
1119
  } else {
1104
1120
  result.rules = "unchanged";
@@ -1115,12 +1131,14 @@ function detectChanges(bundleDir, vaultPath, selectedAgentIds) {
1115
1131
  } else {
1116
1132
  const relNorm = entryRel.replace(/\\/g, "/");
1117
1133
  if (relNorm.includes("02_Areas") && relNorm.includes("Engine")) continue;
1118
- const srcContent = fs.readFileSync(path.join(dir, entry.name), "utf8");
1134
+ const srcContent = fs.readFileSync(path.join(dir, entry.name), "utf8")
1135
+ .replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1119
1136
  const destFile = path.join(vaultPath, entryRel);
1120
1137
  if (!fs.existsSync(destFile)) {
1121
1138
  result.templates.push({ file: entryRel, status: "new" });
1122
1139
  } else {
1123
- const destContent = fs.readFileSync(destFile, "utf8");
1140
+ const destContent = fs.readFileSync(destFile, "utf8")
1141
+ .replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1124
1142
  result.templates.push({
1125
1143
  file: entryRel,
1126
1144
  status: srcContent === destContent ? "unchanged" : "changed",
@@ -1147,12 +1165,14 @@ function detectChanges(bundleDir, vaultPath, selectedAgentIds) {
1147
1165
  if (entry.isDirectory()) {
1148
1166
  const skillFile = path.join(full, "SKILL.md");
1149
1167
  if (fs.existsSync(skillFile)) {
1150
- const srcContent = fs.readFileSync(skillFile, "utf8");
1168
+ const srcContent = fs.readFileSync(skillFile, "utf8")
1169
+ .replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1151
1170
  const destSkill = path.join(skillsDest, entry.name, "SKILL.md");
1152
1171
  if (!fs.existsSync(destSkill)) {
1153
1172
  result.skills.push({ file: entry.name, status: "new" });
1154
1173
  } else {
1155
- const destContent = fs.readFileSync(destSkill, "utf8");
1174
+ const destContent = fs.readFileSync(destSkill, "utf8")
1175
+ .replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1156
1176
  result.skills.push({ file: entry.name, status: srcContent === destContent ? "unchanged" : "changed" });
1157
1177
  }
1158
1178
  } else {
@@ -1903,13 +1923,13 @@ const AGENT_REGISTRY = {
1903
1923
  // ── Enhanced Tier ──────────────────────────────────────────────────────────
1904
1924
  "codex": {
1905
1925
  name: "Codex",
1906
- tier: "enhanced",
1907
- tierDesc: "AGENTS.md, skills (skills = commands)",
1926
+ tier: "full",
1927
+ tierDesc: "AGENTS.md, skills (skills = commands), 5 hooks",
1908
1928
  detect: () => cmdExists("codex") || fs.existsSync(path.join(H, ".codex")),
1909
1929
  rules: { type: "agents-md", dest: () => path.join(H, ".codex", "AGENTS.md") },
1910
1930
  skills: { dest: () => path.join(H, ".codex", "skills") },
1911
1931
  commands: null,
1912
- hooks: null,
1932
+ hooks: { type: "codex-hooks-json", dest: () => path.join(H, ".codex", "hooks.json") },
1913
1933
  },
1914
1934
  "antigravity": {
1915
1935
  name: "Antigravity",
@@ -2220,6 +2240,167 @@ Stuck: /debug-resistance
2220
2240
  }
2221
2241
 
2222
2242
  // ─── Claude Code hooks (settings.json) ──────────────────────────────────────
2243
+ // ─── Codex hook config generator ────────────────────────────────────────────
2244
+ // v4.7.5: Codex hooks invoked through mover-hook-adapter.js for schema
2245
+ // translation. MVP scope: session-start, engine-protection, git-safety,
2246
+ // plan-sync-reminder, dirty-tree-guard (no session-log-reminder under Codex
2247
+ // — that script is Claude-transcript-specific).
2248
+ //
2249
+ // IMPORTANT: Codex runs hook commands through cmd.exe on Windows, which does
2250
+ // NOT expand $HOME. We resolve absolute paths at install time so the same
2251
+ // hooks.json works on macOS/Linux/Windows.
2252
+ function generateCodexHooks() {
2253
+ const home = os.homedir();
2254
+ // Forward slashes work on all three OSes when invoking node directly.
2255
+ const fwd = (p) => p.split(path.sep).join("/");
2256
+ const hooksRoot = fwd(path.join(home, ".codex", "hooks"));
2257
+ const adapter = `"${hooksRoot}/mover-hook-adapter.js"`;
2258
+ const hookDir = `"${hooksRoot}`;
2259
+ return JSON.stringify(
2260
+ {
2261
+ hooks: {
2262
+ SessionStart: [
2263
+ {
2264
+ matcher: "startup|resume|clear",
2265
+ hooks: [
2266
+ {
2267
+ type: "command",
2268
+ command: `node ${adapter} codex SessionStart ${hookDir}/session-start.sh" full`,
2269
+ timeout: 5,
2270
+ },
2271
+ ],
2272
+ },
2273
+ ],
2274
+ PreToolUse: [
2275
+ {
2276
+ matcher: "Bash",
2277
+ hooks: [
2278
+ {
2279
+ type: "command",
2280
+ command: `node ${adapter} codex PreToolUse ${hookDir}/git-safety.sh"`,
2281
+ timeout: 5,
2282
+ },
2283
+ ],
2284
+ },
2285
+ {
2286
+ matcher: "Edit|Write|apply_patch",
2287
+ hooks: [
2288
+ {
2289
+ type: "command",
2290
+ command: `node ${adapter} codex PreToolUse ${hookDir}/engine-protection.sh"`,
2291
+ timeout: 5,
2292
+ },
2293
+ ],
2294
+ },
2295
+ ],
2296
+ PostToolUse: [
2297
+ {
2298
+ matcher: "Edit|Write|apply_patch",
2299
+ hooks: [
2300
+ {
2301
+ type: "command",
2302
+ command: `node ${adapter} codex PostToolUse ${hookDir}/plan-sync-reminder.sh"`,
2303
+ timeout: 5,
2304
+ },
2305
+ ],
2306
+ },
2307
+ ],
2308
+ Stop: [
2309
+ {
2310
+ hooks: [
2311
+ {
2312
+ type: "command",
2313
+ command: `node ${adapter} codex Stop ${hookDir}/dirty-tree-guard.sh"`,
2314
+ timeout: 10,
2315
+ },
2316
+ ],
2317
+ },
2318
+ ],
2319
+ },
2320
+ },
2321
+ null,
2322
+ 2
2323
+ );
2324
+ }
2325
+
2326
+ // ─── Gemini hook config generator ───────────────────────────────────────────
2327
+ // v4.7.5: Gemini events differ from Claude/Codex — UserPromptSubmit→BeforeAgent,
2328
+ // PreToolUse→BeforeTool, PostToolUse→AfterTool, Stop→AfterAgent. Adapter
2329
+ // translates schema; we map event names here.
2330
+ // Timeouts are in milliseconds per Gemini hook spec (default 60000).
2331
+ // Absolute paths used so cmd.exe on Windows can resolve correctly.
2332
+ function generateGeminiHooks() {
2333
+ const home = os.homedir();
2334
+ const fwd = (p) => p.split(path.sep).join("/");
2335
+ const hooksRoot = fwd(path.join(home, ".gemini", "hooks"));
2336
+ const adapter = `"${hooksRoot}/mover-hook-adapter.js"`;
2337
+ const hookDir = `"${hooksRoot}`;
2338
+ return {
2339
+ SessionStart: [
2340
+ {
2341
+ matcher: "startup",
2342
+ hooks: [
2343
+ {
2344
+ name: "mover-session-start",
2345
+ type: "command",
2346
+ command: `node ${adapter} gemini SessionStart ${hookDir}/session-start.sh" full`,
2347
+ timeout: 5000,
2348
+ },
2349
+ ],
2350
+ },
2351
+ ],
2352
+ BeforeTool: [
2353
+ {
2354
+ matcher: "write_file|replace",
2355
+ hooks: [
2356
+ {
2357
+ name: "mover-engine-protection",
2358
+ type: "command",
2359
+ command: `node ${adapter} gemini BeforeTool ${hookDir}/engine-protection.sh"`,
2360
+ timeout: 5000,
2361
+ },
2362
+ ],
2363
+ },
2364
+ {
2365
+ matcher: "run_shell_command",
2366
+ hooks: [
2367
+ {
2368
+ name: "mover-git-safety",
2369
+ type: "command",
2370
+ command: `node ${adapter} gemini BeforeTool ${hookDir}/git-safety.sh"`,
2371
+ timeout: 5000,
2372
+ },
2373
+ ],
2374
+ },
2375
+ ],
2376
+ AfterTool: [
2377
+ {
2378
+ matcher: "write_file|replace",
2379
+ hooks: [
2380
+ {
2381
+ name: "mover-plan-sync",
2382
+ type: "command",
2383
+ command: `node ${adapter} gemini AfterTool ${hookDir}/plan-sync-reminder.sh"`,
2384
+ timeout: 5000,
2385
+ },
2386
+ ],
2387
+ },
2388
+ ],
2389
+ AfterAgent: [
2390
+ {
2391
+ hooks: [
2392
+ {
2393
+ name: "mover-dirty-tree-guard",
2394
+ type: "command",
2395
+ command: `node ${adapter} gemini AfterAgent ${hookDir}/dirty-tree-guard.sh"`,
2396
+ timeout: 10000,
2397
+ },
2398
+ ],
2399
+ },
2400
+ ],
2401
+ };
2402
+ }
2403
+
2223
2404
  function generateClaudeSettings() {
2224
2405
  return JSON.stringify(
2225
2406
  {
@@ -2949,6 +3130,227 @@ function installHooksForClaude(bundleDir, vaultPath) {
2949
3130
  return count;
2950
3131
  }
2951
3132
 
3133
+ // ─── Multi-agent hook installer (v4.7.5) ────────────────────────────────────
3134
+ // Copies the MVP hook set (5 enforcement hooks + adapter + shared lib) to the
3135
+ // agent's hook directory and writes its hook config. Used by Codex and Gemini.
3136
+ const MVP_HOOK_SCRIPTS = [
3137
+ "session-start.sh",
3138
+ "engine-protection.sh",
3139
+ "git-safety.sh",
3140
+ "plan-sync-reminder.sh",
3141
+ "dirty-tree-guard.sh",
3142
+ "mover-lib.sh", // sourced by the others
3143
+ ];
3144
+
3145
+ // Section-aware TOML upsert. Sets [section] key = value, preserving comments,
3146
+ // existing keys, and other sections. If section exists with the key set to a
3147
+ // different value, the value is REPLACED (not duplicated). If the section
3148
+ // header has whitespace variants like `[ features ]`, treat as the same section.
3149
+ function upsertTomlKey(filePath, section, key, value) {
3150
+ let content = "";
3151
+ if (fs.existsSync(filePath)) {
3152
+ content = fs.readFileSync(filePath, "utf8");
3153
+ }
3154
+
3155
+ // Match table header in any whitespace variant: [section], [ section ], etc.
3156
+ // Also tolerate trailing comment after header: [section] # ...
3157
+ const headerRe = (name) =>
3158
+ new RegExp(
3159
+ `^\\s*\\[\\s*${name.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}\\s*\\](?:[^\\n]*)$`,
3160
+ "m"
3161
+ );
3162
+ // Any section header (used to find section boundaries)
3163
+ const anyHeaderRe = /^\s*\[\s*[^\]]+\s*\](?:[^\n]*)$/m;
3164
+
3165
+ const lines = content.split("\n");
3166
+ let inSection = false;
3167
+ let sectionStart = -1;
3168
+ let sectionEnd = lines.length;
3169
+ for (let i = 0; i < lines.length; i++) {
3170
+ if (headerRe(section).test(lines[i])) {
3171
+ inSection = true;
3172
+ sectionStart = i;
3173
+ // Find next header (or EOF) — that's section end
3174
+ for (let j = i + 1; j < lines.length; j++) {
3175
+ if (anyHeaderRe.test(lines[j])) {
3176
+ sectionEnd = j;
3177
+ break;
3178
+ }
3179
+ }
3180
+ break;
3181
+ }
3182
+ }
3183
+
3184
+ const newKv = `${key} = ${value}`;
3185
+ // Match existing key in this section: tolerates whitespace + comment
3186
+ const keyRe = new RegExp(
3187
+ `^\\s*${key.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}\\s*=.*$`
3188
+ );
3189
+
3190
+ if (inSection) {
3191
+ let replaced = false;
3192
+ for (let i = sectionStart + 1; i < sectionEnd; i++) {
3193
+ if (keyRe.test(lines[i])) {
3194
+ lines[i] = newKv;
3195
+ replaced = true;
3196
+ break;
3197
+ }
3198
+ }
3199
+ if (!replaced) {
3200
+ // Insert key right after section header
3201
+ lines.splice(sectionStart + 1, 0, newKv);
3202
+ }
3203
+ } else {
3204
+ // Section absent — append fresh section at EOF
3205
+ if (lines.length > 0 && lines[lines.length - 1].trim() !== "") {
3206
+ lines.push("");
3207
+ }
3208
+ lines.push(`[${section}]`);
3209
+ lines.push(newKv);
3210
+ }
3211
+
3212
+ fs.writeFileSync(filePath, lines.join("\n"), "utf8");
3213
+ }
3214
+
3215
+ function copyMvpHooks(bundleDir, destDir) {
3216
+ const hooksSrc = path.join(bundleDir, "src", "hooks");
3217
+ if (!fs.existsSync(hooksSrc)) return 0;
3218
+ fs.mkdirSync(destDir, { recursive: true });
3219
+ let count = 0;
3220
+ for (const file of MVP_HOOK_SCRIPTS) {
3221
+ const src = path.join(hooksSrc, file);
3222
+ if (!fs.existsSync(src)) continue;
3223
+ const dst = path.join(destDir, file);
3224
+ const content = fs
3225
+ .readFileSync(src, "utf8")
3226
+ .replace(/\r\n/g, "\n")
3227
+ .replace(/\r/g, "\n");
3228
+ fs.writeFileSync(dst, content, { mode: 0o755 });
3229
+ count++;
3230
+ }
3231
+ // Adapter (Node script — copy preserving binary mode)
3232
+ const adapterSrc = path.join(hooksSrc, "mover-hook-adapter.js");
3233
+ if (fs.existsSync(adapterSrc)) {
3234
+ const dst = path.join(destDir, "mover-hook-adapter.js");
3235
+ const content = fs
3236
+ .readFileSync(adapterSrc, "utf8")
3237
+ .replace(/\r\n/g, "\n")
3238
+ .replace(/\r/g, "\n");
3239
+ fs.writeFileSync(dst, content, { mode: 0o755 });
3240
+ count++;
3241
+ }
3242
+ return count;
3243
+ }
3244
+
3245
+ function installHooksForCodex(bundleDir) {
3246
+ const home = os.homedir();
3247
+ const codexDir = path.join(home, ".codex");
3248
+ const hooksDst = path.join(codexDir, "hooks");
3249
+
3250
+ // Detect Codex install
3251
+ if (!fs.existsSync(codexDir) && !cmdExists("codex")) return 0;
3252
+
3253
+ fs.mkdirSync(codexDir, { recursive: true });
3254
+ const count = copyMvpHooks(bundleDir, hooksDst);
3255
+ if (count === 0) return 0;
3256
+
3257
+ // Write hooks.json (deep-merge if existing).
3258
+ // v4.7.5 fix: per-entry idempotency, not per-event. Previous coarse check
3259
+ // skipped sibling entries when one Mover hook was already registered.
3260
+ const hooksJsonPath = path.join(codexDir, "hooks.json");
3261
+ const newConfig = JSON.parse(generateCodexHooks());
3262
+ if (fs.existsSync(hooksJsonPath)) {
3263
+ try {
3264
+ const existing = JSON.parse(fs.readFileSync(hooksJsonPath, "utf8"));
3265
+ mergeHooksConfig(existing, newConfig);
3266
+ fs.writeFileSync(hooksJsonPath, JSON.stringify(existing, null, 2), "utf8");
3267
+ } catch {
3268
+ try {
3269
+ fs.copyFileSync(hooksJsonPath, hooksJsonPath + ".bak");
3270
+ } catch {}
3271
+ fs.writeFileSync(hooksJsonPath, JSON.stringify(newConfig, null, 2), "utf8");
3272
+ }
3273
+ } else {
3274
+ fs.writeFileSync(hooksJsonPath, JSON.stringify(newConfig, null, 2), "utf8");
3275
+ }
3276
+
3277
+ // Enable codex_hooks feature in config.toml (section-aware upsert).
3278
+ // Handles edge cases the prior regex approach missed:
3279
+ // [features] # comment — replacement misses the table header
3280
+ // codex_hooks = false — would create duplicate key
3281
+ // [ features ] — bare-line regex misses whitespace variant
3282
+ // Strategy: parse file into sections, locate or create [features],
3283
+ // upsert codex_hooks=true within that section, reassemble.
3284
+ const configToml = path.join(codexDir, "config.toml");
3285
+ upsertTomlKey(configToml, "features", "codex_hooks", "true");
3286
+
3287
+ return count;
3288
+ }
3289
+
3290
+ function installHooksForGemini(bundleDir) {
3291
+ const home = os.homedir();
3292
+ const geminiDir = path.join(home, ".gemini");
3293
+ const hooksDst = path.join(geminiDir, "hooks");
3294
+
3295
+ // Detect Gemini install
3296
+ if (
3297
+ !fs.existsSync(geminiDir) &&
3298
+ !cmdExists("gemini") &&
3299
+ !fs.existsSync(path.join(geminiDir, "settings.json"))
3300
+ )
3301
+ return 0;
3302
+
3303
+ fs.mkdirSync(geminiDir, { recursive: true });
3304
+ const count = copyMvpHooks(bundleDir, hooksDst);
3305
+ if (count === 0) return 0;
3306
+
3307
+ // Deep-merge into settings.json
3308
+ const settingsPath = path.join(geminiDir, "settings.json");
3309
+ const newHooks = generateGeminiHooks();
3310
+ let existing = {};
3311
+ if (fs.existsSync(settingsPath)) {
3312
+ try {
3313
+ existing = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
3314
+ } catch {
3315
+ try {
3316
+ fs.copyFileSync(settingsPath, settingsPath + ".bak");
3317
+ } catch {}
3318
+ existing = {};
3319
+ }
3320
+ }
3321
+ if (!existing.hooks) existing.hooks = {};
3322
+ mergeHooksConfig(existing, { hooks: newHooks });
3323
+ fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2), "utf8");
3324
+
3325
+ return count;
3326
+ }
3327
+
3328
+ // Per-entry hook merge. For each new entry, check if an entry with the SAME
3329
+ // matcher AND a Mover adapter command already exists. Skip only that exact
3330
+ // duplicate. Sibling entries (different matchers, or non-Mover entries) are
3331
+ // left intact.
3332
+ function mergeHooksConfig(existing, newConfig) {
3333
+ if (!existing.hooks) existing.hooks = {};
3334
+ for (const [event, newEntries] of Object.entries(newConfig.hooks || {})) {
3335
+ if (!existing.hooks[event]) {
3336
+ existing.hooks[event] = newEntries;
3337
+ continue;
3338
+ }
3339
+ for (const newEntry of newEntries) {
3340
+ const matcher = newEntry.matcher; // may be undefined
3341
+ const newCmds = (newEntry.hooks || []).map((h) => h.command || "");
3342
+ const isDup = existing.hooks[event].some((existingEntry) => {
3343
+ if ((existingEntry.matcher || "") !== (matcher || "")) return false;
3344
+ const existingCmds = (existingEntry.hooks || []).map(
3345
+ (h) => h.command || ""
3346
+ );
3347
+ return newCmds.every((c) => existingCmds.includes(c));
3348
+ });
3349
+ if (!isDup) existing.hooks[event].push(newEntry);
3350
+ }
3351
+ }
3352
+ }
3353
+
2952
3354
  // ─── Per-agent install orchestrators ────────────────────────────────────────
2953
3355
  function installClaudeCode(bundleDir, vaultPath, skillOpts) {
2954
3356
  const home = os.homedir();
@@ -3102,6 +3504,12 @@ function installCodex(bundleDir, vaultPath, skillOpts) {
3102
3504
  if (skCount > 0) steps.push(`${skCount} skills`);
3103
3505
  }
3104
3506
 
3507
+ // v4.7.5: Native Codex hook support via mover-hook-adapter.js
3508
+ if (!skillOpts?.skipHooks) {
3509
+ const hkCount = installHooksForCodex(bundleDir);
3510
+ if (hkCount > 0) steps.push(`${hkCount} hooks`);
3511
+ }
3512
+
3105
3513
  return steps;
3106
3514
  }
3107
3515
 
@@ -3163,6 +3571,12 @@ function installGeminiCli(bundleDir, vaultPath, skillOpts, writtenFiles) {
3163
3571
  if (skCount > 0) steps.push(`${skCount} skills`);
3164
3572
  }
3165
3573
 
3574
+ // v4.7.5: Native Gemini hook support via mover-hook-adapter.js
3575
+ if (!skillOpts?.skipHooks) {
3576
+ const hkCount = installHooksForGemini(bundleDir);
3577
+ if (hkCount > 0) steps.push(`${hkCount} hooks`);
3578
+ }
3579
+
3166
3580
  return steps;
3167
3581
  }
3168
3582
 
@@ -5280,8 +5694,12 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
5280
5694
  }
5281
5695
  }
5282
5696
  fs.writeFileSync(path.join(vaultPath, ".mover-version"), `${require("./package.json").version}\n`, "utf8");
5283
- writeMoverConfig(vaultPath, selectedIds);
5697
+ writeMoverConfig(vaultPath, selectedIds, updateKey);
5284
5698
  barLn();
5699
+ if (totalChanged > 0) {
5700
+ barLn(dim(" Restart your AI session to load updated rules and skills."));
5701
+ barLn();
5702
+ }
5285
5703
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
5286
5704
  await successAnimation(`${totalChanged} files updated in ${elapsed}s.`);
5287
5705
  return;
@@ -5610,7 +6028,7 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
5610
6028
  }
5611
6029
  }
5612
6030
  saveUpdateManifest(newManifest);
5613
- writeMoverConfig(vaultPath, selectedIds);
6031
+ writeMoverConfig(vaultPath, selectedIds, updateKey);
5614
6032
 
5615
6033
  // ── Summary ──
5616
6034
  barLn();
@@ -5630,6 +6048,10 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
5630
6048
  }
5631
6049
 
5632
6050
  barLn();
6051
+ if (updated.length > 0 || autoMerged.length > 0) {
6052
+ barLn(dim(" Restart your AI session to load updated rules and skills."));
6053
+ barLn();
6054
+ }
5633
6055
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
5634
6056
  if (needsUpdate) {
5635
6057
  outro(`System updated. Run ${bold("/update")} to resolve ${conflicts.length} conflict${conflicts.length > 1 ? "s" : ""}. ${dim(`(${elapsed}s)`)}`);
@@ -5643,13 +6065,15 @@ function resolveVaultPath(explicitVault) {
5643
6065
  if (explicitVault) {
5644
6066
  let v = explicitVault;
5645
6067
  if (v.startsWith("~")) v = path.join(os.homedir(), v.slice(1));
6068
+ v = normalizeWinPath(v);
5646
6069
  return path.resolve(v);
5647
6070
  }
5648
6071
  // Try config.json
5649
6072
  const cfgPath = path.join(os.homedir(), ".mover", "config.json");
5650
6073
  if (fs.existsSync(cfgPath)) {
5651
6074
  try {
5652
- const v = JSON.parse(fs.readFileSync(cfgPath, "utf8")).vaultPath;
6075
+ let v = JSON.parse(fs.readFileSync(cfgPath, "utf8")).vaultPath;
6076
+ v = normalizeWinPath(v);
5653
6077
  if (v && fs.existsSync(v)) return v;
5654
6078
  } catch {}
5655
6079
  }
@@ -5913,6 +6337,7 @@ async function main() {
5913
6337
  }
5914
6338
 
5915
6339
  if (vaultPath.startsWith("~")) vaultPath = path.join(os.homedir(), vaultPath.slice(1));
6340
+ vaultPath = normalizeWinPath(vaultPath);
5916
6341
  vaultPath = path.resolve(vaultPath);
5917
6342
 
5918
6343
  // ── Fresh install only — redirect if existing ──
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mover-os",
3
- "version": "4.7.3",
3
+ "version": "4.7.5",
4
4
  "description": "Your AI co-founder. Remembers your goals, pushes back when you drift. Works with 15 AI agents.",
5
5
  "bin": {
6
6
  "moveros": "install.js"