mover-os 4.7.4 → 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.
- package/install.js +397 -3
- package/package.json +1 -1
package/install.js
CHANGED
|
@@ -1923,13 +1923,13 @@ const AGENT_REGISTRY = {
|
|
|
1923
1923
|
// ── Enhanced Tier ──────────────────────────────────────────────────────────
|
|
1924
1924
|
"codex": {
|
|
1925
1925
|
name: "Codex",
|
|
1926
|
-
tier: "
|
|
1927
|
-
tierDesc: "AGENTS.md, skills (skills = commands)",
|
|
1926
|
+
tier: "full",
|
|
1927
|
+
tierDesc: "AGENTS.md, skills (skills = commands), 5 hooks",
|
|
1928
1928
|
detect: () => cmdExists("codex") || fs.existsSync(path.join(H, ".codex")),
|
|
1929
1929
|
rules: { type: "agents-md", dest: () => path.join(H, ".codex", "AGENTS.md") },
|
|
1930
1930
|
skills: { dest: () => path.join(H, ".codex", "skills") },
|
|
1931
1931
|
commands: null,
|
|
1932
|
-
hooks:
|
|
1932
|
+
hooks: { type: "codex-hooks-json", dest: () => path.join(H, ".codex", "hooks.json") },
|
|
1933
1933
|
},
|
|
1934
1934
|
"antigravity": {
|
|
1935
1935
|
name: "Antigravity",
|
|
@@ -2240,6 +2240,167 @@ Stuck: /debug-resistance
|
|
|
2240
2240
|
}
|
|
2241
2241
|
|
|
2242
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
|
+
|
|
2243
2404
|
function generateClaudeSettings() {
|
|
2244
2405
|
return JSON.stringify(
|
|
2245
2406
|
{
|
|
@@ -2969,6 +3130,227 @@ function installHooksForClaude(bundleDir, vaultPath) {
|
|
|
2969
3130
|
return count;
|
|
2970
3131
|
}
|
|
2971
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
|
+
|
|
2972
3354
|
// ─── Per-agent install orchestrators ────────────────────────────────────────
|
|
2973
3355
|
function installClaudeCode(bundleDir, vaultPath, skillOpts) {
|
|
2974
3356
|
const home = os.homedir();
|
|
@@ -3122,6 +3504,12 @@ function installCodex(bundleDir, vaultPath, skillOpts) {
|
|
|
3122
3504
|
if (skCount > 0) steps.push(`${skCount} skills`);
|
|
3123
3505
|
}
|
|
3124
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
|
+
|
|
3125
3513
|
return steps;
|
|
3126
3514
|
}
|
|
3127
3515
|
|
|
@@ -3183,6 +3571,12 @@ function installGeminiCli(bundleDir, vaultPath, skillOpts, writtenFiles) {
|
|
|
3183
3571
|
if (skCount > 0) steps.push(`${skCount} skills`);
|
|
3184
3572
|
}
|
|
3185
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
|
+
|
|
3186
3580
|
return steps;
|
|
3187
3581
|
}
|
|
3188
3582
|
|