Skip to content

Comments

feat: add hooks management and translation for Claude Code plugins#42

Merged
yordis merged 1 commit intomainfrom
yordis/feat-2
Dec 18, 2025
Merged

feat: add hooks management and translation for Claude Code plugins#42
yordis merged 1 commit intomainfrom
yordis/feat-2

Conversation

@yordis
Copy link
Member

@yordis yordis commented Dec 13, 2025

No description provided.

@coderabbitai
Copy link

coderabbitai bot commented Dec 13, 2025

Warning

Rate limit exceeded

@yordis has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 9 minutes and 58 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between fe1cadb and ce8ab82.

📒 Files selected for processing (11)
  • src/commands/plugin-uninstall.ts (3 hunks)
  • src/commands/sync.ts (6 hunks)
  • src/constants.ts (2 hunks)
  • src/helpers/hooks-merger.ts (1 hunks)
  • src/helpers/hooks-translator.ts (1 hunks)
  • src/helpers/sync-strategy.ts (4 hunks)
  • src/schema.ts (2 hunks)
  • tests/commands/plugin-uninstall.test.ts (2 hunks)
  • tests/commands/sync.test.ts (2 hunks)
  • tests/helpers/hooks-merger.test.ts (1 hunks)
  • tests/helpers/hooks-translator.test.ts (1 hunks)

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds translation, merging, and cleanup for Claude Code plugin hooks: translates per-plugin Claude hooks to Cursor format during sync, merges AIPM-managed hooks into .cursor/hooks.json while preserving user hooks, and removes a plugin’s AIPM hooks on uninstall. Adds constants, schemas, helpers, sync/uninstall integration, and tests.

Changes

Cohort / File(s) Summary
Constants & Schemas
src/constants.ts, src/schema.ts
Add hook-related constants (CLAUDE_PLUGIN_ROOT_VAR, FILE_HOOKS_JSON, AIPM_HOOK_PREFIX, HOOK_MANAGED_BY_FIELD, HOOK_ID_FIELD, DIR_HOOKS, etc.) and hook validation/types: ClaudeCodeHookSchema, AipmManagedHookSchema, UserHookSchema, CursorHooksConfigSchema and related TS types.
Hook Translator
src/helpers/hooks-translator.ts
New translator translateClaudeCodeHook and countTranslatedHooks: map Claude events → Cursor events, resolve CLAUDE_PLUGIN_ROOT to plugin path, generate x-managedBy/x-hookId, and produce CursorHooksConfig.
Hook Merger
src/helpers/hooks-merger.ts, tests/helpers/hooks-merger.test.ts
New utilities readExistingHooks, preserveUserHooks, mergeHooks: read/write .cursor/hooks.json robustly, preserve non‑AIPM user hooks, replace/append AIPM-managed hooks, and tests for merge/preservation/corruption cases.
Sync Strategy & Integration
src/helpers/sync-strategy.ts, src/commands/sync.ts, tests/commands/sync.test.ts
Add hook translation/capture to sync flow (translatedHooks on sync result), collect translated hooks across plugins, and call mergeHooks after plugin processing (respect dry-run and includeConfig). Tests for translation, path resolution, and cleanup scenarios.
Uninstall Hook Cleanup
src/commands/plugin-uninstall.ts, tests/commands/plugin-uninstall.test.ts
After file removals, read .cursor/hooks.json, filter out AIPM-managed hooks whose x-hookId matches the uninstalled plugin prefix, and write filtered hooks back; tests for removal, preservation, missing/malformed file, and dry-run.
Tests — Translator
tests/helpers/hooks-translator.test.ts
Unit tests for event mapping, nested arrays, CLAUDE_PLUGIN_ROOT replacements, ID formatting, counting, and edge cases (special chars, multiple replacements).

Sequence Diagram(s)

sequenceDiagram
  participant SyncCmd as Sync Command
  participant SyncHelper as Sync Helper (sync-strategy)
  participant Translator as Hooks Translator
  participant Merger as Hooks Merger
  participant FS as File System

  Note over SyncCmd,Merger: Plugin sync -> translate hooks -> final merge
  SyncCmd->>SyncHelper: syncPluginToCursor(plugin)
  SyncHelper->>FS: check for plugin `hooks.json`
  alt hooks.json found
    SyncHelper->>Translator: translateClaudeCodeHook(hooks.json, marketplace, plugin, pluginPath)
    Translator-->>SyncHelper: CursorHooksConfig (translated)
    SyncHelper->>FS: copy plugin files (exclude plugin hooks.json)
    SyncHelper->>FS: remove plugin hooks.json in target
    SyncHelper-->>SyncCmd: return translatedHooks
  else not found
    SyncHelper-->>SyncCmd: return no translatedHooks
  end
  SyncCmd->>Merger: mergeHooks(targetDir, collectedPluginHooks)
  Merger->>FS: read `.cursor/hooks.json`
  Merger->>Merger: preserve non-AIPM user hooks + append AIPM hooks
  Merger->>FS: write merged `.cursor/hooks.json`
Loading
sequenceDiagram
  participant UninstallCmd as Uninstall Command
  participant MergerReader as Hooks Merger Reader
  participant FS as File System

  Note over UninstallCmd,MergerReader: Uninstall-time hook cleanup
  UninstallCmd->>MergerReader: readExistingHooks(cursorDir)
  MergerReader->>FS: read `.cursor/hooks.json`
  alt valid hooks.json
    MergerReader->>MergerReader: filter hooks where x-managedBy=="aipm" and x-hookId startsWith(pluginPrefix)
    MergerReader->>FS: write filtered `.cursor/hooks.json`
  else missing/invalid
    MergerReader-->>UninstallCmd: no-op (graceful)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Areas to focus:
    • src/helpers/hooks-merger.ts — correctness preserving non‑AIPM hooks, handling malformed JSON, and write semantics.
    • src/helpers/hooks-translator.ts — event mapping, nested arrays handling, unique x-hookId generation, and safe CLAUDE_PLUGIN_ROOT replacement.
    • Integrations: src/helpers/sync-strategy.ts, src/commands/sync.ts, src/commands/plugin-uninstall.ts — dry-run behavior, includeConfig/subdir filtering, aggregation and final merge semantics, and uninstall prefix matching.
    • Tests — ensure edge-case coverage for corrupted/missing hooks.json, disabled/uninstalled plugins, and path normalization.

Possibly related PRs

Poem

🐇 I tidy hooks with a curious hop,
I swap CLAUDE roots so commands reach the top,
I keep user lines and sweep aipm’s trace,
I stitch plugin hooks into the proper place,
A little rabbit’s patch — neat every hop ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to assess whether the description relates to the changeset. Add a pull request description explaining the purpose, scope, and implementation details of the hooks management and translation features being introduced.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add hooks management and translation for Claude Code plugins' accurately reflects the main changes, which involve adding new hooks management capabilities and translation logic for Claude Code plugins throughout the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cursor
Copy link

cursor bot commented Dec 13, 2025

PR Summary

Adds end-to-end hooks support: translate Claude Code hooks to Cursor format, merge/preserve user hooks, and clean up hooks on sync/uninstall.

  • Hooks management:
    • Add helpers/hooks-translator.ts to translate Claude Code hooks/hooks.json to Cursor format with x-managedBy/x-hookId and resolved ${CLAUDE_PLUGIN_ROOT}.
    • Add helpers/hooks-merger.ts to read/validate existing /.cursor/hooks.json, preserve user hooks, merge AIPM hooks, and handle malformed entries.
    • Extend schema.ts with ClaudeCodeHookSchema, AipmManagedHookSchema, flexible UserHookSchema, and CursorHooksConfig validation/types.
  • Commands:
    • sync: collect per-plugin translated hooks, honor include (incl. INTEGRATION_INCLUDE_ALL) and DIR_HOOKS, and mergeHooks into /.cursor/hooks.json; also clean up hooks when no plugins enabled.
    • plugin-uninstall: when removeFiles=true, delete plugin assets and remove matching AIPM hooks (by x-hookId prefix) from /.cursor/hooks.json (respects dryRun).
  • Constants:
    • Add hook constants (DIR_HOOKS, FILE_HOOKS_JSON, AIPM_HOOK_PREFIX, HOOK_MANAGED_BY_FIELD, HOOK_ID_FIELD, CLAUDE_PLUGIN_ROOT_VAR, INTEGRATION_INCLUDE_ALL) and mark many constants as as const.
  • Tests:
    • New tests for hooks translation/merging and updates to sync/plugin-uninstall covering translation, preservation, cleanup, malformed entries, and dry-run behavior.

Written by Cursor Bugbot for commit ce8ab82. This will update automatically on new commits. Configure here.

@yordis yordis marked this pull request as ready for review December 17, 2025 02:09
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
src/helpers/sync-strategy.ts (3)

174-265: Consider extracting duplicated cleanup logic.

The three branches (translation success, translation failure, no hooks.json) share nearly identical cleanup logic:

  1. Copy hooks directory via syncDirectory
  2. Delete hooks.json if it exists
  3. Remove empty hooks directory

Consider extracting this into a helper:

+async function cleanupHooksDirectory(targetHooksDir: string): Promise<void> {
+  const copiedHooksJson = join(targetHooksDir, FILE_HOOKS_JSON);
+  if (await fileExists(copiedHooksJson)) {
+    await rm(copiedHooksJson);
+  }
+  try {
+    const remainingFiles = await readdir(targetHooksDir);
+    if (remainingFiles.length === 0) {
+      await rm(targetHooksDir, { recursive: true });
+    }
+  } catch {
+    // Directory doesn't exist or can't be read - ignore
+  }
+}

Then each branch can call await cleanupHooksDirectory(targetHooksDir).


189-197: Minor inefficiency: copying then immediately deleting hooks.json.

Line 191 copies all files (including hooks.json) via syncDirectory(..., []), then lines 194-197 immediately delete the copied hooks.json. Consider excluding hooks.json during copy to avoid this redundant I/O:

-      await syncDirectory(join(pluginPath, 'hooks'), targetHooksDir, []);
-
-      // Remove the original hooks.json since it's been translated and merged into .cursor/hooks.json
-      const copiedHooksJson = join(targetHooksDir, FILE_HOOKS_JSON);
-      if (await fileExists(copiedHooksJson)) {
-        await rm(copiedHooksJson);
-      }
+      // Copy hooks directory excluding hooks.json (already translated)
+      await syncDirectoryExcluding(join(pluginPath, 'hooks'), targetHooksDir, [FILE_HOOKS_JSON]);

This is a minor optimization since hooks.json is typically small.


209-211: Clarify or remove misleading comment.

The comment "Scripts stay in global plugin location - don't copy them" is confusing since syncDirectory on line 191 does copy files. If the intent is to explain that hook commands reference scripts at their original location (rather than copied scripts), consider rephrasing:

-      // Scripts stay in global plugin location - don't copy them
+      // Note: Translated hook commands reference scripts at their original plugin path

Or if the comment is no longer accurate, remove it.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7a1ac5f and 6a39b1d.

📒 Files selected for processing (11)
  • src/commands/plugin-uninstall.ts (2 hunks)
  • src/commands/sync.ts (5 hunks)
  • src/constants.ts (1 hunks)
  • src/helpers/hooks-merger.ts (1 hunks)
  • src/helpers/hooks-translator.ts (1 hunks)
  • src/helpers/sync-strategy.ts (4 hunks)
  • src/schema.ts (1 hunks)
  • tests/commands/plugin-uninstall.test.ts (2 hunks)
  • tests/commands/sync.test.ts (2 hunks)
  • tests/helpers/hooks-merger.test.ts (1 hunks)
  • tests/helpers/hooks-translator.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use bun <file> instead of node <file> or ts-node <file> for running scripts
Bun automatically loads .env files, so don't use the dotenv package
Use Bun.serve() with built-in WebSocket, HTTPS, and routes support instead of express
Use bun:sqlite for SQLite database access instead of better-sqlite3
Use Bun.redis for Redis client instead of ioredis
Use Bun.sql for Postgres database access instead of pg or postgres.js
Use the built-in WebSocket API instead of the ws package
Prefer Bun.file over node:fs's readFile/writeFile methods
Use Bun.$ for shell command execution instead of execa

Files:

  • src/constants.ts
  • src/commands/plugin-uninstall.ts
  • tests/helpers/hooks-merger.test.ts
  • tests/helpers/hooks-translator.test.ts
  • tests/commands/plugin-uninstall.test.ts
  • src/schema.ts
  • src/helpers/hooks-translator.ts
  • tests/commands/sync.test.ts
  • src/helpers/hooks-merger.ts
  • src/commands/sync.ts
  • src/helpers/sync-strategy.ts
**/*.{html,ts,tsx,css}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun build <file.html|file.ts|file.css> instead of webpack or esbuild for building

Files:

  • src/constants.ts
  • src/commands/plugin-uninstall.ts
  • tests/helpers/hooks-merger.test.ts
  • tests/helpers/hooks-translator.test.ts
  • tests/commands/plugin-uninstall.test.ts
  • src/schema.ts
  • src/helpers/hooks-translator.ts
  • tests/commands/sync.test.ts
  • src/helpers/hooks-merger.ts
  • src/commands/sync.ts
  • src/helpers/sync-strategy.ts
**/*.{tsx,jsx,ts,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Import .css files directly in TypeScript/JavaScript frontend files and Bun's CSS bundler will handle bundling

Files:

  • src/constants.ts
  • src/commands/plugin-uninstall.ts
  • tests/helpers/hooks-merger.test.ts
  • tests/helpers/hooks-translator.test.ts
  • tests/commands/plugin-uninstall.test.ts
  • src/schema.ts
  • src/helpers/hooks-translator.ts
  • tests/commands/sync.test.ts
  • src/helpers/hooks-merger.ts
  • src/commands/sync.ts
  • src/helpers/sync-strategy.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun --hot <file.ts> for hot module reloading during development

Files:

  • src/constants.ts
  • src/commands/plugin-uninstall.ts
  • tests/helpers/hooks-merger.test.ts
  • tests/helpers/hooks-translator.test.ts
  • tests/commands/plugin-uninstall.test.ts
  • src/schema.ts
  • src/helpers/hooks-translator.ts
  • tests/commands/sync.test.ts
  • src/helpers/hooks-merger.ts
  • src/commands/sync.ts
  • src/helpers/sync-strategy.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun test instead of jest or vitest for running tests

Files:

  • tests/helpers/hooks-merger.test.ts
  • tests/helpers/hooks-translator.test.ts
  • tests/commands/plugin-uninstall.test.ts
  • tests/commands/sync.test.ts
🧬 Code graph analysis (8)
src/commands/plugin-uninstall.ts (3)
src/constants.ts (2)
  • DIR_CURSOR (4-4)
  • FILE_HOOKS_JSON (59-59)
src/helpers/hooks-merger.ts (1)
  • readExistingHooks (13-38)
src/helpers/fs.ts (1)
  • writeJsonFile (35-49)
tests/helpers/hooks-translator.test.ts (2)
src/schema.ts (1)
  • ClaudeCodeHook (196-196)
src/helpers/hooks-translator.ts (2)
  • translateClaudeCodeHook (25-92)
  • countTranslatedHooks (97-103)
tests/commands/plugin-uninstall.test.ts (4)
src/constants.ts (1)
  • FILE_HOOKS_JSON (59-59)
src/commands/plugin-uninstall.ts (1)
  • pluginUninstall (36-222)
src/helpers/fs.ts (2)
  • readJsonFile (51-64)
  • fileExists (26-33)
src/schema.ts (1)
  • CursorHooksConfig (198-198)
src/helpers/hooks-translator.ts (3)
src/schema.ts (3)
  • ClaudeCodeHook (196-196)
  • CursorHooksConfig (198-198)
  • CursorHook (197-197)
src/helpers/io.ts (1)
  • defaultIO (79-79)
src/constants.ts (1)
  • CLAUDE_PLUGIN_ROOT_VAR (58-58)
tests/commands/sync.test.ts (3)
src/constants.ts (1)
  • FILE_HOOKS_JSON (59-59)
src/helpers/fs.ts (2)
  • fileExists (26-33)
  • readJsonFile (51-64)
src/schema.ts (1)
  • CursorHooksConfig (198-198)
src/helpers/hooks-merger.ts (4)
src/schema.ts (1)
  • CursorHooksConfig (198-198)
src/constants.ts (1)
  • FILE_HOOKS_JSON (59-59)
src/helpers/fs.ts (3)
  • fileExists (26-33)
  • readJsonFile (51-64)
  • writeJsonFile (35-49)
src/helpers/io.ts (1)
  • defaultIO (79-79)
src/commands/sync.ts (2)
src/schema.ts (1)
  • CursorHooksConfig (198-198)
src/helpers/hooks-merger.ts (1)
  • mergeHooks (89-100)
src/helpers/sync-strategy.ts (5)
src/schema.ts (2)
  • CursorHooksConfig (198-198)
  • ClaudeCodeHookSchema (160-177)
src/constants.ts (2)
  • FILE_HOOKS_JSON (59-59)
  • DIR_AIPM_NAMESPACE (9-9)
src/helpers/fs.ts (2)
  • fileExists (26-33)
  • readJsonFile (51-64)
src/helpers/hooks-translator.ts (2)
  • translateClaudeCodeHook (25-92)
  • countTranslatedHooks (97-103)
src/helpers/io.ts (1)
  • defaultIO (79-79)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (34)
src/constants.ts (1)

54-59: LGTM!

The new hook-related constants follow the existing naming conventions and provide a consistent way to reference the hooks configuration file and the Claude plugin root variable placeholder across the codebase.

src/commands/plugin-uninstall.ts (2)

11-11: LGTM!

The new imports (FILE_HOOKS_JSON, writeJsonFile, readExistingHooks) are necessary for implementing the hooks cleanup functionality and properly scoped from their respective modules.

Also applies to: 16-16, 18-18


176-214: Hooks cleanup logic looks good, type guard addresses prior concern.

The hook cleanup implementation correctly:

  • Filters out hooks belonging to the uninstalled plugin by checking the hookIdPrefix
  • Preserves user hooks and hooks from other AIPM-managed plugins
  • Includes proper type guards at line 198 (typeof hook['x-hookId'] === 'string') before calling string methods, addressing the past review comment

The logic integrates well with the hooks merger utilities and only executes when removeFiles=true.

src/commands/sync.ts (5)

9-9: LGTM!

The new imports (mergeHooks, CursorHooksConfig, IntegrationConfigSchema) are appropriate for the hooks collection and merging functionality.

Also applies to: 19-19


71-72: LGTM!

Early initialization of collectedPluginHooks is appropriate to enable hook cleanup even when no plugins are enabled. The comment clearly explains the intent.


74-80: LGTM!

The logic correctly handles the case when no plugins are enabled by still calling mergeHooks to clean up hooks from previously enabled but now disabled/uninstalled plugins. The dry-run guard is properly applied.


177-183: Hooks collection now properly respects the enabled config.

The code correctly checks whether hooks are enabled (line 179) before collecting translatedHooks into collectedPluginHooks. This addresses the past review comment concern about hooks being merged even when disabled in config.


203-207: LGTM!

Always calling mergeHooks after processing plugins (even with an empty array) ensures proper cleanup of hooks from disabled or uninstalled plugins. The dry-run guard and comments are appropriate.

tests/commands/plugin-uninstall.test.ts (5)

6-7: LGTM!

The new imports (FILE_HOOKS_JSON, CursorHooksConfig, fileExists, readJsonFile) are necessary for implementing comprehensive hooks cleanup tests.

Also applies to: 9-9


644-714: LGTM!

The test comprehensively verifies hook cleanup behavior when removeFiles=true, including:

  • Removal of hooks from the uninstalled plugin
  • Preservation of hooks from other plugins
  • Preservation of user-defined hooks

The use of type assertions (line 712) for user hooks is appropriate since they don't conform to the strict AIPM schema.


716-759: LGTM!

The test correctly verifies that hooks.json is not modified when removeFiles=false, confirming that hook cleanup is properly guarded by the removeFiles flag.


761-788: LGTM!

The test properly verifies graceful handling when hooks.json doesn't exist, ensuring the uninstall operation doesn't fail or create an unnecessary file.


790-864: LGTM!

The test thoroughly validates robustness against malformed hooks.json data with non-string x-hookId values. It confirms:

  • The type guard correctly preserves malformed hooks (null, number types)
  • Valid plugin hooks with string x-hookId are still removed
  • No TypeError is thrown despite malformed data

This provides excellent coverage for edge cases.

src/schema.ts (3)

157-177: LGTM!

The ClaudeCodeHookSchema accurately models the Claude Code hooks.json format with proper nesting and optional fields. The schema structure aligns well with the translator implementation.


179-186: LGTM!

The CursorHookSchema properly defines AIPM-managed hooks with strict validation using z.literal('aipm') for the x-managedBy field. This ensures type safety and consistency across the hooks management system.


188-198: LGTM!

The CursorHooksConfigSchema correctly models the Cursor hooks.json structure with version enforcement (z.literal(1)) and proper record mapping for event-based hooks. The type exports follow the established pattern and provide good type safety.

src/helpers/hooks-translator.ts (4)

1-3: LGTM!

The imports are appropriate and necessary for the translation functionality, including the constant for variable replacement, types for validation, and IO for logging.


5-14: LGTM!

The event mapping from Claude Code to Cursor hooks is reasonable. The comment on line 11 appropriately notes that PostToolUseafterShellExecution is an approximation, acknowledging the semantic gap between the two event systems.


25-92: LGTM!

The translateClaudeCodeHook function correctly translates Claude Code hooks to Cursor format with:

  • Proper event mapping via EVENT_MAP
  • Graceful handling of unknown events with warnings
  • Unique hook ID generation following the aipm/{marketplace}/{plugin}/{name} pattern
  • Safe CLAUDE_PLUGIN_ROOT resolution using regex with proper escaping (line 75)
  • Null checks preventing runtime errors

The implementation is robust and well-structured.


94-103: LGTM!

The countTranslatedHooks utility function is simple and correct, summing the lengths of all hook arrays in the configuration.

tests/helpers/hooks-translator.test.ts (1)

1-272: Excellent test coverage!

The test suite comprehensively covers:

  • Event translation (SessionStart, Stop, UserPromptSubmit)
  • Nested hooks array handling
  • CLAUDE_PLUGIN_ROOT variable resolution
  • Hook ID generation and format
  • Optional fields handling
  • Matcher field behavior
  • Hook counting utility

The tests are well-structured with proper setup/teardown and realistic test data.

src/helpers/hooks-merger.ts (4)

1-5: LGTM!

The imports are appropriate for the hooks merging functionality, including path utilities, constants, types, file system helpers, and IO for logging.


13-38: LGTM!

The readExistingHooks function appropriately:

  • Reads hooks.json without strict schema validation to allow user-defined hooks
  • Performs basic structural validation (version and hooks fields)
  • Returns null gracefully for missing or invalid files with informative logging
  • Documents the non-strict validation approach clearly (line 21)

This design correctly balances validation with flexibility for user hooks.


47-81: LGTM!

The preserveUserHooks function correctly implements the preserve-then-merge strategy:

  • First preserves user hooks (without x-managedBy: 'aipm')
  • Then merges all AIPM-managed hooks from enabled plugins
  • Properly initializes and builds the result structure
  • Uses appropriate filtering and spreading operations

This ensures user hooks are never lost during sync operations.


89-100: LGTM!

The mergeHooks function correctly orchestrates the merge workflow:

  • Reads existing hooks (which may include user hooks)
  • Merges using preserveUserHooks to maintain user hooks
  • Writes the result without strict schema validation (line 99), appropriately documented since user hooks may not conform to the AIPM schema

The implementation ensures data integrity while allowing user customization.

tests/commands/sync.test.ts (3)

9-11: LGTM!

The new imports for FILE_HOOKS_JSON, readJsonFile, fileExists, and CursorHooksConfig are correctly sourced and align with the test requirements for validating hooks translation and cleanup.


511-598: LGTM! Comprehensive test for hooks translation.

The test thoroughly validates:

  • Hooks structure with version and event mappings
  • x-managedBy and x-hookId metadata
  • Resolution of ${CLAUDE_PLUGIN_ROOT} to absolute paths
  • Hook scripts remain in global plugin location (not copied locally)

The path extraction regex on line 585 works for this controlled test scenario. For broader robustness in production code, consider more sophisticated path parsing if needed.


600-711: LGTM! Good coverage of hooks cleanup scenarios.

The tests properly validate:

  1. Hooks are removed when a plugin is disabled (lines 600-676)
  2. All AIPM hooks are cleaned up when no plugins are enabled (lines 678-711)

The conditional checks for beforeSubmitPrompt being undefined (lines 667-675, 704-710) correctly handle the edge case where the entire hook array is removed.

tests/helpers/hooks-merger.test.ts (4)

1-23: LGTM!

Test setup follows best practices with proper cleanup in afterEach. The temp directory pattern ensures test isolation.


62-100: LGTM! Important edge case coverage.

The as any type assertion on line 70 is necessary since CursorHookSchema requires x-managedBy, but real user hooks may omit this field. The comment explains this well. This test validates that the merger correctly preserves such hooks, which is critical for not breaking user configurations.


141-164: LGTM!

Good test pattern for simulating plugin disabling by merging an empty array. This validates that previously added hooks are correctly removed when no plugins contribute hooks.


249-261: LGTM! Good error handling validation.

Testing that readExistingHooks returns null for missing files (line 251) and invalid JSON (line 260) ensures graceful degradation without throwing exceptions that could break the sync flow.

src/helpers/sync-strategy.ts (2)

1-10: LGTM!

Imports are correctly updated to include the new hooks translation types and helpers.


13-21: LGTM!

Adding translatedHooks?: CursorHooksConfig | null to AipmPluginSyncResult is a clean way to surface translation results for downstream merging without breaking existing consumers.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
tests/commands/sync.test.ts (1)

582-588: Path extraction regex could be more robust.

The regex pattern used to extract and validate absolute paths works for common cases but could be fragile with unusual path formats or edge cases (e.g., paths with quotes, spaces, or special characters).

Consider a more robust approach for test validation:

-      // Extract the path from the command and verify it's absolute
-      const pathMatch = command.match(/(\/[^\s"]+|"[^"]+")/);
-      expect(pathMatch).toBeTruthy();
-      const extractedPath = pathMatch![0].replace(/^"|"$/g, ''); // Remove quotes if present
-      expect(extractedPath).toMatch(/^\/|^[A-Z]:/); // Should be absolute path (Unix or Windows)
+      // Verify the command contains the absolute plugin path
+      expect(command).toContain(pluginPath);
+      // Verify path is absolute by checking it doesn't start with relative indicators
+      expect(command).not.toMatch(/\.\//);
+      expect(command).not.toMatch(/\.\.\//);
src/helpers/sync-strategy.ts (1)

165-265: Consider extracting duplicated cleanup logic.

The cleanup code that removes hooks.json and empty directories is repeated identically in all three branches (translation success, translation failure, no hooks.json). This duplication increases maintenance burden and the risk of inconsistency if changes are needed.

Consider extracting the cleanup logic into a helper function:

/**
 * Cleanup after syncing hooks: remove hooks.json and empty directories
 */
async function cleanupHooksDirectory(targetHooksDir: string): Promise<void> {
  // Remove hooks.json if it exists (we don't need the original Claude Code format)
  const copiedHooksJson = join(targetHooksDir, FILE_HOOKS_JSON);
  if (await fileExists(copiedHooksJson)) {
    await rm(copiedHooksJson);
  }

  // Remove empty hooks directory if it only contained hooks.json
  try {
    const remainingFiles = await readdir(targetHooksDir);
    if (remainingFiles.length === 0) {
      await rm(targetHooksDir, { recursive: true });
    }
  } catch {
    // Directory doesn't exist or can't be read - ignore
  }
}

Then use it in all three branches:

const targetHooksDir = join(cursorDir, 'hooks', DIR_AIPM_NAMESPACE, marketplaceName, pluginName);
const count = await syncDirectory(join(pluginPath, 'hooks'), targetHooksDir, []);
await cleanupHooksDirectory(targetHooksDir);

This would reduce duplication from ~45 lines to ~15 lines and make future maintenance easier.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6a39b1d and 2d74873.

📒 Files selected for processing (11)
  • src/commands/plugin-uninstall.ts (2 hunks)
  • src/commands/sync.ts (5 hunks)
  • src/constants.ts (1 hunks)
  • src/helpers/hooks-merger.ts (1 hunks)
  • src/helpers/hooks-translator.ts (1 hunks)
  • src/helpers/sync-strategy.ts (4 hunks)
  • src/schema.ts (1 hunks)
  • tests/commands/plugin-uninstall.test.ts (2 hunks)
  • tests/commands/sync.test.ts (2 hunks)
  • tests/helpers/hooks-merger.test.ts (1 hunks)
  • tests/helpers/hooks-translator.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/helpers/hooks-merger.ts
  • src/schema.ts
  • tests/helpers/hooks-translator.test.ts
  • src/helpers/hooks-translator.ts
  • src/commands/sync.ts
  • src/commands/plugin-uninstall.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use bun <file> instead of node <file> or ts-node <file> for running scripts
Bun automatically loads .env files, so don't use the dotenv package
Use Bun.serve() with built-in WebSocket, HTTPS, and routes support instead of express
Use bun:sqlite for SQLite database access instead of better-sqlite3
Use Bun.redis for Redis client instead of ioredis
Use Bun.sql for Postgres database access instead of pg or postgres.js
Use the built-in WebSocket API instead of the ws package
Prefer Bun.file over node:fs's readFile/writeFile methods
Use Bun.$ for shell command execution instead of execa

Files:

  • src/constants.ts
  • src/helpers/sync-strategy.ts
  • tests/commands/sync.test.ts
  • tests/commands/plugin-uninstall.test.ts
  • tests/helpers/hooks-merger.test.ts
**/*.{html,ts,tsx,css}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun build <file.html|file.ts|file.css> instead of webpack or esbuild for building

Files:

  • src/constants.ts
  • src/helpers/sync-strategy.ts
  • tests/commands/sync.test.ts
  • tests/commands/plugin-uninstall.test.ts
  • tests/helpers/hooks-merger.test.ts
**/*.{tsx,jsx,ts,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Import .css files directly in TypeScript/JavaScript frontend files and Bun's CSS bundler will handle bundling

Files:

  • src/constants.ts
  • src/helpers/sync-strategy.ts
  • tests/commands/sync.test.ts
  • tests/commands/plugin-uninstall.test.ts
  • tests/helpers/hooks-merger.test.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun --hot <file.ts> for hot module reloading during development

Files:

  • src/constants.ts
  • src/helpers/sync-strategy.ts
  • tests/commands/sync.test.ts
  • tests/commands/plugin-uninstall.test.ts
  • tests/helpers/hooks-merger.test.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun test instead of jest or vitest for running tests

Files:

  • tests/commands/sync.test.ts
  • tests/commands/plugin-uninstall.test.ts
  • tests/helpers/hooks-merger.test.ts
🧬 Code graph analysis (4)
src/helpers/sync-strategy.ts (5)
src/schema.ts (2)
  • CursorHooksConfig (198-198)
  • ClaudeCodeHookSchema (160-177)
src/constants.ts (2)
  • FILE_HOOKS_JSON (59-59)
  • DIR_AIPM_NAMESPACE (9-9)
src/helpers/fs.ts (2)
  • fileExists (26-33)
  • readJsonFile (51-64)
src/helpers/hooks-translator.ts (2)
  • translateClaudeCodeHook (25-92)
  • countTranslatedHooks (97-103)
src/helpers/io.ts (1)
  • defaultIO (79-79)
tests/commands/sync.test.ts (3)
src/constants.ts (1)
  • FILE_HOOKS_JSON (59-59)
src/helpers/fs.ts (2)
  • fileExists (26-33)
  • readJsonFile (51-64)
src/schema.ts (1)
  • CursorHooksConfig (198-198)
tests/commands/plugin-uninstall.test.ts (4)
src/constants.ts (1)
  • FILE_HOOKS_JSON (59-59)
src/commands/plugin-uninstall.ts (1)
  • pluginUninstall (36-226)
src/helpers/fs.ts (2)
  • readJsonFile (51-64)
  • fileExists (26-33)
src/schema.ts (1)
  • CursorHooksConfig (198-198)
tests/helpers/hooks-merger.test.ts (4)
src/schema.ts (1)
  • CursorHooksConfig (198-198)
src/helpers/hooks-merger.ts (3)
  • mergeHooks (93-104)
  • preserveUserHooks (47-85)
  • readExistingHooks (13-38)
src/helpers/fs.ts (2)
  • readJsonFile (51-64)
  • fileExists (26-33)
src/constants.ts (1)
  • FILE_HOOKS_JSON (59-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (6)
src/constants.ts (1)

54-59: LGTM!

The new hook-related constants are well-named, properly documented, and follow the existing code conventions. They provide clear references for the hooks management functionality introduced in this PR.

tests/commands/plugin-uninstall.test.ts (1)

644-865: Excellent test coverage for hooks cleanup!

The test suite comprehensively validates the hooks cleanup behavior during plugin uninstallation, including:

  • Selective removal of plugin hooks while preserving other plugins' hooks and user hooks
  • Conditional cleanup based on the removeFiles flag
  • Graceful handling of missing or malformed hooks.json files

The edge case testing for malformed data (null/non-string x-hookId values) is particularly thorough and demonstrates defensive coding practices.

tests/commands/sync.test.ts (1)

511-711: Comprehensive hooks translation and cleanup tests!

The test suite thoroughly validates:

  • Translation from Claude Code hooks format to Cursor format, including path resolution and metadata generation
  • Cleanup behavior when plugins are disabled
  • Cleanup when no plugins are enabled

The tests correctly verify that hook scripts remain in the global plugin location rather than being copied to Cursor-local directories.

tests/helpers/hooks-merger.test.ts (1)

1-391: Excellent comprehensive test coverage!

This test suite provides thorough validation of the hooks merger functionality, including:

  • Merging hooks from multiple plugins with proper version and metadata preservation
  • Preserving user hooks (both with and without x-managedBy field)
  • Removing disabled plugin hooks while keeping others intact
  • Robust handling of missing files and malformed data (nulls, primitives, invalid JSON)

The defensive testing for corrupted data demonstrates production-ready error handling.

src/helpers/sync-strategy.ts (2)

1-21: LGTM!

The new imports support the hooks translation functionality, and the addition of the optional translatedHooks field to AipmPluginSyncResult is backward compatible and properly typed.


85-88: LGTM!

The integration of the new syncHooks function into the main sync flow is clean and straightforward, properly populating both the hooksCount and translatedHooks fields in the result.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
src/helpers/sync-strategy.ts (2)

165-265: Consider extracting duplicated cleanup logic.

The cleanup code for removing hooks.json and empty directories is repeated three times (lines 193-207, 220-234, 245-259). Extracting this into a helper function would reduce duplication and improve maintainability.

+async function cleanupHooksDirectory(targetHooksDir: string): Promise<void> {
+  const copiedHooksJson = join(targetHooksDir, FILE_HOOKS_JSON);
+  if (await fileExists(copiedHooksJson)) {
+    await rm(copiedHooksJson);
+  }
+  try {
+    const remainingFiles = await readdir(targetHooksDir);
+    if (remainingFiles.length === 0) {
+      await rm(targetHooksDir, { recursive: true });
+    }
+  } catch {
+    // Directory doesn't exist or can't be read - ignore
+  }
+}

Then replace each cleanup block with a single call to await cleanupHooksDirectory(targetHooksDir);


212-216: Consider extracting error message for cleaner logging.

Using ${error} directly in the template literal may produce [object Object] for some error types or include the full stack trace. Consider using a utility like getErrorMessage(error) (already imported in other files) for consistent error formatting.

src/helpers/hooks-merger.ts (1)

24-28: Comment says "must have version and hooks" but only checks for hooks.

The validation comment mentions checking for both version and hooks, but line 25 only validates rawData.hooks. Consider adding typeof rawData.version !== 'number' to the condition for consistency with the comment, or update the comment to reflect that only hooks is required.

-    // Basic validation - must have version and hooks
-    if (!rawData || typeof rawData !== 'object' || !rawData.hooks) {
+    // Basic validation - must have hooks object (version is optional, defaults to 1 if missing)
+    if (!rawData || typeof rawData !== 'object' || !rawData.hooks) {
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2d74873 and 69a2c11.

📒 Files selected for processing (11)
  • src/commands/plugin-uninstall.ts (2 hunks)
  • src/commands/sync.ts (5 hunks)
  • src/constants.ts (1 hunks)
  • src/helpers/hooks-merger.ts (1 hunks)
  • src/helpers/hooks-translator.ts (1 hunks)
  • src/helpers/sync-strategy.ts (4 hunks)
  • src/schema.ts (1 hunks)
  • tests/commands/plugin-uninstall.test.ts (2 hunks)
  • tests/commands/sync.test.ts (2 hunks)
  • tests/helpers/hooks-merger.test.ts (1 hunks)
  • tests/helpers/hooks-translator.test.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • tests/helpers/hooks-merger.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/schema.ts
  • src/helpers/hooks-translator.ts
  • tests/helpers/hooks-translator.test.ts
  • src/commands/sync.ts
  • tests/commands/plugin-uninstall.test.ts
  • tests/commands/sync.test.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use bun <file> instead of node <file> or ts-node <file> for running scripts
Bun automatically loads .env files, so don't use the dotenv package
Use Bun.serve() with built-in WebSocket, HTTPS, and routes support instead of express
Use bun:sqlite for SQLite database access instead of better-sqlite3
Use Bun.redis for Redis client instead of ioredis
Use Bun.sql for Postgres database access instead of pg or postgres.js
Use the built-in WebSocket API instead of the ws package
Prefer Bun.file over node:fs's readFile/writeFile methods
Use Bun.$ for shell command execution instead of execa

Files:

  • src/constants.ts
  • src/commands/plugin-uninstall.ts
  • src/helpers/hooks-merger.ts
  • src/helpers/sync-strategy.ts
**/*.{html,ts,tsx,css}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun build <file.html|file.ts|file.css> instead of webpack or esbuild for building

Files:

  • src/constants.ts
  • src/commands/plugin-uninstall.ts
  • src/helpers/hooks-merger.ts
  • src/helpers/sync-strategy.ts
**/*.{tsx,jsx,ts,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Import .css files directly in TypeScript/JavaScript frontend files and Bun's CSS bundler will handle bundling

Files:

  • src/constants.ts
  • src/commands/plugin-uninstall.ts
  • src/helpers/hooks-merger.ts
  • src/helpers/sync-strategy.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun --hot <file.ts> for hot module reloading during development

Files:

  • src/constants.ts
  • src/commands/plugin-uninstall.ts
  • src/helpers/hooks-merger.ts
  • src/helpers/sync-strategy.ts
🧬 Code graph analysis (2)
src/helpers/hooks-merger.ts (4)
src/schema.ts (1)
  • CursorHooksConfig (198-198)
src/constants.ts (1)
  • FILE_HOOKS_JSON (59-59)
src/helpers/fs.ts (3)
  • fileExists (26-33)
  • readJsonFile (51-64)
  • writeJsonFile (35-49)
src/helpers/io.ts (1)
  • defaultIO (79-79)
src/helpers/sync-strategy.ts (5)
src/schema.ts (2)
  • CursorHooksConfig (198-198)
  • ClaudeCodeHookSchema (160-177)
src/constants.ts (2)
  • FILE_HOOKS_JSON (59-59)
  • DIR_AIPM_NAMESPACE (9-9)
src/helpers/fs.ts (2)
  • fileExists (26-33)
  • readJsonFile (51-64)
src/helpers/hooks-translator.ts (2)
  • translateClaudeCodeHook (25-90)
  • countTranslatedHooks (95-101)
src/helpers/io.ts (1)
  • defaultIO (79-79)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (6)
src/commands/plugin-uninstall.ts (2)

192-208: Type guards properly implemented.

The filtering logic now correctly guards against malformed hook entries:

  • Non-object entries (null, primitives, arrays) are filtered out at lines 194-196
  • The typeof hook['x-hookId'] === 'string' check at line 202 prevents TypeError when calling startsWith()

This addresses the concerns from the previous review.


176-218: Hooks cleanup only runs when removeFiles is true.

The hooks cleanup logic is nested inside the if (cmd.removeFiles) block, meaning hooks for the uninstalled plugin will persist in hooks.json if the user runs plugin-uninstall without --remove-files. This may be intentional (keeping hooks as "orphaned" until files are removed), but consider whether hooks should be cleaned regardless of removeFiles since the plugin config is being removed.

src/constants.ts (1)

54-59: LGTM!

The new constants are well-organized under the "Hook-related constants" section and follow the existing naming conventions. Centralizing FILE_HOOKS_JSON prevents magic strings across the codebase.

src/helpers/sync-strategy.ts (1)

189-211: Clarify comment vs. code behavior for script copying.

The comments at lines 209, 236, and 261 state "Scripts stay in global plugin location - don't copy them," but syncDirectory with an empty extensions array copies all files from the hooks directory to targetHooksDir. If scripts should not be copied, consider filtering them out. If the current behavior is intentional, update the comments to reflect that scripts are copied alongside the translation.

src/helpers/hooks-merger.ts (2)

59-66: Type guards properly implemented.

The filtering logic at lines 60-63 correctly guards against non-object hook entries before using the in operator, addressing the concerns from the previous review.


47-85: LGTM!

The preserveUserHooks function correctly:

  1. Filters out malformed hook entries
  2. Preserves user hooks (without x-managedBy: "aipm")
  3. Merges AIPM hooks from multiple plugins

The defensive spread pattern at line 69 ensures safety even if event names somehow appeared multiple times.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/helpers/sync-strategy.ts (1)

165-265: Consider extracting duplicate cleanup logic.

The three code paths (translation success, fallback, no hooks.json) all contain identical cleanup logic for removing hooks.json and empty directories (lines 194-207, 221-234, 246-259). Extracting this into a helper function would improve maintainability and reduce duplication.

Example refactor:

async function cleanupHooksDirectory(targetHooksDir: string): Promise<void> {
  // Remove hooks.json if it exists
  const copiedHooksJson = join(targetHooksDir, FILE_HOOKS_JSON);
  if (await fileExists(copiedHooksJson)) {
    await rm(copiedHooksJson);
  }

  // Remove empty hooks directory if it only contained hooks.json
  try {
    const remainingFiles = await readdir(targetHooksDir);
    if (remainingFiles.length === 0) {
      await rm(targetHooksDir, { recursive: true });
    }
  } catch {
    // Directory doesn't exist or can't be read - ignore
  }
}

Then call await cleanupHooksDirectory(targetHooksDir); in each path.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 69a2c11 and 253ae85.

📒 Files selected for processing (11)
  • src/commands/plugin-uninstall.ts (2 hunks)
  • src/commands/sync.ts (5 hunks)
  • src/constants.ts (1 hunks)
  • src/helpers/hooks-merger.ts (1 hunks)
  • src/helpers/hooks-translator.ts (1 hunks)
  • src/helpers/sync-strategy.ts (4 hunks)
  • src/schema.ts (1 hunks)
  • tests/commands/plugin-uninstall.test.ts (2 hunks)
  • tests/commands/sync.test.ts (2 hunks)
  • tests/helpers/hooks-merger.test.ts (1 hunks)
  • tests/helpers/hooks-translator.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/schema.ts
  • src/helpers/hooks-translator.ts
  • src/constants.ts
  • tests/helpers/hooks-merger.test.ts
  • tests/commands/sync.test.ts
  • tests/commands/plugin-uninstall.test.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use bun <file> instead of node <file> or ts-node <file> for running scripts
Bun automatically loads .env files, so don't use the dotenv package
Use Bun.serve() with built-in WebSocket, HTTPS, and routes support instead of express
Use bun:sqlite for SQLite database access instead of better-sqlite3
Use Bun.redis for Redis client instead of ioredis
Use Bun.sql for Postgres database access instead of pg or postgres.js
Use the built-in WebSocket API instead of the ws package
Prefer Bun.file over node:fs's readFile/writeFile methods
Use Bun.$ for shell command execution instead of execa

Files:

  • src/commands/sync.ts
  • src/helpers/hooks-merger.ts
  • src/commands/plugin-uninstall.ts
  • tests/helpers/hooks-translator.test.ts
  • src/helpers/sync-strategy.ts
**/*.{html,ts,tsx,css}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun build <file.html|file.ts|file.css> instead of webpack or esbuild for building

Files:

  • src/commands/sync.ts
  • src/helpers/hooks-merger.ts
  • src/commands/plugin-uninstall.ts
  • tests/helpers/hooks-translator.test.ts
  • src/helpers/sync-strategy.ts
**/*.{tsx,jsx,ts,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Import .css files directly in TypeScript/JavaScript frontend files and Bun's CSS bundler will handle bundling

Files:

  • src/commands/sync.ts
  • src/helpers/hooks-merger.ts
  • src/commands/plugin-uninstall.ts
  • tests/helpers/hooks-translator.test.ts
  • src/helpers/sync-strategy.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun --hot <file.ts> for hot module reloading during development

Files:

  • src/commands/sync.ts
  • src/helpers/hooks-merger.ts
  • src/commands/plugin-uninstall.ts
  • tests/helpers/hooks-translator.test.ts
  • src/helpers/sync-strategy.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun test instead of jest or vitest for running tests

Files:

  • tests/helpers/hooks-translator.test.ts
🧬 Code graph analysis (5)
src/commands/sync.ts (3)
src/schema.ts (1)
  • CursorHooksConfig (198-198)
src/helpers/io.ts (1)
  • defaultIO (79-79)
src/helpers/hooks-merger.ts (1)
  • mergeHooks (107-118)
src/helpers/hooks-merger.ts (4)
src/schema.ts (1)
  • CursorHooksConfig (198-198)
src/constants.ts (1)
  • FILE_HOOKS_JSON (59-59)
src/helpers/fs.ts (3)
  • fileExists (26-33)
  • readJsonFile (51-64)
  • writeJsonFile (35-49)
src/helpers/io.ts (1)
  • defaultIO (79-79)
src/commands/plugin-uninstall.ts (3)
src/constants.ts (2)
  • DIR_CURSOR (4-4)
  • FILE_HOOKS_JSON (59-59)
src/helpers/hooks-merger.ts (1)
  • readExistingHooks (13-52)
src/helpers/fs.ts (1)
  • writeJsonFile (35-49)
tests/helpers/hooks-translator.test.ts (2)
src/schema.ts (1)
  • ClaudeCodeHook (196-196)
src/helpers/hooks-translator.ts (2)
  • translateClaudeCodeHook (25-90)
  • countTranslatedHooks (95-101)
src/helpers/sync-strategy.ts (5)
src/schema.ts (2)
  • CursorHooksConfig (198-198)
  • ClaudeCodeHookSchema (160-177)
src/constants.ts (2)
  • FILE_HOOKS_JSON (59-59)
  • DIR_AIPM_NAMESPACE (9-9)
src/helpers/fs.ts (2)
  • fileExists (26-33)
  • readJsonFile (51-64)
src/helpers/hooks-translator.ts (2)
  • translateClaudeCodeHook (25-90)
  • countTranslatedHooks (95-101)
src/helpers/io.ts (1)
  • defaultIO (79-79)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (6)
tests/helpers/hooks-translator.test.ts (1)

1-344: LGTM! Excellent test coverage for hook translation logic.

The test suite comprehensively covers:

  • Event mapping (SessionStart → beforeSubmitPrompt, Stop → stop)
  • CLAUDE_PLUGIN_ROOT variable replacement including multiple occurrences
  • Special replacement characters ($1, $&amp;, $$) that could corrupt paths
  • Nested hook arrays and hook ID generation
  • Edge cases like optional fields and ignored matchers

The tests for special replacement characters (lines 307-343) are particularly valuable for preventing path corruption bugs.

src/helpers/sync-strategy.ts (1)

20-20: LGTM! Type augmentation correctly supports translated hooks.

The addition of translatedHooks?: CursorHooksConfig | null to AipmPluginSyncResult properly extends the type to support the new hook translation feature. The integration at lines 85-88 correctly destructures and assigns both the count and translated hooks from syncHooks.

Also applies to: 85-88

src/commands/sync.ts (1)

71-79: LGTM! Hook collection correctly respects the include config.

The code properly addresses the previous review concern about hooks being merged when disabled. Lines 179-182 now check if hooks are enabled via includeConfig before collecting translatedHooks, ensuring that disabled hooks are not added to the merge.

The mergeHooks calls at lines 78 and 206 correctly execute even with an empty array to clean up hooks from disabled or uninstalled plugins.

Also applies to: 177-183, 203-207

src/helpers/hooks-merger.ts (3)

13-52: LGTM! Validation properly addresses past review concerns.

The validation logic correctly addresses the previous review comments:

  • Lines 30-34 ensure hooks is an object and not an array, preventing TypeError when calling .filter() later
  • Lines 36-42 validate that all hook event values are arrays, preventing runtime errors

The non-strict parsing approach (line 22) appropriately allows user-defined hooks while still validating the core structure.


61-99: LGTM! Type guards properly prevent TypeError on malformed hooks.

The type guards at lines 74-76 correctly address the previous review comment about potential TypeError when hook entries are non-object values. The guard checks for null, undefined, primitives, and arrays before using the in operator at line 79.

The merge logic properly:

  1. Filters out malformed entries
  2. Preserves user hooks (without x-managedBy: "aipm")
  3. Appends all AIPM-managed hooks from enabled plugins

107-118: LGTM! Merge orchestration is clean and correct.

The mergeHooks function properly orchestrates the merge workflow:

  1. Reads existing hooks (handling missing/invalid gracefully via readExistingHooks)
  2. Merges user and AIPM hooks via preserveUserHooks
  3. Writes without strict validation to accommodate user hooks that lack AIPM metadata

The approach correctly balances validation for safety while allowing flexibility for user-defined hooks.

@yordis yordis force-pushed the yordis/feat-2 branch 2 times, most recently from d8d607a to 9c87e56 Compare December 17, 2025 03:45
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/helpers/sync-strategy.ts (1)

214-239: Same misleading comment appears in fallback paths.

The comments at lines 236 and 261 have the same issue as line 209: they claim scripts aren't copied, but syncDirectory copies all files. This creates unused script duplicates in the target directory across all three code paths (translation success, failure, and no hooks.json).

Consider either filtering scripts during copy or updating the comments for consistency.

Also applies to: 241-264

src/commands/plugin-uninstall.ts (1)

218-220: Respect dry-run flag when writing hooks.json.

The writeJsonFile call should respect the dryRun flag for consistency with the rest of the uninstall flow. This was flagged in previous reviews but remains unaddressed.

Apply this diff to add dry-run support:

-        await writeJsonFile(hooksPath, cleanedHooks);
+        await writeJsonFile(hooksPath, cleanedHooks, undefined, cmd.dryRun);
🧹 Nitpick comments (2)
src/helpers/sync-strategy.ts (2)

189-211: Misleading comment: scripts ARE being copied.

The comment at line 209 states "Scripts stay in global plugin location - don't copy them", but the syncDirectory call at line 191 with an empty extensions array copies all files, including scripts. The translated hooks correctly reference the global plugin location (via CLAUDE_PLUGIN_ROOT_VAR replacement), so the copied scripts in .cursor/hooks/aipm/marketplace/plugin/ are unused duplicates.

If the intent is to not copy scripts, you should either:

  1. Pass specific extensions to syncDirectory to exclude scripts, or
  2. Update the comment to reflect that scripts are copied but not referenced by translated hooks

Consider applying this diff to filter out common script extensions:

-      await syncDirectory(join(pluginPath, 'hooks'), targetHooksDir, []);
+      // Copy non-script files only (scripts stay in global plugin location)
+      const scriptExtensions = ['.sh', '.bash', '.js', '.ts', '.py', '.rb'];
+      const entries = await readdir(join(pluginPath, 'hooks'), { withFileTypes: true });
+      for (const entry of entries) {
+        if (entry.isFile() && !scriptExtensions.some(ext => entry.name.endsWith(ext))) {
+          await cp(join(pluginPath, 'hooks', entry.name), join(targetHooksDir, entry.name));
+        }
+      }

Alternatively, update the comment to clarify that scripts are copied but unused.


165-265: Consider extracting common cleanup logic.

All three code paths (translation success, failure, and no hooks.json) share similar cleanup logic:

  1. Remove hooks.json from target
  2. Remove empty hooks directory

This could be extracted to a helper function to reduce duplication, though the current structure is clear enough.

Example helper function:

async function cleanupHooksTarget(targetHooksDir: string): Promise<void> {
  // Remove hooks.json (we don't need it in target)
  const copiedHooksJson = join(targetHooksDir, FILE_HOOKS_JSON);
  if (await fileExists(copiedHooksJson)) {
    await rm(copiedHooksJson);
  }

  // Remove empty hooks directory
  try {
    const remainingFiles = await readdir(targetHooksDir);
    if (remainingFiles.length === 0) {
      await rm(targetHooksDir, { recursive: true });
    }
  } catch {
    // Directory doesn't exist or can't be read - ignore
  }
}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 253ae85 and d8d607a.

📒 Files selected for processing (11)
  • src/commands/plugin-uninstall.ts (2 hunks)
  • src/commands/sync.ts (5 hunks)
  • src/constants.ts (1 hunks)
  • src/helpers/hooks-merger.ts (1 hunks)
  • src/helpers/hooks-translator.ts (1 hunks)
  • src/helpers/sync-strategy.ts (4 hunks)
  • src/schema.ts (2 hunks)
  • tests/commands/plugin-uninstall.test.ts (2 hunks)
  • tests/commands/sync.test.ts (2 hunks)
  • tests/helpers/hooks-merger.test.ts (1 hunks)
  • tests/helpers/hooks-translator.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • tests/helpers/hooks-merger.test.ts
  • src/helpers/hooks-translator.ts
  • tests/commands/sync.test.ts
  • tests/helpers/hooks-translator.test.ts
  • src/helpers/hooks-merger.ts
  • src/schema.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use bun <file> instead of node <file> or ts-node <file> for running scripts
Bun automatically loads .env files, so don't use the dotenv package
Use Bun.serve() with built-in WebSocket, HTTPS, and routes support instead of express
Use bun:sqlite for SQLite database access instead of better-sqlite3
Use Bun.redis for Redis client instead of ioredis
Use Bun.sql for Postgres database access instead of pg or postgres.js
Use the built-in WebSocket API instead of the ws package
Prefer Bun.file over node:fs's readFile/writeFile methods
Use Bun.$ for shell command execution instead of execa

Files:

  • src/commands/sync.ts
  • src/helpers/sync-strategy.ts
  • src/commands/plugin-uninstall.ts
  • tests/commands/plugin-uninstall.test.ts
  • src/constants.ts
**/*.{html,ts,tsx,css}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun build <file.html|file.ts|file.css> instead of webpack or esbuild for building

Files:

  • src/commands/sync.ts
  • src/helpers/sync-strategy.ts
  • src/commands/plugin-uninstall.ts
  • tests/commands/plugin-uninstall.test.ts
  • src/constants.ts
**/*.{tsx,jsx,ts,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Import .css files directly in TypeScript/JavaScript frontend files and Bun's CSS bundler will handle bundling

Files:

  • src/commands/sync.ts
  • src/helpers/sync-strategy.ts
  • src/commands/plugin-uninstall.ts
  • tests/commands/plugin-uninstall.test.ts
  • src/constants.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun --hot <file.ts> for hot module reloading during development

Files:

  • src/commands/sync.ts
  • src/helpers/sync-strategy.ts
  • src/commands/plugin-uninstall.ts
  • tests/commands/plugin-uninstall.test.ts
  • src/constants.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun test instead of jest or vitest for running tests

Files:

  • tests/commands/plugin-uninstall.test.ts
🧬 Code graph analysis (4)
src/commands/sync.ts (2)
src/schema.ts (1)
  • CursorHooksConfig (199-199)
src/helpers/hooks-merger.ts (1)
  • mergeHooks (107-118)
src/helpers/sync-strategy.ts (5)
src/schema.ts (2)
  • CursorHooksConfig (199-199)
  • ClaudeCodeHookSchema (161-178)
src/constants.ts (2)
  • FILE_HOOKS_JSON (59-59)
  • DIR_AIPM_NAMESPACE (9-9)
src/helpers/fs.ts (2)
  • fileExists (26-33)
  • readJsonFile (51-64)
src/helpers/hooks-translator.ts (2)
  • translateClaudeCodeHook (25-90)
  • countTranslatedHooks (95-101)
src/helpers/io.ts (1)
  • defaultIO (79-79)
src/commands/plugin-uninstall.ts (3)
src/constants.ts (5)
  • DIR_CURSOR (4-4)
  • AIPM_HOOK_PREFIX (60-60)
  • HOOK_MANAGED_BY_FIELD (61-61)
  • HOOK_ID_FIELD (62-62)
  • FILE_HOOKS_JSON (59-59)
src/helpers/hooks-merger.ts (1)
  • readExistingHooks (13-52)
src/helpers/fs.ts (1)
  • writeJsonFile (35-49)
tests/commands/plugin-uninstall.test.ts (3)
src/constants.ts (1)
  • FILE_HOOKS_JSON (59-59)
src/helpers/fs.ts (2)
  • readJsonFile (51-64)
  • fileExists (26-33)
src/schema.ts (1)
  • CursorHooksConfig (199-199)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (13)
src/constants.ts (1)

54-62: LGTM! Well-organized hook constants.

The new constants are clearly documented and follow consistent naming patterns. The grouping under a dedicated comment section improves code organization.

src/commands/plugin-uninstall.ts (3)

6-6: LGTM! Imports support the new hooks cleanup flow.

All new imports are properly utilized in the hooks cleanup logic added below.

Also applies to: 12-14, 19-19, 21-21


196-207: LGTM! Type guards properly address past review concerns.

The type guards at lines 196-199 (checking for non-object entries) and lines 205-206 (verifying x-hookId is a string) correctly prevent TypeError when processing malformed hooks. This addresses the previous review comment about missing type guards.


180-217: Well-structured hooks cleanup logic.

The implementation correctly:

  • Constructs plugin-specific hook ID prefix for filtering
  • Preserves user hooks and hooks from other plugins
  • Removes empty hook event arrays from the output
  • Handles edge cases with appropriate type guards
tests/commands/plugin-uninstall.test.ts (2)

6-9: LGTM! Imports support comprehensive hooks testing.

The imports enable thorough testing of the hooks cleanup functionality with proper typing.


644-865: Excellent test coverage for hooks cleanup!

The test suite comprehensively validates:

  • Correct removal of plugin-specific hooks while preserving others
  • Respecting the removeFiles flag
  • Graceful handling of missing hooks.json
  • Robust handling of malformed data (non-string x-hookId values)

The malformed data test at lines 790-864 is particularly valuable, as it confirms the type guards prevent runtime errors while preserving invalid entries (rather than crashing).

src/commands/sync.ts (4)

9-9: LGTM! Imports enable hooks merging flow.

The new imports support the centralized hooks management introduced in this PR.

Also applies to: 19-19


71-72: Good design: early initialization enables cleanup of disabled plugins.

Initializing collectedPluginHooks before the plugin loop allows mergeHooks to clean up hooks from disabled or uninstalled plugins, even when no plugins are currently enabled.


177-183: LGTM! Properly respects hooks enabled/disabled configuration.

The hooksEnabled check at lines 179-180 correctly verifies whether hooks are enabled in the integration config before collecting translated hooks. This addresses the previous review concern about hooks being merged even when disabled.


203-207: LGTM! Final hooks merge ensures cleanup.

Always calling mergeHooks after processing all plugins (even with an empty array) correctly ensures hooks from disabled or uninstalled plugins are removed. The dry-run check is appropriate.

src/helpers/sync-strategy.ts (3)

1-11: LGTM! Imports support hooks translation flow.

All new imports are properly utilized in the hooks synchronization and translation logic.


20-20: LGTM! Field enables hooks collection for later merging.

The optional translatedHooks field allows the sync result to communicate translated hooks back to the caller, enabling centralized hooks management in the sync command.


85-88: LGTM! Clean integration of hooks translation.

The syncHooks call and result propagation cleanly integrates hooks translation into the overall plugin sync flow.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/helpers/sync-strategy.ts (1)

174-265: Refactor to eliminate code duplication.

The three code paths (translate success, translate failure, no hooks.json) have nearly identical cleanup logic duplicated at Lines 189-210, 217-237, and 242-262. The repeated blocks include:

  • syncDirectory call
  • Remove hooks.json if exists
  • Remove empty directory cleanup
  • Comment about scripts

Consider extracting this cleanup into a helper function to reduce duplication and improve maintainability.

Apply this refactor:

+async function cleanupHooksDirectory(
+  pluginPath: string,
+  targetHooksDir: string,
+  marketplaceName: string,
+  pluginName: string,
+): Promise<number> {
+  // Copy the hooks directory files to the target location (excluding hooks.json since it's translated and merged)
+  const count = await syncDirectory(join(pluginPath, 'hooks'), targetHooksDir, []);
+
+  // Remove the hooks.json since it's been translated/processed
+  const copiedHooksJson = join(targetHooksDir, FILE_HOOKS_JSON);
+  if (await fileExists(copiedHooksJson)) {
+    await rm(copiedHooksJson);
+  }
+
+  // Remove empty hooks directory if it only contained hooks.json
+  try {
+    const remainingFiles = await readdir(targetHooksDir);
+    if (remainingFiles.length === 0) {
+      await rm(targetHooksDir, { recursive: true });
+    }
+  } catch {
+    // Directory doesn't exist or can't be read - ignore
+  }
+
+  // Scripts stay in global plugin location (pluginPath/scripts/), not in hooks/ subdirectory
+  return count;
+}
+
 async function syncHooks(
   pluginPath: string,
   marketplaceName: string,
   pluginName: string,
   cursorDir: string,
 ): Promise<{ count: number; translatedHooks: CursorHooksConfig | null }> {
   const hooksJsonPath = join(pluginPath, 'hooks', FILE_HOOKS_JSON);
+  const targetHooksDir = join(cursorDir, 'hooks', DIR_AIPM_NAMESPACE, marketplaceName, pluginName);

   if (await fileExists(hooksJsonPath)) {
     try {
       // Translate Claude Code hooks
       const claudeHook = await readJsonFile(hooksJsonPath, ClaudeCodeHookSchema);
       const translated = translateClaudeCodeHook(claudeHook, marketplaceName, pluginName, pluginPath);
-      const count = countTranslatedHooks(translated);
-
-      // Copy the hooks directory files to the target location (excluding hooks.json since it's translated and merged)
-      const targetHooksDir = join(cursorDir, 'hooks', DIR_AIPM_NAMESPACE, marketplaceName, pluginName);
-      await syncDirectory(join(pluginPath, 'hooks'), targetHooksDir, []);
-
-      // Remove the original hooks.json since it's been translated and merged into .cursor/hooks.json
-      const copiedHooksJson = join(targetHooksDir, FILE_HOOKS_JSON);
-      if (await fileExists(copiedHooksJson)) {
-        await rm(copiedHooksJson);
-      }
-
-      // Remove empty hooks directory if it only contained hooks.json
-      try {
-        const remainingFiles = await readdir(targetHooksDir);
-        if (remainingFiles.length === 0) {
-          await rm(targetHooksDir, { recursive: true });
-        }
-      } catch {
-        // Directory doesn't exist or can't be read - ignore
-      }
-
-      // Scripts stay in global plugin location - don't copy them
+      const translatedCount = countTranslatedHooks(translated);
+      await cleanupHooksDirectory(pluginPath, targetHooksDir, marketplaceName, pluginName);

-      return { count, translatedHooks: translated };
+      return { count: translatedCount, translatedHooks: translated };
     } catch (error) {
       // If translation fails, fall back to copying hooks directory as-is
       defaultIO.logInfo(
         `⚠️  Failed to translate hooks.json for ${pluginName}@${marketplaceName}: ${error}. Copying as-is.`,
       );
-      const targetHooksDir = join(cursorDir, 'hooks', DIR_AIPM_NAMESPACE, marketplaceName, pluginName);
-      const count = await syncDirectory(join(pluginPath, 'hooks'), targetHooksDir, []);
-
-      // Remove hooks.json if it exists (we don't need the original Claude Code format)
-      const copiedHooksJson = join(targetHooksDir, FILE_HOOKS_JSON);
-      if (await fileExists(copiedHooksJson)) {
-        await rm(copiedHooksJson);
-      }
-
-      // Remove empty hooks directory if it only contained hooks.json
-      try {
-        const remainingFiles = await readdir(targetHooksDir);
-        if (remainingFiles.length === 0) {
-          await rm(targetHooksDir, { recursive: true });
-        }
-      } catch {
-        // Directory doesn't exist or can't be read - ignore
-      }
-
-      // Scripts stay in global plugin location - don't copy them
+      const count = await cleanupHooksDirectory(pluginPath, targetHooksDir, marketplaceName, pluginName);

       return { count, translatedHooks: null };
     }
   } else {
     // No hooks.json, copy as-is (AIPM format or scripts)
-    const targetHooksDir = join(cursorDir, 'hooks', DIR_AIPM_NAMESPACE, marketplaceName, pluginName);
-    const count = await syncDirectory(join(pluginPath, 'hooks'), targetHooksDir, []);
-
-    // Remove hooks.json if it exists (we don't need it if there's no translation)
-    const copiedHooksJson = join(targetHooksDir, FILE_HOOKS_JSON);
-    if (await fileExists(copiedHooksJson)) {
-      await rm(copiedHooksJson);
-    }
-
-    // Remove empty hooks directory if it only contained hooks.json
-    try {
-      const remainingFiles = await readdir(targetHooksDir);
-      if (remainingFiles.length === 0) {
-        await rm(targetHooksDir, { recursive: true });
-      }
-    } catch {
-      // Directory doesn't exist or can't be read - ignore
-    }
-
-    // Scripts stay in global plugin location - don't copy them
+    const count = await cleanupHooksDirectory(pluginPath, targetHooksDir, marketplaceName, pluginName);

     return { count, translatedHooks: null };
   }
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d8d607a and 9c87e56.

📒 Files selected for processing (11)
  • src/commands/plugin-uninstall.ts (2 hunks)
  • src/commands/sync.ts (5 hunks)
  • src/constants.ts (1 hunks)
  • src/helpers/hooks-merger.ts (1 hunks)
  • src/helpers/hooks-translator.ts (1 hunks)
  • src/helpers/sync-strategy.ts (4 hunks)
  • src/schema.ts (2 hunks)
  • tests/commands/plugin-uninstall.test.ts (2 hunks)
  • tests/commands/sync.test.ts (2 hunks)
  • tests/helpers/hooks-merger.test.ts (1 hunks)
  • tests/helpers/hooks-translator.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/schema.ts
  • src/helpers/hooks-translator.ts
  • src/commands/plugin-uninstall.ts
  • tests/commands/plugin-uninstall.test.ts
  • tests/helpers/hooks-merger.test.ts
  • tests/helpers/hooks-translator.test.ts
  • src/constants.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use bun <file> instead of node <file> or ts-node <file> for running scripts
Bun automatically loads .env files, so don't use the dotenv package
Use Bun.serve() with built-in WebSocket, HTTPS, and routes support instead of express
Use bun:sqlite for SQLite database access instead of better-sqlite3
Use Bun.redis for Redis client instead of ioredis
Use Bun.sql for Postgres database access instead of pg or postgres.js
Use the built-in WebSocket API instead of the ws package
Prefer Bun.file over node:fs's readFile/writeFile methods
Use Bun.$ for shell command execution instead of execa

Files:

  • src/helpers/sync-strategy.ts
  • src/commands/sync.ts
  • tests/commands/sync.test.ts
  • src/helpers/hooks-merger.ts
**/*.{html,ts,tsx,css}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun build <file.html|file.ts|file.css> instead of webpack or esbuild for building

Files:

  • src/helpers/sync-strategy.ts
  • src/commands/sync.ts
  • tests/commands/sync.test.ts
  • src/helpers/hooks-merger.ts
**/*.{tsx,jsx,ts,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Import .css files directly in TypeScript/JavaScript frontend files and Bun's CSS bundler will handle bundling

Files:

  • src/helpers/sync-strategy.ts
  • src/commands/sync.ts
  • tests/commands/sync.test.ts
  • src/helpers/hooks-merger.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun --hot <file.ts> for hot module reloading during development

Files:

  • src/helpers/sync-strategy.ts
  • src/commands/sync.ts
  • tests/commands/sync.test.ts
  • src/helpers/hooks-merger.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun test instead of jest or vitest for running tests

Files:

  • tests/commands/sync.test.ts
🧬 Code graph analysis (3)
src/helpers/sync-strategy.ts (4)
src/schema.ts (2)
  • CursorHooksConfig (199-199)
  • ClaudeCodeHookSchema (161-178)
src/constants.ts (2)
  • FILE_HOOKS_JSON (59-59)
  • DIR_AIPM_NAMESPACE (9-9)
src/helpers/fs.ts (2)
  • fileExists (26-33)
  • readJsonFile (51-64)
src/helpers/hooks-translator.ts (2)
  • translateClaudeCodeHook (25-90)
  • countTranslatedHooks (95-101)
src/commands/sync.ts (2)
src/schema.ts (1)
  • CursorHooksConfig (199-199)
src/helpers/hooks-merger.ts (1)
  • mergeHooks (107-118)
src/helpers/hooks-merger.ts (4)
src/schema.ts (1)
  • CursorHooksConfig (199-199)
src/constants.ts (3)
  • FILE_HOOKS_JSON (59-59)
  • HOOK_MANAGED_BY_FIELD (61-61)
  • AIPM_HOOK_PREFIX (60-60)
src/helpers/fs.ts (3)
  • fileExists (26-33)
  • readJsonFile (51-64)
  • writeJsonFile (35-49)
src/helpers/io.ts (1)
  • defaultIO (79-79)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (10)
src/commands/sync.ts (3)

71-80: LGTM! Good cleanup strategy.

Initializing collectedPluginHooks early and calling mergeHooks even with an empty array ensures proper cleanup of hooks from disabled/uninstalled plugins, even when no plugins are currently enabled.


177-183: Hooks collection correctly respects include config.

The conditional logic properly checks if hooks are enabled before collecting translatedHooks. This addresses the past review comment about hooks being merged even when disabled in config.


203-207: LGTM! Final merge step properly positioned.

Calling mergeHooks after processing all plugins ensures that the final .cursor/hooks.json reflects only enabled plugins' hooks while preserving user hooks. The dry-run check is appropriate.

tests/commands/sync.test.ts (3)

511-598: Comprehensive test coverage for Claude Code hooks translation.

The test properly validates:

  • Translation of Claude Code hooks format to Cursor format
  • Metadata fields (x-managedBy, x-hookId)
  • Variable substitution (${CLAUDE_PLUGIN_ROOT})
  • Path absolutization
  • Scripts remaining at plugin root (not copied to .cursor/hooks/)

600-676: LGTM! Cleanup test properly validates hook removal.

The test correctly verifies that disabling a plugin removes its hooks from .cursor/hooks.json while preserving other hooks. The conditional check handles both scenarios (hooks array with filtered entries vs. completely empty/undefined).


678-711: Good edge case coverage for cleanup with no enabled plugins.

The test validates that mergeHooks is called even when no plugins are enabled (as implemented in Lines 76-79 of sync.ts), ensuring AIPM-managed hooks are removed while preserving any user hooks.

src/helpers/sync-strategy.ts (1)

20-20: LGTM! Optional field properly typed.

The optional translatedHooks field allows the result to distinguish between: translated hooks (object), failed translation (null), and pre-hooks-feature code (undefined).

src/helpers/hooks-merger.ts (3)

13-52: LGTM! Robust validation addresses past review concerns.

The validation at Lines 30-42 properly addresses the past review comment about missing validation. The function now:

  • Validates hooks is an object (not array)
  • Validates all hook event values are arrays
  • Returns null for malformed structures, allowing a fresh hooks.json to be created

This prevents TypeErrors in preserveUserHooks when calling .filter() on hook arrays.


61-99: Defensive filtering addresses past review concerns.

The guard at Lines 74-77 properly addresses the past review comment about TypeError with non-object values. The filter correctly checks for null, undefined, primitives, and arrays before using the in operator.

The silent filtering of malformed entries is acceptable for resilience. If debugging assistance is desired, consider adding a debug log when malformed hooks are encountered, but this is optional.


107-118: LGTM! Clean orchestration with appropriate validation strategy.

The function correctly orchestrates the merge flow. Skipping strict schema validation on write (Line 115-116) is appropriate since user hooks may not include AIPM-specific metadata fields like x-managedBy.

@yordis yordis force-pushed the yordis/feat-2 branch 3 times, most recently from 92e3d91 to fe1cadb Compare December 17, 2025 04:26
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
src/schema.ts (1)

222-234: Type mismatch between schema and TypeScript type is intentional but warrants a brief note.

The CursorHooksConfigSchema accepts malformed entries via CursorHookValidationSchema, but the CursorHooksConfig type uses the strict CursorHook[]. This gap is bridged by filtering in preserveUserHooks, but a reader might find the cast at line 24 of hooks-merger.ts confusing.

Consider adding a brief inline comment in readExistingHooks explaining why the cast is safe (validation allows malformed, filtering removes them before use).

src/helpers/sync-strategy.ts (1)

174-250: Significant code duplication in cleanup logic.

The cleanup pattern (remove hooks.json, check empty dir, remove dir) is repeated three times (lines 191-204, 214-226, 234-246). Consider extracting a helper function to reduce duplication and maintenance burden.

+async function cleanupHooksDirectory(targetHooksDir: string): Promise<void> {
+  const copiedHooksJson = join(targetHooksDir, FILE_HOOKS_JSON);
+  if (await fileExists(copiedHooksJson)) {
+    await rm(copiedHooksJson);
+  }
+
+  try {
+    const remainingFiles = await readdir(targetHooksDir);
+    if (remainingFiles.length === 0) {
+      await rm(targetHooksDir, { recursive: true });
+    }
+  } catch {
+    // Ignore - directory doesn't exist or can't be read
+  }
+}

Then use it in each branch:

await cleanupHooksDirectory(targetHooksDir);
src/constants.ts (1)

56-63: Hook constants are well-organized and actively used across the codebase.

The new constants provide a single source of truth for hook-related magic strings and are referenced in src/helpers/hooks-translator.ts and src/commands/plugin-uninstall.ts. However, src/schema.ts (lines 185-186) uses inline string literals 'x-managedBy' and 'x-hookId' instead of these constants, creating an inconsistency.

Consider updating the schema for consistency:

// In src/schema.ts
+import { AIPM_HOOK_PREFIX, HOOK_MANAGED_BY_FIELD, HOOK_ID_FIELD } from './constants';

 export const AipmManagedHookSchema = z.object({
-  'x-managedBy': z.literal(AIPM_HOOK_PREFIX),
-  'x-hookId': z.string(),
+  [HOOK_MANAGED_BY_FIELD]: z.literal(AIPM_HOOK_PREFIX),
+  [HOOK_ID_FIELD]: z.string(),
   command: z.string(),
 });
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9c87e56 and fe1cadb.

📒 Files selected for processing (11)
  • src/commands/plugin-uninstall.ts (3 hunks)
  • src/commands/sync.ts (5 hunks)
  • src/constants.ts (2 hunks)
  • src/helpers/hooks-merger.ts (1 hunks)
  • src/helpers/hooks-translator.ts (1 hunks)
  • src/helpers/sync-strategy.ts (4 hunks)
  • src/schema.ts (2 hunks)
  • tests/commands/plugin-uninstall.test.ts (2 hunks)
  • tests/commands/sync.test.ts (2 hunks)
  • tests/helpers/hooks-merger.test.ts (1 hunks)
  • tests/helpers/hooks-translator.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/helpers/hooks-translator.ts
  • tests/helpers/hooks-translator.test.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use bun <file> instead of node <file> or ts-node <file> for running scripts
Bun automatically loads .env files, so don't use the dotenv package
Use Bun.serve() with built-in WebSocket, HTTPS, and routes support instead of express
Use bun:sqlite for SQLite database access instead of better-sqlite3
Use Bun.redis for Redis client instead of ioredis
Use Bun.sql for Postgres database access instead of pg or postgres.js
Use the built-in WebSocket API instead of the ws package
Prefer Bun.file over node:fs's readFile/writeFile methods
Use Bun.$ for shell command execution instead of execa

Files:

  • src/helpers/hooks-merger.ts
  • src/commands/sync.ts
  • tests/helpers/hooks-merger.test.ts
  • src/helpers/sync-strategy.ts
  • src/commands/plugin-uninstall.ts
  • tests/commands/sync.test.ts
  • src/schema.ts
  • src/constants.ts
  • tests/commands/plugin-uninstall.test.ts
**/*.{html,ts,tsx,css}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun build <file.html|file.ts|file.css> instead of webpack or esbuild for building

Files:

  • src/helpers/hooks-merger.ts
  • src/commands/sync.ts
  • tests/helpers/hooks-merger.test.ts
  • src/helpers/sync-strategy.ts
  • src/commands/plugin-uninstall.ts
  • tests/commands/sync.test.ts
  • src/schema.ts
  • src/constants.ts
  • tests/commands/plugin-uninstall.test.ts
**/*.{tsx,jsx,ts,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Import .css files directly in TypeScript/JavaScript frontend files and Bun's CSS bundler will handle bundling

Files:

  • src/helpers/hooks-merger.ts
  • src/commands/sync.ts
  • tests/helpers/hooks-merger.test.ts
  • src/helpers/sync-strategy.ts
  • src/commands/plugin-uninstall.ts
  • tests/commands/sync.test.ts
  • src/schema.ts
  • src/constants.ts
  • tests/commands/plugin-uninstall.test.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun --hot <file.ts> for hot module reloading during development

Files:

  • src/helpers/hooks-merger.ts
  • src/commands/sync.ts
  • tests/helpers/hooks-merger.test.ts
  • src/helpers/sync-strategy.ts
  • src/commands/plugin-uninstall.ts
  • tests/commands/sync.test.ts
  • src/schema.ts
  • src/constants.ts
  • tests/commands/plugin-uninstall.test.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun test instead of jest or vitest for running tests

Files:

  • tests/helpers/hooks-merger.test.ts
  • tests/commands/sync.test.ts
  • tests/commands/plugin-uninstall.test.ts
🧬 Code graph analysis (6)
src/helpers/hooks-merger.ts (4)
src/schema.ts (4)
  • CursorHooksConfig (231-234)
  • CursorHooksConfigSchema (222-225)
  • AipmManagedHookSchema (184-188)
  • UserHookSchema (194-194)
src/constants.ts (1)
  • FILE_HOOKS_JSON (60-60)
src/helpers/fs.ts (3)
  • fileExists (26-33)
  • readJsonFile (51-64)
  • writeJsonFile (35-49)
src/helpers/io.ts (1)
  • defaultIO (79-79)
src/commands/sync.ts (3)
src/schema.ts (1)
  • CursorHooksConfig (231-234)
src/helpers/io.ts (1)
  • defaultIO (79-79)
src/helpers/hooks-merger.ts (1)
  • mergeHooks (85-90)
tests/helpers/hooks-merger.test.ts (4)
src/schema.ts (1)
  • CursorHooksConfig (231-234)
src/helpers/hooks-merger.ts (3)
  • mergeHooks (85-90)
  • preserveUserHooks (38-77)
  • readExistingHooks (14-29)
src/helpers/fs.ts (2)
  • readJsonFile (51-64)
  • fileExists (26-33)
src/constants.ts (1)
  • FILE_HOOKS_JSON (60-60)
src/helpers/sync-strategy.ts (5)
src/schema.ts (2)
  • CursorHooksConfig (231-234)
  • ClaudeCodeHookSchema (161-178)
src/constants.ts (3)
  • DIR_HOOKS (21-21)
  • FILE_HOOKS_JSON (60-60)
  • DIR_AIPM_NAMESPACE (9-9)
src/helpers/fs.ts (2)
  • fileExists (26-33)
  • readJsonFile (51-64)
src/helpers/hooks-translator.ts (2)
  • translateClaudeCodeHook (26-84)
  • countTranslatedHooks (89-95)
src/helpers/io.ts (1)
  • defaultIO (79-79)
tests/commands/sync.test.ts (3)
src/constants.ts (1)
  • FILE_HOOKS_JSON (60-60)
src/helpers/fs.ts (2)
  • fileExists (26-33)
  • readJsonFile (51-64)
src/schema.ts (1)
  • CursorHooksConfig (231-234)
src/schema.ts (1)
src/constants.ts (1)
  • AIPM_HOOK_PREFIX (61-61)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (18)
tests/commands/plugin-uninstall.test.ts (1)

643-913: LGTM! Comprehensive test coverage for hooks cleanup.

The new test suite thoroughly covers:

  • Hook removal when removeFiles=true
  • Hook preservation when removeFiles=false
  • Graceful handling of missing hooks.json
  • Resilience against malformed x-hookId values (null, number)
  • Dry-run mode respecting hooks.json

The use of any type assertions for test fixtures with non-strict hook schemas is appropriate.

src/commands/plugin-uninstall.ts (1)

179-211: Well-structured hooks cleanup with proper type safety.

The implementation correctly addresses the previous review concerns:

  1. Type guards via safeParse (lines 193-198) prevent TypeError on non-string x-hookId values
  2. The cmd.dryRun parameter at line 210 provides defense-in-depth, though this code path is only reachable when dryRun=false due to the branching at line 68

The filtering logic cleanly separates AIPM-managed hooks (removed if matching the plugin prefix) from user hooks (preserved).

src/commands/sync.ts (2)

176-182: Past review concern properly addressed.

The hook collection is now correctly gated by the includeConfig setting (line 178). When hooks: false is set in the integration config, hooks won't be added to collectedPluginHooks, ensuring they're cleaned up rather than persisted.


71-79: Good: Early initialization enables cleanup even with no enabled plugins.

Initializing collectedPluginHooks before the enabled plugins check (line 72) and calling mergeHooks in the early-return path (line 78) ensures proper cleanup of stale hooks when all plugins are disabled.

tests/commands/sync.test.ts (2)

511-598: Thorough test for hooks translation workflow.

The test comprehensively validates:

  • Claude Code → Cursor format translation
  • ${CLAUDE_PLUGIN_ROOT} replacement with absolute paths
  • Proper x-managedBy and x-hookId assignment
  • Scripts remaining in the plugin location (not copied)

600-712: Good coverage for hooks cleanup scenarios.

Both tests properly verify:

  1. Hooks are removed when a plugin is disabled and re-synced
  2. AIPM hooks are cleaned up when syncing with no enabled plugins

The conditional checks at lines 667-676 and 705-711 appropriately handle both cases where hooks might be undefined or an empty filtered array.

tests/helpers/hooks-merger.test.ts (3)

1-24: Well-structured test setup.

The test harness properly creates and cleans up temporary directories for isolation. The cleanup function pattern ensures proper teardown even if tests fail.


325-385: Excellent edge case coverage for malformed hook entries.

This test verifies that preserveUserHooks gracefully handles corrupted data (null, primitives, arrays) without throwing TypeError. The assertions confirm that:

  • Malformed entries are filtered out
  • Valid user hooks are preserved
  • New AIPM hooks are added
  • Old AIPM hooks are replaced

263-323: Good schema validation tests for hooks.json structure.

These tests verify that readExistingHooks properly rejects invalid structures:

  • hooks as an array instead of object
  • Hook event values as strings/objects instead of arrays
  • Mixed valid/invalid event values

This ensures the merger handles malformed files gracefully by returning null.

src/helpers/hooks-merger.ts (3)

14-29: LGTM! Resilient reading with graceful fallback.

The function properly handles missing files and invalid JSON by returning null, allowing callers to proceed with a clean slate. The try-catch around schema validation ensures malformed files don't crash the sync process.


85-90: Clean orchestration of the merge flow.

The function correctly sequences read → preserve → write operations. One consideration: writeJsonFile here doesn't pass a schema for validation, which is intentional since the merged result may contain user hooks with arbitrary extra properties that wouldn't pass strict validation.


47-65: The edge case mentioned is not possible—schema validation is already in place upstream. The CursorHooksConfigSchema enforces z.record(z.string(), z.array(...)), which means hooks[eventName] is guaranteed to be an array. In readExistingHooks, any JSON that violates this structure is rejected during validation (line 26), causing the function to return null rather than passing invalid data to preserveUserHooks. By the time existingHooks.hooks is accessed in preserveUserHooks, the structure is already validated. The .filter() call cannot throw because the type system and schema both guarantee an array. No additional defensive validation is needed.

Likely an incorrect or invalid review comment.

src/schema.ts (3)

201-209: Private validation schema serves resilience needs.

The CursorHookValidationSchema union allows parsing to succeed even with malformed entries (null, primitives, arrays), which are then filtered out in preserveUserHooks. This is a sound design for handling user-edited files.

Note: The schema is not exported (no export), which correctly limits its use to internal validation only.


161-178: Schema structure looks correct for Claude Code hooks format.

The nested structure (hooks → event → array of matchers → array of hook objects) matches typical Claude Code plugin hooks.json patterns.


184-194: AIPM and User hook schemas are well-defined.

AipmManagedHookSchema enforces the required AIPM metadata fields, while UserHookSchema with .loose() (Zod 4 syntax) allows user hooks to have any additional properties beyond the required command field.

src/helpers/sync-strategy.ts (2)

206-228: Count semantics are now consistent.

The past review flagged inconsistent hooksCount semantics. This has been addressed:

  • Translation success: returns actual hook count via countTranslatedHooks
  • Translation failure: returns count: 0
  • No hooks.json: returns count: 0

This ensures formatSyncResult displays accurate hook counts.


13-21: Good addition of translatedHooks to the result type.

The optional translatedHooks field allows downstream code (sync.ts) to collect and merge hooks from multiple plugins, enabling the centralized hooks.json merge workflow.

src/constants.ts (1)

4-9: Good addition of as const for type narrowing.

Adding as const to existing constants enables TypeScript to infer string literal types instead of string, improving type safety when these constants are used as discriminators or in template literal types.

Also applies to: 21-21, 26-28, 33-33, 38-48, 53-54

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@yordis yordis merged commit 4e478e7 into main Dec 18, 2025
6 checks passed
@yordis yordis deleted the yordis/feat-2 branch December 18, 2025 06:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant